diff --git a/package.json b/package.json index 4f9656f28..63c11a291 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.3.0-rc.1", + "version": "4.3.0", "publisher": "Orta", "engines": { - "vscode": "^1.59.0" + "vscode": "^1.63.0" }, "author": { "name": "Orta Therox, ConnectDotz & Sean Poulter", @@ -443,7 +443,8 @@ "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\" \"*.json\" \"*.js\" ", "test": "jest", "watch-test": "yarn test -- --watch", - "tsc": "tsc --noEmit" + "tsc": "tsc --noEmit", + "update-vscode-type": "npx vscode-dts main; mv ./vscode.d.ts ./typings" }, "dependencies": { "istanbul-lib-coverage": "^3.0.0", diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts index 4b0ea5040..24c0a37d0 100644 --- a/src/JestExt/core.ts +++ b/src/JestExt/core.ts @@ -23,13 +23,7 @@ import { resultsWithoutAnsiEscapeSequence } from '../TestResults/TestResult'; import { CoverageMapData } from 'istanbul-lib-coverage'; import { Logging } from '../logging'; import { createProcessSession, ProcessSession } from './process-session'; -import { - DebugFunction, - JestExtContext, - JestSessionEvents, - JestExtSessionContext, - JestRunEvent, -} from './types'; +import { JestExtContext, JestSessionEvents, JestExtSessionContext, JestRunEvent } from './types'; import * as messaging from '../messaging'; import { SupportedLanguageIds } from '../appGlobals'; import { createJestExtContext, getExtensionResourceSettings, prefixWorkspace } from './helper'; @@ -310,15 +304,15 @@ export class JestExt { public triggerUpdateSettings(newSettings?: PluginResourceSettings): Promise { const updatedSettings = newSettings ?? getExtensionResourceSettings(this.extContext.workspace.uri); - this.extContext = createJestExtContext(this.extContext.workspace, updatedSettings); // debug this.testResultProvider.verbose = updatedSettings.debugMode ?? false; // coverage const showCoverage = this.coverageOverlay.enabled ?? updatedSettings.showCoverageOnLoad; - this.coverageOverlay.dispose(); + updatedSettings.showCoverageOnLoad = showCoverage; + this.coverageOverlay.dispose(); this.coverageOverlay = new CoverageOverlay( this.vscodeContext, this.coverageMapProvider, @@ -326,8 +320,8 @@ export class JestExt { updatedSettings.coverageFormatter, updatedSettings.coverageColors ); - this.extContext.runnerWorkspace.collectCoverage = showCoverage; - this.coverageOverlay.enabled = showCoverage; + + this.extContext = createJestExtContext(this.extContext.workspace, updatedSettings); return this.startSession(true); } @@ -415,7 +409,7 @@ export class JestExt { } //** commands */ - public debugTests: DebugFunction = async ( + public debugTests = async ( document: vscode.TextDocument | string, ...ids: DebugTestIdentifier[] ): Promise => { @@ -437,22 +431,23 @@ export class JestExt { let testId: DebugTestIdentifier | undefined; switch (ids.length) { case 0: - return; + //no testId, will run all tests in the file + break; case 1: testId = ids[0]; break; default: testId = await selectTest(ids); + // if nothing is selected, abort + if (!testId) { + return; + } break; } - if (!testId) { - return; - } - this.debugConfigurationProvider.prepareTestRun( typeof document === 'string' ? document : document.fileName, - escapeRegExp(idString('full-name', testId)) + testId ? escapeRegExp(idString('full-name', testId)) : '.*' ); const configs = vscode.workspace @@ -463,12 +458,9 @@ export class JestExt { configs?.find((c) => c.name === 'vscode-jest-tests'); if (!debugConfig) { - messaging.systemWarningMessage( - prefixWorkspace( - this.extContext, - 'No debug config named "vscode-jest-tests.v2" or "vscode-jest-tests" found in launch.json, will use a default config.\nIf you encountered debugging problems, feel free to try the setup wizard below' - ), - this.setupWizardAction('debugConfig') + this.logging( + 'debug', + 'No debug config named "vscode-jest-tests.v2" or "vscode-jest-tests" found in launch.json, will use a default config.' ); debugConfig = this.debugConfigurationProvider.provideDebugConfigurations( this.extContext.workspace @@ -484,13 +476,24 @@ export class JestExt { } } else { const name = editor.document.fileName; - if ( - this.processSession.scheduleProcess({ + let pInfo; + if (this.testResultProvider.isTestFile(name) !== 'yes') { + // run related tests from source file + pInfo = this.processSession.scheduleProcess({ type: 'by-file', testFileName: name, - notTestFile: this.testResultProvider.isTestFile(name) !== 'yes', - }) - ) { + notTestFile: true, + }); + } else { + // note: use file-pattern instead of file-path to increase compatibility, such as for angular users. + // However, we should keep an eye on performance, as matching by pattern could be slower than by explicit path. + // If performance ever become an issue, we could consider optimization... + pInfo = this.processSession.scheduleProcess({ + type: 'by-file-pattern', + testFileNamePattern: name, + }); + } + if (pInfo) { this.dirtyFiles.delete(name); return; } diff --git a/src/JestExt/helper.ts b/src/JestExt/helper.ts index ffc742f10..f7ca57aa6 100644 --- a/src/JestExt/helper.ts +++ b/src/JestExt/helper.ts @@ -14,7 +14,7 @@ import { import { AutoRunMode } from '../StatusBar'; import { pathToJest, pathToConfig, toFilePath } from '../helpers'; import { workspaceLogging } from '../logging'; -import { AutoRunAccessor, JestExtContext } from './types'; +import { AutoRunAccessor, JestExtContext, RunnerWorkspaceOptions } from './types'; import { CoverageColors } from '../Coverage'; export const isWatchRequest = (request: JestProcessRequest): boolean => @@ -83,23 +83,26 @@ export const createJestExtContext = ( workspaceFolder: vscode.WorkspaceFolder, settings: PluginResourceSettings ): JestExtContext => { - const currentJestVersion = 20; - const [jestCommandLine, pathToConfig] = getJestCommandSettings(settings); - const runnerWorkspace = new ProjectWorkspace( - toFilePath(settings.rootPath), - jestCommandLine, - pathToConfig, - currentJestVersion, - workspaceFolder.name, - settings.showCoverageOnLoad, - settings.debugMode, - settings.nodeEnv, - settings.shell - ); + const createRunnerWorkspace = (options?: RunnerWorkspaceOptions) => { + const ws = workspaceFolder.name; + const currentJestVersion = 20; + const [jestCommandLine, pathToConfig] = getJestCommandSettings(settings); + return new ProjectWorkspace( + toFilePath(settings.rootPath), + jestCommandLine, + pathToConfig, + currentJestVersion, + options?.outputFileSuffix ? `${ws}_${options.outputFileSuffix}` : ws, + options?.collectCoverage ?? settings.showCoverageOnLoad, + settings.debugMode, + settings.nodeEnv, + settings.shell + ); + }; return { workspace: workspaceFolder, settings, - runnerWorkspace, + createRunnerWorkspace, loggingFactory: workspaceLogging(workspaceFolder.name, settings.debugMode ?? false), autoRun: AutoRun(settings), }; diff --git a/src/JestExt/process-session.ts b/src/JestExt/process-session.ts index 54b36f3a2..353ed8f1c 100644 --- a/src/JestExt/process-session.ts +++ b/src/JestExt/process-session.ts @@ -6,6 +6,7 @@ import { requestString, QueueType, JestProcessInfo, + JestProcessRequestTransform, } from '../JestProcessManagement'; import { JestTestProcessType } from '../Settings'; import { RunTestListener, ListTestFileListener } from './process-listeners'; @@ -24,6 +25,14 @@ export type InternalRequestBase = }; export type JestExtRequestType = JestProcessRequestBase | InternalRequestBase; +const isJestProcessRequestBase = (request: JestExtRequestType): request is JestProcessRequestBase => + // eslint-disable-next-line @typescript-eslint/no-explicit-any + typeof (request as any).transform === 'function'; +const getTransform = (request: JestExtRequestType): JestProcessRequestTransform | undefined => { + if (isJestProcessRequestBase(request)) { + return request.transform; + } +}; const ProcessScheduleStrategy: Record = { // abort if there is already an pending request @@ -36,19 +45,19 @@ const ProcessScheduleStrategy: Record = { // abort if there is already identical pending request 'by-file': { - queue: 'blocking', + queue: 'blocking-2', dedup: { filterByStatus: ['pending'] }, }, 'by-file-test': { - queue: 'blocking', + queue: 'blocking-2', dedup: { filterByStatus: ['pending'], filterByContent: true }, }, 'by-file-pattern': { - queue: 'blocking', + queue: 'blocking-2', dedup: { filterByStatus: ['pending'] }, }, 'by-file-test-pattern': { - queue: 'blocking', + queue: 'blocking-2', dedup: { filterByStatus: ['pending'], filterByContent: true }, }, 'not-test': { @@ -130,6 +139,11 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes }; const createProcessRequest = (request: JestExtRequestType): JestProcessRequest => { + const transform = (pRequest: JestProcessRequest): JestProcessRequest => { + const t = getTransform(request); + return t ? t(pRequest) : pRequest; + }; + const lSession = listenerSession; switch (request.type) { case 'all-tests': @@ -140,11 +154,11 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes case 'by-file-test': case 'by-file-test-pattern': { const schedule = ProcessScheduleStrategy[request.type]; - return { + return transform({ ...request, listener: new RunTestListener(lSession), schedule, - }; + }); } case 'update-snapshot': { const snapshotRequest = createSnapshotRequest(request.baseRequest); @@ -153,21 +167,21 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes queue: 'non-blocking' as QueueType, }; - return { + return transform({ ...snapshotRequest, listener: new RunTestListener(lSession), schedule, - }; + }); } case 'list-test-files': { const schedule = ProcessScheduleStrategy['not-test']; - return { + return transform({ ...request, type: 'not-test', args: ['--listTests', '--json', '--watchAll=false'], listener: new ListTestFileListener(lSession, request.onResult), schedule, - }; + }); } } throw new Error(`Unexpected process type ${request.type}`); diff --git a/src/JestExt/types.ts b/src/JestExt/types.ts index ba14df8d0..0f6b18874 100644 --- a/src/JestExt/types.ts +++ b/src/JestExt/types.ts @@ -26,12 +26,16 @@ export interface AutoRunAccessor { onStartup: OnStartupType | undefined; mode: AutoRunMode; } +export interface RunnerWorkspaceOptions { + outputFileSuffix?: string; + collectCoverage?: boolean; +} export interface JestExtContext { settings: PluginResourceSettings; workspace: vscode.WorkspaceFolder; - runnerWorkspace: ProjectWorkspace; loggingFactory: LoggingFactory; autoRun: AutoRunAccessor; + createRunnerWorkspace: (options?: RunnerWorkspaceOptions) => ProjectWorkspace; } export interface JestExtSessionContext extends JestExtContext { diff --git a/src/JestProcessManagement/JestProcess.ts b/src/JestProcessManagement/JestProcess.ts index ac9235c37..b4839cc6d 100644 --- a/src/JestProcessManagement/JestProcess.ts +++ b/src/JestProcessManagement/JestProcess.ts @@ -118,10 +118,13 @@ export class JestProcess implements JestProcessInfo { } break; case 'by-file': { - options.testFileNamePattern = this.quoteFileName(this.request.testFileName); + const fileName = this.quoteFileName(this.request.testFileName); args.push('--watchAll=false'); if (this.request.notTestFile) { - args.push('--findRelatedTests'); + args.push('--findRelatedTests', fileName); + } else { + options.testFileNamePattern = fileName; + args.push('--runTestsByPath'); } if (this.request.updateSnapshot) { args.push('--updateSnapshot'); @@ -167,7 +170,11 @@ export class JestProcess implements JestProcessInfo { break; } - const runner = new Runner(this.extContext.runnerWorkspace, options); + const runnerWorkspace = this.extContext.createRunnerWorkspace( + this.request.schedule.queue === 'blocking-2' ? { outputFileSuffix: '2' } : undefined + ); + + const runner = new Runner(runnerWorkspace, options); this.registerListener(runner); let taskInfo: Omit; diff --git a/src/JestProcessManagement/JestProcessManager.ts b/src/JestProcessManagement/JestProcessManager.ts index 9016947ea..7134c7db4 100644 --- a/src/JestProcessManagement/JestProcessManager.ts +++ b/src/JestProcessManagement/JestProcessManager.ts @@ -7,19 +7,22 @@ import { JestExtContext } from '../JestExt'; export class JestProcessManager implements TaskArrayFunctions { private extContext: JestExtContext; - private blockingQueue: TaskQueue; - private nonBlockingQueue: TaskQueue; + private queues: Map>; private logging: Logging; constructor(extContext: JestExtContext) { this.extContext = extContext; this.logging = extContext.loggingFactory.create('JestProcessManager'); - this.blockingQueue = createTaskQueue('blocking-queue', 1); - this.nonBlockingQueue = createTaskQueue('non-blocking-queue', 3); + this.queues = new Map([ + ['blocking', createTaskQueue('blocking-queue', 1)], + ['blocking-2', createTaskQueue('blocking-queue-2', 1)], + ['non-blocking', createTaskQueue('non-blocking-queue', 3)], + ]); } private getQueue(type: QueueType): TaskQueue { - return type === 'blocking' ? this.blockingQueue : this.nonBlockingQueue; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.queues.get(type)!; } private foundDup(request: JestProcessRequest): boolean { @@ -83,8 +86,7 @@ export class JestProcessManager implements TaskArrayFunctions { public async stopAll(queueType?: QueueType): Promise { let promises: Promise[]; if (!queueType) { - const queueTypes: QueueType[] = ['blocking', 'non-blocking']; - promises = queueTypes.map((q: QueueType) => this.stopAll(q)); + promises = Array.from(this.queues.keys()).map((q) => this.stopAll(q)); } else { const queue = this.getQueue(queueType); promises = queue.map((t) => t.data.stop()); @@ -98,14 +100,15 @@ export class JestProcessManager implements TaskArrayFunctions { if (queueType) { return this.getQueue(queueType).size(); } - return this.numberOfProcesses('blocking') + this.numberOfProcesses('non-blocking'); + return Array.from(this.queues.values()).reduce((pCount, q) => { + pCount += q.size(); + return pCount; + }, 0); } // task array functions private getQueues(queueType?: QueueType): TaskQueue[] { - return queueType - ? [this.getQueue(queueType)] - : [this.getQueue('blocking'), this.getQueue('non-blocking')]; + return queueType ? [this.getQueue(queueType)] : Array.from(this.queues.values()); } public map(f: (task: Task) => M, queueType?: QueueType): M[] { const queues = this.getQueues(queueType); diff --git a/src/JestProcessManagement/types.ts b/src/JestProcessManagement/types.ts index 1c395590b..89987c269 100644 --- a/src/JestProcessManagement/types.ts +++ b/src/JestProcessManagement/types.ts @@ -18,7 +18,7 @@ export interface Task { status: TaskStatus; } -export type QueueType = 'blocking' | 'non-blocking'; +export type QueueType = 'blocking' | 'blocking-2' | 'non-blocking'; /** * predicate to match task @@ -43,7 +43,8 @@ interface JestProcessRequestCommon { schedule: ScheduleStrategy; listener: JestProcessListener; } -export type JestProcessRequestBase = + +export type JestProcessRequestSimple = | { type: Extract; } @@ -79,6 +80,11 @@ export type JestProcessRequestBase = args: string[]; }; +export type JestProcessRequestTransform = (request: JestProcessRequest) => JestProcessRequest; + +export type JestProcessRequestBase = JestProcessRequestSimple & { + transform?: JestProcessRequestTransform; +}; export type JestProcessRequest = JestProcessRequestBase & JestProcessRequestCommon; export interface TaskArrayFunctions { diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts index 970010ffe..bb538522d 100644 --- a/src/test-provider/test-item-data.ts +++ b/src/test-provider/test-item-data.ts @@ -10,10 +10,10 @@ import { Logging } from '../logging'; import { TestSuitChangeEvent } from '../TestResults/test-result-events'; import { Debuggable, TestItemData, TestItemRun } from './types'; import { JestTestProviderContext } from './test-provider-context'; -import { JestProcessInfo } from '../JestProcessManagement'; +import { JestProcessInfo, JestProcessRequest } from '../JestProcessManagement'; interface JestRunable { - getJestRunRequest: (profile: vscode.TestRunProfile) => JestExtRequestType; + getJestRunRequest: () => JestExtRequestType; } interface WithUri { uri: vscode.Uri; @@ -41,8 +41,8 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { return this.item.uri!; } - scheduleTest(run: vscode.TestRun, end: () => void, profile: vscode.TestRunProfile): void { - const jestRequest = this.getJestRunRequest(profile); + scheduleTest(run: vscode.TestRun, end: () => void): void { + const jestRequest = this.getJestRunRequest(); const itemRun: TestItemRun = { item: this.item, run, end }; deepItemState(this.item, run.enqueued); @@ -58,23 +58,7 @@ abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri { } } - abstract getJestRunRequest(profile: vscode.TestRunProfile): JestExtRequestType; - - isRunnable(): boolean { - return !this.context.ext.autoRun.isWatch; - } - isDebuggable(): boolean { - return false; - } - canRun(profile: vscode.TestRunProfile): boolean { - if (profile.kind === vscode.TestRunProfileKind.Run) { - return this.isRunnable(); - } - if (profile.kind === vscode.TestRunProfileKind.Debug) { - return this.isDebuggable(); - } - return false; - } + abstract getJestRunRequest(): JestExtRequestType; } /** @@ -100,7 +84,9 @@ export class WorkspaceRoot extends TestItemDataBase { `${extensionId}:${this.context.ext.workspace.name}`, this.context.ext.workspace.name, this.context.ext.workspace.uri, - this + this, + undefined, + ['run'] ); item.description = `(${this.context.ext.autoRun.mode})`; @@ -108,8 +94,12 @@ export class WorkspaceRoot extends TestItemDataBase { return item; } - getJestRunRequest(_profile: vscode.TestRunProfile): JestExtRequestType { - return { type: 'all-tests' }; + getJestRunRequest(): JestExtRequestType { + const transform = (request: JestProcessRequest) => { + request.schedule.queue = 'blocking-2'; + return request; + }; + return { type: 'all-tests', transform }; } discoverTest(run: vscode.TestRun): void { const testList = this.context.ext.testResolveProvider.getTestList(); @@ -363,12 +353,12 @@ export class FolderData extends TestItemDataBase { } private createTestItem(name: string, parent: vscode.TestItem) { const uri = FolderData.makeUri(parent, name); - const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent); + const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent, ['run']); item.canResolveChildren = false; return item; } - getJestRunRequest(_profile: vscode.TestRunProfile): JestExtRequestType { + getJestRunRequest(): JestExtRequestType { return { type: 'by-file-pattern', testFileNamePattern: this.uri.fsPath, @@ -549,13 +539,16 @@ export class TestDocumentRoot extends TestResultData { ); } - getJestRunRequest = (_profile: vscode.TestRunProfile): JestExtRequestType => { + getJestRunRequest = (): JestExtRequestType => { return { - type: 'by-file', - testFileName: this.item.id, + type: 'by-file-pattern', + testFileNamePattern: this.uri.fsPath, }; }; + getDebugInfo(): ReturnType { + return { fileName: this.uri.fsPath }; + } public onTestMatched = (): void => { this.item.children.forEach((childItem) => this.context.getData(childItem)?.onTestMatched() @@ -588,17 +581,14 @@ export class TestData extends TestResultData implements Debuggable { return item; } - getJestRunRequest(_profile: vscode.TestRunProfile): JestExtRequestType { + getJestRunRequest(): JestExtRequestType { return { type: 'by-file-test-pattern', testFileNamePattern: this.uri.fsPath, testNamePattern: this.node.fullName, }; } - isDebuggable(): boolean { - return true; - } - getDebugInfo(): { fileName: string; testNamePattern: string } { + getDebugInfo(): ReturnType { return { fileName: this.uri.fsPath, testNamePattern: this.node.fullName }; } private updateItemRange(): void { diff --git a/src/test-provider/test-provider-context.ts b/src/test-provider/test-provider-context.ts index 78110d6f1..48ec9a7f4 100644 --- a/src/test-provider/test-provider-context.ts +++ b/src/test-provider/test-provider-context.ts @@ -16,13 +16,14 @@ const COLORS = { ['yellow']: '\x1b[0;33m', ['end']: '\x1b[0m', }; - +export type TagIdType = 'run' | 'debug'; export class JestTestProviderContext { private testItemData: WeakMap; constructor( public readonly ext: JestExtExplorerContext, - private readonly controller: vscode.TestController + private readonly controller: vscode.TestController, + private readonly profiles: vscode.TestRunProfile[] ) { this.testItemData = new WeakMap(); } @@ -31,12 +32,19 @@ export class JestTestProviderContext { label: string, uri: vscode.Uri, data: TestItemData, - parent?: vscode.TestItem + parent?: vscode.TestItem, + tagIds: TagIdType[] = ['run', 'debug'] ): vscode.TestItem => { const testItem = this.controller.createTestItem(id, label, uri); this.testItemData.set(testItem, data); const collection = parent ? parent.children : this.controller.items; collection.add(testItem); + tagIds?.forEach((tId) => { + const tag = this.getTag(tId); + if (tag) { + testItem.tags = [...testItem.tags, tag]; + } + }); return testItem; }; @@ -82,6 +90,10 @@ export class JestTestProviderContext { run.appendOutput(`${text}${newLine ? '\r\n' : ''}`); showTestExplorerTerminal(); }; + + // tags + getTag = (tagId: TagIdType): vscode.TestTag | undefined => + this.profiles.find((p) => p.tag?.id === tagId)?.tag; } /** show TestExplorer Terminal on first invocation only */ diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts index d0162fc43..9d941b7c6 100644 --- a/src/test-provider/test-provider.ts +++ b/src/test-provider/test-provider.ts @@ -24,35 +24,46 @@ export class JestTestProvider { this.log = jestContext.loggingFactory.create('JestTestProvider'); const wsFolder = jestContext.workspace; - this.controller = this.createController(wsFolder, jestContext); + this.controller = this.createController(wsFolder); - this.context = new JestTestProviderContext(jestContext, this.controller); + this.context = new JestTestProviderContext( + jestContext, + this.controller, + this.createProfiles(this.controller) + ); this.workspaceRoot = new WorkspaceRoot(this.context); } - private createController = ( - wsFolder: vscode.WorkspaceFolder, - jestContext: JestExtExplorerContext - ): vscode.TestController => { + private createController = (wsFolder: vscode.WorkspaceFolder): vscode.TestController => { const controller = vscode.tests.createTestController( `${extensionId}:TestProvider:${wsFolder.name}`, `Jest Test Provider (${wsFolder.name})` ); controller.resolveHandler = this.discoverTest; - if (!jestContext.autoRun.isWatch) { - controller.createRunProfile('run', vscode.TestRunProfileKind.Run, this.runTests, true); - } - controller.createRunProfile('debug', vscode.TestRunProfileKind.Debug, this.runTests, true); - controller.createRunProfile( - 'run with coverage', - vscode.TestRunProfileKind.Coverage, - this.runTests, - true - ); - return controller; }; + private createProfiles = (controller: vscode.TestController): vscode.TestRunProfile[] => { + const runTag = new vscode.TestTag('run'); + const debugTag = new vscode.TestTag('debug'); + const profiles = [ + controller.createRunProfile( + 'run', + vscode.TestRunProfileKind.Run, + this.runTests, + true, + runTag + ), + controller.createRunProfile( + 'debug', + vscode.TestRunProfileKind.Debug, + this.runTests, + true, + debugTag + ), + ]; + return profiles; + }; private discoverTest = (item: vscode.TestItem | undefined): void => { const theItem = item ?? this.workspaceRoot.item; @@ -95,7 +106,11 @@ export class JestTestProvider { try { const debugInfo = tData.getDebugInfo(); this.context.appendOutput(`launching debugger for ${tData.item.id}`, run); - await this.context.ext.debugTests(debugInfo.fileName, debugInfo.testNamePattern); + if (debugInfo.testNamePattern) { + await this.context.ext.debugTests(debugInfo.fileName, debugInfo.testNamePattern); + } else { + await this.context.ext.debugTests(debugInfo.fileName); + } return; } catch (e) { error = `item ${tData.item.id} failed to debug: ${JSON.stringify(e)}`; @@ -115,19 +130,11 @@ export class JestTestProvider { this.log('error', 'not supporting runRequest without profile', request); return Promise.reject('cnot supporting runRequest without profile'); } - const profile = request.profile; - const run = this.context.createTestRun(request, this.controller.id); const tests = (request.include ?? this.getAllItems()).filter( (t) => !request.exclude?.includes(t) ); - this.context.appendOutput( - `executing profile: "${request.profile.label}" for ${tests.length} tests...`, - run - ); - const notRunnable: string[] = []; - const promises: Promise[] = []; try { for (const test of tests) { @@ -136,18 +143,17 @@ export class JestTestProvider { run.skipped(test); continue; } - if (!tData.canRun(profile)) { - run.skipped(test); - notRunnable.push(test.id); - continue; - } + this.context.appendOutput( + `executing profile: "${request.profile.label}" for ${test.id}...`, + run + ); if (request.profile.kind === vscode.TestRunProfileKind.Debug) { await this.debugTest(tData, run); } else { promises.push( new Promise((resolve, reject) => { try { - tData.scheduleTest(run, resolve, profile); + tData.scheduleTest(run, resolve); } catch (e) { const msg = `failed to schedule test for ${tData.item.id}: ${JSON.stringify(e)}`; this.log('error', msg, e); @@ -158,16 +164,6 @@ export class JestTestProvider { ); } } - - // TODO: remove this when testItem can determine its run/debug eligibility, i.e. shows correct UI buttons. - // for example: we only support debugging indivisual test, when users try to debug the whole test file or folder, it will be ignored - // another example is to run indivisual test/test-file/folder in a watch-mode workspace is not necessary and thus will not be executed - if (notRunnable.length > 0) { - const msgs = [`the following items do not support "${request.profile.label}":`]; - notRunnable.forEach((id) => msgs.push(id)); - this.context.appendOutput(msgs.join('\n'), run); - vscode.window.showWarningMessage(msgs.join('\r\n')); - } } catch (e) { const msg = `failed to execute profile "${request.profile.label}": ${JSON.stringify(e)}`; this.context.appendOutput(msg, run, true, 'red'); diff --git a/src/test-provider/types.ts b/src/test-provider/types.ts index 3793a72af..42d69d411 100644 --- a/src/test-provider/types.ts +++ b/src/test-provider/types.ts @@ -25,10 +25,9 @@ export interface TestItemData { readonly uri: vscode.Uri; context: JestTestProviderContext; discoverTest?: (run: vscode.TestRun) => void; - scheduleTest: (run: vscode.TestRun, end: () => void, profile: vscode.TestRunProfile) => void; - canRun: (profile: vscode.TestRunProfile) => boolean; + scheduleTest: (run: vscode.TestRun, end: () => void) => void; } export interface Debuggable { - getDebugInfo: () => { fileName: string; testNamePattern: string }; + getDebugInfo: () => { fileName: string; testNamePattern?: string }; } diff --git a/tests/JestExt/core.test.ts b/tests/JestExt/core.test.ts index 4b500dac8..d436f4c8b 100644 --- a/tests/JestExt/core.test.ts +++ b/tests/JestExt/core.test.ts @@ -35,7 +35,6 @@ import * as extHelper from '../..//src/JestExt/helper'; import { workspaceLogging } from '../../src/logging'; import { ProjectWorkspace } from 'jest-editor-support'; import { mockProjectWorkspace, mockWworkspaceLogging } from '../test-helper'; -import { startWizard } from '../../src/setup-wizard'; import { JestTestProvider } from '../../src/test-provider'; /* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "expectItTakesNoAction"] }] */ @@ -188,11 +187,16 @@ describe('JestExt', () => { }); it.each` desc | testIds | testIdStringCount | startDebug - ${'0 id'} | ${[]} | ${0} | ${false} + ${'no id'} | ${undefined} | ${0} | ${true} + ${'empty id'} | ${[]} | ${0} | ${true} ${'1 string id '} | ${['test-1']} | ${0} | ${true} ${'1 testIdentifier id '} | ${[makeIdentifier('test-1', ['d-1'])]} | ${1} | ${true} `('no selection needed: $desc', async ({ testIds, testIdStringCount, startDebug }) => { - await sut.debugTests(document, ...testIds); + if (testIds) { + await sut.debugTests(document, ...testIds); + } else { + await sut.debugTests(document); + } expect(mockShowQuickPick).not.toBeCalled(); expect(mockHelpers.testIdString).toBeCalledTimes(testIdStringCount); if (testIdStringCount >= 1) { @@ -207,6 +211,17 @@ describe('JestExt', () => { const configuration = startDebugging.mock.calls[startDebugging.mock.calls.length - 1][1]; expect(configuration).toBeDefined(); expect(configuration.type).toBe('dummyconfig'); + if (testIds?.length === 1) { + expect(sut.debugConfigurationProvider.prepareTestRun).toHaveBeenCalledWith( + document.fileName, + testIds[0] + ); + } else { + expect(sut.debugConfigurationProvider.prepareTestRun).toHaveBeenCalledWith( + document.fileName, + '.*' + ); + } expect(sut.debugConfigurationProvider.prepareTestRun).toHaveBeenCalled(); } else { expect(sut.debugConfigurationProvider.prepareTestRun).not.toHaveBeenCalled(); @@ -267,24 +282,28 @@ describe('JestExt', () => { expect(mockShowQuickPick).toHaveBeenCalledTimes(1); expect(vscode.debug.startDebugging).not.toHaveBeenCalled(); }); - it('if pass zero testId, nothing will be run', async () => { + it('if pass zero testId, all tests will be run', async () => { await sut.debugTests(document); expect(mockShowQuickPick).not.toHaveBeenCalled(); expect(mockHelpers.testIdString).not.toBeCalled(); - expect(vscode.debug.startDebugging).not.toHaveBeenCalled(); + expect(sut.debugConfigurationProvider.prepareTestRun).toBeCalledWith( + document.fileName, + '.*' + ); + expect(vscode.debug.startDebugging).toHaveBeenCalled(); }); }); }); it.each` - configNames | shouldShowWarning | debugMode | v2 - ${undefined} | ${true} | ${true} | ${false} - ${[]} | ${true} | ${true} | ${false} - ${['a', 'b']} | ${true} | ${false} | ${false} - ${['a', 'vscode-jest-tests.v2', 'b']} | ${false} | ${false} | ${true} - ${['a', 'vscode-jest-tests', 'b']} | ${false} | ${false} | ${false} + configNames | useDefaultConfig | debugMode | v2 + ${undefined} | ${true} | ${true} | ${false} + ${[]} | ${true} | ${true} | ${false} + ${['a', 'b']} | ${true} | ${false} | ${false} + ${['a', 'vscode-jest-tests.v2', 'b']} | ${false} | ${false} | ${true} + ${['a', 'vscode-jest-tests', 'b']} | ${false} | ${false} | ${false} `( - 'provides setup wizard in warning message if no "vscode-jest-tests" in launch.json: $configNames', - async ({ configNames, shouldShowWarning, debugMode, v2 }) => { + 'will find appropriate debug config: $configNames', + async ({ configNames, useDefaultConfig, debugMode, v2 }) => { expect.hasAssertions(); const testNamePattern = 'testNamePattern'; mockConfigurations = configNames ? configNames.map((name) => ({ name })) : undefined; @@ -295,7 +314,7 @@ describe('JestExt', () => { await sut.debugTests(document, testNamePattern); expect(startDebugging).toBeCalledTimes(1); - if (shouldShowWarning) { + if (useDefaultConfig) { // debug with generated config expect(vscode.debug.startDebugging).toHaveBeenLastCalledWith( workspaceFolder, @@ -313,22 +332,7 @@ describe('JestExt', () => { testNamePattern ); - if (shouldShowWarning) { - expect(messaging.systemWarningMessage).toHaveBeenCalled(); - - //verify the message button does invoke the setup wizard command - const button = (messaging.systemWarningMessage as jest.Mocked).mock.calls[0][1]; - expect(button.action).not.toBeUndefined(); - vscode.commands.executeCommand = jest.fn(); - button.action(); - expect(startWizard).toBeCalledWith(sut.debugConfigurationProvider, { - workspace: workspaceFolder, - taskId: 'debugConfig', - verbose: debugMode, - }); - } else { - expect(messaging.systemWarningMessage).not.toHaveBeenCalled(); - } + expect(messaging.systemWarningMessage).not.toHaveBeenCalled(); } ); }); @@ -562,15 +566,19 @@ describe('JestExt', () => { const settings = { showCoverageOnLoad: true } as any; const sut = newJestExt({ settings }); - const { runnerWorkspace } = (createProcessSession as jest.Mocked).mock.calls[0][0]; + const { createRunnerWorkspace } = (createProcessSession as jest.Mocked).mock.calls[0][0]; + let runnerWorkspace = createRunnerWorkspace(); expect(runnerWorkspace.collectCoverage).toBe(true); sut.coverageOverlay.enabled = false; await sut.toggleCoverageOverlay(); - const { runnerWorkspace: runnerWorkspace2 } = (createProcessSession as jest.Mocked).mock - .calls[1][0]; - expect(runnerWorkspace2.collectCoverage).toBe(false); + const { createRunnerWorkspace: f2, settings: settings2 } = ( + createProcessSession as jest.Mocked + ).mock.calls[1][0]; + runnerWorkspace = f2(); + expect(settings2.showCoverageOnLoad).toBe(false); + expect(runnerWorkspace.collectCoverage).toBe(false); }); }); @@ -949,11 +957,18 @@ describe('JestExt', () => { (sut.testResultProvider.isTestFile as jest.Mocked).mockReturnValueOnce(isTestFile); sut.runAllTests(editor); - expect(mockProcessSession.scheduleProcess).toBeCalledWith({ - type: 'by-file', - testFileName: editor.document.fileName, - notTestFile: notTestFile, - }); + if (notTestFile) { + expect(mockProcessSession.scheduleProcess).toBeCalledWith({ + type: 'by-file', + testFileName: editor.document.fileName, + notTestFile: true, + }); + } else { + expect(mockProcessSession.scheduleProcess).toBeCalledWith({ + type: 'by-file-pattern', + testFileNamePattern: editor.document.fileName, + }); + } } ); }); diff --git a/tests/JestExt/helper.test.ts b/tests/JestExt/helper.test.ts index f7e15a2f5..50f20473e 100644 --- a/tests/JestExt/helper.test.ts +++ b/tests/JestExt/helper.test.ts @@ -12,6 +12,7 @@ import { workspaceLogging } from '../../src/logging'; import { pathToJest, pathToConfig } from '../../src/helpers'; import { mockProjectWorkspace } from '../test-helper'; import { toFilePath } from '../../src/helpers'; +import { RunnerWorkspaceOptions } from '../../src/JestExt/types'; describe('createJestExtContext', () => { const workspaceFolder: any = { name: 'workspace' }; @@ -68,7 +69,8 @@ describe('createJestExtContext', () => { pathToConfig: 'whatever', rootPath: '', }; - const { runnerWorkspace } = createJestExtContext(workspaceFolder, settings); + const { createRunnerWorkspace } = createJestExtContext(workspaceFolder, settings); + const runnerWorkspace = createRunnerWorkspace(); expect(runnerWorkspace.jestCommandLine).toEqual('path-to-jest'); expect(runnerWorkspace.pathToConfig).toEqual('path-to-config'); }); @@ -78,21 +80,51 @@ describe('createJestExtContext', () => { pathToJest: 'abc', pathToConfig: 'whatever', }; - const { runnerWorkspace } = createJestExtContext(workspaceFolder, settings); + const { createRunnerWorkspace } = createJestExtContext(workspaceFolder, settings); + const runnerWorkspace = createRunnerWorkspace(); expect(runnerWorkspace.jestCommandLine).toEqual(settings.jestCommandLine); expect(runnerWorkspace.pathToConfig).toEqual(''); }); }); }); - it('will create runnerWorkspace', () => { - const rootPath = 'abc'; - const settings: any = { rootPath }; - const mockRunnerWorkspace = { rootPath }; - (ProjectWorkspace as jest.Mocked).mockReturnValue(mockRunnerWorkspace); - const context = createJestExtContext(workspaceFolder, settings); - expect(ProjectWorkspace).toBeCalled(); - expect(toFilePath).toBeCalledWith(rootPath); - expect(context.runnerWorkspace).toEqual(mockRunnerWorkspace); + describe('runnerWorkspace', () => { + it('will return runnerWorkspace factory method', () => { + const rootPath = 'abc'; + const settings: any = { rootPath }; + + jest.clearAllMocks(); + const mockRunnerWorkspace = { rootPath }; + (ProjectWorkspace as jest.Mocked).mockReturnValue(mockRunnerWorkspace); + + const context = createJestExtContext(workspaceFolder, settings); + expect(typeof context.createRunnerWorkspace).toEqual('function'); + expect(ProjectWorkspace).not.toBeCalled(); + + const runnerWorkspace = context.createRunnerWorkspace(); + expect(ProjectWorkspace).toBeCalled(); + expect(toFilePath).toBeCalledWith(rootPath); + expect(runnerWorkspace).toEqual(mockRunnerWorkspace); + }); + it('allow creating runnerWorkspace with custom options', () => { + const settings: any = { showCoverageOnLoad: false }; + + jest.clearAllMocks(); + + const { createRunnerWorkspace } = createJestExtContext(workspaceFolder, settings); + + let options: RunnerWorkspaceOptions = { outputFileSuffix: 'extra' }; + createRunnerWorkspace(options); + let args = (ProjectWorkspace as jest.Mocked).mock.calls[0]; + const [outputFileSuffix, collectCoverage] = [args[4], args[5]]; + expect(outputFileSuffix.endsWith('extra')).toBeTruthy(); + expect(collectCoverage).toEqual(false); + + options = { collectCoverage: true }; + createRunnerWorkspace(options); + args = (ProjectWorkspace as jest.Mocked).mock.calls[1]; + const collectCoverage2 = args[5]; + expect(collectCoverage2).toEqual(true); + }); }); it('will create logging factory', () => { const settings: any = {}; diff --git a/tests/JestExt/process-session.test.ts b/tests/JestExt/process-session.test.ts index 977bbff1b..56c006501 100644 --- a/tests/JestExt/process-session.test.ts +++ b/tests/JestExt/process-session.test.ts @@ -28,6 +28,10 @@ describe('ProcessSession', () => { context = mockJestProcessContext(); }); describe('scheduleProcess', () => { + const transform = (rRequest) => { + rRequest.schedule.queue = 'blocking-2'; + return rRequest; + }; it('will fire event for successful schedule', () => { const sm = createProcessSession(context); @@ -43,15 +47,16 @@ describe('ProcessSession', () => { expect(context.onRunEvent.fire).toBeCalledWith({ type: 'scheduled', process }); }); it.each` - type | inputProperty | expectedSchedule | expectedExtraProperty - ${'all-tests'} | ${undefined} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} - ${'watch-tests'} | ${undefined} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} - ${'watch-all-tests'} | ${undefined} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} - ${'by-file'} | ${{ testFileName: 'abc' }} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} - ${'by-file-test'} | ${{ testFileName: 'abc', testNamePattern: 'a test' }} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'], filterByContent: true } }} | ${undefined} - ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} - ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'a test' }} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'], filterByContent: true } }} | ${undefined} - ${'list-test-files'} | ${undefined} | ${{ queue: 'non-blocking', dedup: { filterByStatus: ['pending'] } }} | ${{ type: 'not-test', args: ['--listTests', '--json', '--watchAll=false'] }} + type | inputProperty | expectedSchedule | expectedExtraProperty + ${'all-tests'} | ${undefined} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} + ${'all-tests'} | ${{ transform }} | ${{ queue: 'blocking-2', dedup: { filterByStatus: ['pending'] } }} | ${undefined} + ${'watch-tests'} | ${undefined} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} + ${'watch-all-tests'} | ${undefined} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${undefined} + ${'by-file'} | ${{ testFileName: 'abc' }} | ${{ queue: 'blocking-2', dedup: { filterByStatus: ['pending'] } }} | ${undefined} + ${'by-file-test'} | ${{ testFileName: 'abc', testNamePattern: 'a test' }} | ${{ queue: 'blocking-2', dedup: { filterByStatus: ['pending'], filterByContent: true } }} | ${undefined} + ${'by-file-pattern'} | ${{ testFileNamePattern: 'abc' }} | ${{ queue: 'blocking-2', dedup: { filterByStatus: ['pending'] } }} | ${undefined} + ${'by-file-test-pattern'} | ${{ testFileNamePattern: 'abc', testNamePattern: 'a test' }} | ${{ queue: 'blocking-2', dedup: { filterByStatus: ['pending'], filterByContent: true } }} | ${undefined} + ${'list-test-files'} | ${undefined} | ${{ queue: 'non-blocking', dedup: { filterByStatus: ['pending'] } }} | ${{ type: 'not-test', args: ['--listTests', '--json', '--watchAll=false'] }} `( "can schedule '$type' request with ProcessManager", ({ type, inputProperty, expectedSchedule, expectedExtraProperty }) => { diff --git a/tests/JestProcessManagement/JestProcess.test.ts b/tests/JestProcessManagement/JestProcess.test.ts index 818b8fb68..0fccce9b6 100644 --- a/tests/JestProcessManagement/JestProcess.test.ts +++ b/tests/JestProcessManagement/JestProcess.test.ts @@ -140,8 +140,8 @@ describe('JestProcess', () => { ${'all-tests'} | ${undefined} | ${[false, false]} | ${true} | ${undefined} ${'watch-tests'} | ${undefined} | ${[true, false]} | ${true} | ${undefined} ${'watch-all-tests'} | ${undefined} | ${[true, true]} | ${true} | ${undefined} - ${'by-file'} | ${{ testFileName: '"c:\\a\\b.ts"' }} | ${[false, false]} | ${true} | ${{ args: { args: [] }, testFileNamePattern: '"C:\\a\\b.ts"' }} - ${'by-file'} | ${{ testFileName: '"c:\\a\\b.ts"', notTestFile: true }} | ${[false, false]} | ${true} | ${{ args: { args: ['--findRelatedTests'] }, testFileNamePattern: '"C:\\a\\b.ts"' }} + ${'by-file'} | ${{ testFileName: '"c:\\a\\b.ts"' }} | ${[false, false]} | ${true} | ${{ args: { args: ['--runTestsByPath'] }, testFileNamePattern: '"C:\\a\\b.ts"' }} + ${'by-file'} | ${{ testFileName: '"c:\\a\\b.ts"', notTestFile: true }} | ${[false, false]} | ${true} | ${{ args: { args: ['--findRelatedTests', '"C:\\a\\b.ts"'] } }} ${'by-file-test'} | ${{ testFileName: '"/a/b.js"', testNamePattern: 'a test' }} | ${[false, false]} | ${true} | ${{ args: { args: ['--runTestsByPath'] }, testFileNamePattern: '"/a/b.js"', testNamePattern: '"a test"' }} ${'by-file-pattern'} | ${{ testFileNamePattern: '"c:\\a\\b.ts"' }} | ${[false, false]} | ${true} | ${{ args: { args: ['--testPathPattern', '"c:\\\\a\\\\b\\.ts"'] } }} ${'by-file-test-pattern'} | ${{ testFileNamePattern: '/a/b.js', testNamePattern: 'a test' }} | ${[false, false]} | ${true} | ${{ args: { args: ['--testPathPattern', '"/a/b\\.js"'] }, testNamePattern: '"a test"' }} @@ -232,7 +232,7 @@ describe('JestProcess', () => { expect(options.args.args).not.toContain('--updateSnapshot'); } }); - it('starting on a running process does nothing but returns the same promise', async () => { + it('starting on a running process does nothing but returns the same promise', () => { expect.hasAssertions(); const request = mockRequest('all-tests'); jestProcess = new JestProcess(extContext, request); @@ -275,6 +275,22 @@ describe('JestProcess', () => { } ); }); + it('uses different output suffix for blocking-2 queue', () => { + expect.hasAssertions(); + const request = mockRequest('all-tests'); + + const jestProcess1 = new JestProcess(extContext, request); + jestProcess1.start(); + expect(extContext.createRunnerWorkspace).toBeCalledWith(undefined); + + const request2 = mockRequest('by-file', { testFileName: 'abc' }); + request2.schedule.queue = 'blocking-2'; + const jestProcess2 = new JestProcess(extContext, request2); + jestProcess2.start(); + expect(extContext.createRunnerWorkspace).toBeCalledWith( + expect.objectContaining({ outputFileSuffix: expect.anything() }) + ); + }); }); describe('to interrupt the process', () => { diff --git a/tests/JestProcessManagement/JestProcessManager.test.ts b/tests/JestProcessManagement/JestProcessManager.test.ts index e4dfa26cd..868dbd3ab 100644 --- a/tests/JestProcessManagement/JestProcessManager.test.ts +++ b/tests/JestProcessManagement/JestProcessManager.test.ts @@ -66,16 +66,16 @@ describe('JestProcessManager', () => { expect(jestProcessManager).not.toBe(null); }); - it('created 2 queues for blocking and non-blocking processes', () => { + it('created 2 blocking queues and 1 non-blocking queue', () => { const mockCreateTaskQueue = jest.spyOn(taskQueue, 'createTaskQueue'); const jestProcessManager = new JestProcessManager(extContext); expect(jestProcessManager).not.toBe(null); - expect(mockCreateTaskQueue).toBeCalledTimes(2); + expect(mockCreateTaskQueue).toBeCalledTimes(3); const maxWorkers = mockCreateTaskQueue.mock.calls.map((c) => c[1]); // blocking queue has 1 worker - expect(maxWorkers.includes(1)).toBeTruthy(); + expect(maxWorkers.filter((n) => n === 1)).toHaveLength(2); // non-blocking queue has more than 1 worker - expect(maxWorkers.find((n) => n > 1)).not.toBeUndefined(); + expect(maxWorkers.filter((n) => n > 1)).toHaveLength(1); }); }); describe('start a jest process', () => { @@ -362,28 +362,34 @@ describe('JestProcessManager', () => { describe('stop processes', () => { const blockingSchedule: ScheduleStrategy = { queue: 'blocking' }; + const blocking2Schedule: ScheduleStrategy = { queue: 'blocking-2' }; const nonBlockingSchedule: ScheduleStrategy = { queue: 'non-blocking' }; const blockingRequests = [ mockProcessRequest('all-tests', { schedule: blockingSchedule }), mockProcessRequest('watch-tests', { schedule: blockingSchedule }), ]; + const blockingRequests2 = [mockProcessRequest('by-file', { schedule: blocking2Schedule })]; const nonBlockingRequests = [mockProcessRequest('not-test', { schedule: nonBlockingSchedule })]; let pm; let blockingP; + let blockingP2; let nonBlockingP; beforeEach(() => { pm = new JestProcessManager(extContext); blockingP = blockingRequests.map((r) => mockJestProcess(r)); + blockingP2 = blockingRequests2.map((r) => mockJestProcess(r)); nonBlockingP = nonBlockingRequests.map((r) => mockJestProcess(r)); blockingRequests.forEach((r) => pm.scheduleJestProcess(r)); + blockingRequests2.forEach((r) => pm.scheduleJestProcess(r)); nonBlockingRequests.forEach((r) => pm.scheduleJestProcess(r)); }); - it.each([['blocking'], ['non-blocking'], [undefined]])( + it.each([['blocking'], ['blocking-2'], ['non-blocking'], [undefined]])( 'can stop all processes from queue: %s', async (queueType) => { // before stopping expect(getState(pm, blockingP[0])).toEqual({ inQ: true, started: true, qSize: 2 }); expect(getState(pm, blockingP[1])).toEqual({ inQ: true, started: false, qSize: 2 }); + expect(getState(pm, blockingP2[0])).toEqual({ inQ: true, started: true, qSize: 1 }); expect(getState(pm, nonBlockingP[0])).toEqual({ inQ: true, started: true, qSize: 1 }); await pm.stopAll(queueType); @@ -392,14 +398,22 @@ describe('JestProcessManager', () => { if (queueType === 'blocking') { expect(getState(pm, blockingP[0])).toEqual({ inQ: false, qSize: 0 }); expect(getState(pm, blockingP[1])).toEqual({ inQ: false, qSize: 0 }); + expect(getState(pm, blockingP2[0])).toEqual({ inQ: true, qSize: 1, started: true }); + expect(getState(pm, nonBlockingP[0])).toEqual({ inQ: true, started: true, qSize: 1 }); + } else if (queueType === 'blocking-2') { + expect(getState(pm, blockingP[0])).toEqual({ inQ: true, started: true, qSize: 2 }); + expect(getState(pm, blockingP[1])).toEqual({ inQ: true, started: false, qSize: 2 }); + expect(getState(pm, blockingP2[0])).toEqual({ inQ: false, qSize: 0 }); expect(getState(pm, nonBlockingP[0])).toEqual({ inQ: true, started: true, qSize: 1 }); } else if (queueType === 'non-blocking') { expect(getState(pm, blockingP[0])).toEqual({ inQ: true, started: true, qSize: 2 }); expect(getState(pm, blockingP[1])).toEqual({ inQ: true, started: false, qSize: 2 }); + expect(getState(pm, blockingP2[0])).toEqual({ inQ: true, qSize: 1, started: true }); expect(getState(pm, nonBlockingP[0])).toEqual({ inQ: false, qSize: 0 }); } else { expect(getState(pm, blockingP[0])).toEqual({ inQ: false, qSize: 0 }); expect(getState(pm, blockingP[1])).toEqual({ inQ: false, qSize: 0 }); + expect(getState(pm, blockingP2[0])).toEqual({ inQ: false, qSize: 0 }); expect(getState(pm, nonBlockingP[0])).toEqual({ inQ: false, qSize: 0 }); } } @@ -455,4 +469,25 @@ describe('JestProcessManager', () => { expect(found.data).toEqual(nonBlockingP[0]); }); }); + describe('numberOfProcesses', () => { + it('will add up processes in all queues', () => { + const blockingSchedule: ScheduleStrategy = { queue: 'blocking' }; + const blocking2Schedule: ScheduleStrategy = { queue: 'blocking-2' }; + const nonBlockingSchedule: ScheduleStrategy = { queue: 'non-blocking' }; + const blockingRequests = [ + mockProcessRequest('all-tests', { schedule: blockingSchedule }), + mockProcessRequest('watch-tests', { schedule: blockingSchedule }), + ]; + const blockingRequests2 = [mockProcessRequest('by-file', { schedule: blocking2Schedule })]; + const nonBlockingRequests = [ + mockProcessRequest('not-test', { schedule: nonBlockingSchedule }), + ]; + const pm = new JestProcessManager(extContext); + blockingRequests.forEach((r) => pm.scheduleJestProcess(r)); + blockingRequests2.forEach((r) => pm.scheduleJestProcess(r)); + nonBlockingRequests.forEach((r) => pm.scheduleJestProcess(r)); + + expect(pm.numberOfProcesses()).toEqual(4); + }); + }); }); diff --git a/tests/test-helper.ts b/tests/test-helper.ts index e48d25cef..713bdd651 100644 --- a/tests/test-helper.ts +++ b/tests/test-helper.ts @@ -150,7 +150,7 @@ export const mockJestExtEvents: any = () => ({ export const mockJestExtContext = (autoRun?: AutoRunAccessor): any => { return { workspace: jest.fn(), - runnerWorkspace: jest.fn(), + createRunnerWorkspace: jest.fn(), settings: jest.fn(), loggingFactory: { create: jest.fn(() => jest.fn()) }, autoRun: autoRun ?? jest.fn(), diff --git a/tests/test-provider/test-helper.ts b/tests/test-provider/test-helper.ts index 0fc14c4c4..bb0acfe3c 100644 --- a/tests/test-provider/test-helper.ts +++ b/tests/test-provider/test-helper.ts @@ -70,13 +70,20 @@ export const mockController = (): any => { return run; }), dispose: jest.fn(), - createRunProfile: jest.fn(), + createRunProfile: jest.fn().mockImplementation((label, kind, runHandler, isDefault, tags) => ({ + label, + kind, + runHandler, + isDefault, + tags: tags ?? [], + })), createTestItem: jest.fn().mockImplementation((id, label, uri) => { const item: any = { id, label, uri, errored: jest.fn(), + tags: [], }; item.children = new TestItemCollectionMock(item); return item; diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts index ef969648b..77013483d 100644 --- a/tests/test-provider/test-item-data.test.ts +++ b/tests/test-provider/test-item-data.test.ts @@ -72,7 +72,8 @@ describe('test-item-data', () => { beforeEach(() => { controllerMock = mockController(); - context = new JestTestProviderContext(mockExtExplorerContext('ws-1'), controllerMock); + 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(); @@ -493,17 +494,18 @@ describe('test-item-data', () => { context.ext.session.scheduleProcess.mockReturnValue({ id: 'pid' }); }); describe('run request', () => { - it('WorkspaceRoot runs all tests in the workspace', () => { + it('WorkspaceRoot runs all tests in the workspace in blocking-2 queue', () => { const wsRoot = new WorkspaceRoot(context); - wsRoot.scheduleTest(runMock, resolveMock, profile); - expect(context.ext.session.scheduleProcess).toBeCalledWith( - expect.objectContaining({ type: 'all-tests' }) - ); + wsRoot.scheduleTest(runMock, resolveMock); + const r = context.ext.session.scheduleProcess.mock.calls[0][0]; + expect(r.type).toEqual('all-tests'); + const transformed = r.transform({ schedule: { queue: 'blocking' } }); + expect(transformed.schedule.queue).toEqual('blocking-2'); }); 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, profile); + folderData.scheduleTest(runMock, resolveMock); expect(context.ext.session.scheduleProcess).toBeCalledWith( expect.objectContaining({ type: 'by-file-pattern', @@ -518,11 +520,11 @@ describe('test-item-data', () => { { fsPath: '/ws-1/a.test.ts' } as any, parent ); - docRoot.scheduleTest(runMock, resolveMock, profile); + docRoot.scheduleTest(runMock, resolveMock); expect(context.ext.session.scheduleProcess).toBeCalledWith( expect.objectContaining({ - type: 'by-file', - testFileName: '/ws-1/a.test.ts', + type: 'by-file-pattern', + testFileNamePattern: '/ws-1/a.test.ts', }) ); }); @@ -531,7 +533,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, profile); + tData.scheduleTest(runMock, resolveMock); expect(context.ext.session.scheduleProcess).toBeCalledWith( expect.objectContaining({ type: 'by-file-test-pattern', @@ -545,14 +547,14 @@ 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, profile)).toBeUndefined(); + expect(docRoot.scheduleTest(runMock, resolveMock)).toBeUndefined(); expect(runMock.errored).toBeCalledWith(docRoot.item, expect.anything()); expect(resolveMock).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, profile); + folderData.scheduleTest(runMock, resolveMock); const request = context.ext.session.scheduleProcess.mock.calls[0][0]; expect(request.itemRun.run).toEqual(runMock); @@ -910,34 +912,48 @@ describe('test-item-data', () => { }); }); }); - describe('canRun', () => { - it('watch-mode workspace does not support Run profile', () => { - const wsRoot = new WorkspaceRoot(context); - const profile: any = { kind: vscode.TestRunProfileKind.Run }; - - context.ext.autoRun.isWatch = true; - expect(wsRoot.canRun(profile)).toBeFalsy(); - - context.ext.autoRun.isWatch = false; - expect(wsRoot.canRun(profile)).toBeTruthy(); + describe('tags', () => { + let wsRoot, folder, doc, test; + beforeEach(() => { + wsRoot = new WorkspaceRoot(context); + folder = new FolderData(context, 'dir', wsRoot.item); + const uri: any = { fsPath: 'whatever' }; + doc = new TestDocumentRoot(context, uri, folder.item); + const node: any = { fullName: 'a test', attrs: {}, data: {} }; + test = new TestData(context, uri, node, doc.item); }); - it('only TestData support Debug profile', () => { - const wsRoot = new WorkspaceRoot(context); - const profile: any = { kind: vscode.TestRunProfileKind.Debug }; - expect(wsRoot.canRun(profile)).toBeFalsy(); - - const parentItem: any = controllerMock.createTestItem('parent', 'parent', {}); + it('all TestItem supports run tag', () => { + [wsRoot, folder, doc, test].forEach((itemData) => + expect(itemData.item.tags.find((t) => t.id === 'run')).toBeTruthy() + ); + }); + it('only TestData and TestDocument supports debug tags', () => { + [doc, test].forEach((itemData) => + expect(itemData.item.tags.find((t) => t.id === 'debug')).toBeTruthy() + ); + [wsRoot, folder].forEach((itemData) => + expect(itemData.item.tags.find((t) => t.id === 'debug')).toBeUndefined() + ); + }); + }); + describe('getDebugInfo', () => { + let doc, test; + beforeEach(() => { + const uri: any = { fsPath: 'whatever' }; + const parentItem: any = controllerMock.createTestItem('ws-1', 'ws-1', uri); + doc = new TestDocumentRoot(context, uri, parentItem); const node: any = { fullName: 'a test', attrs: {}, data: {} }; - - const test = new TestData(context, { fsPath: 'whatever' } as any, node, parentItem); - expect(test.canRun(profile)).toBeTruthy(); - - expect(test.getDebugInfo()).toEqual({ fileName: 'whatever', testNamePattern: node.fullName }); + test = new TestData(context, uri, node, doc.item); }); - it('any other profile kind is not supported at this point', () => { - const wsRoot = new WorkspaceRoot(context); - const profile: any = { kind: vscode.TestRunProfileKind.Coverage }; - expect(wsRoot.canRun(profile)).toBeFalsy(); + it('TestData returns file and test info', () => { + const debugInfo = test.getDebugInfo(); + expect(debugInfo.fileName).toEqual(test.item.uri.fsPath); + expect(debugInfo.testNamePattern).toEqual('a test'); + }); + it('TestDocumentRoot returns only file info', () => { + const debugInfo = doc.getDebugInfo(); + expect(debugInfo.fileName).toEqual(doc.item.uri.fsPath); + expect(debugInfo.testNamePattern).toBeUndefined(); }); }); describe('WorkspaceRoot listens to jest run events', () => { diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts index 1f5e576f5..2d19d7c80 100644 --- a/tests/test-provider/test-provider.test.ts +++ b/tests/test-provider/test-provider.test.ts @@ -20,7 +20,6 @@ describe('JestTestProvider', () => { discoverTest: jest.fn(), scheduleTest: jest.fn(), dispose: jest.fn(), - canRun: jest.fn().mockReturnValue(true), }; if (debuggable) { data.getDebugInfo = jest.fn(); @@ -41,6 +40,7 @@ describe('JestTestProvider', () => { let controllerMock; let extExplorerContextMock; let workspaceRootMock; + let mockTestTag; beforeEach(() => { jest.resetAllMocks(); @@ -54,6 +54,9 @@ describe('JestTestProvider', () => { return controllerMock; }); + mockTestTag = jest.fn((id) => ({ id })); + (vscode.TestTag as jest.Mocked) = mockTestTag; + (vscode.workspace.getWorkspaceFolder as jest.Mocked).mockImplementation((uri) => ({ name: uri, })); @@ -74,47 +77,42 @@ describe('JestTestProvider', () => { `${extensionId}:TestProvider:ws-1`, expect.stringContaining('ws-1') ); - expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(3); + expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(2); [ - vscode.TestRunProfileKind.Run, - vscode.TestRunProfileKind.Debug, - vscode.TestRunProfileKind.Coverage, - ].forEach((kind) => { + [vscode.TestRunProfileKind.Run, 'run'], + [vscode.TestRunProfileKind.Debug, 'debug'], + ].forEach(([kind, id]) => { expect(controllerMock.createRunProfile).toHaveBeenCalledWith( expect.anything(), kind, expect.anything(), - true + true, + expect.objectContaining({ id }) ); }); expect(WorkspaceRoot).toBeCalled(); }); it.each` - isWatchMode | createRunProfile - ${true} | ${false} - ${false} | ${true} - `( - 'will createRunProfile($createRunProfile) if isWatchMode=$isWatchMode', - ({ isWatchMode, createRunProfile }) => { - extExplorerContextMock.autoRun.isWatch = isWatchMode; - new JestTestProvider(extExplorerContextMock); - const kinds = [vscode.TestRunProfileKind.Debug, vscode.TestRunProfileKind.Coverage]; - if (createRunProfile) { - kinds.push(vscode.TestRunProfileKind.Run); - } + isWatchMode + ${true} + ${false} + `('will create Profiles regardless isWatchMode=$isWatchMode', ({ isWatchMode }) => { + extExplorerContextMock.autoRun.isWatch = isWatchMode; + new JestTestProvider(extExplorerContextMock); + const kinds = [vscode.TestRunProfileKind.Debug, vscode.TestRunProfileKind.Run]; - expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(kinds.length); - kinds.forEach((kind) => { - expect(controllerMock.createRunProfile).toHaveBeenCalledWith( - expect.anything(), - kind, - expect.anything(), - true - ); - }); - } - ); + expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(kinds.length); + kinds.forEach((kind) => { + expect(controllerMock.createRunProfile).toHaveBeenCalledWith( + expect.anything(), + kind, + expect.anything(), + true, + expect.anything() + ); + }); + }); }); describe('can discover tests', () => { @@ -210,7 +208,6 @@ describe('JestTestProvider', () => { itemDataList.forEach((d) => { d.context = { workspace: { name: 'whatever' } }; d.getDebugInfo = jest.fn().mockReturnValueOnce({}); - d.canRun = jest.fn().mockReturnValue(true); }); return itemDataList; }; @@ -229,16 +226,17 @@ describe('JestTestProvider', () => { debugDone = () => resolve(); }); it.each` - debugInfo | debugTests | hasError - ${undefined} | ${() => Promise.resolve()} | ${true} - ${{ fileName: 'file', testNamePattern: 'a test' }} | ${() => Promise.resolve()} | ${false} - ${{ fileName: 'file', testNamePattern: 'a test' }} | ${() => Promise.reject('error')} | ${true} - ${{ fileName: 'file', testNamePattern: 'a test' }} | ${throwError} | ${true} + 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} `( - "invoke debug test async: debugInfo = '$debugInfo' when resultContextMock.debugTests = $resultContextMock.debugTests => error? $hasError", - async ({ debugInfo, debugTests, hasError }) => { + "invoke debug test async: debugInfo = '$debugInfo', testNamePattern='$testNamePattern' when resultContextMock.debugTests = $resultContextMock.debugTests => error? $hasError", + async ({ debugInfo, testNamePattern, debugTests, hasError }) => { expect.hasAssertions(); - extExplorerContextMock.debugTests = jest.fn(() => { + extExplorerContextMock.debugTests = jest.fn().mockImplementation(() => { if (debugTests) { return debugTests(); } @@ -247,9 +245,12 @@ describe('JestTestProvider', () => { const itemDataList = setupItemData(workspaceRootMock.context, [1]); itemDataList.forEach((d) => { - d.canRun.mockReturnValue(true); if (debugInfo) { - d.getDebugInfo.mockReturnValueOnce(debugInfo); + d.getDebugInfo = jest + .fn() + .mockImplementation(() => + testNamePattern ? { ...debugInfo, testNamePattern } : debugInfo + ); } else { d.getDebugInfo = undefined; } @@ -267,6 +268,12 @@ describe('JestTestProvider', () => { expect.anything() ); expect(vscode.TestMessage).toBeCalledTimes(1); + } else { + if (testNamePattern) { + expect(extExplorerContextMock.debugTests).toBeCalledWith('file', testNamePattern); + } else { + expect(extExplorerContextMock.debugTests).toBeCalledWith('file'); + } } } ); @@ -438,9 +445,8 @@ describe('JestTestProvider', () => { itemDataList.forEach((d) => { expect(d.scheduleTest).toBeCalled(); - const [run, resolve, profile] = d.scheduleTest.mock.calls[0]; + const [run, resolve] = d.scheduleTest.mock.calls[0]; expect(run).toBe(runMock); - expect(profile).toBe(request.profile); // close the schedule resolve(); }); @@ -470,9 +476,8 @@ describe('JestTestProvider', () => { itemDataList.forEach((d) => { expect(d.scheduleTest).toBeCalled(); - const [run, resolve, profile] = d.scheduleTest.mock.calls[0]; + const [run, resolve] = d.scheduleTest.mock.calls[0]; expect(run).toBe(runMock); - expect(profile).toBe(request.profile); // close the schedule resolve(); }); @@ -509,9 +514,8 @@ describe('JestTestProvider', () => { itemDataList.forEach((d, idx) => { expect(d.scheduleTest).toBeCalled(); - const [run, resolve, profile] = d.scheduleTest.mock.calls[0]; + const [run, resolve] = d.scheduleTest.mock.calls[0]; expect(run).toBe(runMock); - expect(profile).toBe(request.profile); /* eslint-disable jest/no-conditional-expect */ if (idx === 1) { @@ -538,9 +542,8 @@ describe('JestTestProvider', () => { const p = testProvider.runTests(request, cancelToken); const runMock = controllerMock.lastRunMock(); expect(workspaceRootMock.scheduleTest).toBeCalledTimes(1); - const [run, resolve, profile] = workspaceRootMock.scheduleTest.mock.calls[0]; + const [run, resolve] = workspaceRootMock.scheduleTest.mock.calls[0]; expect(run).toBe(runMock); - expect(profile).toBe(request.profile); resolve(); await p; @@ -557,43 +560,5 @@ describe('JestTestProvider', () => { expect(controllerMock.createTestRun).not.toBeCalled(); }); }); - it('will report error for testItems not supporting the given runProfile', async () => { - expect.hasAssertions(); - - const testProvider = new JestTestProvider(extExplorerContextMock); - const itemDataList = setupItemData(workspaceRootMock.context); - itemDataList.forEach((d, idx) => { - d.scheduleTest.mockReturnValueOnce(`pid-${idx}`); - if (idx === 1) { - d.canRun.mockReturnValue(false); - } - }); - const request: any = { - include: itemDataList.map((d) => d.item), - profile: { kind: vscode.TestRunProfileKind.Run }, - }; - - const p = testProvider.runTests(request, cancelToken); - - expect(controllerMock.createTestRun).toBeCalled(); - const runMock = controllerMock.lastRunMock(); - - itemDataList.forEach((d, idx) => { - if (idx !== 1) { - expect(d.scheduleTest).toBeCalled(); - const [run, resolve, profile] = d.scheduleTest.mock.calls[0]; - expect(run).toBe(runMock); - expect(profile).toBe(request.profile); - resolve(); - } else { - expect(d.scheduleTest).not.toBeCalled(); - expect(vscode.window.showWarningMessage).toBeCalled(); - } - }); - - await p; - expect(runMock.end).toBeCalled(); - expect(vscode.window.showWarningMessage).toBeCalled(); - }); }); }); diff --git a/typings/vscode.d.ts b/typings/vscode.d.ts index 3e35c9098..431e53625 100644 --- a/typings/vscode.d.ts +++ b/typings/vscode.d.ts @@ -541,7 +541,7 @@ declare module 'vscode' { * The {@link TextEditorSelectionChangeKind change kind} which has triggered this * event. Can be `undefined`. */ - readonly kind?: TextEditorSelectionChangeKind; + readonly kind: TextEditorSelectionChangeKind | undefined; } /** @@ -766,8 +766,9 @@ declare module 'vscode' { preserveFocus?: boolean; /** - * An optional flag that controls if an {@link TextEditor editor}-tab will be replaced - * with the next editor or if it will be kept. + * 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. */ preview?: boolean; @@ -817,7 +818,7 @@ declare module 'vscode' { /** * The optional ThemeColor of the icon. The color is currently only used in {@link TreeItem}. */ - readonly color?: ThemeColor; + readonly color?: ThemeColor | undefined; /** * Creates a reference to a theme icon. @@ -1106,13 +1107,13 @@ declare module 'vscode' { /** * The selections in this text editor. The primary selection is always at index 0. */ - selections: Selection[]; + selections: readonly Selection[]; /** * The current visible ranges in the editor (vertically). * This accounts only for vertical scrolling, and not for horizontal scrolling. */ - readonly visibleRanges: Range[]; + readonly visibleRanges: readonly Range[]; /** * Text editor options. @@ -1124,7 +1125,7 @@ declare module 'vscode' { * isn't one of the main editors, e.g. an embedded editor, or when the editor * column is larger than three. */ - readonly viewColumn?: ViewColumn; + readonly viewColumn: ViewColumn | undefined; /** * Perform an edit on the document associated with this text editor. @@ -1276,16 +1277,16 @@ declare module 'vscode' { * `Uri.parse('file://' + path)` because the path might contain characters that are * interpreted (# and ?). See the following sample: * ```ts - const good = URI.file('/coding/c#/project1'); - good.scheme === 'file'; - good.path === '/coding/c#/project1'; - good.fragment === ''; - - const bad = URI.parse('file://' + '/coding/c#/project1'); - bad.scheme === 'file'; - bad.path === '/coding/c'; // path is now broken - bad.fragment === '/project1'; - ``` + * const good = URI.file('/coding/c#/project1'); + * good.scheme === 'file'; + * good.path === '/coding/c#/project1'; + * good.fragment === ''; + * + * const bad = URI.parse('file://' + '/coding/c#/project1'); + * bad.scheme === 'file'; + * bad.path === '/coding/c'; // path is now broken + * bad.fragment === '/project1'; + * ``` * * @param path A file system or UNC path. * @return A new Uri instance. @@ -1369,11 +1370,11 @@ declare module 'vscode' { * The *difference* to the {@linkcode Uri.path path}-property is the use of the platform specific * path separator and the handling of UNC paths. The sample below outlines the difference: * ```ts - const u = URI.parse('file://server/c$/folder/file.txt') - u.authority === 'server' - u.path === '/shares/c$/file.txt' - u.fsPath === '\\server\c$\folder\file.txt' - ``` + * const u = URI.parse('file://server/c$/folder/file.txt') + * u.authority === 'server' + * u.path === '/shares/c$/file.txt' + * u.fsPath === '\\server\c$\folder\file.txt' + * ``` */ readonly fsPath: string; @@ -1660,8 +1661,10 @@ declare module 'vscode' { detail?: string; /** - * Optional flag indicating if this item is picked initially. - * (Only honored when the picker allows multiple selections.) + * Optional flag indicating if this item is picked initially. This is only honored when using + * the {@link window.showQuickPick()} API. To do the same thing with the {@link window.createQuickPick()} API, + * simply set the {@link QuickPick.selectedItems} to the items you want picked initially. + * (*Note:* This is only honored when the picker allows multiple selections.) * * @see {@link QuickPickOptions.canPickMany} */ @@ -1671,6 +1674,14 @@ declare module 'vscode' { * Always show this item. */ alwaysShow?: boolean; + + /** + * Optional buttons that will be rendered on this particular item. These buttons will trigger + * 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. + */ + buttons?: readonly QuickInputButton[]; } /** @@ -2227,7 +2238,7 @@ declare module 'vscode' { * * Actions not of this kind are filtered out before being shown by the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action). */ - readonly only?: CodeActionKind; + readonly only: CodeActionKind | undefined; } /** @@ -2566,11 +2577,12 @@ declare module 'vscode' { } /** - * The MarkdownString represents human-readable text that supports formatting via the - * markdown syntax. Standard markdown is supported, also tables, but no embedded html. + * Human-readable text that supports formatting via the [markdown syntax](https://commonmark.org). * * Rendering of {@link ThemeIcon theme icons} via the `$()`-syntax is supported - * when the {@linkcode MarkdownString.supportThemeIcons supportThemeIcons} is set to `true`. + * when the {@linkcode supportThemeIcons} is set to `true`. + * + * Rendering of embedded html is supported when {@linkcode supportHtml} is set to `true`. */ export class MarkdownString { @@ -2590,6 +2602,18 @@ declare module 'vscode' { */ supportThemeIcons?: boolean; + /** + * Indicates that this markdown string can contain raw html tags. Defaults to `false`. + * + * When `supportHtml` is false, the markdown renderer will strip out any raw html tags + * that appear in the markdown text. This means you can only use markdown syntax for rendering. + * + * When `supportHtml` is true, the markdown render will also allow a safe subset of html tags + * and attributes to be rendered. See https://github.com/microsoft/vscode/blob/6d2920473c6f13759c978dd89104c4270a83422d/src/vs/base/browser/markdownRenderer.ts#L296 + * for a list of all supported tags and attributes. + */ + supportHtml?: boolean; + /** * Creates a new markdown string with the given value. * @@ -2691,7 +2715,7 @@ declare module 'vscode' { /* * If specified the expression overrides the extracted expression. */ - readonly expression?: string; + readonly expression?: string | undefined; /** * Creates a new evaluatable expression object. @@ -2758,7 +2782,7 @@ declare module 'vscode' { /** * If specified the name of the variable to look up. */ - readonly variableName?: string; + readonly variableName?: string | undefined; /** * How to perform the lookup. */ @@ -2787,7 +2811,7 @@ declare module 'vscode' { /** * If specified the expression overrides the extracted expression. */ - readonly expression?: string; + readonly expression?: string | undefined; /** * Creates a new InlineValueEvaluatableExpression object. * @@ -3538,7 +3562,7 @@ declare module 'vscode' { * * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). */ - readonly resultId?: string; + readonly resultId: string | undefined; /** * The actual tokens data. * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format. @@ -3558,7 +3582,7 @@ declare module 'vscode' { * * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented). */ - readonly resultId?: string; + readonly resultId: string | undefined; /** * The edits to the tokens data. * All edits refer to the initial data state. @@ -3584,7 +3608,7 @@ declare module 'vscode' { /** * The elements to insert. */ - readonly data?: Uint32Array; + readonly data: Uint32Array | undefined; constructor(start: number, deleteCount: number, data?: Uint32Array); } @@ -3918,7 +3942,7 @@ declare module 'vscode' { * This is `undefined` when signature help is not triggered by typing, such as when manually invoking * signature help or when moving the cursor. */ - readonly triggerCharacter?: string; + readonly triggerCharacter: string | undefined; /** * `true` if signature help was already showing when it was triggered. @@ -3934,7 +3958,7 @@ declare module 'vscode' { * The `activeSignatureHelp` has its [`SignatureHelp.activeSignature`] field updated based on * the user arrowing through available signatures. */ - readonly activeSignatureHelp?: SignatureHelp; + readonly activeSignatureHelp: SignatureHelp | undefined; } /** @@ -4250,11 +4274,11 @@ declare module 'vscode' { /** * Character that triggered the completion item provider. * - * `undefined` if provider was not triggered by a character. + * `undefined` if the provider was not triggered by a character. * * The trigger character is already in the document when the completion provider is triggered. */ - readonly triggerCharacter?: string; + readonly triggerCharacter: string | undefined; } /** @@ -4767,6 +4791,104 @@ declare module 'vscode' { provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult; } + /** + * Represents an item of a type hierarchy, like a class or an interface. + */ + export class TypeHierarchyItem { + /** + * The name of this item. + */ + name: string; + + /** + * The kind of this item. + */ + kind: SymbolKind; + + /** + * Tags for this item. + */ + tags?: ReadonlyArray; + + /** + * More detail for this item, e.g. the signature of a function. + */ + detail?: string; + + /** + * The resource identifier of this item. + */ + uri: Uri; + + /** + * The range enclosing this symbol not including leading/trailing whitespace + * but everything else, e.g. comments and code. + */ + range: Range; + + /** + * The range that should be selected and revealed when this symbol is being + * picked, e.g. the name of a class. Must be contained by the {@link TypeHierarchyItem.range range}-property. + */ + selectionRange: Range; + + /** + * Creates a new type hierarchy item. + * + * @param kind The kind of the item. + * @param name The name of the item. + * @param detail The details of the item. + * @param uri The Uri of the item. + * @param range The whole range of the item. + * @param selectionRange The selection range of the item. + */ + constructor(kind: SymbolKind, name: string, detail: string, uri: Uri, range: Range, selectionRange: Range); + } + + /** + * The type hierarchy provider interface describes the contract between extensions + * and the type hierarchy feature. + */ + export interface TypeHierarchyProvider { + + /** + * Bootstraps type hierarchy by returning the item that is denoted by the given document + * and position. This item will be used as entry into the type graph. Providers should + * return `undefined` or `null` when there is no item at the given location. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param token A cancellation token. + * @returns One or multiple type hierarchy items or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined`, `null`, or an empty array. + */ + prepareTypeHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + + /** + * Provide all supertypes for an item, e.g all types from which a type is derived/inherited. In graph terms this describes directed + * and annotated edges inside the type graph, e.g the given item is the starting node and the result is the nodes + * that can be reached. + * + * @param item The hierarchy item for which super types should be computed. + * @param token A cancellation token. + * @returns A set of direct supertypes or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideTypeHierarchySupertypes(item: TypeHierarchyItem, token: CancellationToken): ProviderResult; + + /** + * Provide all subtypes for an item, e.g all types which are derived/inherited from the given item. In + * graph terms this describes directed and annotated edges inside the type graph, e.g the given item is the starting + * node and the result is the nodes that can be reached. + * + * @param item The hierarchy item for which subtypes should be computed. + * @param token A cancellation token. + * @returns A set of direct subtypes or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideTypeHierarchySubtypes(item: TypeHierarchyItem, token: CancellationToken): ProviderResult; + } + /** * Represents a list of ranges that can be edited together along with a word pattern to describe valid range contents. */ @@ -4789,7 +4911,7 @@ declare module 'vscode' { * An optional word pattern that describes valid contents for the given ranges. * If no pattern is provided, the language configuration's word pattern will be used. */ - readonly wordPattern?: RegExp; + readonly wordPattern: RegExp | undefined; } /** @@ -5019,18 +5141,17 @@ declare module 'vscode' { * - *Workspace Folder settings* - From one of the {@link workspace.workspaceFolders Workspace Folders} under which requested resource belongs to. * - *Language settings* - Settings defined under requested language. * - * The *effective* value (returned by {@linkcode WorkspaceConfiguration.get get}) is computed by overriding or merging the values in the following order. + * The *effective* value (returned by {@linkcode WorkspaceConfiguration.get get}) is computed by overriding or merging the values in the following order: + * + * 1. `defaultValue` (if defined in `package.json` otherwise derived from the value's type) + * 1. `globalValue` (if defined) + * 1. `workspaceValue` (if defined) + * 1. `workspaceFolderValue` (if defined) + * 1. `defaultLanguageValue` (if defined) + * 1. `globalLanguageValue` (if defined) + * 1. `workspaceLanguageValue` (if defined) + * 1. `workspaceFolderLanguageValue` (if defined) * - * ``` - * `defaultValue` (if defined in `package.json` otherwise derived from the value's type) - * `globalValue` (if defined) - * `workspaceValue` (if defined) - * `workspaceFolderValue` (if defined) - * `defaultLanguageValue` (if defined) - * `globalLanguageValue` (if defined) - * `workspaceLanguageValue` (if defined) - * `workspaceFolderLanguageValue` (if defined) - * ``` * **Note:** Only `object` value types are merged and all other value types are overridden. * * Example 1: Overriding @@ -5545,6 +5666,13 @@ declare module 'vscode' { */ appendLine(value: string): void; + /** + * Replaces all output from the channel with the given value. + * + * @param value A string, falsy values will not be printed. + */ + replace(value: string): void; + /** * Removes all output from the channel. */ @@ -5635,7 +5763,7 @@ declare module 'vscode' { * The priority of this item. Higher value means the item should * be shown more to the left. */ - readonly priority?: number; + readonly priority: number | undefined; /** * The name of the entry, like 'Python Language Indicator', 'Git Status' etc. @@ -5691,7 +5819,7 @@ declare module 'vscode' { /** * Accessibility information used when a screen reader interacts with this StatusBar item */ - accessibilityInformation?: AccessibilityInformation; + accessibilityInformation: AccessibilityInformation | undefined; /** * Shows the entry in the status bar. @@ -5760,6 +5888,11 @@ declare module 'vscode' { */ readonly exitStatus: TerminalExitStatus | undefined; + /** + * The current state of the {@link Terminal}. + */ + readonly state: TerminalState; + /** * Send text to the terminal. The text is written to the stdin of the underlying pty process * (shell) of the terminal. @@ -5789,6 +5922,71 @@ declare module 'vscode' { dispose(): void; } + /** + * The location of the terminal. + */ + export enum TerminalLocation { + /** + * In the terminal view + */ + Panel = 1, + /** + * In the editor area + */ + Editor = 2, + } + + /** + * Assumes a {@link TerminalLocation} of editor and allows specifying a {@link ViewColumn} and + * {@link preserveFocus} property + */ + export interface TerminalEditorLocationOptions { + /** + * A view column in which the {@link Terminal terminal} should be shown in the editor area. + * Use {@link ViewColumn.Active active} to open in the active editor group, 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. + */ + viewColumn: ViewColumn; + /** + * An optional flag that when `true` will stop the {@link Terminal} from taking focus. + */ + preserveFocus?: boolean; + } + + /** + * Uses the parent {@link Terminal}'s location for the terminal + */ + export interface TerminalSplitLocationOptions { + /** + * The parent terminal to split this terminal beside. This works whether the parent terminal + * is in the panel or the editor area. + */ + parentTerminal: Terminal; + } + + /** + * Represents the state of a {@link Terminal}. + */ + export interface TerminalState { + /** + * Whether the {@link Terminal} has been interacted with. Interaction means that the + * terminal has sent data to the process which depending on the terminal's _mode_. By + * default input is sent when a key is pressed or when a command or extension sends text, + * but based on the terminal's mode it can also happen on: + * + * - a pointer click event + * - a pointer scroll event + * - a pointer move event + * - terminal focus in/out + * + * For more information on events that can send data see "DEC Private Mode Set (DECSET)" on + * https://invisible-island.net/xterm/ctlseqs/ctlseqs.html + */ + readonly isInteractedWith: boolean; + } + /** * Provides information on a line in a terminal in order to provide links for it. */ @@ -6244,6 +6442,9 @@ declare module 'vscode' { /** * Store a value. The value must be JSON-stringifyable. * + * *Note* that using `undefined` as value removes the key from the underlying + * storage. + * * @param key A string. * @param value A value. MUST not contain cyclic references. */ @@ -6424,7 +6625,7 @@ declare module 'vscode' { * Whether the task that is part of this group is the default for the group. * This property cannot be set through API, and is controlled by a user's task configurations. */ - readonly isDefault?: boolean; + readonly isDefault: boolean | undefined; /** * The ID of the task group. Is one of TaskGroup.Clean.id, TaskGroup.Build.id, TaskGroup.Rebuild.id, or TaskGroup.Test.id. @@ -6760,7 +6961,7 @@ declare module 'vscode' { /** * The task's scope. */ - readonly scope?: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder; + readonly scope: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder | undefined; /** * The task's name @@ -7549,8 +7750,8 @@ declare module 'vscode' { * * This is the origin that should be used in a content security policy rule: * - * ``` - * img-src https: ${webview.cspSource} ...; + * ```ts + * `img-src https: ${webview.cspSource} ...;` * ``` */ readonly cspSource: string; @@ -7563,7 +7764,7 @@ declare module 'vscode' { /** * Controls if the find widget is enabled in the panel. * - * Defaults to false. + * Defaults to `false`. */ readonly enableFindWidget?: boolean; @@ -7603,7 +7804,7 @@ declare module 'vscode' { /** * Icon for the panel shown in UI. */ - iconPath?: Uri | { light: Uri; dark: Uri }; + iconPath?: Uri | { readonly light: Uri; readonly dark: Uri }; /** * {@linkcode Webview} belonging to the panel. @@ -7619,7 +7820,7 @@ declare module 'vscode' { * Editor position of the panel. This property is only set if the webview is in * one of the editor view columns. */ - readonly viewColumn?: ViewColumn; + readonly viewColumn: ViewColumn | undefined; /** * Whether the panel is active (focused by the user). @@ -7997,14 +8198,14 @@ declare module 'vscode' { * If this is provided, your extension should restore the editor from the backup instead of reading the file * from the user's workspace. */ - readonly backupId?: string; + readonly backupId: string | undefined; /** * If the URI is an untitled file, this will be populated with the byte data of that file * * If this is provided, your extension should utilize this byte data rather than executing fs APIs on the URI passed in */ - readonly untitledDocumentData?: Uint8Array; + readonly untitledDocumentData: Uint8Array | undefined; } /** @@ -8218,7 +8419,10 @@ declare module 'vscode' { export const appRoot: string; /** - * The environment in which the app is hosted in. i.e. 'desktop', 'codespaces', 'web'. + * The hosted location of the application + * On desktop this is 'desktop' + * In the web this is the specified embedder i.e. 'github.dev', 'codespaces', or 'web' if the embedder + * does not provide that information */ export const appHost: string; @@ -8279,7 +8483,7 @@ declare module 'vscode' { /** * The detected default shell for the extension host, this is overridden by the - * `terminal.integrated.shell` setting for the extension host's platform. Note that in + * `terminal.integrated.defaultProfile` setting for the extension host's platform. Note that in * environments that do not support a shell the value is the empty string. */ export const shell: string; @@ -8349,7 +8553,7 @@ declare module 'vscode' { * ``` * * *Note* that extensions should not cache the result of `asExternalUri` as the resolved uri may become invalid due to - * a system or user action — for example, in remote cases, a user may close a port forwarding tunnel that was opened by + * a system or user action — for example, in remote cases, a user may close a port forwarding tunnel that was opened by * `asExternalUri`. * * #### Any other scheme @@ -8442,10 +8646,10 @@ declare module 'vscode' { * * @param command Identifier of the command to execute. * @param rest Parameters passed to the command function. - * @return A thenable that resolves to the returned value of the given command. `undefined` when + * @return A thenable that resolves to the returned value of the given command. Returns `undefined` when * the command handler function doesn't return anything. */ - export function executeCommand(command: string, ...rest: any[]): Thenable; + export function executeCommand(command: string, ...rest: any[]): Thenable; /** * Retrieve the list of all available commands. Commands starting with an underscore are @@ -8500,7 +8704,7 @@ declare module 'vscode' { /** * The currently visible editors or an empty array. */ - export let visibleTextEditors: TextEditor[]; + export let visibleTextEditors: readonly TextEditor[]; /** * An {@link Event} which fires when the {@link window.activeTextEditor active editor} @@ -8513,7 +8717,7 @@ declare module 'vscode' { * An {@link Event} which fires when the array of {@link window.visibleTextEditors visible editors} * has changed. */ - export const onDidChangeVisibleTextEditors: Event; + export const onDidChangeVisibleTextEditors: Event; /** * An {@link Event} which fires when the selection in an editor has changed. @@ -8564,6 +8768,11 @@ declare module 'vscode' { */ export const onDidCloseTerminal: Event; + /** + * An {@link Event} which fires when a {@link Terminal.state terminal's state} has changed. + */ + export const onDidChangeTerminalState: Event; + /** * Represents the current window's state. */ @@ -9193,7 +9402,7 @@ declare module 'vscode' { /** * Selected elements. */ - readonly selection: T[]; + readonly selection: readonly T[]; } @@ -9227,7 +9436,7 @@ declare module 'vscode' { /** * Currently selected elements. */ - readonly selection: T[]; + readonly selection: readonly T[]; /** * Event that is fired when the {@link TreeView.selection selection} has changed @@ -9396,17 +9605,17 @@ declare module 'vscode' { * Context value of the tree item. This can be used to contribute item specific actions in the tree. * For example, a tree item is given a context value as `folder`. When contributing actions to `view/item/context` * using `menus` extension point, you can specify context value for key `viewItem` in `when` expression like `viewItem == folder`. - * ``` - * "contributes": { - * "menus": { - * "view/item/context": [ - * { - * "command": "extension.deleteFolder", - * "when": "viewItem == folder" - * } - * ] - * } - * } + * ```json + * "contributes": { + * "menus": { + * "view/item/context": [ + * { + * "command": "extension.deleteFolder", + * "when": "viewItem == folder" + * } + * ] + * } + * } * ``` * This will show action `extension.deleteFolder` only for items with `contextValue` is `folder`. */ @@ -9533,6 +9742,11 @@ declare module 'vscode' { * recommended for the best contrast and consistency across themes. */ color?: ThemeColor; + + /** + * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. + */ + location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; } /** @@ -9561,6 +9775,11 @@ declare module 'vscode' { * recommended for the best contrast and consistency across themes. */ color?: ThemeColor; + + /** + * The {@link TerminalLocation} or {@link TerminalEditorLocationOptions} or {@link TerminalSplitLocationOptions} for the terminal. + */ + location?: TerminalLocation | TerminalEditorLocationOptions | TerminalSplitLocationOptions; } /** @@ -9576,6 +9795,8 @@ declare module 'vscode' { * Note writing `\n` will just move the cursor down 1 row, you need to write `\r` as well * to move the cursor to the left-most cell. * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * **Example:** Write red text to the terminal * ```typescript * const writeEmitter = new vscode.EventEmitter(); @@ -9601,6 +9822,8 @@ declare module 'vscode' { * bar). Set to `undefined` for the terminal to go back to the regular dimensions (fit to * the size of the panel). * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * **Example:** Override the dimensions of a terminal to 20 columns and 10 rows * ```typescript * const dimensionsEmitter = new vscode.EventEmitter(); @@ -9623,6 +9846,8 @@ declare module 'vscode' { /** * An event that when fired will signal that the pty is closed and dispose of the terminal. * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * A number can be used to provide an exit code for the terminal. Exit codes must be * positive and a non-zero exit codes signals failure which shows a notification for a * regular terminal and allows dependent tasks to proceed when used with the @@ -9652,6 +9877,8 @@ declare module 'vscode' { /** * An event that when fired allows changing the name of the terminal. * + * Events fired before {@link Pseudoterminal.open} is called will be be ignored. + * * **Example:** Change the terminal name to "My new terminal". * ```typescript * const writeEmitter = new vscode.EventEmitter(); @@ -10036,6 +10263,12 @@ declare module 'vscode' { */ readonly onDidTriggerButton: Event; + /** + * An event signaling when a button in a particular {@link QuickPickItem} was triggered. + * This event does not fire for buttons in the title bar. + */ + readonly onDidTriggerItemButton: Event>; + /** * Items to pick from. This can be read and updated by the extension. */ @@ -10056,6 +10289,11 @@ declare module 'vscode' { */ matchOnDetail: boolean; + /* + * An optional flag to maintain the scroll position of the quick pick when the quick pick items are updated. Defaults to false. + */ + keepScrollPosition?: boolean; + /** * Active items. This can be read and updated by the extension. */ @@ -10167,6 +10405,21 @@ declare module 'vscode' { private constructor(); } + /** + * An event signaling when a button in a particular {@link QuickPickItem} was triggered. + * This event does not fire for buttons in the title bar. + */ + export interface QuickPickItemButtonEvent { + /** + * The button that was clicked. + */ + readonly button: QuickInputButton; + /** + * The item that the button belongs to. + */ + readonly item: T; + } + /** * An event describing an individual change in the text of a {@link TextDocument document}. */ @@ -10214,9 +10467,9 @@ declare module 'vscode' { /** * The reason why the document was changed. - * Is undefined if the reason is not known. + * Is `undefined` if the reason is not known. */ - readonly reason?: TextDocumentChangeReason; + readonly reason: TextDocumentChangeReason | undefined; } /** @@ -10301,6 +10554,11 @@ declare module 'vscode' { */ export interface FileWillCreateEvent { + /** + * A cancellation token. + */ + readonly token: CancellationToken; + /** * The files that are going to be created. */ @@ -10356,6 +10614,11 @@ declare module 'vscode' { */ export interface FileWillDeleteEvent { + /** + * A cancellation token. + */ + readonly token: CancellationToken; + /** * The files that are going to be deleted. */ @@ -10411,6 +10674,11 @@ declare module 'vscode' { */ export interface FileWillRenameEvent { + /** + * A cancellation token. + */ + readonly token: CancellationToken; + /** * The files that are going to be renamed. */ @@ -10589,6 +10857,11 @@ declare module 'vscode' { /** * An event that is emitted when a workspace folder is added or removed. + * + * **Note:** this event will not fire if the first workspace folder is added, removed or changed, + * because in that case the currently executing extensions (including the one that listens to this + * event) will be terminated and restarted so that the (deprecated) `rootPath` property is updated + * to point to the first workspace folder. */ export const onDidChangeWorkspaceFolders: Event; @@ -10688,8 +10961,8 @@ declare module 'vscode' { * will be matched against the file paths of resulting matches relative to their workspace. Use a {@link RelativePattern relative pattern} * to restrict the search results to a {@link WorkspaceFolder workspace folder}. * @param exclude A {@link GlobPattern glob pattern} that defines files and folders to exclude. The glob pattern - * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default excludes and the user's - * configured excludes will apply. When `null`, no excludes will apply. + * will be matched against the file paths of resulting matches relative to their workspace. When `undefined`, default file-excludes (e.g. the `files.exclude`-setting + * but not `search.exclude`) will apply. When `null`, no excludes will apply. * @param maxResults An upper-bound for the result. * @param token A token that can be used to signal cancellation to the underlying search engine. * @return A thenable that resolves to an array of resource identifiers. Will return no results if no @@ -11098,7 +11371,7 @@ declare module 'vscode' { * 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` + * 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 * @@ -11520,6 +11793,15 @@ declare module 'vscode' { */ export function registerCallHierarchyProvider(selector: DocumentSelector, provider: CallHierarchyProvider): Disposable; + /** + * Register a type hierarchy provider. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider A type hierarchy provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerTypeHierarchyProvider(selector: DocumentSelector, provider: TypeHierarchyProvider): Disposable; + /** * Register a linked editing range provider. * @@ -11604,7 +11886,7 @@ declare module 'vscode' { /** * The most recent {@link NotebookCellExecutionSummary execution summary} for this cell. */ - readonly executionSummary?: NotebookCellExecutionSummary; + readonly executionSummary: NotebookCellExecutionSummary | undefined; } /** @@ -12496,17 +12778,17 @@ declare module 'vscode' { * Context value of the resource state. This can be used to contribute resource specific actions. * For example, if a resource is given a context value as `diffable`. When contributing actions to `scm/resourceState/context` * using `menus` extension point, you can specify context value for key `scmResourceState` in `when` expressions, like `scmResourceState == diffable`. - * ``` - * "contributes": { - * "menus": { - * "scm/resourceState/context": [ - * { - * "command": "extension.diff", - * "when": "scmResourceState == diffable" - * } - * ] - * } - * } + * ```json + * "contributes": { + * "menus": { + * "scm/resourceState/context": [ + * { + * "command": "extension.diff", + * "when": "scmResourceState == diffable" + * } + * ] + * } + * } * ``` * This will show action `extension.diff` only for resources with `contextValue` is `diffable`. */ @@ -12577,8 +12859,9 @@ declare module 'vscode' { * The UI-visible count of {@link SourceControlResourceState resource states} of * this source control. * - * Equals to the total number of {@link SourceControlResourceState resource state} - * of this source control, if undefined. + * If undefined, this source control will + * - display its UI-visible count as zero, and + * - contribute the count of its {@link SourceControlResourceState resource states} to the UI-visible aggregated count for all source controls */ count?: number; @@ -12760,7 +13043,7 @@ declare module 'vscode' { /** * Event specific information. */ - readonly body?: any; + readonly body: any; } /** @@ -12873,7 +13156,7 @@ declare module 'vscode' { /** * The host. */ - readonly host?: string; + readonly host?: string | undefined; /** * Create a description for a debug adapter running as a socket based server. @@ -13046,15 +13329,15 @@ declare module 'vscode' { /** * An optional expression for conditional breakpoints. */ - readonly condition?: string; + readonly condition?: string | undefined; /** * An optional expression that controls how many hits of the breakpoint are ignored. */ - readonly hitCondition?: string; + readonly hitCondition?: string | undefined; /** * An optional message that gets logged when this breakpoint is hit. Embedded expressions within {} are interpolated by the debug adapter. */ - readonly logMessage?: string; + readonly logMessage?: string | undefined; protected constructor(enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string); } @@ -13182,7 +13465,7 @@ declare module 'vscode' { /** * List of breakpoints. */ - export let breakpoints: Breakpoint[]; + export let breakpoints: readonly Breakpoint[]; /** * An {@link Event} which fires when the {@link debug.activeDebugSession active debug session} @@ -13332,15 +13615,7 @@ declare module 'vscode' { * @param extensionId An extension identifier. * @return An extension or `undefined`. */ - export function getExtension(extensionId: string): Extension | undefined; - - /** - * Get an extension by its full identifier in the form of: `publisher.name`. - * - * @param extensionId An extension identifier. - * @return An extension or `undefined`. - */ - export function getExtension(extensionId: string): Extension | undefined; + export function getExtension(extensionId: string): Extension | undefined; /** * All extensions currently known to the system. @@ -13420,17 +13695,17 @@ declare module 'vscode' { * Context value of the comment thread. This can be used to contribute thread specific actions. * For example, a comment thread is given a context value as `editable`. When contributing actions to `comments/commentThread/title` * using `menus` extension point, you can specify context value for key `commentThread` in `when` expression like `commentThread == editable`. - * ``` - * "contributes": { - * "menus": { - * "comments/commentThread/title": [ - * { - * "command": "extension.deleteCommentThread", - * "when": "commentThread == editable" - * } - * ] - * } - * } + * ```json + * "contributes": { + * "menus": { + * "comments/commentThread/title": [ + * { + * "command": "extension.deleteCommentThread", + * "when": "commentThread == editable" + * } + * ] + * } + * } * ``` * This will show action `extension.deleteCommentThread` only for comment threads with `contextValue` is `editable`. */ @@ -13484,7 +13759,7 @@ declare module 'vscode' { readonly count: number; /** - * Whether the [author](CommentAuthorInformation) of the comment has reacted to this reaction + * Whether the {@link CommentAuthorInformation author} of the comment has reacted to this reaction */ readonly authorHasReacted: boolean; } @@ -13690,6 +13965,17 @@ declare module 'vscode' { * Options to be used when getting an {@link AuthenticationSession} from an {@link AuthenticationProvider}. */ export interface AuthenticationGetSessionOptions { + /** + * Whether the existing user session preference should be cleared. + * + * For authentication providers that support being signed into multiple accounts at once, the user will be + * prompted to select an account to use when {@link authentication.getSession getSession} is called. This preference + * is remembered until {@link authentication.getSession getSession} is called with this flag. + * + * Defaults to false. + */ + clearSessionPreference?: boolean; + /** * Whether login should be performed if there is no matching session. * @@ -13701,19 +13987,32 @@ declare module 'vscode' { * will also result in an immediate modal dialog, and false will add a numbered badge to the accounts icon. * * Defaults to false. + * + * Note: you cannot use this option with {@link silent}. */ createIfNone?: boolean; /** - * Whether the existing user session preference should be cleared. + * Whether we should attempt to reauthenticate even if there is already a session available. * - * For authentication providers that support being signed into multiple accounts at once, the user will be - * prompted to select an account to use when {@link authentication.getSession getSession} is called. This preference - * is remembered until {@link authentication.getSession getSession} is called with this flag. + * 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. */ - clearSessionPreference?: boolean; + forceNewSession?: boolean | { detail: string }; + + /** + * Whether we should show the indication to sign in in the Accounts menu. + * + * If false, the user will be shown a badge on the Accounts menu with an option to sign in for the extension. + * If true, no indication will be shown. + * + * Defaults to false. + * + * Note: you cannot use this option with any other options that prompt the user like {@link createIfNone}. + */ + silent?: boolean; } /** @@ -13757,21 +14056,21 @@ declare module 'vscode' { */ export interface AuthenticationProviderAuthenticationSessionsChangeEvent { /** - * The {@link AuthenticationSession}s of the {@link AuthenticationProvider} that have been added. + * The {@link AuthenticationSession AuthenticationSessions} of the {@link AuthenticationProvider} that have been added. */ - readonly added?: readonly AuthenticationSession[]; + readonly added: readonly AuthenticationSession[] | undefined; /** - * The {@link AuthenticationSession}s of the {@link AuthenticationProvider} that have been removed. + * The {@link AuthenticationSession AuthenticationSessions} of the {@link AuthenticationProvider} that have been removed. */ - readonly removed?: readonly AuthenticationSession[]; + readonly removed: readonly AuthenticationSession[] | undefined; /** - * The {@link AuthenticationSession}s of the {@link AuthenticationProvider} that have been changed. + * The {@link AuthenticationSession AuthenticationSessions} of the {@link AuthenticationProvider} that have been changed. * A session changes when its data excluding the id are updated. An example of this is a session refresh that results in a new * access token being set for the session. */ - readonly changed?: readonly AuthenticationSession[]; + readonly changed: readonly AuthenticationSession[] | undefined; } /** @@ -13853,6 +14152,21 @@ declare module 'vscode' { */ export function getSession(providerId: string, scopes: readonly string[], options?: AuthenticationGetSessionOptions): Thenable; + /** + * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. If there are multiple sessions with the same scopes, the user will be shown a + * quickpick to select which account they would like to use. + * + * Currently, there are only two authentication providers that are contributed from built in extensions + * to the editor that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'. + * @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 + */ + export function getSession(providerId: string, scopes: readonly string[], options: AuthenticationGetSessionOptions & { forceNewSession: true | { detail: string } }): Thenable; + /** * An {@link Event} which fires when the authentication sessions of an authentication provider have * been added, removed, or changed. @@ -13900,6 +14214,25 @@ declare module 'vscode' { Coverage = 3, } + /** + * Tags can be associated with {@link TestItem TestItems} and + * {@link TestRunProfile TestRunProfiles}. A profile with a tag can only + * execute tests that include that tag in their {@link TestItem.tags} array. + */ + export class TestTag { + /** + * ID of the test tag. `TestTag` instances with the same ID are considered + * to be identical. + */ + readonly id: string; + + /** + * Creates a new TestTag instance. + * @param id ID of the test tag. + */ + constructor(id: string); + } + /** * A TestRunProfile describes one way to execute tests in a {@link TestController}. */ @@ -13930,13 +14263,19 @@ declare module 'vscode' { */ isDefault: boolean; + /** + * Associated tag for the profile. If this is set, only {@link TestItem} + * instances with the same tag will be eligible to execute in this profile. + */ + tag: TestTag | undefined; + /** * If this method is present, a configuration gear will be present in the * UI, and this method will be invoked when it's clicked. When called, * you can take other editor actions, such as showing a quick pick or * opening a configuration file. */ - configureHandler?: () => void; + configureHandler: (() => void) | undefined; /** * Handler called to start a test run. When invoked, the function should call @@ -13997,10 +14336,11 @@ declare module 'vscode' { * @param kind Configures what kind of execution this profile manages. * @param runHandler Function called to start a test run. * @param isDefault Whether this is the default action for its kind. + * @param tag Profile test tag. * @returns An instance of a {@link TestRunProfile}, which is automatically * associated with this controller. */ - createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean): TestRunProfile; + createRunProfile(label: string, kind: TestRunProfileKind, runHandler: (request: TestRunRequest, token: CancellationToken) => Thenable | void, isDefault?: boolean, tag?: TestTag): TestRunProfile; /** * A function provided by the extension that the editor may call to request @@ -14084,7 +14424,7 @@ declare module 'vscode' { * The process of running tests should resolve the children of any test * items who have not yet been resolved. */ - readonly include?: TestItem[]; + readonly include: readonly TestItem[] | undefined; /** * An array of tests the user has marked as excluded from the test included @@ -14093,14 +14433,14 @@ declare module 'vscode' { * May be omitted if no exclusions were requested. Test controllers should * not run excluded tests or any children of excluded tests. */ - readonly exclude?: TestItem[]; + readonly exclude: readonly TestItem[] | undefined; /** * The profile used for this request. This will always be defined * for requests issued from the editor UI, though extensions may * programmatically create requests not associated with any profile. */ - readonly profile?: TestRunProfile; + readonly profile: TestRunProfile | undefined; /** * @param tests Array of specific tests to run, or undefined to run all tests @@ -14119,7 +14459,7 @@ declare module 'vscode' { * disambiguate multiple sets of results in a test run. It is useful if * tests are run across multiple platforms, for example. */ - readonly name?: string; + readonly name: string | undefined; /** * A cancellation token which will be triggered when the test run is @@ -14183,8 +14523,11 @@ declare module 'vscode' { * such as colors and text styles, are supported. * * @param output Output text to append. + * @param location Indicate that the output was logged at the given + * location. + * @param test Test item to associate the output with. */ - appendOutput(output: string): void; + appendOutput(output: string, location?: Location, test?: TestItem): void; /** * Signals that the end of the test run. Any tests included in the run whose @@ -14256,7 +14599,7 @@ declare module 'vscode' { /** * URI this `TestItem` is associated with. May be a file or directory. */ - readonly uri?: Uri; + readonly uri: Uri | undefined; /** * The children of this test item. For a test suite, this may contain the @@ -14269,7 +14612,13 @@ declare module 'vscode' { * top-level items in the {@link TestController.items} and for items that * aren't yet included in another item's {@link children}. */ - readonly parent?: TestItem; + readonly parent: TestItem | undefined; + + /** + * Tags associated with this test item. May be used in combination with + * {@link TestRunProfile.tags}, or simply as an organizational feature. + */ + tags: readonly TestTag[]; /** * Indicates whether this test item may have children discovered by resolving. @@ -14305,7 +14654,7 @@ declare module 'vscode' { * * This is only meaningful if the `uri` points to a file. */ - range?: Range; + range: Range | undefined; /** * Optional error encountered while loading the test. @@ -14313,7 +14662,7 @@ declare module 'vscode' { * Note that this is not a test result and should only be used to represent errors in * test discovery, such as syntax errors. */ - error?: string | MarkdownString; + error: string | MarkdownString | undefined; } /**