diff --git a/package.json b/package.json index 15bf5c7c..6344e3e1 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "vscode-jest", "displayName": "Jest", "description": "Use Facebook's Jest With Pleasure.", - "version": "4.7.0", + "version": "5.0.0", "publisher": "Orta", "engines": { - "vscode": "^1.63.0" + "vscode": "^1.68.1" }, "author": { "name": "Orta Therox, ConnectDotz & Sean Poulter", @@ -72,7 +72,7 @@ "title": "Jest", "properties": { "jest.showTerminalOnLaunch": { - "description": "Automatically open test explorer's terminal upon launch", + "description": "(deprecated) Automatically open test explorer's terminal upon launch", "type": "boolean", "default": true, "scope": "window" @@ -220,9 +220,7 @@ "jest.testExplorer": { "markdownDescription": "Configure jest TestExplorer. See valid [formats](https://github.com/jest-community/vscode-jest/blob/master/README.md#testexplorer) or [how to use test explorer](https://github.com/jest-community/vscode-jest/blob/master/README.md#how-to-use-the-test-explorer) for more details", "type": "object", - "default": { - "enabled": true - }, + "default": null, "scope": "resource" }, "jest.monitorLongRun": { @@ -464,8 +462,7 @@ "dependencies": { "istanbul-lib-coverage": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", - "jest-editor-support": "^30.1.0", - "vscode-codicons": "^0.0.4" + "jest-editor-support": "^30.2.0" }, "devDependencies": { "@types/istanbul-lib-coverage": "^2.0.2", diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 49658301..6262fc02 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -1,23 +1,18 @@ import * as vscode from 'vscode'; import { JestTotalResults } from 'jest-editor-support'; -import { TestStatus } from '../decorations/test-status'; import { statusBar, StatusBar, Mode, StatusBarUpdate, SBTestStats } from '../StatusBar'; import { - TestReconciliationState, TestResultProvider, - TestResult, resultsWithLowerCaseWindowsDriveLetters, SortedTestResults, - TestResultStatusInfo, - TestReconciliationStateType, } from '../TestResults'; import { testIdString, IdStringType, escapeRegExp, emptyTestStats } from '../helpers'; import { CoverageMapProvider, CoverageCodeLensProvider } from '../Coverage'; import { updateDiagnostics, updateCurrentDiagnostics, resetDiagnostics } from '../diagnostics'; import { DebugCodeLensProvider, DebugTestIdentifier } from '../DebugCodeLens'; import { DebugConfigurationProvider } from '../DebugConfigurationProvider'; -import { DecorationOptions, TestStats } from '../types'; +import { TestStats } from '../types'; import { CoverageOverlay } from '../Coverage/CoverageOverlay'; import { resultsWithoutAnsiEscapeSequence } from '../TestResults/TestResult'; import { CoverageMapData } from 'istanbul-lib-coverage'; @@ -34,6 +29,7 @@ import { JestTestProvider } from '../test-provider'; import { JestProcessInfo } from '../JestProcessManagement'; import { addFolderToDisabledWorkspaceFolders } from '../extensionManager'; import { MessageAction } from '../messaging'; +import { getExitErrorDef } from '../errors'; interface RunTestPickItem extends vscode.QuickPickItem { id: DebugTestIdentifier; @@ -51,11 +47,6 @@ export class JestExt { debugConfigurationProvider: DebugConfigurationProvider; coverageCodeLensProvider: CoverageCodeLensProvider; - // So you can read what's going on - channel: vscode.OutputChannel; - - private decorations: TestStatus; - // The ability to show fails in the problems section private failDiagnostics: vscode.DiagnosticCollection; @@ -84,7 +75,6 @@ export class JestExt { this.extContext = createJestExtContext(workspaceFolder, pluginSettings); this.logging = this.extContext.loggingFactory.create('JestExt'); - this.channel = vscode.window.createOutputChannel(`Jest (${workspaceFolder.name})`); this.failDiagnostics = vscode.languages.createDiagnosticCollection( `Jest (${workspaceFolder.name})` ); @@ -117,8 +107,6 @@ export class JestExt { this.status = statusBar.bind(workspaceFolder.name); - // The theme stuff - this.decorations = new TestStatus(vscodeContext); // reset the jest diagnostics resetDiagnostics(this.failDiagnostics); @@ -127,6 +115,10 @@ export class JestExt { this.setupStatusBar(); } + public showOutput(): void { + this.extContext.output.show(); + } + private getExtExplorerContext(): JestExtExplorerContext { return { ...this.extContext, @@ -168,23 +160,16 @@ export class JestExt { private setupRunEvents(events: JestSessionEvents): void { events.onRunEvent.event((event: JestRunEvent) => { + // only process the test running event + if (event.process.request.type === 'not-test') { + return; + } + // console.log( + // `[core.onRunEvent] "${this.extContext.workspace.name}" event:${event.type} process:${event.process.id}` + // ); switch (event.type) { - case 'scheduled': - this.channel.appendLine(`${event.process.id} is scheduled`); - break; - case 'data': - if (event.newLine) { - this.channel.appendLine(event.text); - } else { - this.channel.append(event.text); - } - if (event.isError) { - this.channel.show(); - } - break; case 'start': this.updateStatusBar({ state: 'running' }); - this.channel.clear(); break; case 'end': this.updateStatusBar({ state: 'done' }); @@ -192,10 +177,6 @@ export class JestExt { case 'exit': if (event.error) { this.updateStatusBar({ state: 'stopped' }); - const msg = `${event.error}\n see troubleshooting: ${messaging.TROUBLESHOOTING_URL}`; - this.channel.appendLine(msg); - this.channel.show(); - messaging.systemErrorMessage( prefixWorkspace(this.extContext, event.error), ...this.buildMessageActions(['help', 'wizard', 'disable-folder']) @@ -262,26 +243,20 @@ export class JestExt { if (newSession) { await this.processSession.stop(); this.processSession = this.createProcessSession(); - this.channel.appendLine('Starting a new Jest Process Session'); - } else { - this.channel.appendLine('Starting Jest Session'); } this.testProvider?.dispose(); - if (this.extContext.settings.testExplorer.enabled) { - this.testProvider = new JestTestProvider(this.getExtExplorerContext()); - } + this.testProvider = new JestTestProvider(this.getExtExplorerContext()); await this.processSession.start(); this.events.onTestSessionStarted.fire({ ...this.extContext, session: this.processSession }); this.updateTestFileList(); - this.channel.appendLine('Jest Session Started'); } catch (e) { const msg = prefixWorkspace(this.extContext, 'Failed to start jest session'); this.logging('error', `${msg}:`, e); - this.channel.appendLine('Failed to start jest session'); + this.extContext.output.write('Failed to start jest session', 'error'); messaging.systemErrorMessage( `${msg}...`, ...this.buildMessageActions(['help', 'wizard', 'disable-folder']) @@ -291,7 +266,6 @@ export class JestExt { public async stopSession(): Promise { try { - this.channel.appendLine('Stopping Jest Session'); await this.processSession.stop(); this.testProvider?.dispose(); @@ -299,12 +273,11 @@ export class JestExt { this.events.onTestSessionStopped.fire(); - this.channel.appendLine('Jest Session Stopped'); this.updateStatusBar({ state: 'stopped' }); } catch (e) { const msg = prefixWorkspace(this.extContext, 'Failed to stop jest session'); this.logging('error', `${msg}:`, e); - this.channel.appendLine('Failed to stop jest session'); + this.extContext.output.write('Failed to stop jest session', 'error'); messaging.systemErrorMessage('${msg}...', ...this.buildMessageActions(['help'])); } } @@ -324,7 +297,7 @@ export class JestExt { try { sortedResults = this.testResultProvider.getSortedResults(filePath); } catch (e) { - this.channel.appendLine(`${filePath}: failed to parse test results: ${e}`); + this.extContext.output.write(`${filePath}: failed to parse test results: ${e}`, 'error'); // assign an empty result so we can clear the outdated decorators/diagnostics etc sortedResults = { fail: [], @@ -338,7 +311,7 @@ export class JestExt { return; } - this.updateDecorators(sortedResults, editor); + this.updateDecorators(); updateCurrentDiagnostics(sortedResults.fail, this.failDiagnostics, editor); } @@ -375,41 +348,7 @@ export class JestExt { return this.startSession(true); } - updateDecorators(testResults: SortedTestResults, editor: vscode.TextEditor): void { - if ( - this.extContext.settings.testExplorer.enabled === false || - this.extContext.settings.testExplorer.showClassicStatus - ) { - // Status indicators (gutter icons) - const styleMap = [ - { - data: testResults.success, - decorationType: this.decorations.passing, - state: TestReconciliationState.KnownSuccess, - }, - { - data: testResults.fail, - decorationType: this.decorations.failing, - state: TestReconciliationState.KnownFail, - }, - { - data: testResults.skip, - decorationType: this.decorations.skip, - state: TestReconciliationState.KnownSkip, - }, - { - data: testResults.unknown, - decorationType: this.decorations.unknown, - state: TestReconciliationState.Unknown, - }, - ]; - - styleMap.forEach((style) => { - const decorators = this.generateDotsForItBlocks(style.data, style.state); - editor.setDecorations(style.decorationType, decorators); - }); - } - + updateDecorators(): void { // Debug CodeLens this.debugCodeLensProvider.didChange(); } @@ -447,7 +386,7 @@ export class JestExt { } public deactivate(): void { this.stopSession(); - this.channel.dispose(); + this.extContext.output.dispose(); this.testResultProvider.dispose(); this.testProvider?.dispose(); @@ -647,20 +586,16 @@ export class JestExt { private updateTestFileList(): void { this.processSession.scheduleProcess({ type: 'list-test-files', - onResult: (files, error) => { + onResult: (files, error, exitCode) => { this.setTestFiles(files); this.logging('debug', `found ${files?.length} testFiles`); if (error) { - const msg = prefixWorkspace( - this.extContext, - 'Failed to obtain test file list, something might not be setup right?' - ); + const msg = + 'failed to retrieve test file list. TestExplorer might show incomplete test items'; + this.extContext.output.write(error, 'new-line'); + const errorType = getExitErrorDef(exitCode) ?? 'error'; + this.extContext.output.write(msg, errorType); this.logging('error', msg, error); - //fire this warning message could risk reporting error multiple times for the given workspace folder - //therefore garding the warning message with the debugMode - if (this.extContext.settings.debugMode) { - messaging.systemWarningMessage(msg, ...this.buildMessageActions(['help', 'wizard'])); - } } }, }); @@ -717,15 +652,4 @@ export class JestExt { this.refreshDocumentChange(); } - - private generateDotsForItBlocks( - blocks: TestResult[], - state: TestReconciliationStateType - ): DecorationOptions[] { - return blocks.map((it) => ({ - range: new vscode.Range(it.start.line, it.start.column, it.start.line, it.start.column + 1), - hoverMessage: TestResultStatusInfo[state].desc, - identifier: it.name, - })); - } } diff --git a/src/JestExt/helper.ts b/src/JestExt/helper.ts index 3a54d15d..43519860 100644 --- a/src/JestExt/helper.ts +++ b/src/JestExt/helper.ts @@ -13,6 +13,7 @@ import { MonitorLongRun, JestExtAutoRunConfig, JestExtAutoRunShortHand, + TestExplorerConfigLegacy, } from '../Settings'; import { AutoRunMode } from '../StatusBar'; import { pathToJest, pathToConfig, toFilePath } from '../helpers'; @@ -20,6 +21,7 @@ import { workspaceLogging } from '../logging'; import { AutoRunAccessor, JestExtContext, RunnerWorkspaceOptions } from './types'; import { CoverageColors } from '../Coverage'; import { platform } from 'os'; +import { JestOutputTerminal } from './output-terminal'; export const isWatchRequest = (request: JestProcessRequest): boolean => request.type === 'watch-tests' || request.type === 'watch-all-tests'; @@ -105,12 +107,14 @@ export const createJestExtContext = ( settings.shell ); }; + const output = new JestOutputTerminal(workspaceFolder.name); return { workspace: workspaceFolder, settings, createRunnerWorkspace, loggingFactory: workspaceLogging(workspaceFolder.name, settings.debugMode ?? false), autoRun: AutoRun(settings), + output, }; }; @@ -171,6 +175,30 @@ const getAutoRunSetting = ( } return setting; }; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const isTestExplorerConfigLegacy = (arg: any): arg is TestExplorerConfigLegacy => + typeof arg.enabled === 'boolean'; + +const DefaultTestExplorerSetting: TestExplorerConfig = {}; +const getTestExplorer = (config: vscode.WorkspaceConfiguration): TestExplorerConfig => { + const setting = config.get('testExplorer'); + if (!setting) { + return DefaultTestExplorerSetting; + } + + if (isTestExplorerConfigLegacy(setting)) { + if (setting.enabled === false || setting.showClassicStatus === true) { + const message = `Invalid TestExplorer setting: please check README to upgrade. Will use the default setting instead`; + console.error(message); + vscode.window.showWarningMessage(message); + return DefaultTestExplorerSetting; + } + return { showInlineError: setting.showInlineError }; + } + + return setting; +}; export const getExtensionResourceSettings = (uri: vscode.Uri): PluginResourceSettings => { const config = vscode.workspace.getConfiguration('jest', uri); @@ -178,7 +206,6 @@ export const getExtensionResourceSettings = (uri: vscode.Uri): PluginResourceSet const runAllTestsFirst = config.get('runAllTestsFirst') ?? undefined; return { - showTerminalOnLaunch: config.get('showTerminalOnLaunch') ?? true, autoEnable, enableSnapshotUpdateMessages: config.get('enableSnapshotUpdateMessages'), pathToConfig: config.get('pathToConfig'), @@ -194,7 +221,7 @@ export const getExtensionResourceSettings = (uri: vscode.Uri): PluginResourceSet coverageFormatter: config.get('coverageFormatter')!, debugMode: config.get('debugMode'), coverageColors: config.get('coverageColors'), - testExplorer: config.get('testExplorer') ?? { enabled: true }, + testExplorer: getTestExplorer(config), nodeEnv: config.get('nodeEnv') ?? undefined, shell: getShell(config) ?? undefined, monitorLongRun: config.get('monitorLongRun') ?? undefined, diff --git a/src/JestExt/output-terminal.ts b/src/JestExt/output-terminal.ts new file mode 100644 index 00000000..a25494fc --- /dev/null +++ b/src/JestExt/output-terminal.ts @@ -0,0 +1,144 @@ +import * as vscode from 'vscode'; +import { ExtErrorDef } from '../errors'; + +/** + * This class write out Jest run output to vscode.Terminal + */ + +export interface JestExtOutput { + write: (msg: string, opt?: OutputOptions) => string; +} + +/** termerinal per workspace */ +export class JestOutputTerminal implements JestExtOutput { + private name; + private pendingMessages: string[]; + private ptyIsOpen: boolean; + private writeEmitter = new vscode.EventEmitter(); + private pty: vscode.Pseudoterminal = { + onDidWrite: this.writeEmitter.event, + open: () => { + this.writeEmitter.fire(`${this.name}: Test Run Output \r\n`); + if (this.pendingMessages.length > 0) { + this.writeEmitter.fire(this.pendingMessages.join('')); + this.pendingMessages = []; + } + this.ptyIsOpen = true; + }, + close: () => { + this._terminal?.dispose(); + this._terminal = undefined; + }, + }; + private _terminal?: vscode.Terminal; + constructor(workspaceName: string) { + this.name = `Jest (${workspaceName})`; + vscode.window.terminals.forEach((t) => { + if (t.name === this.name) { + t.dispose(); + } + }); + this.ptyIsOpen = false; + this.pendingMessages = []; + } + + /** delay creating terminal until we are actually running the tests */ + private createTerminalIfNeeded() { + if (this._terminal) { + return; + } + this._terminal = vscode.window.createTerminal({ + name: this.name, + iconPath: new vscode.ThemeIcon('beaker'), + isTransient: true, + pty: this.pty, + }); + this.ptyIsOpen = false; + } + private appendRaw(text: string): void { + //ensure terminal is created + this.createTerminalIfNeeded(); + + if (this.ptyIsOpen) { + this.writeEmitter.fire(text); + } else { + this.pendingMessages.push(text); + } + } + + write(msg: string, opt?: OutputOptions): string { + const text = toAnsi(msg, opt); + this.appendRaw(text); + + if (isErrorOutputType(opt)) { + this.show(); + } + return text; + } + show(): void { + this._terminal?.show(true); + } + dispose(): void { + this.writeEmitter.dispose(); + this._terminal?.dispose(); + } +} + +export const AnsiSeq = { + error: '\x1b[0;31m', + success: '\x1b[0;32m', + warn: '\x1b[0;33m', + info: '\x1b[0;34m', + bold: '\x1b[1m', + end: '\x1b[0m', + lf: '\r\n', +}; + +export type OutputOptionShort = 'error' | 'warn' | 'new-line' | 'bold'; +export type OutputOptions = + | Array + | OutputOptionShort + | ExtErrorDef; + +const isErrorOutputType = (options?: OutputOptions): boolean => { + if (!options) { + return false; + } + if (Array.isArray(options)) { + return options.some((opt) => isErrorOutputType(opt) === true); + } + if (typeof options === 'string') { + return options === 'error'; + } + return options.type === 'error'; +}; +/** convert string to ansi-coded string based on the options */ +const applyAnsiSeq = (text: string, opt: OutputOptionShort): string => { + switch (opt) { + case 'error': + case 'warn': + return `${AnsiSeq.lf}${AnsiSeq[opt]}[${opt}] ${text}${AnsiSeq.end}${AnsiSeq.lf}`; + case 'bold': + return `${AnsiSeq[opt]}${text}${AnsiSeq.end}`; + case 'new-line': + return `${AnsiSeq.lf}${text}${AnsiSeq.lf}`; + } +}; + +export const toAnsi = (msg: string, options?: OutputOptions): string => { + let text = msg.replace(/\n/g, '\r\n'); + if (!options) { + return text; + } + if (Array.isArray(options)) { + return options.reduce((t, opt) => toAnsi(t, opt), msg); + } + + if (typeof options === 'string') { + text = applyAnsiSeq(text, options); + } else { + text = applyAnsiSeq(text, options.type); + text = `${text}${AnsiSeq.lf}${AnsiSeq.info}[info]${AnsiSeq.end} ${options.desc}, please see: ${options.helpLink}${AnsiSeq.lf}`; + } + return text; +}; diff --git a/src/JestExt/process-listeners.ts b/src/JestExt/process-listeners.ts index f1d02f57..5bbda5e9 100644 --- a/src/JestExt/process-listeners.ts +++ b/src/JestExt/process-listeners.ts @@ -1,9 +1,8 @@ import * as vscode from 'vscode'; import { JestTotalResults } from 'jest-editor-support'; -import { cleanAnsi } from '../helpers'; +import { cleanAnsi, toErrorString } from '../helpers'; import { JestProcess, JestProcessEvent } from '../JestProcessManagement'; import { ListenerSession, ListTestFilesCallback } from './process-session'; -import { isWatchRequest } from './helper'; import { Logging } from '../logging'; import { JestRunEvent } from './types'; import { MonitorLongRun } from '../Settings'; @@ -67,7 +66,7 @@ export class AbstractProcessListener { } protected onProcessStarting(process: JestProcess): void { - this.session.context.onRunEvent.fire({ type: 'start', process }); + this.session.context.onRunEvent.fire({ type: 'process-start', process }); this.logging('debug', `${process.request.type} onProcessStarting`); } protected onExecutableStdErr(process: JestProcess, data: string, _raw: string): void { @@ -106,7 +105,8 @@ export class ListTestFileListener extends AbstractProcessListener { return 'ListTestFileListener'; } private buffer = ''; - private error: string | undefined; + private stderrOutput = ''; + private exitCode?: number; private onResult: ListTestFilesCallback; constructor(session: ListenerSession, onResult: ListTestFilesCallback) { @@ -117,18 +117,21 @@ export class ListTestFileListener extends AbstractProcessListener { protected onExecutableOutput(_process: JestProcess, data: string): void { this.buffer += data; } + protected onExecutableStdErr(process: JestProcess, message: string, raw: string): void { + super.onExecutableStdErr(process, message, raw); + this.stderrOutput += raw; + } + protected onProcessExit(process: JestProcess, code?: number, signal?: string): void { // Note: will not fire 'exit' event, as the output is reported via the onResult() callback super.onProcessExit(process, code, signal); - if (super.isProcessError(code)) { - this.error = `${process.request.type} onProcessExit: process exit with code=${code}, signal=${signal}`; - } + this.exitCode = code; } protected onProcessClose(process: JestProcess): void { super.onProcessClose(process); - if (this.error) { - return this.onResult(undefined, this.error); + if (this.exitCode !== 0) { + return this.onResult(undefined, this.stderrOutput, this.exitCode); } try { @@ -148,7 +151,7 @@ export class ListTestFileListener extends AbstractProcessListener { return this.onResult(uriFiles); } catch (e) { this.logging('warn', 'failed to parse result:', this.buffer, 'error=', e); - this.onResult(undefined, e); + this.onResult(undefined, toErrorString(e), this.exitCode); } } } @@ -160,6 +163,7 @@ const WATCH_IS_NOT_SUPPORTED_REGEXP = /^s*--watch is not supported without git\/hg, please use --watchAlls*/im; const RUN_EXEC_ERROR = /onRunComplete: execError: (.*)/im; const RUN_START_TEST_SUITES_REGEX = /onRunStart: numTotalTestSuites: ((\d)+)/im; +const CONTROL_MESSAGES = /^(onRunStart|onRunComplete|Test results written to)[^\n]+\n/gim; /** * monitor for long test run, default is 1 minute @@ -207,6 +211,7 @@ export class RunTestListener extends AbstractProcessListener { // fire long-run warning once per run private longRunMonitor: LongRunMonitor; private runInfo: RunInfo | undefined; + private exitCode?: number; constructor(session: ListenerSession) { super(session); @@ -242,12 +247,10 @@ export class RunTestListener extends AbstractProcessListener { //=== private methods === private shouldIgnoreOutput(text: string): boolean { // this fails when snapshots change - to be revised - returning always false for now - return ( - text.length <= 0 || - text.includes('Watch Usage') || - text.includes('onRunComplete') || - text.includes('onRunStart') - ); + return text.length <= 0 || text.includes('Watch Usage'); + } + private cleanupOutput(text: string): string { + return text.replace(CONTROL_MESSAGES, ''); } // if snapshot error, offer update snapshot option and execute if user confirms @@ -262,14 +265,17 @@ export class RunTestListener extends AbstractProcessListener { this.session.context.settings.enableSnapshotUpdateMessages && SnapshotFailRegex.test(data) ) { - const scope = - process.request.type === 'by-file' - ? `for file "${process.request.testFileName}"` - : `for all files in "${this.session.context.workspace.name}"`; + const msg = + process.request.type === 'watch-all-tests' || process.request.type === 'watch-tests' + ? 'all files' + : 'files in this run'; vscode.window - .showInformationMessage(`Would you like to update snapshots ${scope}?`, { - title: 'Replace them', - }) + .showInformationMessage( + `[${this.session.context.workspace.name}] Would you like to update snapshots for ${msg}?`, + { + title: 'Replace them', + } + ) .then((response) => { // No response == cancel if (response) { @@ -327,7 +333,12 @@ export class RunTestListener extends AbstractProcessListener { return; } - this.onRunEvent.fire({ type: 'data', process, text: message, raw }); + const cleaned = this.cleanupOutput(raw); + this.handleRunStart(process, message); + + this.onRunEvent.fire({ type: 'data', process, text: message, raw: cleaned }); + + this.handleRunComplete(process, message); this.handleSnapshotTestFailuer(process, message); @@ -343,14 +354,14 @@ export class RunTestListener extends AbstractProcessListener { } } } - protected onExecutableOutput(process: JestProcess, output: string, raw: string): void { + protected handleRunStart(process: JestProcess, output: string): void { if (output.includes('onRunStart')) { this.runStarted({ process, numTotalTestSuites: this.getNumTotalTestSuites(output) }); - if (isWatchRequest(process.request)) { - this.onRunEvent.fire({ type: 'start', process }); - } + this.onRunEvent.fire({ type: 'start', process }); } + } + protected handleRunComplete(process: JestProcess, output: string): void { if (output.includes('onRunComplete')) { this.runEnded(); @@ -365,11 +376,10 @@ export class RunTestListener extends AbstractProcessListener { isError: true, }); } - if (isWatchRequest(process.request)) { - this.onRunEvent.fire({ type: 'end', process }); - } + this.onRunEvent.fire({ type: 'end', process }); } - + } + protected onExecutableOutput(process: JestProcess, output: string, raw: string): void { if (!this.shouldIgnoreOutput(output)) { this.onRunEvent.fire({ type: 'data', process, text: output, raw }); } @@ -381,11 +391,12 @@ export class RunTestListener extends AbstractProcessListener { protected onProcessExit(process: JestProcess, code?: number, signal?: string): void { this.runEnded(); super.onProcessExit(process, code, signal); + this.exitCode = code; } protected onProcessClose(process: JestProcess): void { super.onProcessClose(process); const error = this.handleWatchProcessCrash(process); - this.onRunEvent.fire({ type: 'exit', process, error }); + this.onRunEvent.fire({ type: 'exit', process, error, code: this.exitCode }); } } diff --git a/src/JestExt/process-session.ts b/src/JestExt/process-session.ts index 353ed8f1..7557c790 100644 --- a/src/JestExt/process-session.ts +++ b/src/JestExt/process-session.ts @@ -13,7 +13,11 @@ import { RunTestListener, ListTestFileListener } from './process-listeners'; import { JestExtProcessContext } from './types'; type InternalProcessType = 'list-test-files' | 'update-snapshot'; -export type ListTestFilesCallback = (fileNames?: string[], error?: any) => void; +export type ListTestFilesCallback = ( + fileNames?: string[], + error?: string, + exitCode?: number +) => void; export type InternalRequestBase = | { type: Extract; @@ -106,7 +110,13 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes return process; } catch (e) { - logging('warn', '[scheduleProcess] failed to create/schedule process for ', request); + logging( + 'warn', + '[scheduleProcess] failed to create/schedule process for ', + request, + 'error:', + e + ); return; } }; diff --git a/src/JestExt/types.ts b/src/JestExt/types.ts index f2b1c865..bb37d116 100644 --- a/src/JestExt/types.ts +++ b/src/JestExt/types.ts @@ -12,6 +12,7 @@ import { AutoRunMode } from '../StatusBar'; import { ProcessSession } from './process-session'; import { DebugTestIdentifier } from '../DebugCodeLens'; import { JestProcessInfo } from '../JestProcessManagement'; +import { JestOutputTerminal } from './output-terminal'; export enum WatchMode { None = 'none', @@ -36,6 +37,7 @@ export interface JestExtContext { loggingFactory: LoggingFactory; autoRun: AutoRunAccessor; createRunnerWorkspace: (options?: RunnerWorkspaceOptions) => ProjectWorkspace; + output: JestOutputTerminal; } export interface JestExtSessionContext extends JestExtContext { @@ -48,9 +50,10 @@ export type JestRunEvent = RunEventBase & ( | { type: 'scheduled' } | { type: 'data'; text: string; raw?: string; newLine?: boolean; isError?: boolean } + | { type: 'process-start' } | { type: 'start' } | { type: 'end' } - | { type: 'exit'; error?: string } + | { type: 'exit'; error?: string; code?: number } | { type: 'long-run'; threshold: number; numTotalTestSuites?: number } ); export interface JestSessionEvents { diff --git a/src/JestProcessManagement/helper.ts b/src/JestProcessManagement/helper.ts index 1fefd251..5c85befe 100644 --- a/src/JestProcessManagement/helper.ts +++ b/src/JestProcessManagement/helper.ts @@ -46,9 +46,10 @@ export const isDup = (task: Task, request: JestProcessRequest): boo return true; }; +const skipAttrs = ['listener', 'run']; export const requestString = (request: JestProcessRequest): string => { const replacer = (key: string, value: unknown) => { - if (key === 'listener') { + if (skipAttrs.includes(key)) { return typeof value; } return value; diff --git a/src/Settings/index.ts b/src/Settings/index.ts index d7c2d6a1..a2cd0fb9 100644 --- a/src/Settings/index.ts +++ b/src/Settings/index.ts @@ -25,13 +25,17 @@ export type JestExtAutoRunConfig = }; export type JestExtAutoRunSetting = JestExtAutoRunShortHand | JestExtAutoRunConfig; -export type TestExplorerConfig = +export type TestExplorerConfigLegacy = | { enabled: false } | { enabled: true; showClassicStatus?: boolean; showInlineError?: boolean }; + +export interface TestExplorerConfig { + showInlineError?: boolean; +} + export type NodeEnv = ProjectWorkspace['nodeEnv']; export type MonitorLongRun = 'off' | number; export interface PluginResourceSettings { - showTerminalOnLaunch?: boolean; autoEnable?: boolean; enableSnapshotUpdateMessages?: boolean; jestCommandLine?: string; diff --git a/src/StatusBar.ts b/src/StatusBar.ts index 5780a53c..82fd49bd 100644 --- a/src/StatusBar.ts +++ b/src/StatusBar.ts @@ -101,7 +101,7 @@ export class StatusBar { if (this.activeFolder) { const ext = getExtension(this.activeFolder); if (ext) { - ext.channel.show(); + ext.showOutput(); } } }), diff --git a/src/decorations/question.svg b/src/decorations/question.svg deleted file mode 100644 index ddc64b84..00000000 --- a/src/decorations/question.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/src/decorations/test-status.ts b/src/decorations/test-status.ts deleted file mode 100644 index 33ca1b5f..00000000 --- a/src/decorations/test-status.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { - window, - OverviewRulerLane, - DecorationRangeBehavior, - ExtensionContext, - TextEditorDecorationType, - DecorationRenderOptions, -} from 'vscode'; -import passingIcon from 'vscode-codicons/src/icons/check.svg'; -import failingIcon from 'vscode-codicons/src/icons/chrome-close.svg'; -import skipIcon from 'vscode-codicons/src/icons/debug-step-over.svg'; -import unknownIcon from './question.svg'; -import { prepareIconFile } from '../helpers'; - -export class TestStatus { - public passing: TextEditorDecorationType; - public failing: TextEditorDecorationType; - public skip: TextEditorDecorationType; - public unknown: TextEditorDecorationType; - - constructor(context: ExtensionContext) { - this.passing = this.createStateDecoration([ - prepareIconFile(context, 'passing', passingIcon, '#35A15E'), - 'green', - ]); - this.failing = this.createStateDecoration([ - prepareIconFile(context, 'failing', failingIcon, '#D6443C'), - 'red', - ]); - this.skip = this.createStateDecoration([ - prepareIconFile(context, 'skip', skipIcon, '#fed37f'), - 'yellow', - ]); - this.unknown = this.createStateDecoration( - [prepareIconFile(context, 'unknown', unknownIcon, '#BBBBBB'), 'darkgrey'], - [prepareIconFile(context, 'unknown-light', unknownIcon, '#555555')] - ); - } - - private createStateDecoration( - dark: /* default */ [string, string?], - light?: /* optional overrides */ [string, string?] - ): TextEditorDecorationType { - const [icon, overviewRulerColor] = dark; - const [iconLite, overviewRulerColorLite] = light ?? []; - - const options: DecorationRenderOptions = { - gutterIconPath: icon, - gutterIconSize: 'contain', - overviewRulerLane: OverviewRulerLane.Left, - rangeBehavior: DecorationRangeBehavior.ClosedClosed, - dark: { - gutterIconPath: icon, - }, - light: { - gutterIconPath: iconLite || icon, - }, - }; - - if (overviewRulerColor) { - options['overviewRulerColor'] = overviewRulerColor; - if (options['dark']) { - options['dark']['overviewRulerColor'] = overviewRulerColor; - } - } - - if (overviewRulerColorLite) { - if (options['light']) { - options['light']['overviewRulerColor'] = overviewRulerColorLite; - } - } - - return window.createTextEditorDecorationType(options); - } -} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 00000000..3804ee96 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,32 @@ +export interface ExtErrorDef { + code: number; + type: 'error' | 'warn'; + desc: string; + helpLink: string; +} + +const BASE_URL = 'https://github.com/jest-community/vscode-jest/blob/master/README.md'; +export const GENERIC_ERROR: ExtErrorDef = { + code: 1, + type: 'error', + desc: 'jest test run failed', + helpLink: `${BASE_URL}#troubleshooting`, +}; +export const CMD_NOT_FOUND: ExtErrorDef = { + code: 2, + type: 'error', + desc: 'jest process failed to start, most likely due to env or project configuration issues', + helpLink: `${BASE_URL}#jest-failed-to-run`, +}; +export const LONG_RUNNING_TESTS: ExtErrorDef = { + code: 3, + type: 'warn', + desc: 'jest test run exceed the configured threshold ("jest.monitorLongRun") ', + helpLink: `${BASE_URL}#what-to-do-with-long-running-tests-warning`, +}; + +export const getExitErrorDef = (exitCode?: number): ExtErrorDef | undefined => { + if (exitCode === 127) { + return CMD_NOT_FOUND; + } +}; diff --git a/src/helpers.ts b/src/helpers.ts index 2bd6da92..dc992ff6 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -270,3 +270,17 @@ export const shellQuote = (str: string, shell?: string | LoginShell): string => } } }; + +export const toErrorString = (e: unknown): string => { + if (e == null) { + return ''; + } + if (typeof e === 'string') { + return e; + } + if (e instanceof Error) { + // return `${e.toString()}\r\n${e.stack}`; + return e.stack ?? e.toString(); + } + return JSON.stringify(e); +}; diff --git a/src/reporter.ts b/src/reporter.ts index 2575cb6a..3a86c60d 100644 --- a/src/reporter.ts +++ b/src/reporter.ts @@ -3,21 +3,25 @@ import type { AggregatedResult } from '@jest/test-result'; import { Reporter, Context } from '@jest/reporters'; -export default class VSCodeJestReporter implements Reporter { +class VSCodeJestReporter implements Reporter { onRunStart(aggregatedResults: AggregatedResult): void { - console.log(`onRunStart: numTotalTestSuites: ${aggregatedResults.numTotalTestSuites}`); + process.stderr.write( + `onRunStart: numTotalTestSuites: ${aggregatedResults.numTotalTestSuites}\r\n` + ); } onRunComplete(_contexts: Set, results: AggregatedResult): void { // report exec errors that could have prevented Result file being generated // TODO: replace with checking results.runExecError after Jest release https://github.com/facebook/jest/pull/13203 if (results.numTotalTests === 0 && results.numTotalTestSuites > 0) { - console.log('onRunComplete: execError: no test is run'); + process.stderr.write('onRunComplete: execError: no test is run\r\n'); } else { - console.log('onRunComplete'); + process.stderr.write('onRunComplete\r\n'); } } getLastError(): Error | undefined { return; } } + +module.exports = VSCodeJestReporter; diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index bb538522..93e4ae50 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -8,9 +8,10 @@ import { ItBlock, TestAssertionStatus } from 'jest-editor-support'; import { ContainerNode, DataNode, NodeType, ROOT_NODE_NAME } from '../TestResults/match-node'; import { Logging } from '../logging'; import { TestSuitChangeEvent } from '../TestResults/test-result-events'; -import { Debuggable, TestItemData, TestItemRun } from './types'; -import { JestTestProviderContext } from './test-provider-context'; +import { Debuggable, TestItemData } from './types'; +import { JestTestProviderContext, JestTestRun, JestTestRunOptions } from './test-provider-helper'; import { JestProcessInfo, JestProcessRequest } from '../JestProcessManagement'; +import { GENERIC_ERROR, getExitErrorDef, LONG_RUNNING_TESTS } from '../errors'; interface JestRunable { getJestRunRequest: () => JestExtRequestType; @@ -19,16 +20,12 @@ interface WithUri { uri: vscode.Uri; } -type TestItemRunRequest = JestExtRequestType & { itemRun: TestItemRun }; +type JestTestRunRequest = JestExtRequestType & { run: JestTestRun }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -const isTestItemRunRequest = (arg: any): arg is TestItemRunRequest => - arg.itemRun?.item && arg.itemRun?.run && arg.itemRun?.end; +const isJestTestRunRequest = (arg: any): arg is JestTestRunRequest => + arg.run instanceof JestTestRun; -const deepItemState = (item: vscode.TestItem, setState: (item: vscode.TestItem) => void): void => { - setState(item); - item.children.forEach((child) => deepItemState(child, setState)); -}; abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { item!: vscode.TestItem; log: Logging; @@ -41,20 +38,33 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { return this.item.uri!; } - scheduleTest(run: vscode.TestRun, end: () => void): void { + deepItemState( + item: vscode.TestItem | undefined, + setState: (item: vscode.TestItem) => void + ): void { + if (!item) { + this.log('warn', ': no item to set state'); + return; + } + setState(item); + item.children.forEach((child) => this.deepItemState(child, setState)); + } + + scheduleTest(run: JestTestRun): void { const jestRequest = this.getJestRunRequest(); - const itemRun: TestItemRun = { item: this.item, run, end }; - deepItemState(this.item, run.enqueued); + run.item = this.item; + + this.deepItemState(this.item, run.vscodeRun.enqueued); const process = this.context.ext.session.scheduleProcess({ ...jestRequest, - itemRun, + run, }); if (!process) { const msg = `failed to schedule test for ${this.item.id}`; - run.errored(this.item, new vscode.TestMessage(msg)); - this.context.appendOutput(msg, run, true, 'red'); - end(); + run.vscodeRun.errored(this.item, new vscode.TestMessage(msg)); + run.write(msg, 'error'); + run.end(); } } @@ -68,7 +78,7 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { export class WorkspaceRoot extends TestItemDataBase { private testDocuments: Map; private listeners: vscode.Disposable[]; - private cachedRun: Map; + private cachedRun: Map; constructor(context: JestTestProviderContext) { super(context, 'WorkspaceRoot'); @@ -101,7 +111,7 @@ export class WorkspaceRoot extends TestItemDataBase { }; return { type: 'all-tests', transform }; } - discoverTest(run: vscode.TestRun): void { + discoverTest(run: JestTestRun): void { const testList = this.context.ext.testResolveProvider.getTestList(); // only trigger update when testList is not empty because it's possible test-list is not available yet, // in such case we should just wait for the testListUpdated event to trigger the update @@ -126,9 +136,13 @@ export class WorkspaceRoot extends TestItemDataBase { this.listeners.length = 0; }; - private createRun = (name?: string, item?: vscode.TestItem): vscode.TestRun => { - const target = item ?? this.item; - return this.context.createTestRun(new vscode.TestRunRequest([target]), name ?? target.id); + private createRun = (options?: JestTestRunOptions): JestTestRun => { + const target = options?.item ?? this.item; + return this.context.createTestRun(new vscode.TestRunRequest([target]), { + ...options, + name: options?.name ?? target.id, + item: target, + }); }; private addFolder = (parent: FolderData | undefined, folderName: string): FolderData => { @@ -181,27 +195,22 @@ export class WorkspaceRoot extends TestItemDataBase { */ private onTestListUpdated = ( absoluteFileNames: string[] | undefined, - run?: vscode.TestRun + run?: JestTestRun ): void => { this.item.children.replace([]); const testRoots: TestDocumentRoot[] = []; const aRun = run ?? this.createRun(); - try { - absoluteFileNames?.forEach((f) => - this.addTestFile(f, (testRoot) => { - testRoot.updateResultState(aRun); - testRoots.push(testRoot); - }) - ); - //sync testDocuments - this.testDocuments.clear(); - testRoots.forEach((t) => this.testDocuments.set(t.item.id, t)); - } catch (e) { - this.log('error', `[WorkspaceRoot] "${this.item.id}" onTestListUpdated failed:`, e); - } finally { - aRun.end(); - } + absoluteFileNames?.forEach((f) => + this.addTestFile(f, (testRoot) => { + testRoot.updateResultState(aRun); + testRoots.push(testRoot); + }) + ); + //sync testDocuments + this.testDocuments.clear(); + testRoots.forEach((t) => this.testDocuments.set(t.item.id, t)); + aRun.end(); this.item.canResolveChildren = false; }; @@ -215,20 +224,14 @@ export class WorkspaceRoot extends TestItemDataBase { private onTestSuiteChanged = (event: TestSuitChangeEvent): void => { switch (event.type) { case 'assertions-updated': { - const itemRun = this.getItemRun(event.process); - const run = itemRun?.run ?? this.createRun(event.process.id); + const run = this.getJestRun(event.process) ?? this.createRun({ name: event.process.id }); - this.context.appendOutput( - `update status from run "${event.process.id}": ${event.files.length} files`, - run + this.log( + 'debug', + `update status from run "${event.process.id}": ${event.files.length} files` ); - try { - event.files.forEach((f) => this.addTestFile(f, (testRoot) => testRoot.discoverTest(run))); - } catch (e) { - this.log('error', `"${this.item.id}" onTestSuiteChanged: assertions-updated failed:`, e); - } finally { - (itemRun ?? run).end(); - } + event.files.forEach((f) => this.addTestFile(f, (testRoot) => testRoot.discoverTest(run))); + run.end(); break; } case 'result-matched': { @@ -264,71 +267,89 @@ export class WorkspaceRoot extends TestItemDataBase { return this.testDocuments.get(fileName)?.item; }; - private createTestItemRun = (event: JestRunEvent): TestItemRun => { + private createJestTestRun = (event: JestRunEvent): JestTestRun => { const item = this.getItemFromProcess(event.process) ?? this.item; - const run = this.createRun(`${event.type}:${event.process.id}`, item); - const end = () => { - this.cachedRun.delete(event.process.id); - run.end(); - }; - const itemRun: TestItemRun = { item, run, end }; - this.cachedRun.set(event.process.id, itemRun); - return itemRun; - }; - private getItemRun = (process: JestProcessInfo): TestItemRun | undefined => - isTestItemRunRequest(process.request) - ? process.request.itemRun - : this.cachedRun.get(process.id); + const run = this.createRun({ + name: `${event.type}:${event.process.id}`, + item, + onEnd: () => this.cachedRun.delete(event.process.id), + }); + this.cachedRun.set(event.process.id, run); + return run; + }; + private getJestRun = (process: JestProcessInfo): JestTestRun | undefined => + isJestTestRunRequest(process.request) ? process.request.run : this.cachedRun.get(process.id); + + private runLog(type: string): void { + const d = new Date(); + this.context.output.write(`> Test run ${type} at ${d.toLocaleString()} <\r\n`, [ + 'bold', + 'new-line', + ]); + } private onRunEvent = (event: JestRunEvent) => { if (event.process.request.type === 'not-test') { return; } - let itemRun = this.getItemRun(event.process); + let run = this.getJestRun(event.process); try { switch (event.type) { case 'scheduled': { - if (!itemRun) { - itemRun = this.createTestItemRun(event); - const text = `Scheduled test run "${event.process.id}" for "${itemRun.item.id}"`; - this.context.appendOutput(text, itemRun.run); - deepItemState(itemRun.item, itemRun.run.enqueued); + if (!run) { + run = this.createJestTestRun(event); + this.deepItemState(run.item, run.vscodeRun.enqueued); } break; } case 'data': { - itemRun = itemRun ?? this.createTestItemRun(event); + run = run ?? this.createJestTestRun(event); const text = event.raw ?? event.text; - const color = event.isError ? 'red' : undefined; - this.context.appendOutput(text, itemRun.run, event.newLine ?? false, color); + const opt = event.isError ? 'error' : event.newLine ? 'new-line' : undefined; + run.write(text, opt); break; } case 'start': { - itemRun = itemRun ?? this.createTestItemRun(event); - deepItemState(itemRun.item, itemRun.run.started); + run = run ?? this.createJestTestRun(event); + this.deepItemState(run.item, run.vscodeRun.started); + this.runLog('started'); break; } case 'end': { - itemRun?.end(); + this.runLog('finished'); + run?.end(); break; } case 'exit': { if (event.error) { - if (!itemRun || itemRun.run.token.isCancellationRequested) { - itemRun = this.createTestItemRun(event); + if (!run || run.vscodeRun.token.isCancellationRequested) { + run = this.createJestTestRun(event); + } + const type = getExitErrorDef(event.code) ?? GENERIC_ERROR; + run.write(event.error, type); + if (run.item) { + run.vscodeRun.errored(run.item, new vscode.TestMessage(event.error)); } - this.context.appendOutput(event.error, itemRun.run, true, 'red'); - itemRun.run.errored(itemRun.item, new vscode.TestMessage(event.error)); } - itemRun?.end(); + this.runLog('exited'); + run?.end(); + break; + } + case 'long-run': { + const output = run ?? this.context.output; + output.write( + `Long Running Tests Warning: Tests exceeds ${event.threshold}ms threshold. Please reference Troubleshooting if this is not expected`, + LONG_RUNNING_TESTS + ); break; } } } catch (err) { this.log('error', ` ${event.type} failed:`, err); + this.context.output.write(` ${event.type} failed: ${err}`, 'error'); } }; @@ -383,7 +404,7 @@ abstract class TestResultData extends TestItemDataBase { } updateItemState( - run: vscode.TestRun, + run: JestTestRun, result?: TestSuiteResult | TestAssertionStatus, errorLocation?: vscode.Location ): void { @@ -393,25 +414,22 @@ abstract class TestResultData extends TestItemDataBase { const status = result.status; switch (status) { case 'KnownSuccess': - run.passed(this.item); + run.vscodeRun.passed(this.item); break; case 'KnownSkip': case 'KnownTodo': - run.skipped(this.item); + run.vscodeRun.skipped(this.item); break; case 'KnownFail': { - if ( - this.context.ext.settings.testExplorer.enabled && - this.context.ext.settings.testExplorer.showInlineError - ) { + if (this.context.ext.settings.testExplorer.showInlineError) { const message = new vscode.TestMessage(result.message); if (errorLocation) { message.location = errorLocation; } - run.failed(this.item, message); + run.vscodeRun.failed(this.item, message); } else { - run.failed(this.item, []); + run.vscodeRun.failed(this.item, []); } break; } @@ -506,7 +524,7 @@ export class TestDocumentRoot extends TestResultData { return item; } - discoverTest = (run?: vscode.TestRun, parsedRoot?: ContainerNode): void => { + discoverTest = (run?: JestTestRun, parsedRoot?: ContainerNode): void => { this.createChildItems(parsedRoot); if (run) { this.updateResultState(run); @@ -514,23 +532,19 @@ export class TestDocumentRoot extends TestResultData { }; private createChildItems = (parsedRoot?: ContainerNode): void => { - try { - const container = - parsedRoot ?? - this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id)?.assertionContainer; - if (!container) { - this.item.children.replace([]); - } else { - this.syncChildNodes(container); - } - } catch (e) { - this.log('error', `[TestDocumentRoot] "${this.item.id}" createChildItems failed:`, e); - } finally { - this.item.canResolveChildren = false; + const container = + parsedRoot ?? + this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id)?.assertionContainer; + if (!container) { + this.item.children.replace([]); + } else { + this.syncChildNodes(container); } + + this.item.canResolveChildren = false; }; - public updateResultState(run: vscode.TestRun): void { + public updateResultState(run: JestTestRun): void { const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id); this.updateItemState(run, suiteResult); @@ -621,7 +635,7 @@ export class TestData extends TestResultData implements Debuggable { ); } - public updateResultState(run: vscode.TestRun): void { + public updateResultState(run: JestTestRun): void { if (this.node && isAssertDataNode(this.node)) { const assertion = this.node.data; const errorLine = diff --git a/src/test-provider/test-provider-context.ts b/src/test-provider/test-provider-helper.ts similarity index 63% rename from src/test-provider/test-provider-context.ts rename to src/test-provider/test-provider-helper.ts index 3273e71c..4437ed33 100644 --- a/src/test-provider/test-provider-context.ts +++ b/src/test-provider/test-provider-helper.ts @@ -1,11 +1,7 @@ import * as vscode from 'vscode'; +import { JestExtOutput, JestOutputTerminal, OutputOptions } from '../JestExt/output-terminal'; import { JestExtExplorerContext, TestItemData } from './types'; -let showTerminal = true; -export const _resetShowTerminal = (): void => { - showTerminal = true; -}; - /** * provide context information from JestExt and test provider state: * 1. TestData <-> TestItem @@ -13,14 +9,6 @@ export const _resetShowTerminal = (): void => { * as well as factory functions to create TestItem and TestRun that could impact the state */ -// output color support -export type OUTPUT_COLOR = 'red' | 'green' | 'yellow'; -const COLORS = { - ['red']: '\x1b[0;31m', - ['green']: '\x1b[0;32m', - ['yellow']: '\x1b[0;33m', - ['end']: '\x1b[0m', -}; export type TagIdType = 'run' | 'debug'; export class JestTestProviderContext { @@ -33,6 +21,10 @@ export class JestTestProviderContext { ) { this.testItemData = new WeakMap(); } + get output(): JestOutputTerminal { + return this.ext.output; + } + createTestItem = ( id: string, label: string, @@ -83,29 +75,49 @@ export class JestTestProviderContext { return this.testItemData.get(item) as T | undefined; }; - createTestRun = (request: vscode.TestRunRequest, name: string): vscode.TestRun => { - return this.controller.createTestRun(request, name); - }; - - appendOutput = (msg: string, run: vscode.TestRun, newLine = true, color?: OUTPUT_COLOR): void => { - const converted = msg.replace(/\n/g, '\r\n'); - let text = newLine ? `[${this.ext.workspace.name}]: ${converted}` : converted; - if (color) { - text = `${COLORS[color]}${text}${COLORS['end']}`; - } - run.appendOutput(`${text}${newLine ? '\r\n' : ''}`); - this.showTestExplorerTerminal(); - }; - - /** show TestExplorer Terminal on first invocation only */ - showTestExplorerTerminal = (): void => { - if (showTerminal && this.ext.settings.showTerminalOnLaunch !== false) { - showTerminal = false; - vscode.commands.executeCommand('testing.showMostRecentOutput'); - } + createTestRun = (request: vscode.TestRunRequest, options?: JestTestRunOptions): JestTestRun => { + const vscodeRun = this.controller.createTestRun(request, options?.name ?? 'unknown'); + return new JestTestRun(this, vscodeRun, options); }; // tags getTag = (tagId: TagIdType): vscode.TestTag | undefined => this.profiles.find((p) => p.tag?.id === tagId)?.tag; } + +export interface JestTestRunOptions { + name?: string; + item?: vscode.TestItem; + // in addition to the regular end() method + onEnd?: () => void; + // if true, when the run ends, we will not end the vscodeRun, this is used when multiple test items + // in a single request, that the run should be closed when all items are done. + disableVscodeRunEnd?: boolean; +} + +export class JestTestRun implements JestExtOutput { + private output: JestOutputTerminal; + public item?: vscode.TestItem; + + constructor( + context: JestTestProviderContext, + public vscodeRun: vscode.TestRun, + private options?: JestTestRunOptions + ) { + this.output = context.output; + this.item = options?.item; + } + + end(): void { + if (this.options?.disableVscodeRunEnd !== true) { + this.vscodeRun.end(); + } + this.options?.onEnd?.(); + } + + write(msg: string, opt?: OutputOptions): string { + const text = this.output.write(msg, opt); + this.vscodeRun.appendOutput(text); + return text; + } +} diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index 9d941b7c..b62e5194 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -1,9 +1,10 @@ import * as vscode from 'vscode'; -import { JestTestProviderContext } from './test-provider-context'; +import { JestTestProviderContext, JestTestRun } from './test-provider-helper'; import { WorkspaceRoot } from './test-item-data'; import { Debuggable, JestExtExplorerContext, TestItemData } from './types'; import { extensionId } from '../appGlobals'; import { Logging } from '../logging'; +import { toErrorString } from '../helpers'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const isDebuggable = (arg: any): arg is Debuggable => arg && typeof arg.getDebugInfo === 'function'; @@ -70,17 +71,15 @@ export class JestTestProvider { if (!theItem.canResolveChildren) { return; } - const run = this.context.createTestRun( - new vscode.TestRunRequest([theItem]), - `disoverTest: ${this.controller.id}` - ); + const run = this.context.createTestRun(new vscode.TestRunRequest([theItem]), { + name: `disoverTest: ${this.controller.id}`, + }); try { const data = this.context.getData(theItem); if (data && data.discoverTest) { - this.context.appendOutput(`resolving children for ${theItem.id}`, run, true); data.discoverTest(run); } else { - this.context.appendOutput(`no data found for item ${theItem.id}`, run, true, 'red'); + run.write(`no data found for item ${theItem.id}`, 'error'); } } catch (e) { this.log('error', `[JestTestProvider]: discoverTest error for "${theItem.id}" : `, e); @@ -100,12 +99,11 @@ export class JestTestProvider { * invoke JestExt debug function for the given data, handle unexpected exception and set item state accordingly. * should never throw or reject. */ - debugTest = async (tData: TestItemData, run: vscode.TestRun): Promise => { + debugTest = async (tData: TestItemData, run: JestTestRun): Promise => { let error; if (isDebuggable(tData)) { try { const debugInfo = tData.getDebugInfo(); - this.context.appendOutput(`launching debugger for ${tData.item.id}`, run); if (debugInfo.testNamePattern) { await this.context.ext.debugTests(debugInfo.fileName, debugInfo.testNamePattern); } else { @@ -117,8 +115,8 @@ export class JestTestProvider { } } error = error ?? `item ${tData.item.id} is not debuggable`; - run.errored(tData.item, new vscode.TestMessage(error)); - this.context.appendOutput(`${error}`, run, true, 'red'); + run.vscodeRun.errored(tData.item, new vscode.TestMessage(error)); + run.write(error, 'error'); return Promise.resolve(); }; @@ -130,7 +128,7 @@ export class JestTestProvider { this.log('error', 'not supporting runRequest without profile', request); return Promise.reject('cnot supporting runRequest without profile'); } - const run = this.context.createTestRun(request, this.controller.id); + const run = this.context.createTestRun(request, { name: this.controller.id }); const tests = (request.include ?? this.getAllItems()).filter( (t) => !request.exclude?.includes(t) ); @@ -140,24 +138,26 @@ export class JestTestProvider { for (const test of tests) { const tData = this.context.getData(test); if (!tData || cancelToken.isCancellationRequested) { - run.skipped(test); + run.vscodeRun.skipped(test); continue; } - this.context.appendOutput( - `executing profile: "${request.profile.label}" for ${test.id}...`, - run - ); + this.log('debug', `executing profile: "${request.profile.label}" for ${test.id}...`); if (request.profile.kind === vscode.TestRunProfileKind.Debug) { await this.debugTest(tData, run); } else { promises.push( new Promise((resolve, reject) => { try { - tData.scheduleTest(run, resolve); + const itemRun = new JestTestRun(this.context, run.vscodeRun, { + item: test, + onEnd: resolve, + disableVscodeRunEnd: true, + }); + tData.scheduleTest(itemRun); } catch (e) { - const msg = `failed to schedule test for ${tData.item.id}: ${JSON.stringify(e)}`; + const msg = `failed to schedule test for ${tData.item.id}: ${toErrorString(e)}`; this.log('error', msg, e); - run.errored(test, new vscode.TestMessage(msg)); + run.vscodeRun.errored(test, new vscode.TestMessage(msg)); reject(msg); } }) @@ -166,7 +166,7 @@ export class JestTestProvider { } } catch (e) { const msg = `failed to execute profile "${request.profile.label}": ${JSON.stringify(e)}`; - this.context.appendOutput(msg, run, true, 'red'); + run.write(msg, 'error'); } await Promise.allSettled(promises); diff --git a/src/test-provider/types.ts b/src/test-provider/types.ts index 42d69d41..a51d196c 100644 --- a/src/test-provider/types.ts +++ b/src/test-provider/types.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { DebugFunction, JestSessionEvents, JestExtSessionContext } from '../JestExt'; import { TestResultProvider } from '../TestResults'; import { WorkspaceRoot, FolderData, TestData, TestDocumentRoot } from './test-item-data'; -import { JestTestProviderContext } from './test-provider-context'; +import { JestTestProviderContext, JestTestRun } from './test-provider-helper'; export type TestItemDataType = WorkspaceRoot | FolderData | TestDocumentRoot | TestData; @@ -13,19 +13,12 @@ export interface JestExtExplorerContext extends JestExtSessionContext { debugTests: DebugFunction; } -export interface TestItemRun { - item: vscode.TestItem; - run: vscode.TestRun; - end: () => void; -} - -export type RunType = vscode.TestRun | TestItemRun; export interface TestItemData { readonly item: vscode.TestItem; readonly uri: vscode.Uri; context: JestTestProviderContext; - discoverTest?: (run: vscode.TestRun) => void; - scheduleTest: (run: vscode.TestRun, end: () => void) => void; + discoverTest?: (run: JestTestRun) => void; + scheduleTest: (run: JestTestRun) => void; } export interface Debuggable { diff --git a/tests/JestExt/__snapshots__/outpt-terminal.test.ts.snap b/tests/JestExt/__snapshots__/outpt-terminal.test.ts.snap new file mode 100644 index 00000000..935637b8 --- /dev/null +++ b/tests/JestExt/__snapshots__/outpt-terminal.test.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JestOutputTerminal can write output with options can write with option: case 1 1`] = `"regular text"`; + +exports[`JestOutputTerminal can write output with options can write with option: case 2 1`] = ` +"text with newline + +" +`; + +exports[`JestOutputTerminal can write output with options can write with option: case 3 1`] = ` +" +[error] error text +" +`; + +exports[`JestOutputTerminal can write output with options can write with option: case 4 1`] = ` +" +[warn] warning text +" +`; + +exports[`JestOutputTerminal can write output with options can write with option: case 5 1`] = `"bold text"`; + +exports[`JestOutputTerminal can write output with options can write with option: case 6 1`] = ` +" +bold text with newLine +" +`; diff --git a/tests/JestExt/core.test.ts b/tests/JestExt/core.test.ts index 9c309855..b19cdd5d 100644 --- a/tests/JestExt/core.test.ts +++ b/tests/JestExt/core.test.ts @@ -2,6 +2,7 @@ jest.unmock('events'); jest.unmock('../../src/JestExt/core'); jest.unmock('../../src/JestExt/helper'); jest.unmock('../../src/appGlobals'); +jest.unmock('../../src/errors'); jest.unmock('../test-helper'); jest.mock('../../src/DebugCodeLens', () => ({ @@ -11,9 +12,6 @@ const mockPlatform = jest.fn(); const mockRelease = jest.fn(); mockRelease.mockReturnValue(''); jest.mock('os', () => ({ platform: mockPlatform, release: mockRelease })); -jest.mock('../../src/decorations/test-status', () => ({ - TestStatus: jest.fn(), -})); const sbUpdateMock = jest.fn(); const statusBar = { @@ -27,7 +25,6 @@ jest.mock('jest-editor-support'); import * as vscode from 'vscode'; import { JestExt } from '../../src/JestExt/core'; import { createProcessSession } from '../../src/JestExt/process-session'; -import { TestStatus } from '../../src/decorations/test-status'; import { updateCurrentDiagnostics, updateDiagnostics } from '../../src/diagnostics'; import { CoverageMapProvider } from '../../src/Coverage'; import * as helper from '../../src/helpers'; @@ -41,9 +38,16 @@ import { mockProjectWorkspace, mockWworkspaceLogging } from '../test-helper'; import { JestTestProvider } from '../../src/test-provider'; import { MessageAction } from '../../src/messaging'; import { addFolderToDisabledWorkspaceFolders } from '../../src/extensionManager'; +import { JestOutputTerminal } from '../../src/JestExt/output-terminal'; +import * as errors from '../../src/errors'; /* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectItTakesNoAction"] }] */ const mockHelpers = helper as jest.Mocked; +const mockOutputTerminal = { + write: jest.fn(), + show: jest.fn(), + dispose: jest.fn(), +}; const EmptySortedResult = { fail: [], @@ -55,16 +59,9 @@ const mockGetExtensionResourceSettings = jest.spyOn(extHelper, 'getExtensionReso describe('JestExt', () => { const getConfiguration = vscode.workspace.getConfiguration as jest.Mock; - const StateDecorationsMock = TestStatus as jest.Mock; const context: any = { asAbsolutePath: (text) => text } as vscode.ExtensionContext; const workspaceFolder = { name: 'test-folder' } as any; - const channelStub = { - appendLine: jest.fn(), - append: jest.fn(), - clear: jest.fn(), - show: jest.fn(), - dispose: jest.fn(), - } as any; + const debugCodeLensProvider = {} as any; const debugConfigurationProvider = { provideDebugConfigurations: jest.fn(), @@ -119,12 +116,11 @@ describe('JestExt', () => { getConfiguration.mockReturnValue({}); - (vscode.window.createOutputChannel as jest.Mocked).mockReturnValue(channelStub); - (createProcessSession as jest.Mocked).mockReturnValue(mockProcessSession); (ProjectWorkspace as jest.Mocked).mockImplementation(mockProjectWorkspace); (workspaceLogging as jest.Mocked).mockImplementation(mockWworkspaceLogging); (JestTestProvider as jest.Mocked).mockImplementation(() => mockTestProvider); + (JestOutputTerminal as jest.Mocked).mockImplementation(() => mockOutputTerminal); (vscode.EventEmitter as jest.Mocked) = jest.fn().mockImplementation(() => { return { fire: jest.fn(), event: jest.fn(), dispose: jest.fn() }; }); @@ -645,9 +641,12 @@ describe('JestExt', () => { const updateDecoratorsSpy = jest.spyOn(sut, 'updateDecorators'); sut.triggerUpdateActiveEditor(editor); - expect(channelStub.appendLine).toBeCalledWith(expect.stringContaining('force error')); + expect(mockOutputTerminal.write).toBeCalledWith( + expect.stringContaining('force error'), + 'error' + ); - expect(updateDecoratorsSpy).toBeCalledWith(EmptySortedResult, editor); + expect(updateDecoratorsSpy).toBeCalled(); expect(updateCurrentDiagnostics).toBeCalledWith(EmptySortedResult.fail, undefined, editor); }); describe('can skip non test-file related updates', () => { @@ -722,83 +721,83 @@ describe('JestExt', () => { }); }); - describe('updateDecorators', () => { - let sut: JestExt; - const mockEditor: any = { document: { uri: { fsPath: `file://a/b/c.js` } } }; - const emptyTestResults = { success: [], fail: [], skip: [], unknown: [] }; - - const settings: any = { - debugCodeLens: {}, - enableInlineErrorMessages: false, - }; - - const tr1 = { - start: { line: 1, column: 0 }, - }; - const tr2 = { - start: { line: 100, column: 0 }, - }; - - beforeEach(() => { - StateDecorationsMock.mockImplementation(() => ({ - passing: { key: 'pass' } as vscode.TextEditorDecorationType, - failing: { key: 'fail' } as vscode.TextEditorDecorationType, - skip: { key: 'skip' } as vscode.TextEditorDecorationType, - unknown: { key: 'unknown' } as vscode.TextEditorDecorationType, - })); - - mockEditor.setDecorations = jest.fn(); - }); - - describe('when "showClassicStatus" is on', () => { - beforeEach(() => { - sut = newJestExt({ - settings: { ...settings, testExplorer: { enabled: true, showClassicStatus: true } }, - }); - sut.debugCodeLensProvider.didChange = jest.fn(); - }); - it('will reset decorator if testResults is empty', () => { - sut.updateDecorators(emptyTestResults, mockEditor); - expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4); - for (const args of mockEditor.setDecorations.mock.calls) { - expect(args[1].length).toBe(0); - } - }); - it('will generate dot dectorations for test results', () => { - const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] }; - sut.updateDecorators(testResults2, mockEditor); - expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4); - for (const args of mockEditor.setDecorations.mock.calls) { - let expectedLength = -1; - switch (args[0].key) { - case 'fail': - case 'pass': - expectedLength = 1; - break; - case 'skip': - case 'unknown': - expectedLength = 0; - break; - } - expect(args[1].length).toBe(expectedLength); - } - }); - }); - describe('when showDecorations for "status.classic" is off', () => { - it.each([[{ enabled: true }], [{ enabled: true, showClassicStatus: false }]])( - 'no dot decorators will be generatred for testExplore config: %s', - (testExplorerConfig) => { - sut = newJestExt({ - settings: { ...settings, testExplorer: testExplorerConfig }, - }); - sut.debugCodeLensProvider.didChange = jest.fn(); - const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] }; - sut.updateDecorators(testResults2, mockEditor); - expect(mockEditor.setDecorations).toHaveBeenCalledTimes(0); - } - ); - }); - }); + // describe('updateDecorators', () => { + // let sut: JestExt; + // const mockEditor: any = { document: { uri: { fsPath: `file://a/b/c.js` } } }; + // const emptyTestResults = { success: [], fail: [], skip: [], unknown: [] }; + + // const settings: any = { + // debugCodeLens: {}, + // enableInlineErrorMessages: false, + // }; + + // const tr1 = { + // start: { line: 1, column: 0 }, + // }; + // const tr2 = { + // start: { line: 100, column: 0 }, + // }; + + // beforeEach(() => { + // StateDecorationsMock.mockImplementation(() => ({ + // passing: { key: 'pass' } as vscode.TextEditorDecorationType, + // failing: { key: 'fail' } as vscode.TextEditorDecorationType, + // skip: { key: 'skip' } as vscode.TextEditorDecorationType, + // unknown: { key: 'unknown' } as vscode.TextEditorDecorationType, + // })); + + // mockEditor.setDecorations = jest.fn(); + // }); + + // describe('when "showClassicStatus" is on', () => { + // beforeEach(() => { + // sut = newJestExt({ + // settings: { ...settings, testExplorer: { enabled: true, showClassicStatus: true } }, + // }); + // sut.debugCodeLensProvider.didChange = jest.fn(); + // }); + // it('will reset decorator if testResults is empty', () => { + // sut.updateDecorators(emptyTestResults, mockEditor); + // expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4); + // for (const args of mockEditor.setDecorations.mock.calls) { + // expect(args[1].length).toBe(0); + // } + // }); + // it('will generate dot dectorations for test results', () => { + // const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] }; + // sut.updateDecorators(testResults2, mockEditor); + // expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4); + // for (const args of mockEditor.setDecorations.mock.calls) { + // let expectedLength = -1; + // switch (args[0].key) { + // case 'fail': + // case 'pass': + // expectedLength = 1; + // break; + // case 'skip': + // case 'unknown': + // expectedLength = 0; + // break; + // } + // expect(args[1].length).toBe(expectedLength); + // } + // }); + // }); + // describe('when showDecorations for "status.classic" is off', () => { + // it.each([[{ enabled: true }], [{ enabled: true, showClassicStatus: false }]])( + // 'no dot decorators will be generatred for testExplore config: %s', + // (testExplorerConfig) => { + // sut = newJestExt({ + // settings: { ...settings, testExplorer: testExplorerConfig }, + // }); + // sut.debugCodeLensProvider.didChange = jest.fn(); + // const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] }; + // sut.updateDecorators(testResults2, mockEditor); + // expect(mockEditor.setDecorations).toHaveBeenCalledTimes(0); + // } + // ); + // }); + // }); describe('session', () => { const createJestExt = () => { @@ -834,16 +833,6 @@ describe('JestExt', () => { expect(mockTestProvider.dispose).toBeCalledTimes(1); expect(JestTestProvider).toHaveBeenCalledTimes(2); }); - it.each([[true], [false]])( - 'only create test provider if TestExplorer(=%s) is enabled.', - async (enabled) => { - expect.hasAssertions(); - const sut = createJestExt(); - sut.extContext.settings.testExplorer.enabled = enabled; - await sut.startSession(); - expect(JestTestProvider).toHaveBeenCalledTimes(enabled ? 1 : 0); - } - ); describe('will update test file list', () => { it.each` fileNames | error | expectedTestFiles @@ -1080,7 +1069,7 @@ describe('JestExt', () => { const sut = newJestExt(); sut.deactivate(); expect(mockProcessSession.stop).toBeCalledTimes(1); - expect(channelStub.dispose).toBeCalledTimes(1); + expect(mockOutputTerminal.dispose).toBeCalledTimes(1); }); it('will dispose test provider if initialized', () => { const sut = newJestExt(); @@ -1121,38 +1110,22 @@ describe('JestExt', () => { beforeEach(() => { sut = newJestExt(); onRunEvent = (sut.events.onRunEvent.event as jest.Mocked).mock.calls[0][0]; - process = { id: 'a process id' }; + process = { id: 'a process id', request: { type: 'watch' } }; + sbUpdateMock.mockClear(); }); describe('can process run events', () => { it('register onRunEvent listener', () => { expect(sut.events.onRunEvent.event).toBeCalledTimes(1); }); - it('scheduled event: output to channel', () => { - onRunEvent({ type: 'scheduled', process }); - expect(sut.channel.appendLine).toBeCalledWith(expect.stringContaining(process.id)); - }); - it('data event: relay clean-text to channel', () => { - onRunEvent({ - type: 'data', - text: 'plain text', - raw: 'raw text', - newLine: true, - isError: true, - process, - }); - expect(sut.channel.appendLine).toBeCalledWith(expect.stringContaining('plain text')); - expect(sut.channel.show).toBeCalled(); - sut.channel.show.mockClear(); - - onRunEvent({ type: 'data', text: 'plain text 2', raw: 'raw text', process }); - expect(sut.channel.append).toBeCalledWith(expect.stringContaining('plain text 2')); - expect(sut.channel.show).not.toBeCalled(); + it('will not process not testing process events', () => { + process.request.type = 'not-test'; + onRunEvent({ type: 'start', process }); + expect(sbUpdateMock).not.toBeCalled(); }); - it('start event: notify status bar and clear channel', () => { + it('start event: notify status bar', () => { onRunEvent({ type: 'start', process }); expect(sbUpdateMock).toBeCalledWith({ state: 'running' }); - expect(sut.channel.clear).toBeCalled(); }); it('end event: notify status bar', () => { onRunEvent({ type: 'end', process }); @@ -1218,14 +1191,15 @@ describe('JestExt', () => { }); }); it.each` - debugMode - ${true} - ${false} + exitCode | errorType + ${undefined} | ${'error'} + ${1} | ${'error'} + ${127} | ${errors.CMD_NOT_FOUND} `( - 'updateTestFileList failure warning visibility: debugMode=$debugMode => visible?$debugMode)', - async ({ debugMode }) => { + 'updateTestFileList error will be logged to output terminal by exitCode ($exitCode)', + async ({ exitCode, errorType }) => { expect.hasAssertions(); - const sut = newJestExt({ settings: { debugMode } }); + const sut = newJestExt(); await sut.startSession(); @@ -1235,12 +1209,13 @@ describe('JestExt', () => { expect(onResult).not.toBeUndefined(); // when process failed - onResult(undefined, 'process error'); - if (debugMode) { - expect(messaging.systemWarningMessage).toHaveBeenCalled(); - } else { - expect(messaging.systemWarningMessage).not.toHaveBeenCalled(); - } + onResult(undefined, 'process error', exitCode); + expect(mockOutputTerminal.write).toBeCalledWith(expect.anything(), errorType); } ); + it('showOutput will show the output terminal', () => { + const sut = newJestExt(); + sut.showOutput(); + expect(mockOutputTerminal.show).toBeCalled(); + }); }); diff --git a/tests/JestExt/helper.test.ts b/tests/JestExt/helper.test.ts index 4ceeb0b6..93269b0d 100644 --- a/tests/JestExt/helper.test.ts +++ b/tests/JestExt/helper.test.ts @@ -21,6 +21,9 @@ import { RunnerWorkspaceOptions } from '../../src/JestExt/types'; jest.mock('jest-editor-support', () => ({ isLoginShell: jest.fn(), ProjectWorkspace: jest.fn() })); describe('createJestExtContext', () => { + beforeAll(() => { + console.error = jest.fn(); + }); const baseSettings = { autoRun: { watch: true } }; const workspaceFolder: any = { name: 'workspace' }; describe('autoRun', () => { @@ -190,8 +193,7 @@ describe('getExtensionResourceSettings()', () => { debugMode: false, coverageColors: null, autoRun: { watch: true }, - testExplorer: { enabled: true }, - showTerminalOnLaunch: true, + testExplorer: {}, monitorLongRun: 60000, }); }); @@ -199,17 +201,44 @@ describe('getExtensionResourceSettings()', () => { describe('can read user settings', () => { it('with nodeEnv and shell path', () => { userSettings = { - testExplorer: { enable: false }, nodeEnv: { whatever: '1' }, shell: '/bin/bash', }; const uri: any = { fsPath: 'workspaceFolder1' }; - expect(getExtensionResourceSettings(uri)).toEqual( + const settings = getExtensionResourceSettings(uri); + expect(settings).toEqual( expect.objectContaining({ ...userSettings, }) ); }); + describe('testExplorer', () => { + it.each` + testExplorer | showWarning | converted + ${{ enabled: true }} | ${false} | ${{}} + ${{ enabled: false }} | ${true} | ${{}} + ${{ enabled: true, showClassicStatus: true }} | ${true} | ${{}} + ${{ enabled: true, showClassicStatus: true, showInlineError: true }} | ${true} | ${{}} + ${{ showInlineError: true }} | ${false} | ${{ showInlineError: true }} + ${{}} | ${false} | ${{}} + ${null} | ${false} | ${{}} + `( + 'testExplorer: $testExplorer => show legacy warning? $showWarning', + ({ testExplorer, showWarning, converted }) => { + userSettings = { testExplorer }; + const uri: any = { fsPath: 'workspaceFolder1' }; + const settings = getExtensionResourceSettings(uri); + expect(settings).toEqual( + expect.objectContaining({ + testExplorer: converted, + }) + ); + if (showWarning) { + expect(vscode.window.showWarningMessage).toBeCalled(); + } + } + ); + }); it.each` pluginSettings | expectedConfig ${{ autoEnable: false }} | ${{ watch: false }} diff --git a/tests/JestExt/outpt-terminal.test.ts b/tests/JestExt/outpt-terminal.test.ts new file mode 100644 index 00000000..cbb44cfd --- /dev/null +++ b/tests/JestExt/outpt-terminal.test.ts @@ -0,0 +1,104 @@ +jest.unmock('../../src/JestExt/output-terminal'); +jest.unmock('../../src/errors'); + +import * as vscode from 'vscode'; +import { JestOutputTerminal } from '../../src/JestExt/output-terminal'; +import * as errors from '../../src/errors'; + +describe('JestOutputTerminal', () => { + let mockTerminal; + let mockEmitter; + + beforeEach(() => { + jest.resetAllMocks(); + + mockTerminal = { + dispose: jest.fn(), + show: jest.fn(), + }; + vscode.window.createTerminal = jest.fn().mockReturnValue(mockTerminal); + (vscode.window.terminals as any) = []; + + mockEmitter = { fire: jest.fn(), event: jest.fn(), dispose: jest.fn() }; + (vscode.EventEmitter as jest.Mocked).mockImplementation(() => mockEmitter); + }); + it('delay creating terminal until the actual write occurs', () => { + const output = new JestOutputTerminal('workspace'); + expect(vscode.window.createTerminal).not.toBeCalled(); + output.write('abc'); + expect(vscode.window.createTerminal).toBeCalled(); + }); + it('if terminal already opened, close and create a new one', () => { + const a = { name: 'Jest (a)', dispose: jest.fn() }; + const b = { name: 'Jest (b)', dispose: jest.fn() }; + (vscode.window.terminals as any) = [a, b]; + new JestOutputTerminal('a'); + expect(a.dispose).toBeCalled(); + expect(b.dispose).not.toBeCalled(); + expect(vscode.window.createTerminal).not.toBeCalled(); + }); + it('can buffer output until open', () => { + const output = new JestOutputTerminal('a'); + output.write('text 1'); + expect(mockEmitter.fire).not.toBeCalled(); + + // after open, the buffered text should be sent again + const { pty } = (vscode.window.createTerminal as jest.Mocked).mock.calls[0][0]; + pty.open(); + expect(mockEmitter.fire).toBeCalledWith('text 1'); + + output.write('text 2'); + expect(mockEmitter.fire).toBeCalledWith('text 2'); + }); + it('if user close the terminal, it will be reopened on the next write', () => { + const output = new JestOutputTerminal('a'); + output.write('1'); + expect(vscode.window.createTerminal).toBeCalledTimes(1); + const { pty } = (vscode.window.createTerminal as jest.Mocked).mock.calls[0][0]; + + // simulate users close the terminal + pty.close(); + expect(mockTerminal.dispose).toBeCalled(); + + // user writes again + output.write('1'); + // terminal should be opened again with the same pty + expect(vscode.window.createTerminal).toBeCalledTimes(2); + const { pty: pty2 } = (vscode.window.createTerminal as jest.Mocked).mock.calls[1][0]; + expect(pty2).toBe(pty); + }); + it('will show terminal when writing error messages', () => { + const output = new JestOutputTerminal('a'); + + output.write('1'); + expect(mockTerminal.show).not.toBeCalled(); + + output.write('2', 'error'); + expect(mockTerminal.show).toBeCalledTimes(1); + + output.write('2', errors.GENERIC_ERROR); + expect(mockTerminal.show).toBeCalledTimes(2); + }); + it('will properly dispose terminal and emitter', () => { + const output = new JestOutputTerminal('a'); + output.write('1'); + output.dispose(); + expect(mockTerminal.dispose).toBeCalled(); + expect(mockEmitter.dispose).toBeCalled(); + }); + describe('can write output with options', () => { + it.each` + case | text | options + ${1} | ${'regular text'} | ${undefined} + ${2} | ${'text with newline\r\n'} | ${undefined} + ${3} | ${'error text'} | ${'error'} + ${4} | ${'warning text'} | ${'warn'} + ${5} | ${'bold text'} | ${'bold'} + ${6} | ${'bold text with newLine'} | ${['bold', 'new-line']} + `('can write with option: case $case', ({ text, options }) => { + const output = new JestOutputTerminal('a'); + const t = output.write(text, options); + expect(t).toMatchSnapshot(); + }); + }); +}); diff --git a/tests/JestExt/process-listeners.test.ts b/tests/JestExt/process-listeners.test.ts index e4074809..2779442b 100644 --- a/tests/JestExt/process-listeners.test.ts +++ b/tests/JestExt/process-listeners.test.ts @@ -9,7 +9,7 @@ import { AbstractProcessListener, DEFAULT_LONG_RUN_THRESHOLD, } from '../../src/JestExt/process-listeners'; -import { cleanAnsi } from '../../src/helpers'; +import { cleanAnsi, toErrorString } from '../../src/helpers'; import * as messaging from '../../src/messaging'; describe('jest process listeners', () => { @@ -85,9 +85,10 @@ describe('jest process listeners', () => { (vscode.Uri.file as jest.Mocked) = jest.fn((f) => ({ fsPath: f })); const onResult = jest.fn(); const listener = new ListTestFileListener(mockSession, onResult); + (toErrorString as jest.Mocked).mockReturnValue(expectedFiles); output.forEach((m) => listener.onEvent(mockProcess, 'executableOutput', Buffer.from(m))); - listener.onEvent(mockProcess, 'processExit'); + listener.onEvent(mockProcess, 'processExit', 0); listener.onEvent(mockProcess, 'processClose'); // should not fire exit event @@ -103,14 +104,13 @@ describe('jest process listeners', () => { expect(error).toBeUndefined(); } else { expect(fileNames).toBeUndefined(); - expect(error).not.toBeUndefined(); - expect(error.toString()).toContain(expectedFiles); + expect(error).toContain(expectedFiles); } }); it.each` exitCode | isError ${0} | ${false} - ${1} | ${false} + ${1} | ${true} ${999} | ${true} `( 'can handle process error via onResult: exitCode:$exitCode => isError?$isError', @@ -120,7 +120,9 @@ describe('jest process listeners', () => { (vscode.Uri.file as jest.Mocked) = jest.fn((f) => ({ fsPath: f })); const onResult = jest.fn(); const listener = new ListTestFileListener(mockSession, onResult); + (toErrorString as jest.Mocked).mockReturnValue('some error'); + listener.onEvent(mockProcess, 'executableStdErr', Buffer.from('some error')); listener.onEvent(mockProcess, 'executableOutput', Buffer.from('["a", "b"]')); listener.onEvent(mockProcess, 'processExit', exitCode); listener.onEvent(mockProcess, 'processClose'); @@ -131,20 +133,18 @@ describe('jest process listeners', () => { // onResult should be called to report results or error expect(onResult).toBeCalledTimes(1); - const [fileNames, error] = onResult.mock.calls[0]; - const warnLog = ['warn', expect.stringMatching(`${exitCode}`), expect.anything()]; + const [fileNames, error, code] = onResult.mock.calls[0]; + // const warnLog = ['warn']; if (!isError) { - expect(mockLogging).not.toBeCalledWith(...warnLog); const expectedFiles = ['a', 'b']; expect(vscode.Uri.file).toBeCalledTimes(expectedFiles.length); expect(fileNames).toEqual(expectedFiles); expect(error).toBeUndefined(); } else { - expect(mockLogging).toBeCalledWith(...warnLog); + expect(code).toEqual(exitCode); expect(fileNames).toBeUndefined(); - expect(error).not.toBeUndefined(); - expect(error.toString()).toContain(`${exitCode}`); + expect(error).toEqual('some error'); } } ); @@ -179,8 +179,8 @@ describe('jest process listeners', () => { describe.each` output | stdout | stderr | error ${'whatever'} | ${'data'} | ${'data'} | ${'data'} - ${'onRunStart'} | ${'start'} | ${undefined} | ${'data'} - ${'onRunComplete'} | ${'end'} | ${undefined} | ${'data'} + ${'onRunStart'} | ${'data'} | ${'start'} | ${'data'} + ${'onRunComplete'} | ${'data'} | ${'end'} | ${'data'} ${'Watch Usage'} | ${undefined} | ${undefined} | ${'data'} `('propagate run events: $output', ({ output, stdout, stderr, error }) => { it('from stdout: eventType=$stdout', () => { @@ -237,7 +237,7 @@ describe('jest process listeners', () => { listener.onEvent(mockProcess, 'processStarting'); expect(mockSession.context.onRunEvent.fire).toBeCalledTimes(1); expect(mockSession.context.onRunEvent.fire).toBeCalledWith( - expect.objectContaining({ type: 'start' }) + expect.objectContaining({ type: 'process-start' }) ); mockSession.context.onRunEvent.fire.mockClear(); @@ -249,19 +249,40 @@ describe('jest process listeners', () => { expect.objectContaining({ type: 'exit' }) ); }); - it('when reporters reports start/end', () => { - expect.hasAssertions(); - const listener = new RunTestListener(mockSession); - - // stdout - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart'); - expect(mockSession.context.onRunEvent.fire).toBeCalledWith( - expect.objectContaining({ type: 'start' }) + describe('when reporters reports start/end', () => { + it.each(['watch', 'all-tests'])( + 'will notify start/end regardless request types', + (requestType) => { + expect.hasAssertions(); + const listener = new RunTestListener(mockSession); + mockProcess.request.type = requestType; + + // stderr + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart'); + expect(mockSession.context.onRunEvent.fire).toBeCalledWith( + expect.objectContaining({ type: 'start' }) + ); + + listener.onEvent(mockProcess, 'executableStdErr', 'onRunComplete'); + expect(mockSession.context.onRunEvent.fire).toBeCalledWith( + expect.objectContaining({ type: 'end' }) + ); + } ); - - listener.onEvent(mockProcess, 'executableOutput', 'onRunComplete'); - expect(mockSession.context.onRunEvent.fire).toBeCalledWith( - expect.objectContaining({ type: 'end' }) + it.each` + message | raw + ${'the data before\r\nonRunStart: xxx\r\ndata after 1'} | ${'the data before\r\ndata after 1'} + ${'the data before\r\nonRunComplete\r\ndata after 2\r\n'} | ${'the data before\r\ndata after 2\r\n'} + `( + 'will still report message: "$message" excluding the reporter output', + ({ message, raw }) => { + const listener = new RunTestListener(mockSession); + // stderr + listener.onEvent(mockProcess, 'executableStdErr', message, message); + expect(mockSession.context.onRunEvent.fire).toBeCalledWith( + expect.objectContaining({ type: 'data', raw }) + ); + } ); }); }); @@ -279,7 +300,7 @@ describe('jest process listeners', () => { expect(setTimeout).not.toBeCalled(); - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart: numTotalTestSuites: 100'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart: numTotalTestSuites: 100'); expect(clearTimeout).not.toBeCalled(); if (threshold > 0) { @@ -306,14 +327,14 @@ describe('jest process listeners', () => { it.each` eventType | args ${'processExit'} | ${[]} - ${'executableOutput'} | ${['onRunComplete']} + ${'executableStdErr'} | ${['onRunComplete']} `( 'should not trigger timeout after process/run ended with $eventType', ({ eventType, args }) => { mockSession.context.settings.monitorLongRun = undefined; const listener = new RunTestListener(mockSession); - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart: numTotalTestSuites: 100'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart: numTotalTestSuites: 100'); expect(setTimeout).toBeCalledTimes(1); expect(clearTimeout).not.toBeCalled(); @@ -330,12 +351,12 @@ describe('jest process listeners', () => { mockSession.context.settings.monitorLongRun = undefined; const listener = new RunTestListener(mockSession); - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart: numTotalTestSuites: 100'); - listener.onEvent(mockProcess, 'executableOutput', 'onRunComplete: execError: whatever'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart: numTotalTestSuites: 100'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunComplete: execError: whatever'); expect(setTimeout).toBeCalledTimes(1); expect(clearTimeout).toBeCalledTimes(1); - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart: numTotalTestSuites: 70'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart: numTotalTestSuites: 70'); expect(setTimeout).toBeCalledTimes(2); expect(clearTimeout).toBeCalledTimes(1); }); @@ -343,8 +364,8 @@ describe('jest process listeners', () => { mockSession.context.settings.monitorLongRun = undefined; const listener = new RunTestListener(mockSession); - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart: numTotalTestSuites: 100'); - listener.onEvent(mockProcess, 'executableOutput', 'onRunStart: numTotalTestSuites: 70'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart: numTotalTestSuites: 100'); + listener.onEvent(mockProcess, 'executableStdErr', 'onRunStart: numTotalTestSuites: 70'); expect(setTimeout).toBeCalledTimes(2); expect(clearTimeout).toBeCalledTimes(1); }); @@ -398,6 +419,38 @@ describe('jest process listeners', () => { expect(vscode.window.showInformationMessage).toBeCalledTimes(1); expect(mockSession.scheduleProcess).not.toBeCalled(); }); + it('auto update snapsot only apply for runs not already updating snapshots', async () => { + expect.hasAssertions(); + mockSession.context.settings.enableSnapshotUpdateMessages = true; + (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue( + Promise.resolve('something') + ); + + const listener = new RunTestListener(mockSession); + + await listener.onEvent( + mockProcess, + 'executableStdErr', + Buffer.from('Snapshot test failed') + ); + expect(vscode.window.showInformationMessage).toBeCalledTimes(1); + expect(mockSession.scheduleProcess).toBeCalledWith({ + type: 'update-snapshot', + baseRequest: mockProcess.request, + }); + + // for a process already with updateSnapshot flag: do nothing + (vscode.window.showInformationMessage as jest.Mocked).mockClear(); + mockSession.scheduleProcess.mockClear(); + mockProcess.request.updateSnapshot = true; + await listener.onEvent( + mockProcess, + 'executableStdErr', + Buffer.from('Snapshot test failed') + ); + expect(vscode.window.showInformationMessage).not.toBeCalled(); + expect(mockSession.scheduleProcess).not.toBeCalled(); + }); }); describe('when "--watch" is not supported', () => { diff --git a/tests/TestResults/TestResultProvider.test.ts b/tests/TestResults/TestResultProvider.test.ts index 948dda46..cdeeedf1 100644 --- a/tests/TestResults/TestResultProvider.test.ts +++ b/tests/TestResults/TestResultProvider.test.ts @@ -150,6 +150,7 @@ describe('TestResultProvider', () => { beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); + console.warn = jest.fn(); mockTestReconciler.mockReturnValue(mockReconciler); (vscode.EventEmitter as jest.Mocked) = jest.fn().mockImplementation(helper.mockEvent); }); diff --git a/tests/__snapshots__/helpers.test.ts.snap b/tests/__snapshots__/helpers.test.ts.snap new file mode 100644 index 00000000..dd9f4269 --- /dev/null +++ b/tests/__snapshots__/helpers.test.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`toErrorString: arbitrary object 1`] = `"{\\"text\\":\\"anything\\",\\"value\\":1}"`; + +exports[`toErrorString: string 1`] = `"regular error"`; + +exports[`toErrorString: undefined 1`] = `""`; diff --git a/tests/decorations/test-status.test.ts b/tests/decorations/test-status.test.ts deleted file mode 100644 index 3744b0fb..00000000 --- a/tests/decorations/test-status.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -jest.unmock('../../src/decorations/test-status'); - -jest.mock('vscode', () => { - return { - DecorationRangeBehavior: { - ClosedClosed: {}, - }, - OverviewRulerLane: { - Left: {}, - }, - window: { - createTextEditorDecorationType: jest.fn(jest.fn), - }, - }; -}); -jest.mock('../../src/helpers', () => ({ - prepareIconFile: (icon) => icon, -})); - -import * as vscode from 'vscode'; -import { TestStatus } from '../../src/decorations/test-status'; - -const defaultContextMock = { - asAbsolutePath: (name: string) => name, -} as vscode.ExtensionContext; - -function testStatusStyle(property: string) { - it('should be decoration', () => { - const mock = vscode.window.createTextEditorDecorationType as unknown as jest.Mock; - mock.mockReturnValue(property); - - const decorations = new TestStatus(defaultContextMock); - - expect(decorations[property]).toBe(mock()); - }); - - it('should have been created with proper attributes', () => { - const mock = vscode.window.createTextEditorDecorationType as unknown as jest.Mock; - mock.mockImplementation((args) => args); - - const decoration = new TestStatus(defaultContextMock)[property]; - - expect(decoration.rangeBehavior).toBe(vscode.DecorationRangeBehavior.ClosedClosed); - expect(decoration.overviewRulerLane).toBe(vscode.OverviewRulerLane.Left); - expect(decoration.overviewRulerColor).toBeTruthy(); - expect(decoration.gutterIconPath).toBeTruthy(); - expect(decoration.dark.gutterIconPath).toBeTruthy(); - expect(decoration.light.gutterIconPath).toBeTruthy(); - }); -} - -describe('Decorations', () => { - it('is initializing a class with public fields and methods', () => { - const decorations = new TestStatus(defaultContextMock); - - expect(decorations).toBeInstanceOf(TestStatus); - }); - - describe('passing', () => { - testStatusStyle('passing'); - }); - - describe('failing', () => { - testStatusStyle('failing'); - }); - - describe('skip', () => { - testStatusStyle('skip'); - }); - - describe('unknown', () => { - testStatusStyle('unknown'); - }); -}); diff --git a/tests/helpers.test.ts b/tests/helpers.test.ts index 4d95dc43..7581a6ad 100644 --- a/tests/helpers.test.ts +++ b/tests/helpers.test.ts @@ -36,6 +36,7 @@ import { toLowerCaseDriveLetter, toUpperCaseDriveLetter, shellQuote, + toErrorString, } from '../src/helpers'; // Manually (forcefully) set the executable's file extension to test its addition independendly of the operating system. @@ -328,3 +329,16 @@ describe('shellQuote', () => { expect(shellQuote(str, shell)).toEqual(expected); }); }); +it.each` + name | e | matchString + ${'undefined'} | ${undefined} | ${undefined} + ${'string'} | ${'regular error'} | ${undefined} + ${'an Error object'} | ${new Error('test error')} | ${'Error: test error'} + ${'arbitrary object'} | ${{ text: 'anything', value: 1 }} | ${undefined} +`('toErrorString: $name', ({ e, matchString }) => { + if (matchString) { + expect(toErrorString(e)).toEqual(expect.stringContaining(matchString)); + } else { + expect(toErrorString(e)).toMatchSnapshot(); + } +}); diff --git a/tests/reporter.test.ts b/tests/reporter.test.ts index 287b6de2..36d2922f 100644 --- a/tests/reporter.test.ts +++ b/tests/reporter.test.ts @@ -1,17 +1,18 @@ jest.unmock('../src/reporter'); -import VSCodeJestReporter from '../src/reporter'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const VSCodeJestReporter = require('../src/reporter'); describe('VSCodeJest Reporter', () => { beforeEach(() => { jest.resetAllMocks(); - console.log = jest.fn(); + process.stderr.write = jest.fn(); }); it('reports on RunStart and RunComplete via console.log', () => { const reporter = new VSCodeJestReporter(); reporter.onRunStart({} as any); - expect(console.log).toBeCalledWith(expect.stringContaining('onRunStart')); + expect(process.stderr.write).toBeCalledWith(expect.stringContaining('onRunStart')); reporter.onRunComplete(new Set(), {} as any); - expect(console.log).toBeCalledWith('onRunComplete'); + expect(process.stderr.write).toBeCalledWith('onRunComplete\r\n'); }); it.each` numTotalTests | numTotalTestSuites | hasError @@ -24,13 +25,17 @@ describe('VSCodeJest Reporter', () => { const reporter = new VSCodeJestReporter(); const args: any = { numTotalTestSuites }; reporter.onRunStart(args); - expect(console.log).toBeCalledWith(`onRunStart: numTotalTestSuites: ${numTotalTestSuites}`); + expect(process.stderr.write).toBeCalledWith( + `onRunStart: numTotalTestSuites: ${numTotalTestSuites}\r\n` + ); const result: any = { numTotalTests, numTotalTestSuites }; reporter.onRunComplete(new Set(), result); if (hasError) { - expect(console.log).toBeCalledWith(expect.stringContaining('onRunComplete: execError')); + expect(process.stderr.write).toBeCalledWith( + expect.stringContaining('onRunComplete: execError') + ); } else { - expect(console.log).toBeCalledWith('onRunComplete'); + expect(process.stderr.write).toBeCalledWith('onRunComplete\r\n'); } } ); diff --git a/tests/test-provider/test-helper.ts b/tests/test-provider/test-helper.ts index bb0acfe3..1d7d1f78 100644 --- a/tests/test-provider/test-helper.ts +++ b/tests/test-provider/test-helper.ts @@ -42,6 +42,7 @@ export const mockExtExplorerContext = (wsName = 'ws-1', override: any = {}): any debugTests: jest.fn(), sessionEvents: mockJestExtEvents(), settings: { testExplorer: { enabled: true } }, + output: { write: jest.fn(), dispose: jest.fn() }, ...override, }; }; diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index e70517f5..e9deb376 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -1,10 +1,11 @@ jest.unmock('../../src/test-provider/test-item-data'); -jest.unmock('../../src/test-provider/test-provider-context'); +jest.unmock('../../src/test-provider/test-provider-helper'); jest.unmock('../../src/appGlobals'); jest.unmock('../../src/TestResults/match-node'); jest.unmock('../../src/TestResults/match-by-context'); jest.unmock('../test-helper'); jest.unmock('./test-helper'); +jest.unmock('../../src/errors'); jest.mock('path', () => { let sep = '/'; @@ -32,16 +33,14 @@ import { WorkspaceRoot, } from '../../src/test-provider/test-item-data'; import * as helper from '../test-helper'; -import { - JestTestProviderContext, - _resetShowTerminal, -} from '../../src/test-provider/test-provider-context'; +import { JestTestProviderContext } from '../../src/test-provider/test-provider-helper'; import { buildAssertionContainer, buildSourceContainer, } from '../../src/TestResults/match-by-context'; import * as path from 'path'; import { mockController, mockExtExplorerContext } from './test-helper'; +import * as errors from '../../src/errors'; const mockPathSep = (newSep: string) => { (path as jest.Mocked).setSep(newSep); @@ -68,53 +67,22 @@ const mockScheduleProcess = (context) => { }; describe('test-item-data', () => { let context; - let profile; - let runMock; + let jestRun; + let runEndSpy; let controllerMock; - let resolveMock; beforeEach(() => { controllerMock = mockController(); const profiles: any = [{ tag: { id: 'run' } }, { tag: { id: 'debug' } }]; context = new JestTestProviderContext(mockExtExplorerContext('ws-1'), controllerMock, profiles); - runMock = context.createTestRun(); - profile = { kind: vscode.TestRunProfileKind.Run }; - resolveMock = jest.fn(); + context.output.write = jest.fn((t) => t); + jestRun = context.createTestRun({}, { disableVscodeRunEnd: true }); + runEndSpy = jest.spyOn(jestRun, 'end'); vscode.Uri.joinPath = jest .fn() .mockImplementation((uri, p) => ({ fsPath: `${uri.fsPath}/${p}` })); vscode.Uri.file = jest.fn().mockImplementation((f) => ({ fsPath: f })); - - _resetShowTerminal(); - }); - describe('show TestExplorer Terminal', () => { - it('show TestExplorer Terminal on first run output only', () => { - context.appendOutput('first time should open terminal', runMock); - expect(vscode.commands.executeCommand).toBeCalledTimes(1); - expect(vscode.commands.executeCommand).toBeCalledWith('testing.showMostRecentOutput'); - - context.appendOutput('subsequent calls will not open terminal again', runMock); - expect(vscode.commands.executeCommand).toBeCalledTimes(1); - }); - it.each` - showTerminalOnLaunch | callTimes - ${undefined} | ${1} - ${true} | ${1} - ${false} | ${0} - `( - 'controlled by showTerminalOnLaunch($showTerminalOnLaunch) => callTimes($callTimes)', - ({ showTerminalOnLaunch, callTimes }) => { - context.ext.settings.showTerminalOnLaunch = showTerminalOnLaunch; - (vscode.commands.executeCommand as jest.Mocked).mockClear(); - - context.appendOutput('first time should open terminal', runMock); - expect(vscode.commands.executeCommand).toBeCalledTimes(callTimes); - - context.appendOutput('subsequent calls will not open terminal again', runMock); - expect(vscode.commands.executeCommand).toBeCalledTimes(callTimes); - } - ); }); describe('discover children', () => { describe('WorkspaceRoot', () => { @@ -126,7 +94,7 @@ describe('test-item-data', () => { ]; context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); // verify tree structure expect(wsRoot.item.children.size).toEqual(1); @@ -159,26 +127,26 @@ describe('test-item-data', () => { it('if no testFiles yet, will still turn off canResolveChildren and close the run', () => { context.ext.testResolveProvider.getTestList.mockReturnValue([]); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(0); expect(wsRoot.item.canResolveChildren).toBe(false); - expect(runMock.end).toBeCalledTimes(1); + expect(runEndSpy).toBeCalledTimes(1); }); it('will not wipe out existing test items', () => { // first time discover 1 file context.ext.testResolveProvider.getTestList.mockReturnValue(['/ws-1/a.test.ts']); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(1); expect(wsRoot.item.canResolveChildren).toBe(false); - expect(runMock.end).toBeCalledTimes(1); + expect(runEndSpy).toBeCalledTimes(1); // 2nd time if no test-file: testItems will not change context.ext.testResolveProvider.getTestList.mockReturnValue([]); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(1); expect(wsRoot.item.canResolveChildren).toBe(false); - expect(runMock.end).toBeCalledTimes(2); + expect(runEndSpy).toBeCalledTimes(2); }); }); it('will only discover up to the test file level', () => { @@ -191,7 +159,7 @@ describe('test-item-data', () => { assertionContainer, }); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); const docItem = wsRoot.item.children.get(testFiles[0]); expect(docItem.children.size).toEqual(0); expect(context.ext.testResolveProvider.getTestSuiteResult).toBeCalled(); @@ -208,7 +176,7 @@ describe('test-item-data', () => { const wsRoot = new WorkspaceRoot(context); // first discover all test files and build the tree - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(2); let folderItem = wsRoot.item.children.get('/ws-1/tests1'); let docItem = folderItem.children.get(testFiles[0]); @@ -219,7 +187,7 @@ describe('test-item-data', () => { // now remove '/ws-1/tests2/b.test.ts' and rediscover testFiles.length = 1; - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toEqual(1); folderItem = wsRoot.item.children.get('/ws-1/tests2'); expect(folderItem).toBeUndefined(); @@ -261,7 +229,7 @@ describe('test-item-data', () => { it('testListUpdated event will be fired', () => { const wsRoot = new WorkspaceRoot(context); context.ext.testResolveProvider.getTestList.mockReturnValueOnce([]); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toBe(0); // invoke testListUpdated event listener @@ -270,7 +238,7 @@ describe('test-item-data', () => { ]); // should have created a new run const runMock2 = controllerMock.lastRunMock(); - expect(runMock2).not.toBe(runMock); + expect(runMock2).not.toBe(jestRun.vscodeRun); expect(wsRoot.item.children.size).toBe(1); const docItem = getChildItem(wsRoot.item, 'a.test.ts'); @@ -284,7 +252,7 @@ describe('test-item-data', () => { context.ext.settings = { testExplorer: { enabled: true, showInlineError: true } }; const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toBe(0); @@ -336,7 +304,7 @@ describe('test-item-data', () => { }); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalledTimes(1); expect(wsRoot.item.children.size).toBe(1); @@ -411,7 +379,7 @@ describe('test-item-data', () => { const testContainer = buildSourceContainer(sourceRoot); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalledTimes(1); expect(wsRoot.item.children.size).toBe(1); @@ -458,7 +426,7 @@ describe('test-item-data', () => { '/ws-1/a.test.ts', '/ws-1/b.test.ts', ]); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); expect(wsRoot.item.children.size).toBe(2); // a.test.ts should still have 2 children docItem = getChildItem(wsRoot.item, 'a.test.ts'); @@ -480,18 +448,18 @@ describe('test-item-data', () => { }); const parentItem: any = controllerMock.createTestItem('ws-1', 'ws-1', uri); const docRoot = new TestDocumentRoot(context, uri, parentItem); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(1); const tData = context.getData(getChildItem(docRoot.item, 'test-1')); expect(tData instanceof TestData).toBeTruthy(); - expect(runMock.passed).toBeCalledWith(tData.item); + expect(jestRun.vscodeRun.passed).toBeCalledWith(tData.item); }); it('if no test suite result yet, children list is empty', () => { context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(undefined); const uri: any = { fsPath: '/ws-1/a.test.ts' }; const parentItem: any = controllerMock.createTestItem('ws-1', 'ws-1', uri); const docRoot = new TestDocumentRoot(context, uri, parentItem); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(0); }); }); @@ -519,7 +487,7 @@ describe('test-item-data', () => { describe('run request', () => { it('WorkspaceRoot runs all tests in the workspace in blocking-2 queue', () => { const wsRoot = new WorkspaceRoot(context); - wsRoot.scheduleTest(runMock, resolveMock); + wsRoot.scheduleTest(jestRun); const r = context.ext.session.scheduleProcess.mock.calls[0][0]; expect(r.type).toEqual('all-tests'); const transformed = r.transform({ schedule: { queue: 'blocking' } }); @@ -528,7 +496,7 @@ describe('test-item-data', () => { it('FolderData runs all tests inside the folder', () => { const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' }); const folderData = new FolderData(context, 'folder', parent); - folderData.scheduleTest(runMock, resolveMock); + folderData.scheduleTest(jestRun); expect(context.ext.session.scheduleProcess).toBeCalledWith( expect.objectContaining({ type: 'by-file-pattern', @@ -543,7 +511,7 @@ describe('test-item-data', () => { { fsPath: '/ws-1/a.test.ts' } as any, parent ); - docRoot.scheduleTest(runMock, resolveMock); + docRoot.scheduleTest(jestRun); expect(context.ext.session.scheduleProcess).toBeCalledWith( expect.objectContaining({ type: 'by-file-pattern', @@ -556,7 +524,7 @@ describe('test-item-data', () => { const node: any = { fullName: 'a test', attrs: {}, data: {} }; const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', uri); const tData = new TestData(context, uri, node, parent); - tData.scheduleTest(runMock, resolveMock); + tData.scheduleTest(jestRun); expect(context.ext.session.scheduleProcess).toBeCalledWith( expect.objectContaining({ type: 'by-file-test-pattern', @@ -570,18 +538,18 @@ describe('test-item-data', () => { context.ext.session.scheduleProcess.mockReturnValue(undefined); const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' }); const docRoot = new TestDocumentRoot(context, { fsPath: '/ws-1/a.test.ts' } as any, parent); - expect(docRoot.scheduleTest(runMock, resolveMock)).toBeUndefined(); - expect(runMock.errored).toBeCalledWith(docRoot.item, expect.anything()); - expect(resolveMock).toBeCalled(); + expect(docRoot.scheduleTest(jestRun)).toBeUndefined(); + expect(jestRun.vscodeRun.errored).toBeCalledWith(docRoot.item, expect.anything()); + expect(runEndSpy).toBeCalled(); }); it('schedule request will contain itemRun info', () => { const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' }); const folderData = new FolderData(context, 'folder', parent); - folderData.scheduleTest(runMock, resolveMock); + folderData.scheduleTest(jestRun); const request = context.ext.session.scheduleProcess.mock.calls[0][0]; - expect(request.itemRun.run).toEqual(runMock); - expect(request.itemRun.item).toEqual(folderData.item); + expect(request.run).toBe(jestRun); + expect(request.run.item).toBe(folderData.item); }); }); @@ -632,11 +600,11 @@ describe('test-item-data', () => { // simulate an internal run has been scheduled const process = mockScheduleProcess(context); - const runMock = context.createTestRun(); - const resolve = jest.fn(); + const jestRun = context.createTestRun({}, { disableVscodeRunEnd: true }); + const runEndSpy = jest.spyOn(jestRun, 'end'); controllerMock.createTestRun.mockClear(); - wsRoot.scheduleTest(runMock, resolve, {}); + wsRoot.scheduleTest(jestRun); expect(controllerMock.createTestRun).not.toHaveBeenCalled(); @@ -657,9 +625,9 @@ describe('test-item-data', () => { const dItem = getChildItem(wsRoot.item, 'a.test.ts'); expect(dItem.children.size).toBe(2); const tItem = getChildItem(dItem, 'test-a'); - expect(runMock.passed).toBeCalledWith(tItem); - expect(runMock.end).not.toBeCalled(); - expect(resolve).toBeCalled(); + expect(jestRun.vscodeRun.passed).toBeCalledWith(tItem); + expect(jestRun.vscodeRun.end).not.toBeCalled(); + expect(runEndSpy).toBeCalled(); }); it.each` config | hasLocation @@ -675,7 +643,7 @@ describe('test-item-data', () => { controllerMock.createTestRun.mockClear(); - wsRoot.scheduleTest(runMock, resolveMock, {}); + wsRoot.scheduleTest(jestRun); // triggers testSuiteChanged event listener context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({ @@ -689,7 +657,7 @@ describe('test-item-data', () => { const dItem = getChildItem(wsRoot.item, 'a.test.ts'); const tItem = getChildItem(dItem, 'test-b'); - expect(runMock.failed).toBeCalledWith(tItem, expect.anything()); + expect(jestRun.vscodeRun.failed).toBeCalledWith(tItem, expect.anything()); if (hasLocation) { expect(vscode.TestMessage).toBeCalled(); } else { @@ -722,7 +690,7 @@ describe('test-item-data', () => { ]; context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); const wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); // verify tree structure expect(wsRoot.item.children.size).toEqual(1); @@ -757,7 +725,7 @@ describe('test-item-data', () => { testFiles = ['/ws-1/src/a.test.ts', '/ws-1/src/b.test.ts', '/ws-1/src/app/app.test.ts']; context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles); wsRoot = new WorkspaceRoot(context); - wsRoot.discoverTest(runMock); + wsRoot.discoverTest(jestRun); }); it('add', () => { // add 2 new files @@ -831,7 +799,7 @@ describe('test-item-data', () => { assertionContainer, }); docRoot = new TestDocumentRoot(context, { fsPath: '/ws-1/a.test.ts' } as any, parent); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); }); it('add', () => { // add test-2 under existing desc-1 and a new desc-2/test-3 @@ -843,29 +811,29 @@ describe('test-item-data', () => { status: 'KnownFail', assertionContainer, }); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(2); - expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything()); + expect(jestRun.vscodeRun.failed).toBeCalledWith(docRoot.item, expect.anything()); const desc1 = getChildItem(docRoot.item, 'desc-1'); expect(desc1.children.size).toEqual(2); const t1 = getChildItem(desc1, 'desc-1 test-1'); expect(t1).not.toBeUndefined(); - expect(runMock.passed).toBeCalledWith(t1); + expect(jestRun.vscodeRun.passed).toBeCalledWith(t1); const t2 = getChildItem(desc1, 'desc-1 test-2'); expect(t2).not.toBeUndefined(); - expect(runMock.failed).toBeCalledWith(t2, expect.anything()); + expect(jestRun.vscodeRun.failed).toBeCalledWith(t2, expect.anything()); const desc2 = getChildItem(docRoot.item, 'desc-2'); const t3 = getChildItem(desc2, 'desc-2 test-3'); expect(t3).not.toBeUndefined(); - expect(runMock.passed).toBeCalledWith(t3); + expect(jestRun.vscodeRun.passed).toBeCalledWith(t3); const t4 = getChildItem(desc2, 'desc-2 test-4'); expect(t4).not.toBeUndefined(); - expect(runMock.skipped).toBeCalledWith(t4); + expect(jestRun.vscodeRun.skipped).toBeCalledWith(t4); }); it('delete', () => { // delete the only test -1 @@ -874,7 +842,7 @@ describe('test-item-data', () => { status: 'Unknown', assertionContainer, }); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(0); }); it('rename', () => { @@ -885,17 +853,17 @@ describe('test-item-data', () => { assertionContainer, }); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(1); - expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything()); + expect(jestRun.vscodeRun.failed).toBeCalledWith(docRoot.item, expect.anything()); const t2 = getChildItem(docRoot.item, 'test-2'); expect(t2).not.toBeUndefined(); - expect(runMock.failed).toBeCalledWith(t2, expect.anything()); + expect(jestRun.vscodeRun.failed).toBeCalledWith(t2, expect.anything()); }); describe('duplicate test names', () => { const setup = (assertions) => { - runMock.passed.mockClear(); - runMock.failed.mockClear(); + jestRun.vscodeRun.passed.mockClear(); + jestRun.vscodeRun.failed.mockClear(); const assertionContainer = buildAssertionContainer(assertions); context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({ @@ -907,31 +875,31 @@ describe('test-item-data', () => { const a2 = helper.makeAssertion('test-1', 'KnownFail', [], [1, 0]); const a3 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]); setup([a2, a3]); - docRoot.discoverTest(runMock); + docRoot.discoverTest(jestRun); expect(docRoot.item.children.size).toEqual(2); - expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything()); + expect(jestRun.vscodeRun.failed).toBeCalledWith(docRoot.item, expect.anything()); const items = []; docRoot.item.children.forEach((item) => items.push(item)); expect(items[0].id).not.toEqual(items[1].id); items.forEach((item) => expect(item.id).toEqual(expect.stringContaining('test-1'))); - expect(runMock.failed).toBeCalledTimes(2); - expect(runMock.passed).toBeCalledTimes(1); + expect(jestRun.vscodeRun.failed).toBeCalledTimes(2); + expect(jestRun.vscodeRun.passed).toBeCalledTimes(1); }); it('can still sync with test results', () => { const a2 = helper.makeAssertion('test-1', 'KnownFail', [], [1, 0]); const a3 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]); setup([a2, a3]); - docRoot.discoverTest(runMock); - expect(runMock.failed).toBeCalledTimes(2); - expect(runMock.passed).toBeCalledTimes(1); + docRoot.discoverTest(jestRun); + expect(jestRun.vscodeRun.failed).toBeCalledTimes(2); + expect(jestRun.vscodeRun.passed).toBeCalledTimes(1); //update a2 status a2.status = 'KnownSuccess'; setup([a2, a3]); - docRoot.discoverTest(runMock); - expect(runMock.failed).toBeCalledTimes(1); - expect(runMock.passed).toBeCalledTimes(2); + docRoot.discoverTest(jestRun); + expect(jestRun.vscodeRun.failed).toBeCalledTimes(1); + expect(jestRun.vscodeRun.passed).toBeCalledTimes(2); }); }); }); @@ -989,9 +957,8 @@ describe('test-item-data', () => { }); it('can adapt raw output to terminal output', () => { const coloredText = '[2Kyarn run v1.22.5\n'; - const converted = '[2Kyarn run v1.22.5\r\n'; - context.appendOutput(coloredText, runMock); - expect(runMock.appendOutput).toBeCalledWith(expect.stringContaining(converted)); + jestRun.write(coloredText); + expect(jestRun.vscodeRun.appendOutput).toBeCalledWith(expect.stringContaining(coloredText)); }); describe('handle run event to set item status and show output', () => { const file = '/ws-1/tests/a.test.ts'; @@ -1040,7 +1007,7 @@ describe('test-item-data', () => { }; const item = getItem(); const data = context.getData(item); - data.scheduleTest(runMock, resolveMock, profile); + data.scheduleTest(jestRun); controllerMock.createTestRun.mockClear(); return item; @@ -1058,63 +1025,60 @@ describe('test-item-data', () => { `('will use run passed from explorer throughout for $targetItem item', ({ itemType }) => { it('item will be enqueued after schedule', () => { const item = setup(itemType); - expect(process.request.itemRun.run.enqueued).toBeCalledWith(item); + expect(process.request.run.vscodeRun.enqueued).toBeCalledWith(item); }); it('item will show started when jest run started', () => { const item = setup(itemType); - process.request.itemRun.run.enqueued.mockClear(); + process.request.run.vscodeRun.enqueued.mockClear(); // scheduled event has no effect onRunEvent({ type: 'scheduled', process }); - expect(process.request.itemRun.run.enqueued).not.toBeCalled(); + expect(process.request.run.vscodeRun.enqueued).not.toBeCalled(); // starting the process onRunEvent({ type: 'start', process }); - expect(process.request.itemRun.item).toBe(item); - expect(process.request.itemRun.run.started).toBeCalledWith(item); + expect(process.request.run.item).toBe(item); + expect(process.request.run.vscodeRun.started).toBeCalledWith(item); //will not create new run expect(controllerMock.createTestRun).not.toBeCalled(); }); it.each` - text | raw | newLine | isError | outputText | outputNewLine | outputColor - ${'text'} | ${'raw'} | ${true} | ${false} | ${'raw'} | ${true} | ${undefined} - ${'text'} | ${'raw'} | ${false} | ${undefined} | ${'raw'} | ${false} | ${undefined} - ${'text'} | ${'raw'} | ${undefined} | ${undefined} | ${'raw'} | ${false} | ${undefined} - ${'text'} | ${'raw'} | ${true} | ${true} | ${'raw'} | ${true} | ${'red'} - ${'text'} | ${undefined} | ${true} | ${true} | ${'text'} | ${true} | ${'red'} + case | text | raw | newLine | isError | outputText | outputOptions + ${1} | ${'text'} | ${'raw'} | ${true} | ${false} | ${'raw'} | ${'new-line'} + ${2} | ${'text'} | ${'raw'} | ${false} | ${undefined} | ${'raw'} | ${undefined} + ${3} | ${'text'} | ${'raw'} | ${undefined} | ${undefined} | ${'raw'} | ${undefined} + ${4} | ${'text'} | ${'raw'} | ${true} | ${true} | ${'raw'} | ${'error'} + ${5} | ${'text'} | ${undefined} | ${true} | ${true} | ${'text'} | ${'error'} `( - 'can output process data: $text, $raw, $newLine, $isError', - ({ text, raw, newLine, isError, outputText, outputNewLine, outputColor }) => { + 'can output process data: case $case', + ({ text, raw, newLine, isError, outputText, outputOptions }) => { setup(itemType); - const appendOutput = jest.spyOn(context, 'appendOutput'); onRunEvent({ type: 'start', process }); onRunEvent({ type: 'data', process, text, raw, newLine, isError }); expect(controllerMock.createTestRun).not.toBeCalled(); - expect(appendOutput).toBeCalledWith( - outputText, - process.request.itemRun.run, - outputNewLine, - outputColor - ); + expect(context.output.write).toBeCalledWith(outputText, outputOptions); } ); - it.each([['end'], ['exit']])( - "will only resolve the promise and not close the run for event '%s'", - (eventType) => { - setup(itemType); - onRunEvent({ type: 'start', process }); - expect(controllerMock.createTestRun).not.toBeCalled(); - expect(process.request.itemRun.run.started).toBeCalled(); + it.each([ + { type: 'end' }, + { type: 'exit', error: 'something is wrong' }, + { type: 'exit', error: 'something is wrong', code: 127 }, + { type: 'exit', error: 'something is wrong', code: 1 }, + ])("will only resolve the promise and not close the run for event '%s'", (event) => { + setup(itemType); + onRunEvent({ type: 'start', process }); + expect(controllerMock.createTestRun).not.toBeCalled(); + expect(process.request.run.vscodeRun.started).toBeCalled(); - onRunEvent({ type: eventType, process }); - expect(process.request.itemRun.run.end).not.toBeCalled(); - expect(resolveMock).toBeCalled(); - } - ); + onRunEvent({ ...event, process }); + expect(process.request.run.vscodeRun.end).not.toBeCalled(); + + expect(runEndSpy).toBeCalled(); + }); it('can report exit error even if run is ended', () => { setup(itemType); @@ -1122,15 +1086,15 @@ describe('test-item-data', () => { onRunEvent({ type: 'end', process }); expect(controllerMock.createTestRun).not.toBeCalled(); - expect(process.request.itemRun.run.end).not.toBeCalled(); - expect(resolveMock).toBeCalled(); + expect(process.request.run.vscodeRun.end).not.toBeCalled(); + expect(runEndSpy).toBeCalled(); const error = 'something is wrong'; onRunEvent({ type: 'exit', error, process }); // no new run need to be created expect(controllerMock.createTestRun).not.toBeCalled(); - expect(process.request.itemRun.run.appendOutput).toBeCalledWith( + expect(process.request.run.vscodeRun.appendOutput).toBeCalledWith( expect.stringContaining(error) ); }); @@ -1184,25 +1148,22 @@ describe('test-item-data', () => { expect(controllerMock.createTestRun).toBeCalledTimes(1); }); it.each` - text | raw | newLine | isError | outputText | outputNewLine | outputColor - ${'text'} | ${'raw'} | ${true} | ${false} | ${'raw'} | ${true} | ${undefined} - ${'text'} | ${'raw'} | ${false} | ${undefined} | ${'raw'} | ${false} | ${undefined} - ${'text'} | ${'raw'} | ${undefined} | ${undefined} | ${'raw'} | ${false} | ${undefined} - ${'text'} | ${'raw'} | ${true} | ${true} | ${'raw'} | ${true} | ${'red'} - ${'text'} | ${undefined} | ${true} | ${true} | ${'text'} | ${true} | ${'red'} + case | text | raw | newLine | isError | outputText | outputOptions + ${1} | ${'text'} | ${'raw'} | ${true} | ${false} | ${'raw'} | ${'new-line'} + ${2} | ${'text'} | ${'raw'} | ${false} | ${undefined} | ${'raw'} | ${undefined} + ${3} | ${'text'} | ${'raw'} | ${undefined} | ${undefined} | ${'raw'} | ${undefined} + ${4} | ${'text'} | ${'raw'} | ${true} | ${true} | ${'raw'} | ${'error'} + ${5} | ${'text'} | ${undefined} | ${true} | ${true} | ${'text'} | ${'error'} `( - 'can output process data: ($text, $raw, $newLine, $isError)', - ({ text, raw, newLine, isError, outputText, outputNewLine, outputColor }) => { + 'can output process data: case $case', + ({ text, raw, newLine, isError, outputText, outputOptions }) => { const process = { id: 'whatever', request }; - const appendOutput = jest.spyOn(context, 'appendOutput'); onRunEvent({ type: 'start', process }); onRunEvent({ type: 'data', process, text, raw, newLine, isError }); expect(controllerMock.createTestRun).toBeCalledTimes(1); - const runMock = controllerMock.lastRunMock(); - - expect(appendOutput).toBeCalledWith(outputText, runMock, outputNewLine, outputColor); + expect(context.output.write).toBeCalledWith(outputText, outputOptions); } ); it.each([['end'], ['exit']])("close the run on event '%s'", (eventType) => { @@ -1217,7 +1178,6 @@ describe('test-item-data', () => { expect(runMock.end).toBeCalled(); }); it('can report exit error even if run is ended', () => { - const appendOutput = jest.spyOn(context, 'appendOutput'); const process = { id: 'whatever', request: { type: 'all-tests' } }; onRunEvent({ type: 'start', process }); onRunEvent({ type: 'end', process }); @@ -1232,12 +1192,7 @@ describe('test-item-data', () => { expect(controllerMock.createTestRun).toBeCalledTimes(2); const runMock2 = controllerMock.lastRunMock(); - expect(appendOutput).toBeCalledWith( - error, - runMock2, - expect.anything(), - expect.anything() - ); + expect(context.output.write).toBeCalledWith(error, expect.anything()); expect(runMock2.errored).toBeCalled(); expect(runMock2.end).toBeCalled(); }); @@ -1275,13 +1230,23 @@ describe('test-item-data', () => { const process = mockScheduleProcess(context); const testFileData = context.getData(testFile); - testFileData.scheduleTest(runMock, resolveMock, profile); - expect(runMock.enqueued).toBeCalledTimes(2); - [testFile, testBlock].forEach((t) => expect(runMock.enqueued).toBeCalledWith(t)); + testFileData.scheduleTest(jestRun); + expect(jestRun.vscodeRun.enqueued).toBeCalledTimes(2); + [testFile, testBlock].forEach((t) => expect(jestRun.vscodeRun.enqueued).toBeCalledWith(t)); onRunEvent({ type: 'start', process }); - expect(runMock.started).toBeCalledTimes(2); - [testFile, testBlock].forEach((t) => expect(runMock.started).toBeCalledWith(t)); + expect(jestRun.vscodeRun.started).toBeCalledTimes(2); + [testFile, testBlock].forEach((t) => expect(jestRun.vscodeRun.started).toBeCalledWith(t)); + }); + it('log long-run event', () => { + const process = mockScheduleProcess(context); + + onRunEvent({ type: 'long-run', threshold: 60000, process }); + expect(context.output.write).toBeCalledTimes(1); + expect(context.output.write).toBeCalledWith( + expect.stringContaining('60000'), + errors.LONG_RUNNING_TESTS + ); }); }); }); diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts index 2d19d7c8..7d14e0c3 100644 --- a/tests/test-provider/test-provider.test.ts +++ b/tests/test-provider/test-provider.test.ts @@ -1,12 +1,12 @@ jest.unmock('../../src/test-provider/test-provider'); -jest.unmock('../../src/test-provider/test-provider-context'); +jest.unmock('../../src/test-provider/test-provider-helper'); jest.unmock('./test-helper'); jest.unmock('../../src/appGlobals'); import * as vscode from 'vscode'; import { JestTestProvider } from '../../src/test-provider/test-provider'; import { WorkspaceRoot } from '../../src/test-provider/test-item-data'; -import { JestTestProviderContext } from '../../src/test-provider/test-provider-context'; +import { JestTestProviderContext } from '../../src/test-provider/test-provider-helper'; import { extensionId } from '../../src/appGlobals'; import { mockController, mockExtExplorerContext } from './test-helper'; @@ -135,7 +135,9 @@ describe('JestTestProvider', () => { controllerMock.resolveHandler(); expect(controllerMock.createTestRun).toBeCalled(); expect(workspaceRootMock.discoverTest).toBeCalledTimes(1); - expect(workspaceRootMock.discoverTest).toBeCalledWith(controllerMock.lastRunMock()); + expect(workspaceRootMock.discoverTest).toBeCalledWith( + expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() }) + ); // run will be created with the controller's id expect(controllerMock.lastRunMock().name).toEqual( expect.stringContaining(controllerMock.id) @@ -151,7 +153,9 @@ describe('JestTestProvider', () => { data.item.canResolveChildren = true; controllerMock.resolveHandler(data.item); expect(controllerMock.createTestRun).toBeCalled(); - expect(data.discoverTest).toBeCalledWith(controllerMock.lastRunMock()); + expect(data.discoverTest).toBeCalledWith( + expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() }) + ); // run will be created with the controller's id expect(controllerMock.lastRunMock().name).toEqual( expect.stringContaining(controllerMock.id) @@ -226,14 +230,14 @@ describe('JestTestProvider', () => { debugDone = () => resolve(); }); it.each` - debugInfo | testNamePattern | debugTests | hasError - ${undefined} | ${undefined} | ${() => Promise.resolve()} | ${true} - ${{ fileName: 'file' }} | ${'a test'} | ${() => Promise.resolve()} | ${false} - ${{ fileName: 'file' }} | ${'a test'} | ${() => Promise.reject('error')} | ${true} - ${{ fileName: 'file' }} | ${'a test'} | ${throwError} | ${true} - ${{ fileName: 'file' }} | ${undefined} | ${() => Promise.resolve()} | ${false} + case | debugInfo | testNamePattern | debugTests | hasError + ${1} | ${undefined} | ${undefined} | ${() => Promise.resolve()} | ${true} + ${2} | ${{ fileName: 'file' }} | ${'a test'} | ${() => Promise.resolve()} | ${false} + ${3} | ${{ fileName: 'file' }} | ${'a test'} | ${() => Promise.reject('error')} | ${true} + ${4} | ${{ fileName: 'file' }} | ${'a test'} | ${throwError} | ${true} + ${5} | ${{ fileName: 'file' }} | ${undefined} | ${() => Promise.resolve()} | ${false} `( - "invoke debug test async: debugInfo = '$debugInfo', testNamePattern='$testNamePattern' when resultContextMock.debugTests = $resultContextMock.debugTests => error? $hasError", + 'invoke debug test async case $case => error? $hasError', async ({ debugInfo, testNamePattern, debugTests, hasError }) => { expect.hasAssertions(); extExplorerContextMock.debugTests = jest.fn().mockImplementation(() => { @@ -445,15 +449,16 @@ describe('JestTestProvider', () => { itemDataList.forEach((d) => { expect(d.scheduleTest).toBeCalled(); - const [run, resolve] = d.scheduleTest.mock.calls[0]; - expect(run).toBe(runMock); - // close the schedule - resolve(); + const [run] = d.scheduleTest.mock.calls[0]; + expect(run).toEqual(expect.objectContaining({ vscodeRun: runMock })); + // simulate each item is done the run + run.end(); }); await p; - expect(runMock.end).toBeCalled(); + expect(runMock.end).toBeCalledTimes(1); }); + it('cancellation is passed to the itemData to handle', async () => { expect.hasAssertions(); @@ -476,10 +481,10 @@ describe('JestTestProvider', () => { itemDataList.forEach((d) => { expect(d.scheduleTest).toBeCalled(); - const [run, resolve] = d.scheduleTest.mock.calls[0]; - expect(run).toBe(runMock); + const [run] = d.scheduleTest.mock.calls[0]; + expect(run).toEqual(expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() })); // close the schedule - resolve(); + run.end(); }); await p; @@ -514,16 +519,16 @@ describe('JestTestProvider', () => { itemDataList.forEach((d, idx) => { expect(d.scheduleTest).toBeCalled(); - const [run, resolve] = d.scheduleTest.mock.calls[0]; - expect(run).toBe(runMock); + const [run] = d.scheduleTest.mock.calls[0]; + expect(run).toEqual(expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() })); /* eslint-disable jest/no-conditional-expect */ if (idx === 1) { - expect(run.errored).toBeCalledWith(d.item, expect.anything()); + expect(run.vscodeRun.errored).toBeCalledWith(d.item, expect.anything()); } else { - expect(run.errored).not.toBeCalledWith(d.item, expect.anything()); + expect(run.vscodeRun.errored).not.toBeCalledWith(d.item, expect.anything()); // close the schedule - resolve(); + run.end(); } }); @@ -542,9 +547,9 @@ describe('JestTestProvider', () => { const p = testProvider.runTests(request, cancelToken); const runMock = controllerMock.lastRunMock(); expect(workspaceRootMock.scheduleTest).toBeCalledTimes(1); - const [run, resolve] = workspaceRootMock.scheduleTest.mock.calls[0]; - expect(run).toBe(runMock); - resolve(); + const [run] = workspaceRootMock.scheduleTest.mock.calls[0]; + expect(run).toEqual(expect.objectContaining({ vscodeRun: controllerMock.lastRunMock() })); + run.end(); await p; expect(runMock.end).toBeCalled(); diff --git a/typings/vscode.d.ts b/typings/vscode.d.ts index 431e5362..485dff49 100644 --- a/typings/vscode.d.ts +++ b/typings/vscode.d.ts @@ -136,9 +136,8 @@ declare module 'vscode' { /** * Save the underlying file. * - * @return A promise that will resolve to true when the file - * has been saved. If the file was not dirty or the save failed, - * will return false. + * @return A promise that will resolve to `true` when the file + * has been saved. If the save failed, will return `false`. */ save(): Thenable; @@ -339,7 +338,7 @@ declare module 'vscode' { * @return A position that reflects the given delta. Will return `this` position if the change * is not changing anything. */ - translate(change: { lineDelta?: number; characterDelta?: number; }): Position; + translate(change: { lineDelta?: number; characterDelta?: number }): Position; /** * Create a new position derived from this position. @@ -357,7 +356,7 @@ declare module 'vscode' { * @return A position that reflects the given change. Will return `this` position if the change * is not changing anything. */ - with(change: { line?: number; character?: number; }): Position; + with(change: { line?: number; character?: number }): Position; } /** @@ -463,7 +462,7 @@ declare module 'vscode' { * @return A range that reflects the given change. Will return `this` range if the change * is not changing anything. */ - with(change: { start?: Position, end?: Position }): Range; + with(change: { start?: Position; end?: Position }): Range; } /** @@ -767,8 +766,9 @@ declare module 'vscode' { /** * An optional flag that controls if an {@link TextEditor editor}-tab shows as preview. Preview tabs will - * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends - * on the `workbench.editor.enablePreview`-setting. + * be replaced and reused until set to stay - either explicitly or through editing. + * + * *Note* that the flag is ignored if a user has disabled preview editors in settings. */ preview?: boolean; @@ -778,6 +778,67 @@ declare module 'vscode' { selection?: Range; } + /** + * Represents an event describing the change in a {@link NotebookEditor.selections notebook editor's selections}. + */ + export interface NotebookEditorSelectionChangeEvent { + /** + * The {@link NotebookEditor notebook editor} for which the selections have changed. + */ + readonly notebookEditor: NotebookEditor; + + /** + * The new value for the {@link NotebookEditor.selections notebook editor's selections}. + */ + readonly selections: readonly NotebookRange[]; + } + + /** + * Represents an event describing the change in a {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. + */ + export interface NotebookEditorVisibleRangesChangeEvent { + /** + * The {@link NotebookEditor notebook editor} for which the visible ranges have changed. + */ + readonly notebookEditor: NotebookEditor; + + /** + * The new value for the {@link NotebookEditor.visibleRanges notebook editor's visibleRanges}. + */ + readonly visibleRanges: readonly NotebookRange[]; + } + + /** + * Represents options to configure the behavior of showing a {@link NotebookDocument notebook document} in an {@link NotebookEditor notebook editor}. + */ + export interface NotebookDocumentShowOptions { + /** + * An optional view column in which the {@link NotebookEditor notebook editor} should be shown. + * The default is the {@link ViewColumn.Active active}, other values are adjusted to + * be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is + * not adjusted. Use {@linkcode ViewColumn.Beside} to open the + * editor to the side of the currently active one. + */ + readonly viewColumn?: ViewColumn; + + /** + * An optional flag that when `true` will stop the {@link NotebookEditor notebook editor} from taking focus. + */ + readonly preserveFocus?: boolean; + + /** + * An optional flag that controls if an {@link NotebookEditor notebook editor}-tab shows as preview. Preview tabs will + * be replaced and reused until set to stay - either explicitly or through editing. The default behaviour depends + * on the `workbench.editor.enablePreview`-setting. + */ + readonly preview?: boolean; + + /** + * An optional selection to apply for the document in the {@link NotebookEditor notebook editor}. + */ + readonly selections?: readonly NotebookRange[]; + } + /** * A reference to one of the workbench colors as defined in https://code.visualstudio.com/docs/getstarted/theme-color-reference. * Using a theme color is preferred over a custom color as it gives theme authors and users the possibility to change the color. @@ -1138,7 +1199,7 @@ declare module 'vscode' { * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit. * @return A promise that resolves with a value indicating if the edits could be applied. */ - edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + edit(callback: (editBuilder: TextEditorEdit) => void, options?: { readonly undoStopBefore: boolean; readonly undoStopAfter: boolean }): Thenable; /** * Insert a {@link SnippetString snippet} and put the editor into snippet mode. "Snippet mode" @@ -1151,7 +1212,7 @@ declare module 'vscode' { * @return A promise that resolves with a value indicating if the snippet could be inserted. Note that the promise does not signal * that the snippet is completely filled-in or accepted. */ - insertSnippet(snippet: SnippetString, location?: Position | Range | readonly Position[] | readonly Range[], options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable; + insertSnippet(snippet: SnippetString, location?: Position | Range | readonly Position[] | readonly Range[], options?: { readonly undoStopBefore: boolean; readonly undoStopAfter: boolean }): Thenable; /** * Adds a set of decorations to the text editor. If a set of decorations already exists with @@ -1254,7 +1315,7 @@ declare module 'vscode' { export class Uri { /** - * Create an URI from a string, e.g. `http://www.msft.com/some/path`, + * Create an URI from a string, e.g. `http://www.example.com/some/path`, * `file:///usr/home`, or `scheme:with/path`. * * *Note* that for a while uris without a `scheme` were accepted. That is not correct @@ -1322,7 +1383,7 @@ declare module 'vscode' { * @param components The component parts of an Uri. * @return A new Uri instance. */ - static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri; + static from(components: { readonly scheme: string; readonly authority?: string; readonly path?: string; readonly query?: string; readonly fragment?: string }): Uri; /** * Use the `file` and `parse` factory functions to create new `Uri` objects. @@ -1330,29 +1391,29 @@ declare module 'vscode' { private constructor(scheme: string, authority: string, path: string, query: string, fragment: string); /** - * Scheme is the `http` part of `http://www.msft.com/some/path?query#fragment`. + * Scheme is the `http` part of `http://www.example.com/some/path?query#fragment`. * The part before the first colon. */ readonly scheme: string; /** - * Authority is the `www.msft.com` part of `http://www.msft.com/some/path?query#fragment`. + * Authority is the `www.example.com` part of `http://www.example.com/some/path?query#fragment`. * The part between the first double slashes and the next slash. */ readonly authority: string; /** - * Path is the `/some/path` part of `http://www.msft.com/some/path?query#fragment`. + * Path is the `/some/path` part of `http://www.example.com/some/path?query#fragment`. */ readonly path: string; /** - * Query is the `query` part of `http://www.msft.com/some/path?query#fragment`. + * Query is the `query` part of `http://www.example.com/some/path?query#fragment`. */ readonly query: string; /** - * Fragment is the `fragment` part of `http://www.msft.com/some/path?query#fragment`. + * Fragment is the `fragment` part of `http://www.example.com/some/path?query#fragment`. */ readonly fragment: string; @@ -1485,22 +1546,25 @@ declare module 'vscode' { export class Disposable { /** - * Combine many disposable-likes into one. Use this method - * when having objects with a dispose function which are not - * instances of Disposable. + * Combine many disposable-likes into one. You can use this method when having objects with + * a dispose function which aren't instances of `Disposable`. * - * @param disposableLikes Objects that have at least a `dispose`-function member. + * @param disposableLikes Objects that have at least a `dispose`-function member. Note that asynchronous + * dispose-functions aren't awaited. * @return Returns a new disposable which, upon dispose, will * dispose all provided disposables. */ static from(...disposableLikes: { dispose: () => any }[]): Disposable; /** - * Creates a new Disposable calling the provided function + * Creates a new disposable that calls the provided function * on dispose. + * + * *Note* that an asynchronous function is not awaited. + * * @param callOnDispose Function that disposes something. */ - constructor(callOnDispose: Function); + constructor(callOnDispose: () => any); /** * Dispose this object. @@ -1544,6 +1608,7 @@ declare module 'vscode' { /** * The event listeners can subscribe to. */ + // eslint-disable-next-line vscode-dts-event-naming event: Event; /** @@ -1636,6 +1701,21 @@ declare module 'vscode' { provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult; } + /** + * The kind of {@link QuickPickItem quick pick item}. + */ + export enum QuickPickItemKind { + /** + * When a {@link QuickPickItem} has a kind of {@link Separator}, the item is just a visual separator and does not represent a real item. + * The only property that applies is {@link QuickPickItem.label label }. All other properties on {@link QuickPickItem} will be ignored and have no effect. + */ + Separator = -1, + /** + * The default {@link QuickPickItem.kind} is an item that can be selected in the quick pick. + */ + Default = 0, + } + /** * Represents an item that can be selected from * a list of items. @@ -1648,15 +1728,25 @@ declare module 'vscode' { */ label: string; + /** + * The kind of QuickPickItem that will determine how this item is rendered in the quick pick. When not specified, + * the default is {@link QuickPickItemKind.Default}. + */ + kind?: QuickPickItemKind; + /** * A human-readable string which is rendered less prominent in the same line. Supports rendering of * {@link ThemeIcon theme icons} via the `$()`-syntax. + * + * Note: this property is ignored when {@link QuickPickItem.kind kind} is set to {@link QuickPickItemKind.Separator} */ description?: string; /** * A human-readable string which is rendered less prominent in a separate line. Supports rendering of * {@link ThemeIcon theme icons} via the `$()`-syntax. + * + * Note: this property is ignored when {@link QuickPickItem.kind kind} is set to {@link QuickPickItemKind.Separator} */ detail?: string; @@ -1667,11 +1757,15 @@ declare module 'vscode' { * (*Note:* This is only honored when the picker allows multiple selections.) * * @see {@link QuickPickOptions.canPickMany} + * + * Note: this property is ignored when {@link QuickPickItem.kind kind} is set to {@link QuickPickItemKind.Separator} */ picked?: boolean; /** * Always show this item. + * + * Note: this property is ignored when {@link QuickPickItem.kind kind} is set to {@link QuickPickItemKind.Separator} */ alwaysShow?: boolean; @@ -1680,6 +1774,8 @@ declare module 'vscode' { * an {@link QuickPickItemButtonEvent} when clicked. Buttons are only rendered when using a quickpick * created by the {@link window.createQuickPick()} API. Buttons are not rendered when using * the {@link window.showQuickPick()} API. + * + * Note: this property is ignored when {@link QuickPickItem.kind kind} is set to {@link QuickPickItemKind.Separator} */ buttons?: readonly QuickInputButton[]; } @@ -1879,6 +1975,32 @@ declare module 'vscode' { detail?: string; } + /** + * Impacts the behavior and appearance of the validation message. + */ + export enum InputBoxValidationSeverity { + Info = 1, + Warning = 2, + Error = 3 + } + + /** + * Object to configure the behavior of the validation message. + */ + export interface InputBoxValidationMessage { + /** + * The validation message to display. + */ + readonly message: string; + + /** + * The severity of the validation message. + * NOTE: When using `InputBoxValidationSeverity.Error`, the user will not be allowed to accept (hit ENTER) the input. + * `Info` and `Warning` will still allow the InputBox to accept the input. + */ + readonly severity: InputBoxValidationSeverity; + } + /** * Options to configure the behavior of the input box UI. */ @@ -1890,12 +2012,12 @@ declare module 'vscode' { title?: string; /** - * The value to prefill in the input box. + * The value to pre-fill in the input box. */ value?: string; /** - * Selection of the prefilled {@linkcode InputBoxOptions.value value}. Defined as tuple of two number where the + * Selection of the pre-filled {@linkcode InputBoxOptions.value value}. Defined as tuple of two number where the * first is the inclusive start index and the second the exclusive end index. When `undefined` the whole * word will be selected, when empty (start equals end) only the cursor will be set, * otherwise the defined range will be selected. @@ -1928,10 +2050,11 @@ declare module 'vscode' { * to the user. * * @param value The current value of the input box. - * @return A human-readable string which is presented as diagnostic message. - * Return `undefined`, `null`, or the empty string when 'value' is valid. + * @return Either a human-readable string which is presented as an error message or an {@link InputBoxValidationMessage} + * which can provide a specific message severity. Return `undefined`, `null`, or the empty string when 'value' is valid. */ - validateInput?(value: string): string | undefined | null | Thenable; + validateInput?(value: string): string | InputBoxValidationMessage | undefined | null | + Thenable; } /** @@ -1945,6 +2068,18 @@ declare module 'vscode' { /** * A base file path to which this pattern will be matched against relatively. */ + baseUri: Uri; + + /** + * A base file path to which this pattern will be matched against relatively. + * + * This matches the `fsPath` value of {@link RelativePattern.baseUri}. + * + * *Note:* updating this value will update {@link RelativePattern.baseUri} to + * be a uri with `file` scheme. + * + * @deprecated This property is deprecated, please use {@link RelativePattern.baseUri} instead. + */ base: string; /** @@ -1978,7 +2113,7 @@ declare module 'vscode' { * Otherwise, a uri or string should only be used if the pattern is for a file path outside the workspace. * @param pattern A file glob pattern like `*.{ts,js}` that will be matched on paths relative to the base. */ - constructor(base: WorkspaceFolder | Uri | string, pattern: string) + constructor(base: WorkspaceFolder | Uri | string, pattern: string); } /** @@ -1986,7 +2121,7 @@ declare module 'vscode' { * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a {@link RelativePattern relative pattern}. * * Glob patterns can have the following syntax: - * * `*` to match one or more characters in a path segment + * * `*` to match zero or more characters in a path segment * * `?` to match on one character in a path segment * * `**` to match any number of path segments, including none * * `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) @@ -2018,6 +2153,18 @@ declare module 'vscode' { */ readonly language?: string; + /** + * The {@link NotebookDocument.notebookType type} of a notebook, like `jupyter-notebook`. This allows + * to narrow down on the type of a notebook that a {@link NotebookCell.document cell document} belongs to. + * + * *Note* that setting the `notebookType`-property changes how `scheme` and `pattern` are interpreted. When set + * they are evaluated against the {@link NotebookDocument.uri notebook uri}, not the document uri. + * + * @example Match python document inside jupyter notebook that aren't stored yet (`untitled`) + * { language: 'python', notebookType: 'jupyter-notebook', scheme: 'untitled' } + */ + readonly notebookType?: string; + /** * A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. */ @@ -2614,6 +2761,27 @@ declare module 'vscode' { */ supportHtml?: boolean; + /** + * Uri that relative paths are resolved relative to. + * + * If the `baseUri` ends with `/`, it is considered a directory and relative paths in the markdown are resolved relative to that directory: + * + * ```ts + * const md = new vscode.MarkdownString(`[link](./file.js)`); + * md.baseUri = vscode.Uri.file('/path/to/dir/'); + * // Here 'link' in the rendered markdown resolves to '/path/to/dir/file.js' + * ``` + * + * If the `baseUri` is a file, relative paths in the markdown are resolved relative to the parent dir of that file: + * + * ```ts + * const md = new vscode.MarkdownString(`[link](./file.js)`); + * md.baseUri = vscode.Uri.file('/path/to/otherFile.js'); + * // Here 'link' in the rendered markdown resolves to '/path/to/file.js' + * ``` + */ + baseUri?: Uri; + /** * Creates a new markdown string with the given value. * @@ -3170,7 +3338,7 @@ declare module 'vscode' { /** * Include the declaration of the current symbol. */ - includeDeclaration: boolean; + readonly includeDeclaration: boolean; } /** @@ -3363,7 +3531,7 @@ declare module 'vscode' { * be applied successfully. * @param metadata Optional metadata for the entry. */ - createFile(uri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; + createFile(uri: Uri, options?: { overwrite?: boolean; ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; /** * Delete a file or folder. @@ -3371,7 +3539,7 @@ declare module 'vscode' { * @param uri The uri of the file that is to be deleted. * @param metadata Optional metadata for the entry. */ - deleteFile(uri: Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; + deleteFile(uri: Uri, options?: { recursive?: boolean; ignoreIfNotExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; /** * Rename a file or folder. @@ -3382,7 +3550,7 @@ declare module 'vscode' { * ignored. When overwrite and ignoreIfExists are both set overwrite wins. * @param metadata Optional metadata for the entry. */ - renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; + renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean; ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; /** * Get all text edits grouped by resource. @@ -3399,8 +3567,8 @@ declare module 'vscode' { * A snippet can define tab stops and placeholders with `$1`, `$2` * and `${3:foo}`. `$0` defines the final tab stop, it defaults to * the end of the snippet. Variables are defined with `$name` and - * `${name:default value}`. The full snippet syntax is documented - * [here](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_creating-your-own-snippets). + * `${name:default value}`. Also see + * [the full snippet syntax](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_creating-your-own-snippets). */ export class SnippetString { @@ -3451,7 +3619,7 @@ declare module 'vscode' { * value starting at 1. * @return This snippet string. */ - appendChoice(values: string[], number?: number): SnippetString; + appendChoice(values: readonly string[], number?: number): SnippetString; /** * Builder-function that appends a variable (`${VAR}`) to @@ -3489,7 +3657,7 @@ declare module 'vscode' { * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol * which is being renamed - when omitted the text in the returned range is used. * - * *Note: * This function should throw an error or return a rejected thenable when the provided location + * *Note:* This function should throw an error or return a rejected thenable when the provided location * doesn't allow for a rename. * * @param document The document in which rename will be invoked. @@ -3497,7 +3665,7 @@ declare module 'vscode' { * @param token A cancellation token. * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`. */ - prepareRename?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + prepareRename?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; } /** @@ -3543,7 +3711,7 @@ declare module 'vscode' { * @param tokenType The token type. * @param tokenModifiers The token modifiers. */ - push(range: Range, tokenType: string, tokenModifiers?: string[]): void; + push(range: Range, tokenType: string, tokenModifiers?: readonly string[]): void; /** * Finish and create a `SemanticTokens` instance. @@ -4163,7 +4331,7 @@ declare module 'vscode' { * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}. * *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position. */ - range?: Range | { inserting: Range; replacing: Range; }; + range?: Range | { inserting: Range; replacing: Range }; /** * An optional set of characters that when pressed while this completion is active will accept it first and @@ -4331,6 +4499,144 @@ declare module 'vscode' { resolveCompletionItem?(item: T, token: CancellationToken): ProviderResult; } + + /** + * The inline completion item provider interface defines the contract between extensions and + * the inline completion feature. + * + * Providers are asked for completions either explicitly by a user gesture or implicitly when typing. + */ + export interface InlineCompletionItemProvider { + + /** + * Provides inline completion items for the given position and document. + * If inline completions are enabled, this method will be called whenever the user stopped typing. + * It will also be called when the user explicitly triggers inline completions or explicitly asks for the next or previous inline completion. + * In that case, all available inline completions should be returned. + * `context.triggerKind` can be used to distinguish between these scenarios. + * + * @param document The document inline completions are requested for. + * @param position The position inline completions are requested for. + * @param context A context object with additional information. + * @param token A cancellation token. + * @return An array of completion items or a thenable that resolves to an array of completion items. + */ + provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + } + + /** + * Represents a collection of {@link InlineCompletionItem inline completion items} to be presented + * in the editor. + */ + export class InlineCompletionList { + /** + * The inline completion items. + */ + items: InlineCompletionItem[]; + + /** + * Creates a new list of inline completion items. + */ + constructor(items: InlineCompletionItem[]); + } + + /** + * Provides information about the context in which an inline completion was requested. + */ + export interface InlineCompletionContext { + /** + * Describes how the inline completion was triggered. + */ + readonly triggerKind: InlineCompletionTriggerKind; + + /** + * Provides information about the currently selected item in the autocomplete widget if it is visible. + * + * If set, provided inline completions must extend the text of the selected item + * and use the same range, otherwise they are not shown as preview. + * As an example, if the document text is `console.` and the selected item is `.log` replacing the `.` in the document, + * the inline completion must also replace `.` and start with `.log`, for example `.log()`. + * + * Inline completion providers are requested again whenever the selected item changes. + */ + readonly selectedCompletionInfo: SelectedCompletionInfo | undefined; + } + + /** + * Describes the currently selected completion item. + */ + export interface SelectedCompletionInfo { + /** + * The range that will be replaced if this completion item is accepted. + */ + readonly range: Range; + + /** + * The text the range will be replaced with if this completion is accepted. + */ + readonly text: string; + } + + /** + * Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. + */ + export enum InlineCompletionTriggerKind { + /** + * Completion was triggered explicitly by a user gesture. + * Return multiple completion items to enable cycling through them. + */ + Invoke = 0, + + /** + * Completion was triggered automatically while editing. + * It is sufficient to return a single completion item in this case. + */ + Automatic = 1, + } + + /** + * An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. + * + * @see {@link InlineCompletionItemProvider.provideInlineCompletionItems} + */ + export class InlineCompletionItem { + /** + * The text to replace the range with. Must be set. + * Is used both for the preview and the accept operation. + */ + insertText: string | SnippetString; + + /** + * A text that is used to decide if this inline completion should be shown. When `falsy` + * the {@link InlineCompletionItem.insertText} is used. + * + * An inline completion is shown if the text to replace is a prefix of the filter text. + */ + filterText?: string; + + /** + * The range to replace. + * Must begin and end on the same line. + * + * Prefer replacements over insertions to provide a better experience when the user deletes typed text. + */ + range?: Range; + + /** + * An optional {@link Command} that is executed *after* inserting this completion. + */ + command?: Command; + + /** + * Creates a new inline completion item. + * + * @param insertText The text to replace the range with. + * @param range The range to replace. If not set, the word at the requested position will be used. + * @param command An optional {@link Command} that is executed *after* inserting this completion. + */ + constructor(insertText: string | SnippetString, range?: Range, command?: Command); + } + /** * A document link is a range in a text document that links to an internal or external resource, like another * text document or a web site. @@ -4450,7 +4756,6 @@ declare module 'vscode' { * * @param range The range the color appears in. Must not be empty. * @param color The value of the color. - * @param format The format in which this color is currently formatted. */ constructor(range: Range, color: Color); } @@ -4518,107 +4823,277 @@ declare module 'vscode' { * @return An array of color presentations or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ - provideColorPresentations(color: Color, context: { document: TextDocument, range: Range }, token: CancellationToken): ProviderResult; + provideColorPresentations(color: Color, context: { readonly document: TextDocument; readonly range: Range }, token: CancellationToken): ProviderResult; } /** - * A line based folding range. To be valid, start and end line must be bigger than zero and smaller than the number of lines in the document. - * Invalid ranges will be ignored. + * Inlay hint kinds. + * + * The kind of an inline hint defines its appearance, e.g the corresponding foreground and background colors are being + * used. */ - export class FoldingRange { - + export enum InlayHintKind { /** - * The zero-based start line of the range to fold. The folded area starts after the line's last character. - * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + * An inlay hint that for a type annotation. */ - start: number; - + Type = 1, /** - * The zero-based end line of the range to fold. The folded area ends with the line's last character. - * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + * An inlay hint that is for a parameter. */ - end: number; + Parameter = 2, + } + + /** + * An inlay hint label part allows for interactive and composite labels of inlay hints. + */ + export class InlayHintLabelPart { /** - * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or - * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands - * like 'Fold all comments'. See - * {@link FoldingRangeKind} for an enumeration of all kinds. - * If not set, the range is originated from a syntax element. + * The value of this label part. */ - kind?: FoldingRangeKind; + value: string; /** - * Creates a new folding range. + * The tooltip text when you hover over this label part. * - * @param start The start line of the folded range. - * @param end The end line of the folded range. - * @param kind The kind of the folding range. + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. */ - constructor(start: number, end: number, kind?: FoldingRangeKind); - } + tooltip?: string | MarkdownString | undefined; - /** - * An enumeration of specific folding range kinds. The kind is an optional field of a {@link FoldingRange} - * and is used to distinguish specific folding ranges such as ranges originated from comments. The kind is used by commands like - * `Fold all comments` or `Fold all regions`. - * If the kind is not set on the range, the range originated from a syntax element other than comments, imports or region markers. - */ - export enum FoldingRangeKind { /** - * Kind for folding range representing a comment. + * An optional {@link Location source code location} that represents this label + * part. + * + * The editor will use this location for the hover and for code navigation features: This + * part will become a clickable link that resolves to the definition of the symbol at the + * given location (not necessarily the location itself), it shows the hover that shows at + * the given location, and it shows a context menu with further code navigation commands. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. */ - Comment = 1, + location?: Location | undefined; + /** - * Kind for folding range representing a import. + * An optional command for this label part. + * + * The editor renders parts with commands as clickable links. The command is added to the context menu + * when a label part defines {@link InlayHintLabelPart.location location} and {@link InlayHintLabelPart.command command} . + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. */ - Imports = 2, + command?: Command | undefined; + /** - * Kind for folding range representing regions originating from folding markers like `#region` and `#endregion`. + * Creates a new inlay hint label part. + * + * @param value The value of the part. */ - Region = 3 - } - - /** - * Folding context (for future use) - */ - export interface FoldingContext { + constructor(value: string); } /** - * The folding range provider interface defines the contract between extensions and - * [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding) in the editor. + * Inlay hint information. */ - export interface FoldingRangeProvider { + export class InlayHint { /** - * An optional event to signal that the folding ranges from this provider have changed. + * The position of this hint. */ - onDidChangeFoldingRanges?: Event; + position: Position; /** - * Returns a list of folding ranges or null and undefined if the provider - * does not want to participate or was cancelled. - * @param document The document in which the command was invoked. - * @param context Additional context information (for future use) - * @param token A cancellation token. + * The label of this hint. A human readable string or an array of {@link InlayHintLabelPart label parts}. + * + * *Note* that neither the string nor the label part can be empty. */ - provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): ProviderResult; - } - - /** - * A selection range represents a part of a selection hierarchy. A selection range - * may have a parent selection range that contains it. - */ - export class SelectionRange { + label: string | InlayHintLabelPart[]; /** - * The {@link Range} of this selection range. + * The tooltip text when you hover over this item. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. */ - range: Range; + tooltip?: string | MarkdownString | undefined; /** - * The parent selection range containing this range. + * The kind of this hint. The inlay hint kind defines the appearance of this inlay hint. + */ + kind?: InlayHintKind; + + /** + * Optional {@link TextEdit text edits} that are performed when accepting this inlay hint. The default + * gesture for accepting an inlay hint is the double click. + * + * *Note* that edits are expected to change the document so that the inlay hint (or its nearest variant) is + * now part of the document and the inlay hint itself is now obsolete. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. + */ + textEdits?: TextEdit[]; + + /** + * Render padding before the hint. Padding will use the editor's background color, + * not the background color of the hint itself. That means padding can be used to visually + * align/separate an inlay hint. + */ + paddingLeft?: boolean; + + /** + * Render padding after the hint. Padding will use the editor's background color, + * not the background color of the hint itself. That means padding can be used to visually + * align/separate an inlay hint. + */ + paddingRight?: boolean; + + /** + * Creates a new inlay hint. + * + * @param position The position of the hint. + * @param label The label of the hint. + * @param kind The {@link InlayHintKind kind} of the hint. + */ + constructor(position: Position, label: string | InlayHintLabelPart[], kind?: InlayHintKind); + } + + /** + * The inlay hints provider interface defines the contract between extensions and + * the inlay hints feature. + */ + export interface InlayHintsProvider { + + /** + * An optional event to signal that inlay hints from this provider have changed. + */ + onDidChangeInlayHints?: Event; + + /** + * Provide inlay hints for the given range and document. + * + * *Note* that inlay hints that are not {@link Range.contains contained} by the given range are ignored. + * + * @param document The document in which the command was invoked. + * @param range The range for which inlay hints should be computed. + * @param token A cancellation token. + * @return An array of inlay hints or a thenable that resolves to such. + */ + provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; + + /** + * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.textEdits text edits}, + * or complete label {@link InlayHintLabelPart parts}. + * + * *Note* that the editor will resolve an inlay hint at most once. + * + * @param hint An inlay hint. + * @param token A cancellation token. + * @return The resolved inlay hint or a thenable that resolves to such. It is OK to return the given `item`. When no result is returned, the given `item` will be used. + */ + resolveInlayHint?(hint: T, token: CancellationToken): ProviderResult; + } + + /** + * A line based folding range. To be valid, start and end line must be bigger than zero and smaller than the number of lines in the document. + * Invalid ranges will be ignored. + */ + export class FoldingRange { + + /** + * The zero-based start line of the range to fold. The folded area starts after the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + start: number; + + /** + * The zero-based end line of the range to fold. The folded area ends with the line's last character. + * To be valid, the end must be zero or larger and smaller than the number of lines in the document. + */ + end: number; + + /** + * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or + * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands + * like 'Fold all comments'. See + * {@link FoldingRangeKind} for an enumeration of all kinds. + * If not set, the range is originated from a syntax element. + */ + kind?: FoldingRangeKind; + + /** + * Creates a new folding range. + * + * @param start The start line of the folded range. + * @param end The end line of the folded range. + * @param kind The kind of the folding range. + */ + constructor(start: number, end: number, kind?: FoldingRangeKind); + } + + /** + * An enumeration of specific folding range kinds. The kind is an optional field of a {@link FoldingRange} + * and is used to distinguish specific folding ranges such as ranges originated from comments. The kind is used by commands like + * `Fold all comments` or `Fold all regions`. + * If the kind is not set on the range, the range originated from a syntax element other than comments, imports or region markers. + */ + export enum FoldingRangeKind { + /** + * Kind for folding range representing a comment. + */ + Comment = 1, + /** + * Kind for folding range representing a import. + */ + Imports = 2, + /** + * Kind for folding range representing regions originating from folding markers like `#region` and `#endregion`. + */ + Region = 3 + } + + /** + * Folding context (for future use) + */ + export interface FoldingContext { + } + + /** + * The folding range provider interface defines the contract between extensions and + * [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding) in the editor. + */ + export interface FoldingRangeProvider { + + /** + * An optional event to signal that the folding ranges from this provider have changed. + */ + onDidChangeFoldingRanges?: Event; + + /** + * Returns a list of folding ranges or null and undefined if the provider + * does not want to participate or was cancelled. + * @param document The document in which the command was invoked. + * @param context Additional context information (for future use) + * @param token A cancellation token. + */ + provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): ProviderResult; + } + + /** + * A selection range represents a part of a selection hierarchy. A selection range + * may have a parent selection range that contains it. + */ + export class SelectionRange { + + /** + * The {@link Range} of this selection range. + */ + range: Range; + + /** + * The parent selection range containing this range. */ parent?: SelectionRange; @@ -4645,7 +5120,7 @@ declare module 'vscode' { * @return Selection ranges or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - provideSelectionRanges(document: TextDocument, positions: Position[], token: CancellationToken): ProviderResult; + provideSelectionRanges(document: TextDocument, positions: readonly Position[], token: CancellationToken): ProviderResult; } /** @@ -5241,8 +5716,8 @@ declare module 'vscode' { defaultValue?: T; globalValue?: T; - workspaceValue?: T, - workspaceFolderValue?: T, + workspaceValue?: T; + workspaceFolderValue?: T; defaultLanguageValue?: T; globalLanguageValue?: T; @@ -5582,6 +6057,82 @@ declare module 'vscode' { dispose(): void; } + /** + * Represents the severity of a language status item. + */ + export enum LanguageStatusSeverity { + Information = 0, + Warning = 1, + Error = 2 + } + + /** + * A language status item is the preferred way to present language status reports for the active text editors, + * such as selected linter or notifying about a configuration problem. + */ + export interface LanguageStatusItem { + + /** + * The identifier of this item. + */ + readonly id: string; + + /** + * The short name of this item, like 'Java Language Status', etc. + */ + name: string | undefined; + + /** + * A {@link DocumentSelector selector} that defines for what editors + * this item shows. + */ + selector: DocumentSelector; + + /** + * The severity of this item. + * + * Defaults to {@link LanguageStatusSeverity.Information information}. You can use this property to + * signal to users that there is a problem that needs attention, like a missing executable or an + * invalid configuration. + */ + severity: LanguageStatusSeverity; + + /** + * The text to show for the entry. You can embed icons in the text by leveraging the syntax: + * + * `My text $(icon-name) contains icons like $(icon-name) this one.` + * + * Where the icon-name is taken from the ThemeIcon [icon set](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing), e.g. + * `light-bulb`, `thumbsup`, `zap` etc. + */ + text: string; + + /** + * Optional, human-readable details for this item. + */ + detail?: string; + + /** + * Controls whether the item is shown as "busy". Defaults to `false`. + */ + busy: boolean; + + /** + * A {@linkcode Command command} for this item. + */ + command: Command | undefined; + + /** + * Accessibility information used when a screen reader interacts with this item + */ + accessibilityInformation?: AccessibilityInformation; + + /** + * Dispose and free associated resources. + */ + dispose(): void; + } + /** * Denotes a location of an editor in the window. Editors can be arranged in a grid * and each column represents one editor location in that grid by counting the editors @@ -5713,7 +6264,7 @@ declare module 'vscode' { /** * Label to be read out by a screen reader once the item has focus. */ - label: string; + readonly label: string; /** * Role of the widget which defines how a screen reader interacts with it. @@ -5721,7 +6272,7 @@ declare module 'vscode' { * If role is not specified the editor will pick the appropriate role automatically. * More about aria roles can be found here https://w3c.github.io/aria/#widget_roles */ - role?: string; + readonly role?: string; } /** @@ -5938,7 +6489,7 @@ declare module 'vscode' { /** * Assumes a {@link TerminalLocation} of editor and allows specifying a {@link ViewColumn} and - * {@link preserveFocus} property + * {@link TerminalEditorLocationOptions.preserveFocus preserveFocus } property */ export interface TerminalEditorLocationOptions { /** @@ -6214,8 +6765,8 @@ declare module 'vscode' { extensionKind: ExtensionKind; /** - * The public API exported by this extension. It is an invalid action - * to access this field before this extension has been activated. + * The public API exported by this extension (return value of `activate`). + * It is an invalid action to access this field before this extension has been activated. */ readonly exports: T; @@ -6263,6 +6814,8 @@ declare module 'vscode' { /** * An array to which disposables can be added. When this * extension is deactivated the disposables will be disposed. + * + * *Note* that asynchronous dispose-functions aren't awaited. */ readonly subscriptions: { dispose(): any }[]; @@ -6499,7 +7052,8 @@ declare module 'vscode' { export enum ColorThemeKind { Light = 1, Dark = 2, - HighContrast = 3 + HighContrast = 3, + HighContrastLight = 4 } /** @@ -6508,7 +7062,7 @@ declare module 'vscode' { export interface ColorTheme { /** - * The kind of this color theme: light, dark or high contrast. + * The kind of this color theme: light, dark, high contrast dark and high contrast light. */ readonly kind: ColorThemeKind; } @@ -6927,7 +7481,7 @@ declare module 'vscode' { /** * Creates a new task. * - * @param definition The task definition as defined in the taskDefinitions extension point. + * @param taskDefinition The task definition as defined in the taskDefinitions extension point. * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. Global tasks are currently not supported. * @param name The task's name. Is presented in the user interface. * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. @@ -6943,7 +7497,7 @@ declare module 'vscode' { * * @deprecated Use the new constructors that allow specifying a scope for the task. * - * @param definition The task definition as defined in the taskDefinitions extension point. + * @param taskDefinition The task definition as defined in the taskDefinitions extension point. * @param name The task's name. Is presented in the user interface. * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface. * @param execution The process or shell execution. @@ -7403,17 +7957,27 @@ declare module 'vscode' { readonly onDidChangeFile: Event; /** - * Subscribe to events in the file or folder denoted by `uri`. + * Subscribes to file change events in the file or folder denoted by `uri`. For folders, + * the option `recursive` indicates whether subfolders, sub-subfolders, etc. should + * be watched for file changes as well. With `recursive: false`, only changes to the + * files that are direct children of the folder should trigger an event. * - * The editor will call this function for files and folders. In the latter case, the - * options differ from defaults, e.g. what files/folders to exclude from watching - * and if subfolders, sub-subfolder, etc. should be watched (`recursive`). + * The `excludes` array is used to indicate paths that should be excluded from file + * watching. It is typically derived from the `files.watcherExclude` setting that + * is configurable by the user. Each entry can be be: + * - the absolute path to exclude + * - a relative path to exclude (for example `build/output`) + * - a simple glob pattern (for example `**​/build`, `output/**`) * - * @param uri The uri of the file to be watched. + * It is the file system provider's job to call {@linkcode FileSystemProvider.onDidChangeFile onDidChangeFile} + * for every change given these rules. No event should be emitted for files that match any of the provided + * excludes. + * + * @param uri The uri of the file or folder to be watched. * @param options Configures the watch. * @returns A disposable that tells the provider to stop watching the `uri`. */ - watch(uri: Uri, options: { recursive: boolean; excludes: string[] }): Disposable; + watch(uri: Uri, options: { readonly recursive: boolean; readonly excludes: readonly string[] }): Disposable; /** * Retrieve metadata about a file. @@ -7467,7 +8031,7 @@ declare module 'vscode' { * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists, `create` is set but `overwrite` is not set. * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. */ - writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void | Thenable; + writeFile(uri: Uri, content: Uint8Array, options: { readonly create: boolean; readonly overwrite: boolean }): void | Thenable; /** * Delete a file. @@ -7477,7 +8041,7 @@ declare module 'vscode' { * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist. * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. */ - delete(uri: Uri, options: { recursive: boolean }): void | Thenable; + delete(uri: Uri, options: { readonly recursive: boolean }): void | Thenable; /** * Rename a file or folder. @@ -7490,7 +8054,7 @@ declare module 'vscode' { * @throws {@linkcode FileSystemError.FileExists FileExists} when `newUri` exists and when the `overwrite` option is not `true`. * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. */ - rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void | Thenable; + rename(oldUri: Uri, newUri: Uri, options: { readonly overwrite: boolean }): void | Thenable; /** * Copy files or folders. Implementing this function is optional but it will speedup @@ -7504,7 +8068,7 @@ declare module 'vscode' { * @throws {@linkcode FileSystemError.FileExists FileExists} when `destination` exists and when the `overwrite` option is not `true`. * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient. */ - copy?(source: Uri, destination: Uri, options: { overwrite: boolean }): void | Thenable; + copy?(source: Uri, destination: Uri, options: { readonly overwrite: boolean }): void | Thenable; } /** @@ -7565,13 +8129,13 @@ declare module 'vscode' { * @param uri The resource that is to be deleted. * @param options Defines if trash can should be used and if deletion of folders is recursive */ - delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Thenable; + delete(uri: Uri, options?: { recursive?: boolean; useTrash?: boolean }): Thenable; /** * Rename a file or folder. * - * @param oldUri The existing file. - * @param newUri The new location. + * @param source The existing file. + * @param target The new location. * @param options Defines if existing files should be overwritten. */ rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable; @@ -7580,7 +8144,7 @@ declare module 'vscode' { * Copy files or folders. * * @param source The existing file. - * @param destination The destination location. + * @param target The destination location. * @param options Defines if existing files should be overwritten. */ copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable; @@ -7630,7 +8194,7 @@ declare module 'vscode' { /** * Controls whether forms are enabled in the webview content or not. * - * Defaults to true if {@link enableScripts scripts are enabled}. Otherwise defaults to false. + * Defaults to true if {@link WebviewOptions.enableScripts scripts are enabled}. Otherwise defaults to false. * Explicitly setting this property to either true or false overrides the default. */ readonly enableForms?: boolean; @@ -7729,6 +8293,19 @@ declare module 'vscode' { * `package.json`, any `ArrayBuffer` values that appear in `message` will be more * efficiently transferred to the webview and will also be correctly recreated inside * of the webview. + * + * @return A promise that resolves when the message is posted to a webview or when it is + * dropped because the message was not deliverable. + * + * Returns `true` if the message was posted to the webview. Messages can only be posted to + * live webviews (i.e. either visible webviews or hidden webviews that set `retainContextWhenHidden`). + * + * A response of `true` does not mean that the message was actually received by the webview. + * For example, no message listeners may be have been hooked up inside the webview or the webview may + * have been destroyed after the message was posted but before it was received. + * + * If you want confirm that a message as actually received, you can try having your webview posting a + * confirmation message back to your extension. */ postMessage(message: any): Thenable; @@ -8548,7 +9125,7 @@ declare module 'vscode' { * } * }); * - * const callableUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://my.extension/did-authenticate`)); + * const callableUri = await vscode.env.asExternalUri(vscode.Uri.parse(vscode.env.uriScheme + '://my.extension/did-authenticate')); * await vscode.env.openExternal(callableUri); * ``` * @@ -8694,6 +9271,11 @@ declare module 'vscode' { */ export namespace window { + /** + * Represents the grid widget within the main editor area + */ + export const tabGroups: TabGroups; + /** * The currently active editor or `undefined`. The active editor is the one * that currently has focus or, when none has focus, the one that has changed @@ -8739,6 +9321,43 @@ declare module 'vscode' { */ export const onDidChangeTextEditorViewColumn: Event; + /** + * The currently visible {@link NotebookEditor notebook editors} or an empty array. + */ + export const visibleNotebookEditors: readonly NotebookEditor[]; + + /** + * An {@link Event} which fires when the {@link window.visibleNotebookEditors visible notebook editors} + * has changed. + */ + export const onDidChangeVisibleNotebookEditors: Event; + + /** + * The currently active {@link NotebookEditor notebook editor} or `undefined`. The active editor is the one + * that currently has focus or, when none has focus, the one that has changed + * input most recently. + */ + export const activeNotebookEditor: NotebookEditor | undefined; + + /** + * An {@link Event} which fires when the {@link window.activeNotebookEditor active notebook editor} + * has changed. *Note* that the event also fires when the active editor changes + * to `undefined`. + */ + export const onDidChangeActiveNotebookEditor: Event; + + /** + * An {@link Event} which fires when the {@link NotebookEditor.selections notebook editor selections} + * have changed. + */ + export const onDidChangeNotebookEditorSelection: Event; + + /** + * An {@link Event} which fires when the {@link NotebookEditor.visibleRanges notebook editor visible ranges} + * have changed. + */ + export const onDidChangeNotebookEditorVisibleRanges: Event; + /** * The currently opened terminals or an empty array. */ @@ -8810,7 +9429,7 @@ declare module 'vscode' { /** * A short-hand for `openTextDocument(uri).then(document => showTextDocument(document, options))`. * - * @see {@link openTextDocument} + * @see {@link workspace.openTextDocument} * * @param uri A resource identifier. * @param options {@link TextDocumentShowOptions Editor options} to configure the behavior of showing the {@link TextEditor editor}. @@ -8818,6 +9437,16 @@ declare module 'vscode' { */ export function showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; + /** + * Show the given {@link NotebookDocument} in a {@link NotebookEditor notebook editor}. + * + * @param document A text document to be shown. + * @param options {@link NotebookDocumentShowOptions Editor options} to configure the behavior of showing the {@link NotebookEditor notebook editor}. + * + * @return A promise that resolves to an {@link NotebookEditor notebook editor}. + */ + export function showNotebookDocument(document: NotebookDocument, options?: NotebookDocumentShowOptions): Thenable; + /** * Create a TextEditorDecorationType that can be used to add decorations to text editors. * @@ -8834,7 +9463,7 @@ declare module 'vscode' { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - export function showInformationMessage(message: string, ...items: string[]): Thenable; + export function showInformationMessage(message: string, ...items: T[]): Thenable; /** * Show an information message to users. Optionally provide an array of items which will be presented as @@ -8845,7 +9474,7 @@ declare module 'vscode' { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - export function showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + export function showInformationMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show an information message. @@ -8879,7 +9508,7 @@ declare module 'vscode' { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - export function showWarningMessage(message: string, ...items: string[]): Thenable; + export function showWarningMessage(message: string, ...items: T[]): Thenable; /** * Show a warning message. @@ -8891,7 +9520,7 @@ declare module 'vscode' { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - export function showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + export function showWarningMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show a warning message. @@ -8925,7 +9554,7 @@ declare module 'vscode' { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - export function showErrorMessage(message: string, ...items: string[]): Thenable; + export function showErrorMessage(message: string, ...items: T[]): Thenable; /** * Show an error message. @@ -8937,7 +9566,7 @@ declare module 'vscode' { * @param items A set of items that will be rendered as actions in the message. * @return A thenable that resolves to the selected item or `undefined` when being dismissed. */ - export function showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; + export function showErrorMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; /** * Show an error message. @@ -8970,7 +9599,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ - export function showQuickPick(items: readonly string[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly string[] | Thenable, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -8990,7 +9619,7 @@ declare module 'vscode' { * @param token A token that can be used to signal cancellation. * @return A promise that resolves to the selected items or `undefined`. */ - export function showQuickPick(items: readonly T[] | Thenable, options: QuickPickOptions & { canPickMany: true; }, token?: CancellationToken): Thenable; + export function showQuickPick(items: readonly T[] | Thenable, options: QuickPickOptions & { canPickMany: true }, token?: CancellationToken): Thenable; /** * Shows a selection list. @@ -9066,11 +9695,16 @@ declare module 'vscode' { export function createInputBox(): InputBox; /** - * Creates a new {@link OutputChannel output channel} with the given name. + * Creates a new {@link OutputChannel output channel} with the given name and language id + * If language id is not provided, then **Log** is used as default language id. + * + * You can access the visible or active output channel as a {@link TextDocument text document} from {@link window.visibleTextEditors visible editors} or {@link window.activeTextEditor active editor} + * and use the language id to contribute language features like syntax coloring, code lens etc., * * @param name Human-readable string which will be used to represent the channel in the UI. + * @param languageId The identifier of the language associated with the channel. */ - export function createOutputChannel(name: string): OutputChannel; + export function createOutputChannel(name: string, languageId?: string): OutputChannel; /** * Create and show a new webview panel. @@ -9082,7 +9716,7 @@ declare module 'vscode' { * * @return New webview panel. */ - export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { viewColumn: ViewColumn, preserveFocus?: boolean }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; + export function createWebviewPanel(viewType: string, title: string, showOptions: ViewColumn | { readonly viewColumn: ViewColumn; readonly preserveFocus?: boolean }, options?: WebviewPanelOptions & WebviewOptions): WebviewPanel; /** * Set a message to the status bar. This is a short hand for the more powerful @@ -9180,7 +9814,7 @@ declare module 'vscode' { * @return A new Terminal. * @throws When running in an environment where a new process cannot be started. */ - export function createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): Terminal; + export function createTerminal(name?: string, shellPath?: string, shellArgs?: readonly string[] | string): Terminal; /** * Creates a {@link Terminal} with a backing shell process. @@ -9246,7 +9880,7 @@ declare module 'vscode' { * Registers a webview panel serializer. * * Extensions that support reviving should have an `"onWebviewPanel:viewType"` activation event and - * make sure that {@link registerWebviewPanelSerializer} is called during activation. + * make sure that `registerWebviewPanelSerializer` is called during activation. * * Only a single serializer may be registered at a time for a given `viewType`. * @@ -9380,6 +10014,11 @@ declare module 'vscode' { * array containing all selected tree items. */ canSelectMany?: boolean; + + /** + * An optional interface to implement drag and drop in the tree view. + */ + dragAndDropController?: TreeDragAndDropController; } /** @@ -9419,26 +10058,131 @@ declare module 'vscode' { } /** - * Represents a Tree view + * A class for encapsulating data transferred during a drag and drop event. + * + * You can use the `value` of the `DataTransferItem` to get back the object you put into it + * so long as the extension that created the `DataTransferItem` runs in the same extension host. */ - export interface TreeView extends Disposable { + export class DataTransferItem { + asString(): Thenable; + readonly value: any; + constructor(value: any); + } + /** + * A map containing a mapping of the mime type of the corresponding transferred data. + * + * Drag and drop controllers that implement {@link TreeDragAndDropController.handleDrag `handleDrag`} can add additional mime types to the + * data transfer. These additional mime types will only be included in the `handleDrop` when the the drag was initiated from + * an element in the same drag and drop controller. + */ + export class DataTransfer { /** - * Event that is fired when an element is expanded + * Retrieves the data transfer item for a given mime type. + * + * @param mimeType The mime type to get the data transfer item for, such as `text/plain` or `image/png`. + * + * Special mime types: + * - `text/uri-list` — A string with `toString()`ed Uris separated by newlines. To specify a cursor position in the file, + * set the Uri's fragment to `L3,5`, where 3 is the line number and 5 is the column number. */ - readonly onDidExpandElement: Event>; + get(mimeType: string): DataTransferItem | undefined; /** - * Event that is fired when an element is collapsed + * Sets a mime type to data transfer item mapping. + * @param mimeType The mime type to set the data for. + * @param value The data transfer item for the given mime type. */ - readonly onDidCollapseElement: Event>; + set(mimeType: string, value: DataTransferItem): void; /** - * Currently selected elements. + * Allows iteration through the data transfer items. + * @param callbackfn Callback for iteration through the data transfer items. */ - readonly selection: readonly T[]; + forEach(callbackfn: (value: DataTransferItem, key: string) => void): void; + } - /** + /** + * Provides support for drag and drop in `TreeView`. + */ + export interface TreeDragAndDropController { + + /** + * The mime types that the {@link TreeDragAndDropController.handleDrop `handleDrop`} method of this `DragAndDropController` supports. + * This could be well-defined, existing, mime types, and also mime types defined by the extension. + * + * To support drops from trees, you will need to add the mime type of that tree. + * This includes drops from within the same tree. + * The mime type of a tree is recommended to be of the format `application/vnd.code.tree.`. + * + * To learn the mime type of a dragged item: + * 1. Set up your `DragAndDropController` + * 2. Use the Developer: Set Log Level... command to set the level to "Debug" + * 3. Open the developer tools and drag the item with unknown mime type over your tree. The mime types will be logged to the developer console + * + * Note that mime types that cannot be sent to the extension will be omitted. + */ + readonly dropMimeTypes: readonly string[]; + + /** + * The mime types that the {@link TreeDragAndDropController.handleDrag `handleDrag`} method of this `TreeDragAndDropController` may add to the tree data transfer. + * This could be well-defined, existing, mime types, and also mime types defined by the extension. + * + * The recommended mime type of the tree (`application/vnd.code.tree.`) will be automatically added. + */ + readonly dragMimeTypes: readonly string[]; + + /** + * When the user starts dragging items from this `DragAndDropController`, `handleDrag` will be called. + * Extensions can use `handleDrag` to add their {@link DataTransferItem `DataTransferItem`} items to the drag and drop. + * + * When the items are dropped on **another tree item** in **the same tree**, your `DataTransferItem` objects + * will be preserved. Use the recommended mime type for the tree (`application/vnd.code.tree.`) to add + * tree objects in a data transfer. See the documentation for `DataTransferItem` for how best to take advantage of this. + * + * To add a data transfer item that can be dragged into the editor, use the application specific mime type "text/uri-list". + * The data for "text/uri-list" should be a string with `toString()`ed Uris separated by newlines. To specify a cursor position in the file, + * set the Uri's fragment to `L3,5`, where 3 is the line number and 5 is the column number. + * + * @param source The source items for the drag and drop operation. + * @param dataTransfer The data transfer associated with this drag. + * @param token A cancellation token indicating that drag has been cancelled. + */ + handleDrag?(source: readonly T[], dataTransfer: DataTransfer, token: CancellationToken): Thenable | void; + + /** + * Called when a drag and drop action results in a drop on the tree that this `DragAndDropController` belongs to. + * + * Extensions should fire {@link TreeDataProvider.onDidChangeTreeData onDidChangeTreeData} for any elements that need to be refreshed. + * + * @param dataTransfer The data transfer items of the source of the drag. + * @param target The target tree element that the drop is occurring on. When undefined, the target is the root. + * @param token A cancellation token indicating that the drop has been cancelled. + */ + handleDrop?(target: T | undefined, dataTransfer: DataTransfer, token: CancellationToken): Thenable | void; + } + + /** + * Represents a Tree view + */ + export interface TreeView extends Disposable { + + /** + * Event that is fired when an element is expanded + */ + readonly onDidExpandElement: Event>; + + /** + * Event that is fired when an element is collapsed + */ + readonly onDidCollapseElement: Event>; + + /** + * Currently selected elements. + */ + readonly selection: readonly T[]; + + /** * Event that is fired when the {@link TreeView.selection selection} has changed */ readonly onDidChangeSelection: Event>; @@ -9483,7 +10227,7 @@ declare module 'vscode' { * * **NOTE:** The {@link TreeDataProvider} that the `TreeView` {@link window.createTreeView is registered with} with must implement {@link TreeDataProvider.getParent getParent} method to access this API. */ - reveal(element: T, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; + reveal(element: T, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }): Thenable; } /** @@ -9495,7 +10239,7 @@ declare module 'vscode' { * This will trigger the view to update the changed element/root and its children recursively (if shown). * To signal that root has changed, do not pass any argument or pass `undefined` or `null`. */ - onDidChangeTreeData?: Event; + onDidChangeTreeData?: Event; /** * Get {@link TreeItem} representation of the `element` @@ -9747,6 +10491,12 @@ declare module 'vscode' { * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. */ location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; + + /** + * Opt-out of the default terminal persistence on restart and reload. + * This will only take effect when `terminal.integrated.enablePersistentSessions` is enabled. + */ + isTransient?: boolean; } /** @@ -9780,6 +10530,12 @@ declare module 'vscode' { * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. */ location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; + + /** + * Opt-out of the default terminal persistence on restart and reload. + * This will only take effect when `terminal.integrated.enablePersistentSessions` is enabled. + */ + isTransient?: boolean; } /** @@ -10089,17 +10845,20 @@ declare module 'vscode' { /** * Show progress for the source control viewlet, as overlay for the icon and as progress bar - * inside the viewlet (when visible). Neither supports cancellation nor discrete progress. + * inside the viewlet (when visible). Neither supports cancellation nor discrete progress nor + * a label to describe the operation. */ SourceControl = 1, /** * Show progress in the status bar of the editor. Neither supports cancellation nor discrete progress. + * Supports rendering of {@link ThemeIcon theme icons} via the `$()`-syntax in the progress label. */ Window = 10, /** - * Show progress as notification with an optional cancel button. Supports to show infinite and discrete progress. + * Show progress as notification with an optional cancel button. Supports to show infinite and discrete + * progress but does not support rendering of icons. */ Notification = 15 } @@ -10366,8 +11125,10 @@ declare module 'vscode' { /** * An optional validation message indicating a problem with the current input value. + * By returning a string, the InputBox will use a default {@link InputBoxValidationSeverity} of Error. + * Returning undefined clears the validation message. */ - validationMessage: string | undefined; + validationMessage: string | InputBoxValidationMessage | undefined; } /** @@ -10533,7 +11294,7 @@ declare module 'vscode' { * * @param thenable A thenable that resolves to {@link TextEdit pre-save-edits}. */ - waitUntil(thenable: Thenable): void; + waitUntil(thenable: Thenable): void; /** * Allows to pause the event loop until the provided thenable resolved. @@ -10682,7 +11443,7 @@ declare module 'vscode' { /** * The files that are going to be renamed. */ - readonly files: ReadonlyArray<{ readonly oldUri: Uri, readonly newUri: Uri }>; + readonly files: ReadonlyArray<{ readonly oldUri: Uri; readonly newUri: Uri }>; /** * Allows to pause the event and to apply a {@link WorkspaceEdit workspace edit}. @@ -10722,7 +11483,7 @@ declare module 'vscode' { /** * The files that got renamed. */ - readonly files: ReadonlyArray<{ readonly oldUri: Uri, readonly newUri: Uri }>; + readonly files: ReadonlyArray<{ readonly oldUri: Uri; readonly newUri: Uri }>; } /** @@ -10930,24 +11691,128 @@ declare module 'vscode' { * @return true if the operation was successfully started and false otherwise if arguments were used that would result * in invalid workspace folder state (e.g. 2 folders with the same URI). */ - export function updateWorkspaceFolders(start: number, deleteCount: number | undefined | null, ...workspaceFoldersToAdd: { uri: Uri, name?: string }[]): boolean; + export function updateWorkspaceFolders(start: number, deleteCount: number | undefined | null, ...workspaceFoldersToAdd: { readonly uri: Uri; readonly name?: string }[]): boolean; /** - * Creates a file system watcher. + * Creates a file system watcher that is notified on file events (create, change, delete) + * depending on the parameters provided. + * + * By default, all opened {@link workspace.workspaceFolders workspace folders} will be watched + * for file changes recursively. + * + * Additional paths can be added for file watching by providing a {@link RelativePattern} with + * a `base` path to watch. If the `pattern` is complex (e.g. contains `**` or path segments), + * the path will be watched recursively and otherwise will be watched non-recursively (i.e. only + * changes to the first level of the path will be reported). + * + * *Note* that requests for recursive file watchers for a `base` path that is inside the opened + * workspace are ignored given all opened {@link workspace.workspaceFolders workspace folders} are + * watched for file changes recursively by default. Non-recursive file watchers however are always + * supported, even inside the opened workspace because they allow to bypass the configured settings + * for excludes (`files.watcherExclude`). If you need to watch in a location that is typically + * excluded (for example `node_modules` or `.git` folder), then you can use a non-recursive watcher + * in the workspace for this purpose. + * + * If possible, keep the use of recursive watchers to a minimum because recursive file watching + * is quite resource intense. + * + * Providing a `string` as `globPattern` acts as convenience method for watching file events in + * all opened workspace folders. It cannot be used to add more folders for file watching, nor will + * it report any file events from folders that are not part of the opened workspace folders. + * + * Optionally, flags to ignore certain kinds of events can be provided. + * + * To stop listening to events the watcher must be disposed. + * + * *Note* that file events from recursive file watchers may be excluded based on user configuration. + * The setting `files.watcherExclude` helps to reduce the overhead of file events from folders + * that are known to produce many file changes at once (such as `node_modules` folders). As such, + * it is highly recommended to watch with simple patterns that do not require recursive watchers + * where the exclude settings are ignored and you have full control over the events. + * + * *Note* that symbolic links are not automatically followed for file watching unless the path to + * watch itself is a symbolic link. * - * A glob pattern that filters the file events on their absolute path must be provided. Optionally, - * flags to ignore certain kinds of events can be provided. To stop listening to events the watcher must be disposed. + * *Note* that file changes for the path to be watched may not be delivered when the path itself + * changes. For example, when watching a path `/Users/somename/Desktop` and the path itself is + * being deleted, the watcher may not report an event and may not work anymore from that moment on. + * The underlying behaviour depends on the path that is provided for watching: + * * if the path is within any of the workspace folders, deletions are tracked and reported unless + * excluded via `files.watcherExclude` setting + * * if the path is equal to any of the workspace folders, deletions are not tracked + * * if the path is outside of any of the workspace folders, deletions are not tracked * - * *Note* that only files within the current {@link workspace.workspaceFolders workspace folders} can be watched. - * *Note* that when watching for file changes such as '**​/*.js', notifications will not be sent when a parent folder is - * moved or deleted (this is a known limitation of the current implementation and may change in the future). + * If you are interested in being notified when the watched path itself is being deleted, you have + * to watch it's parent folder. Make sure to use a simple `pattern` (such as putting the name of the + * folder) to not accidentally watch all sibling folders recursively. * - * @param globPattern A {@link GlobPattern glob pattern} that is applied to the absolute paths of created, changed, - * and deleted files. Use a {@link RelativePattern relative pattern} to limit events to a certain {@link WorkspaceFolder workspace folder}. + * *Note* that the file paths that are reported for having changed may have a different path casing + * compared to the actual casing on disk on case-insensitive platforms (typically macOS and Windows + * but not Linux). We allow a user to open a workspace folder with any desired path casing and try + * to preserve that. This means: + * * if the path is within any of the workspace folders, the path will match the casing of the + * workspace folder up to that portion of the path and match the casing on disk for children + * * if the path is outside of any of the workspace folders, the casing will match the case of the + * path that was provided for watching + * In the same way, symbolic links are preserved, i.e. the file event will report the path of the + * symbolic link as it was provided for watching and not the target. + * + * ### Examples + * + * The basic anatomy of a file watcher is as follows: + * + * ```ts + * const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(, )); + * + * watcher.onDidChange(uri => { ... }); // listen to files being changed + * watcher.onDidCreate(uri => { ... }); // listen to files/folders being created + * watcher.onDidDelete(uri => { ... }); // listen to files/folders getting deleted + * + * watcher.dispose(); // dispose after usage + * ``` + * + * #### Workspace file watching + * + * If you only care about file events in a specific workspace folder: + * + * ```ts + * vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.workspace.workspaceFolders[0], '**​/*.js')); + * ``` + * + * If you want to monitor file events across all opened workspace folders: + * + * ```ts + * vscode.workspace.createFileSystemWatcher('**​/*.js')); + * ``` + * + * *Note:* the array of workspace folders can be empty if no workspace is opened (empty window). + * + * #### Out of workspace file watching + * + * To watch a folder for changes to *.js files outside the workspace (non recursively), pass in a `Uri` to such + * a folder: + * + * ```ts + * vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(), '*.js')); + * ``` + * + * And use a complex glob pattern to watch recursively: + * + * ```ts + * vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.Uri.file(), '**​/*.js')); + * ``` + * + * Here is an example for watching the active editor for file changes: + * + * ```ts + * vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(vscode.window.activeTextEditor.document.uri, '*')); + * ``` + * + * @param globPattern A {@link GlobPattern glob pattern} that controls which file events the watcher should report. * @param ignoreCreateEvents Ignore when files have been created. * @param ignoreChangeEvents Ignore when files have been changed. * @param ignoreDeleteEvents Ignore when files have been deleted. - * @return A new file system watcher instance. + * @return A new file system watcher instance. Must be disposed when no longer needed. */ export function createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; @@ -10974,7 +11839,8 @@ declare module 'vscode' { * Save all dirty files. * * @param includeUntitled Also save files that have been created during this session. - * @return A thenable that resolves when the files have been saved. + * @return A thenable that resolves when the files have been saved. Will return `false` + * for any file that failed to save. */ export function saveAll(includeUntitled?: boolean): Thenable; @@ -11025,7 +11891,7 @@ declare module 'vscode' { /** * A short-hand for `openTextDocument(Uri.file(fileName))`. * - * @see {@link openTextDocument} + * @see {@link workspace.openTextDocument} * @param fileName A name of a file on disk. * @return A promise that resolves to a {@link TextDocument document}. */ @@ -11039,7 +11905,7 @@ declare module 'vscode' { * @param options Options to control how the document will be created. * @return A promise that resolves to a {@link TextDocument document}. */ - export function openTextDocument(options?: { language?: string; content?: string; }): Thenable; + export function openTextDocument(options?: { language?: string; content?: string }): Thenable; /** * Register a text document content provider. @@ -11118,7 +11984,7 @@ declare module 'vscode' { * {@linkcode notebook.onDidCloseNotebookDocument onDidCloseNotebookDocument}-event can occur at any time after. * * *Note* that opening a notebook does not show a notebook editor. This function only returns a notebook document which - * can be showns in a notebook editor but it can also be used for other things. + * can be shown in a notebook editor but it can also be used for other things. * * @param uri The resource to open. * @returns A promise that resolves to a {@link NotebookDocument notebook} @@ -11129,13 +11995,23 @@ declare module 'vscode' { * Open an untitled notebook. The editor will prompt the user for a file * path when the document is to be saved. * - * @see {@link openNotebookDocument} + * @see {@link workspace.openNotebookDocument} * @param notebookType The notebook type that should be used. * @param content The initial contents of the notebook. * @returns A promise that resolves to a {@link NotebookDocument notebook}. */ export function openNotebookDocument(notebookType: string, content?: NotebookData): Thenable; + /** + * An event that is emitted when a {@link NotebookDocument notebook} has changed. + */ + export const onDidChangeNotebookDocument: Event; + + /** + * An event that is emitted when a {@link NotebookDocument notebook} is saved. + */ + export const onDidSaveNotebookDocument: Event; + /** * Register a {@link NotebookSerializer notebook serializer}. * @@ -11143,7 +12019,7 @@ declare module 'vscode' { * the `onNotebook:` activation event, and extensions must register their serializer in return. * * @param notebookType A notebook. - * @param serializer A notebook serialzier. + * @param serializer A notebook serializer. * @param options Optional context options that define what parts of a notebook should be persisted * @return A {@link Disposable} that unregisters this serializer when being disposed. */ @@ -11276,7 +12152,7 @@ declare module 'vscode' { * @param options Immutable metadata about the provider. * @return A {@link Disposable} that unregisters this provider when being disposed. */ - export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean, readonly isReadonly?: boolean }): Disposable; + export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean; readonly isReadonly?: boolean }): Disposable; /** * When true, the user has explicitly trusted the contents of the workspace. @@ -11295,7 +12171,7 @@ declare module 'vscode' { * a '{@link TextDocument}' or * a '{@link WorkspaceFolder}' */ - export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { uri?: Uri, languageId: string }; + export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { uri?: Uri; languageId: string }; /** * An event describing the change in Configuration @@ -11370,10 +12246,10 @@ declare module 'vscode' { * 1. When {@linkcode DocumentSelector} is an array, compute the match for each contained `DocumentFilter` or language identifier and take the maximum value. * 2. A string will be desugared to become the `language`-part of a {@linkcode DocumentFilter}, so `"fooLang"` is like `{ language: "fooLang" }`. * 3. A {@linkcode DocumentFilter} will be matched against the document by comparing its parts with the document. The following rules apply: - * 1. When the `DocumentFilter` is empty (`{}`) the result is `0` - * 2. When `scheme`, `language`, or `pattern` are defined but one doesn't match, the result is `0` - * 3. Matching against `*` gives a score of `5`, matching via equality or via a glob-pattern gives a score of `10` - * 4. The result is the maximum value of each match + * 1. When the `DocumentFilter` is empty (`{}`) the result is `0` + * 2. When `scheme`, `language`, `pattern`, or `notebook` are defined but one doesn't match, the result is `0` + * 3. Matching against `*` gives a score of `5`, matching via equality or via a glob-pattern gives a score of `10` + * 4. The result is the maximum value of each match * * Samples: * ```js @@ -11381,8 +12257,8 @@ declare module 'vscode' { * doc.uri; //'file:///my/file.js' * doc.languageId; // 'javascript' * match('javascript', doc); // 10; - * match({language: 'javascript'}, doc); // 10; - * match({language: 'javascript', scheme: 'file'}, doc); // 10; + * match({ language: 'javascript' }, doc); // 10; + * match({ language: 'javascript', scheme: 'file' }, doc); // 10; * match('*', doc); // 5 * match('fooLang', doc); // 0 * match(['fooLang', '*'], doc); // 5 @@ -11391,8 +12267,16 @@ declare module 'vscode' { * doc.uri; // 'git:/my/file.js' * doc.languageId; // 'javascript' * match('javascript', doc); // 10; - * match({language: 'javascript', scheme: 'git'}, doc); // 10; + * match({ language: 'javascript', scheme: 'git' }, doc); // 10; * match('*', doc); // 5 + * + * // notebook cell document + * doc.uri; // `vscode-notebook-cell:///my/notebook.ipynb#gl65s2pmha`; + * doc.languageId; // 'python' + * match({ notebookType: 'jupyter-notebook' }, doc) // 10 + * match({ notebookType: 'fooNotebook', language: 'python' }, doc) // 0 + * match({ language: 'python' }, doc) // 10 + * match({ notebookType: '*' }, doc) // 5 * ``` * * @param selector A document selector. @@ -11430,6 +12314,14 @@ declare module 'vscode' { */ export function createDiagnosticCollection(name?: string): DiagnosticCollection; + /** + * Creates a new {@link LanguageStatusItem language status item}. + * + * @param id The identifier of the item. + * @param selector The document selector that defines for what editors the item shows. + */ + export function createLanguageStatusItem(id: string, selector: DocumentSelector): LanguageStatusItem; + /** * Register a completion provider. * @@ -11451,6 +12343,19 @@ declare module 'vscode' { */ export function registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable; + /** + * Registers an inline completion provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline completion provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerInlineCompletionItemProvider(selector: DocumentSelector, provider: InlineCompletionItemProvider): Disposable; + /** * Register a code action provider. * @@ -11754,6 +12659,19 @@ declare module 'vscode' { */ export function registerColorProvider(selector: DocumentSelector, provider: DocumentColorProvider): Disposable; + /** + * Register a inlay hints provider. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inlay hints provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerInlayHintsProvider(selector: DocumentSelector, provider: InlayHintsProvider): Disposable; + /** * Register a folding range provider. * @@ -11823,7 +12741,99 @@ declare module 'vscode' { * @return A {@link Disposable} that unsets this configuration. */ export function setLanguageConfiguration(language: string, configuration: LanguageConfiguration): Disposable; + } + + /** + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. + */ + export enum NotebookEditorRevealType { + /** + * The range will be revealed with as little scrolling as possible. + */ + Default = 0, + + /** + * The range will always be revealed in the center of the viewport. + */ + InCenter = 1, + + /** + * If the range is outside the viewport, it will be revealed in the center of the viewport. + * Otherwise, it will be revealed with as little scrolling as possible. + */ + InCenterIfOutsideViewport = 2, + + /** + * The range will always be revealed at the top of the viewport. + */ + AtTop = 3 + } + + /** + * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. + * Additional properties of the NotebookEditor are available in the proposed + * API, which will be finalized later. + */ + export interface NotebookEditor { + + /** + * The {@link NotebookDocument notebook document} associated with this notebook editor. + */ + readonly notebook: NotebookDocument; + + /** + * The primary selection in this notebook editor. + */ + selection: NotebookRange; + + /** + * All selections in this notebook editor. + * + * The primary selection (or focused range) is `selections[0]`. When the document has no cells, the primary selection is empty `{ start: 0, end: 0 }`; + */ + selections: readonly NotebookRange[]; + + /** + * The current visible ranges in the editor (vertically). + */ + readonly visibleRanges: readonly NotebookRange[]; + + /** + * The column in which this editor shows. + */ + readonly viewColumn?: ViewColumn; + + /** + * Scroll as indicated by `revealType` in order to reveal the given range. + * + * @param range A range. + * @param revealType The scrolling strategy for revealing `range`. + */ + revealRange(range: NotebookRange, revealType?: NotebookEditorRevealType): void; + } + /** + * Renderer messaging is used to communicate with a single renderer. It's returned from {@link notebooks.createRendererMessaging}. + */ + export interface NotebookRendererMessaging { + /** + * An event that fires when a message is received from a renderer. + */ + readonly onDidReceiveMessage: Event<{ + readonly editor: NotebookEditor; + readonly message: any; + }>; + + /** + * Send a message to one or all renderer. + * + * @param message Message to send + * @param editor Editor to target with the message. If not provided, the + * message is sent to all renderers. + * @returns a boolean indicating whether the message was successfully + * delivered to any renderer. + */ + postMessage(message: any, editor?: NotebookEditor): Thenable; } /** @@ -11889,39 +12899,6 @@ declare module 'vscode' { readonly executionSummary: NotebookCellExecutionSummary | undefined; } - /** - * Represents a notebook editor that is attached to a {@link NotebookDocument notebook}. - * Additional properties of the NotebookEditor are available in the proposed - * API, which will be finalized later. - */ - export interface NotebookEditor { - - } - - /** - * Renderer messaging is used to communicate with a single renderer. It's returned from {@link notebooks.createRendererMessaging}. - */ - export interface NotebookRendererMessaging { - /** - * An event that fires when a message is received from a renderer. - */ - readonly onDidReceiveMessage: Event<{ - readonly editor: NotebookEditor; - readonly message: any; - }>; - - /** - * Send a message to one or all renderer. - * - * @param message Message to send - * @param editor Editor to target with the message. If not provided, the - * message is sent to all renderers. - * @returns a boolean indicating whether the message was successfully - * delivered to any renderer. - */ - postMessage(message: any, editor?: NotebookEditor): Thenable; - } - /** * Represents a notebook which itself is a sequence of {@link NotebookCell code or markup cells}. Notebook documents are * created from {@link NotebookData notebook data}. @@ -12001,6 +12978,94 @@ declare module 'vscode' { save(): Thenable; } + /** + * Describes a change to a notebook cell. + * + * @see {@link NotebookDocumentChangeEvent} + */ + export interface NotebookDocumentCellChange { + + /** + * The affected notebook. + */ + readonly cell: NotebookCell; + + /** + * The document of the cell or `undefined` when it did not change. + * + * *Note* that you should use the {@link workspace.onDidChangeTextDocument onDidChangeTextDocument}-event + * for detailed change information, like what edits have been performed. + */ + readonly document: TextDocument | undefined; + + /** + * The new metadata of the cell or `undefined` when it did not change. + */ + readonly metadata: { [key: string]: any } | undefined; + + /** + * The new outputs of the cell or `undefined` when they did not change. + */ + readonly outputs: readonly NotebookCellOutput[] | undefined; + + /** + * The new execution summary of the cell or `undefined` when it did not change. + */ + readonly executionSummary: NotebookCellExecutionSummary | undefined; + } + + /** + * Describes a structural change to a notebook document, e.g newly added and removed cells. + * + * @see {@link NotebookDocumentChangeEvent} + */ + export interface NotebookDocumentContentChange { + + /** + * The range at which cells have been either added or removed. + * + * Note that no cells have been {@link NotebookDocumentContentChange.removedCells removed} + * when this range is {@link NotebookRange.isEmpty empty}. + */ + readonly range: NotebookRange; + + /** + * Cells that have been added to the document. + */ + readonly addedCells: readonly NotebookCell[]; + + /** + * Cells that have been removed from the document. + */ + readonly removedCells: readonly NotebookCell[]; + } + + /** + * An event describing a transactional {@link NotebookDocument notebook} change. + */ + export interface NotebookDocumentChangeEvent { + + /** + * The affected notebook. + */ + readonly notebook: NotebookDocument; + + /** + * The new metadata of the notebook or `undefined` when it did not change. + */ + readonly metadata: { [key: string]: any } | undefined; + + /** + * An array of content changes describing added or removed {@link NotebookCell cells}. + */ + readonly contentChanges: readonly NotebookDocumentContentChange[]; + + /** + * An array of {@link NotebookDocumentCellChange cell changes}. + */ + readonly cellChanges: readonly NotebookDocumentCellChange[]; + } + /** * The summary of a notebook cell execution. */ @@ -12019,7 +13084,7 @@ declare module 'vscode' { /** * The times at which execution started and ended, as unix timestamps */ - readonly timing?: { startTime: number, endTime: number }; + readonly timing?: { readonly startTime: number; readonly endTime: number }; } /** @@ -12059,7 +13124,7 @@ declare module 'vscode' { * @return A range that reflects the given change. Will return `this` range if the change * is not changing anything. */ - with(change: { start?: number, end?: number }): NotebookRange; + with(change: { start?: number; end?: number }): NotebookRange; } /** @@ -12289,21 +13354,26 @@ declare module 'vscode' { */ export interface NotebookDocumentContentOptions { /** - * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + * Controls if output change events will trigger notebook document content change events and + * if it will be used in the diff editor, defaults to false. If the content provider doesn't + * persist the outputs in the file document, this should be set to true. */ transientOutputs?: boolean; /** - * Controls if a cell metadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + * Controls if a cell metadata property change event will trigger notebook document content + * change events and if it will be used in the diff editor, defaults to false. If the + * content provider doesn't persist a metadata property in the file document, it should be + * set to true. */ transientCellMetadata?: { [key: string]: boolean | undefined }; /** - * Controls if a document metadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. - */ + * Controls if a document metadata property change event will trigger notebook document + * content change event and if it will be used in the diff editor, defaults to false. If the + * content provider doesn't persist a metadata property in the file document, it should be + * set to true. + */ transientDocumentMetadata?: { [key: string]: boolean | undefined }; } @@ -12434,7 +13504,7 @@ declare module 'vscode' { * _Note_ that controller selection is persisted (by the controllers {@link NotebookController.id id}) and restored as soon as a * controller is re-created or as a notebook is {@link workspace.onDidOpenNotebookDocument opened}. */ - readonly onDidChangeSelectedNotebooks: Event<{ notebook: NotebookDocument, selected: boolean }>; + readonly onDidChangeSelectedNotebooks: Event<{ readonly notebook: NotebookDocument; readonly selected: boolean }>; /** * A controller can set affinities for specific notebook documents. This allows a controller @@ -12515,7 +13585,7 @@ declare module 'vscode' { * this execution. * @return A thenable that resolves when the operation finished. */ - replaceOutput(out: NotebookCellOutput | NotebookCellOutput[], cell?: NotebookCell): Thenable; + replaceOutput(out: NotebookCellOutput | readonly NotebookCellOutput[], cell?: NotebookCell): Thenable; /** * Append to the output of the cell that is executing or to another cell that is affected by this execution. @@ -12525,7 +13595,7 @@ declare module 'vscode' { * this execution. * @return A thenable that resolves when the operation finished. */ - appendOutput(out: NotebookCellOutput | NotebookCellOutput[], cell?: NotebookCell): Thenable; + appendOutput(out: NotebookCellOutput | readonly NotebookCellOutput[], cell?: NotebookCell): Thenable; /** * Replace all output items of existing cell output. @@ -12534,7 +13604,7 @@ declare module 'vscode' { * @param output Output object that already exists. * @return A thenable that resolves when the operation finished. */ - replaceOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; + replaceOutputItems(items: NotebookCellOutputItem | readonly NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; /** * Append output items to existing cell output. @@ -12543,7 +13613,7 @@ declare module 'vscode' { * @param output Output object that already exists. * @return A thenable that resolves when the operation finished. */ - appendOutputItems(items: NotebookCellOutputItem | NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; + appendOutputItems(items: NotebookCellOutputItem | readonly NotebookCellOutputItem[], output: NotebookCellOutput): Thenable; } /** @@ -12929,21 +13999,21 @@ declare module 'vscode' { * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. */ export interface DebugProtocolMessage { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). + // Properties: see [ProtocolMessage details](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). } /** * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. */ export interface DebugProtocolSource { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). + // Properties: see [Source details](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). } /** * A DebugProtocolBreakpoint is an opaque stand-in type for the [Breakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) type defined in the Debug Adapter Protocol. */ export interface DebugProtocolBreakpoint { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint). + // Properties: see [Breakpoint details](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint). } /** @@ -13503,7 +14573,7 @@ declare module 'vscode' { * Registering a single provider with resolve methods for different trigger kinds, results in the same resolve methods called multiple times. * More than one provider can be registered for the same type. * - * @param type The debug type for which the provider is registered. + * @param debugType The debug type for which the provider is registered. * @param provider The {@link DebugConfigurationProvider debug configuration provider} to register. * @param triggerKind The {@link DebugConfigurationProviderTrigger trigger} for which the 'provideDebugConfiguration' method of the provider is registered. If `triggerKind` is missing, the value `DebugConfigurationProviderTriggerKind.Initial` is assumed. * @return A {@link Disposable} that unregisters this provider when being disposed. @@ -13670,7 +14740,7 @@ declare module 'vscode' { /** * The range the comment thread is located within the document. The thread icon will be shown - * at the first line of the range. + * at the last line of the range. */ range: Range; @@ -13813,6 +14883,12 @@ declare module 'vscode' { * Label will be rendered next to authorName if exists. */ label?: string; + + /** + * Optional timestamp that will be displayed in comments. + * The date will be formatted according to the user's locale and settings. + */ + timestamp?: Date; } /** @@ -13917,8 +14993,6 @@ declare module 'vscode' { export function createCommentController(id: string, label: string): CommentController; } - //#endregion - /** * Represents a session of a currently logged in user. */ @@ -13988,7 +15062,7 @@ declare module 'vscode' { * * Defaults to false. * - * Note: you cannot use this option with {@link silent}. + * Note: you cannot use this option with {@link AuthenticationGetSessionOptions.silent silent}. */ createIfNone?: boolean; @@ -13998,7 +15072,10 @@ declare module 'vscode' { * If true, a modal dialog will be shown asking the user to sign in again. This is mostly used for scenarios * where the token needs to be re minted because it has lost some authorization. * - * Defaults to false. + * If there are no existing sessions and forceNewSession is true, it will behave identically to + * {@link AuthenticationGetSessionOptions.createIfNone createIfNone}. + * + * This defaults to false. */ forceNewSession?: boolean | { detail: string }; @@ -14010,7 +15087,7 @@ declare module 'vscode' { * * Defaults to false. * - * Note: you cannot use this option with any other options that prompt the user like {@link createIfNone}. + * Note: you cannot use this option with any other options that prompt the user like {@link AuthenticationGetSessionOptions.createIfNone createIfNone}. */ silent?: boolean; } @@ -14148,9 +15225,9 @@ declare module 'vscode' { * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider * @param options The {@link AuthenticationGetSessionOptions} to use - * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions + * @returns A thenable that resolves to an authentication session */ - export function getSession(providerId: string, scopes: readonly string[], options?: AuthenticationGetSessionOptions): Thenable; + export function getSession(providerId: string, scopes: readonly string[], options: AuthenticationGetSessionOptions & { forceNewSession: true | { detail: string } }): Thenable; /** * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not @@ -14163,9 +15240,9 @@ declare module 'vscode' { * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider * @param options The {@link AuthenticationGetSessionOptions} to use - * @returns A thenable that resolves to an authentication session + * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions */ - export function getSession(providerId: string, scopes: readonly string[], options: AuthenticationGetSessionOptions & { forceNewSession: true | { detail: string } }): Thenable; + export function getSession(providerId: string, scopes: readonly string[], options?: AuthenticationGetSessionOptions): Thenable; /** * An {@link Event} which fires when the authentication sessions of an authentication provider have @@ -14358,10 +15435,23 @@ declare module 'vscode' { * the function returns or the returned thenable resolves. * * @param item An unresolved test item for which children are being - * requested, or `undefined` to resolve the controller's initial {@link items}. + * requested, or `undefined` to resolve the controller's initial {@link TestController.items items}. */ resolveHandler?: (item: TestItem | undefined) => Thenable | void; + /** + * If this method is present, a refresh button will be present in the + * UI, and this method will be invoked when it's clicked. When called, + * the extension should scan the workspace for any new, changed, or + * removed tests. + * + * It's recommended that extensions try to update tests in realtime, using + * a {@link FileSystemWatcher} for example, and use this method as a fallback. + * + * @returns A thenable that resolves when tests have been refreshed. + */ + refreshHandler: ((token: CancellationToken) => Thenable | void) | undefined; + /** * Creates a {@link TestRun}. This should be called by the * {@link TestRunProfile} when a request is made to execute tests, and may @@ -14408,7 +15498,7 @@ declare module 'vscode' { * A TestRunRequest is a precursor to a {@link TestRun}, which in turn is * created by passing a request to {@link tests.runTests}. The TestRunRequest * contains information about which tests should be run, which should not be - * run, and how they are run (via the {@link profile}). + * run, and how they are run (via the {@link TestRunRequest.profile profile}). * * In general, TestRunRequests are created by the editor and pass to * {@link TestRunProfile.runHandler}, however you can also create test @@ -14443,7 +15533,7 @@ declare module 'vscode' { readonly profile: TestRunProfile | undefined; /** - * @param tests Array of specific tests to run, or undefined to run all tests + * @param include Array of specific tests to run, or undefined to run all tests * @param exclude An array of tests to exclude from the run. * @param profile The run profile used for this request. */ @@ -14494,7 +15584,7 @@ declare module 'vscode' { * Indicates a test has failed. You should pass one or more * {@link TestMessage TestMessages} to describe the failure. * @param test Test item to update. - * @param messages Messages associated with the test failure. + * @param message Messages associated with the test failure. * @param duration How long the test took to execute, in milliseconds. */ failed(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; @@ -14505,7 +15595,7 @@ declare module 'vscode' { * from the "failed" state in that it indicates a test that couldn't be * executed at all, from a compilation error for example. * @param test Test item to update. - * @param messages Messages associated with the test failure. + * @param message Messages associated with the test failure. * @param duration How long the test took to execute, in milliseconds. */ errored(test: TestItem, message: TestMessage | readonly TestMessage[], duration?: number): void; @@ -14563,7 +15653,7 @@ declare module 'vscode' { /** * Adds the test item to the children. If an item with the same ID already * exists, it'll be replaced. - * @param items Item to add. + * @param item Item to add. */ add(item: TestItem): void; @@ -14610,7 +15700,7 @@ declare module 'vscode' { /** * The parent of this item. It's set automatically, and is undefined * top-level items in the {@link TestController.items} and for items that - * aren't yet included in another item's {@link children}. + * aren't yet included in another item's {@link TestItem.children children}. */ readonly parent: TestItem | undefined; @@ -14650,7 +15740,14 @@ declare module 'vscode' { description?: string; /** - * Location of the test item in its {@link uri}. + * A string that should be used when comparing this item + * with other items. When `falsy` the {@link TestItem.label label} + * is used. + */ + sortText?: string | undefined; + + /** + * Location of the test item in its {@link TestItem.uri uri}. * * This is only meaningful if the `uri` points to a file. */ @@ -14676,12 +15773,12 @@ declare module 'vscode' { message: string | MarkdownString; /** - * Expected test output. If given with {@link actualOutput}, a diff view will be shown. + * Expected test output. If given with {@link TestMessage.actualOutput actualOutput }, a diff view will be shown. */ expectedOutput?: string; /** - * Actual test output. If given with {@link expectedOutput}, a diff view will be shown. + * Actual test output. If given with {@link TestMessage.expectedOutput expectedOutput }, a diff view will be shown. */ actualOutput?: string; @@ -14704,6 +15801,294 @@ declare module 'vscode' { */ constructor(message: string | MarkdownString); } + + /** + * The tab represents a single text based resource. + */ + export class TabInputText { + /** + * The uri represented by the tab. + */ + readonly uri: Uri; + /** + * Constructs a text tab input with the given URI. + * @param uri The URI of the tab. + */ + constructor(uri: Uri); + } + + /** + * The tab represents two text based resources + * being rendered as a diff. + */ + export class TabInputTextDiff { + /** + * The uri of the original text resource. + */ + readonly original: Uri; + /** + * The uri of the modified text resource. + */ + readonly modified: Uri; + /** + * Constructs a new text diff tab input with the given URIs. + * @param original The uri of the original text resource. + * @param modified The uri of the modified text resource. + */ + constructor(original: Uri, modified: Uri); + } + + /** + * The tab represents a custom editor. + */ + export class TabInputCustom { + /** + * The uri that the tab is representing. + */ + readonly uri: Uri; + /** + * The type of custom editor. + */ + readonly viewType: string; + /** + * Constructs a custom editor tab input. + * @param uri The uri of the tab. + * @param viewType The viewtype of the custom editor. + */ + constructor(uri: Uri, viewType: string); + } + + /** + * The tab represents a webview. + */ + export class TabInputWebview { + /** + * The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} + */ + readonly viewType: string; + /** + * Constructs a webview tab input with the given view type. + * @param viewType The type of webview. Maps to {@linkcode WebviewPanel.viewType WebviewPanel's viewType} + */ + constructor(viewType: string); + } + + /** + * The tab represents a notebook. + */ + export class TabInputNotebook { + /** + * The uri that the tab is representing. + */ + readonly uri: Uri; + /** + * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + readonly notebookType: string; + /** + * Constructs a new tab input for a notebook. + * @param uri The uri of the notebook. + * @param notebookType The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + constructor(uri: Uri, notebookType: string); + } + + /** + * The tabs represents two notebooks in a diff configuration. + */ + export class TabInputNotebookDiff { + /** + * The uri of the original notebook. + */ + readonly original: Uri; + /** + * The uri of the modified notebook. + */ + readonly modified: Uri; + /** + * The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + readonly notebookType: string; + /** + * Constructs a notebook diff tab input. + * @param original The uri of the original unmodified notebook. + * @param modified The uri of the modified notebook. + * @param notebookType The type of notebook. Maps to {@linkcode NotebookDocument.notebookType NotebookDocuments's notebookType} + */ + constructor(original: Uri, modified: Uri, notebookType: string); + } + + /** + * The tab represents a terminal in the editor area. + */ + export class TabInputTerminal { + /** + * Constructs a terminal tab input. + */ + constructor(); + } + + /** + * Represents a tab within a {@link TabGroup group of tabs}. + * Tabs are merely the graphical representation within the editor area. + * A backing editor is not a guarantee. + */ + export interface Tab { + + /** + * The text displayed on the tab. + */ + readonly label: string; + + /** + * The group which the tab belongs to. + */ + readonly group: TabGroup; + + /** + * Defines the structure of the tab i.e. text, notebook, custom, etc. + * Resource and other useful properties are defined on the tab kind. + */ + readonly input: TabInputText | TabInputTextDiff | TabInputCustom | TabInputWebview | TabInputNotebook | TabInputNotebookDiff | TabInputTerminal | unknown; + + /** + * Whether or not the tab is currently active. + * This is dictated by being the selected tab in the group. + */ + readonly isActive: boolean; + + /** + * Whether or not the dirty indicator is present on the tab. + */ + readonly isDirty: boolean; + + /** + * Whether or not the tab is pinned (pin icon is present). + */ + readonly isPinned: boolean; + + /** + * Whether or not the tab is in preview mode. + */ + readonly isPreview: boolean; + } + + /** + * An event describing change to tabs. + */ + export interface TabChangeEvent { + /** + * The tabs that have been opened. + */ + readonly opened: readonly Tab[]; + /** + * The tabs that have been closed. + */ + readonly closed: readonly Tab[]; + /** + * Tabs that have changed, e.g have changed + * their {@link Tab.isActive active} state. + */ + readonly changed: readonly Tab[]; + } + + /** + * An event describing changes to tab groups. + */ + export interface TabGroupChangeEvent { + /** + * Tab groups that have been opened. + */ + readonly opened: readonly TabGroup[]; + /** + * Tab groups that have been closed. + */ + readonly closed: readonly TabGroup[]; + /** + * Tab groups that have changed, e.g have changed + * their {@link TabGroup.isActive active} state. + */ + readonly changed: readonly TabGroup[]; + } + + /** + * Represents a group of tabs. A tab group itself consists of multiple tabs. + */ + export interface TabGroup { + /** + * Whether or not the group is currently active. + * + * *Note* that only one tab group is active at a time, but that multiple tab + * groups can have an {@link TabGroup.aciveTab active tab}. + * + * @see {@link Tab.isActive} + */ + readonly isActive: boolean; + + /** + * The view column of the group. + */ + readonly viewColumn: ViewColumn; + + /** + * The active {@link Tab tab} in the group. This is the tab whose contents are currently + * being rendered. + * + * *Note* that there can be one active tab per group but there can only be one {@link TabGroups.activeTabGroup active group}. + */ + readonly activeTab: Tab | undefined; + + /** + * The list of tabs contained within the group. + * This can be empty if the group has no tabs open. + */ + readonly tabs: readonly Tab[]; + } + + /** + * Represents the main editor area which consists of multple groups which contain tabs. + */ + export interface TabGroups { + /** + * All the groups within the group container. + */ + readonly all: readonly TabGroup[]; + + /** + * The currently active group. + */ + readonly activeTabGroup: TabGroup; + + /** + * An {@link Event event} which fires when {@link TabGroup tab groups} have changed. + */ + readonly onDidChangeTabGroups: Event; + + /** + * An {@link Event event} which fires when {@link Tab tabs} have changed. + */ + readonly onDidChangeTabs: Event; + + /** + * Closes the tab. This makes the tab object invalid and the tab + * should no longer be used for further actions. + * Note: In the case of a dirty tab, a confirmation dialog will be shown which may be cancelled. If cancelled the tab is still valid + * + * @param tab The tab to close. + * @param preserveFocus When `true` focus will remain in its current position. If `false` it will jump to the next tab. + * @returns A promise that resolves to `true` when all tabs have been closed. + */ + close(tab: Tab | readonly Tab[], preserveFocus?: boolean): Thenable; + + /** + * Closes the tab group. This makes the tab group object invalid and the tab group + * should no longer be used for further actions. + * @param tabGroup The tab group to close. + * @param preserveFocus When `true` focus will remain in its current position. + * @returns A promise that resolves to `true` when all tab groups have been closed. + */ + close(tabGroup: TabGroup | readonly TabGroup[], preserveFocus?: boolean): Thenable; + } } /** diff --git a/yarn.lock b/yarn.lock index 0af70df1..dfa4b807 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2626,10 +2626,10 @@ jest-each@^27.2.0: jest-util "^27.2.0" pretty-format "^27.2.0" -jest-editor-support@^30.1.0: - version "30.1.0" - resolved "https://registry.yarnpkg.com/jest-editor-support/-/jest-editor-support-30.1.0.tgz#3599e7992d377363fbac3dacb12e39e767768eb9" - integrity sha512-5zOv2NIR/wGEulsOJ/9VDhd7uX2g4zHACBG8tHww+R5yVuqsi2vyOTp3OFEaLfs7RBDLmTfdMm4ZvDgKnNQpHw== +jest-editor-support@^30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-editor-support/-/jest-editor-support-30.2.0.tgz#62c61b4d2a1b36d070c665acd0705e3fef37a0fb" + integrity sha512-+ylyVaGv9kB1kMkNI8LmOLHl4a4Lr9n7hYkqewTnUY0k5eAENWLpCwbPm7KzOiyHU9FU2K4T4hPCcyQAxz0wFw== dependencies: "@babel/parser" "^7.15.7" "@babel/runtime" "^7.15.4" @@ -4342,11 +4342,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -vscode-codicons@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vscode-codicons/-/vscode-codicons-0.0.4.tgz#dc7f3b82ba217c02b81bebcca2d4f395eba8e46d" - integrity sha512-8ipBNlgTmQ+2s2yDPSN2ejsPL4mJEXiAKM5VcFXgCtqMLHOIY7CxnDtdxkritU81dF3C80fWo3+Mc9oMePGZ2Q== - vscode-test@^1.3.0: version "1.6.1" resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-1.6.1.tgz#44254c67036de92b00fdd72f6ace5f1854e1a563"