diff --git a/.vscode-insiders/argv.json b/.vscode-insiders/argv.json
new file mode 100644
index 000000000..5914b1318
--- /dev/null
+++ b/.vscode-insiders/argv.json
@@ -0,0 +1,3 @@
+{
+ "enable-proposed-api": ["orta.vscode-jest"]
+}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e6cfff70..23e06b4a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,8 @@ Bug-fixes within the same version aren't needed
* fix type warning in settings.json when using the `{"autoRun": "off"}` option - @tommy
* fix couple of test result matching issues - @connectdotz (#737)
* update match API/attributes to support up-coming vscode test provider API. - @connectdotz (#737)
-
+* support official vscode Test Explorer and API in v1.59 - @connectdotz (#742)
+*
-->
### 4.0.3
diff --git a/README.md b/README.md
index 96628e473..68d6f04c0 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,19 @@
# vscode-jest
-[![Build Status](https://travis-ci.org/jest-community/vscode-jest.svg?branch=master)](https://travis-ci.org/jest-community/vscode-jest) [![Coverage Status](https://coveralls.io/repos/github/jest-community/vscode-jest/badge.svg?branch=master)](https://coveralls.io/github/jest-community/vscode-jest?branch=master) [![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/Orta.vscode-jest?color=success&label=Visual%20Studio%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest)
+[![Build Status](https://github.com/jest-community/vscode-jest/actions/workflows/node-ci.yml/badge.svg)](https://github.com/jest-community/vscode-jest/actions) [![Coverage Status](https://coveralls.io/repos/github/jest-community/vscode-jest/badge.svg?branch=master)](https://coveralls.io/github/jest-community/vscode-jest?branch=master) [![Visual Studio Marketplace](https://img.shields.io/visual-studio-marketplace/v/Orta.vscode-jest?color=success&label=Visual%20Studio%20Marketplace)](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest)
---
-## v4 Release
+## v4.1 with Test Explorer
+
+Test explorer is one of the highly requested feature for this extension. Last year vscode announced the plan for the official test explorer and we have been trying out the new proposed API since. Now the vscode test explore is live (2021 July release - v1.59), we are excited to release `v4.1` and our users can finally tap into this great new UI experience 🎉
+
+
+![test-explore.png](images/test-explorer.png)
+
+Please note, the test explorer is new and some of the features might be missing or imperfect (see [How to use test explore](#how-to-use-the-test-explorer) for more details), nevertheless, we will continue to improve it. Please do not hesitate to ask questions or file issues, thanks and have fun!
+
+
+v4 Release
After longer than expected development, [v4](https://github.com/jest-community/vscode-jest/releases/tag/v4.0.0) is finally released! 😄
@@ -14,6 +24,7 @@ Knowing the scope of the changes is significant, while we tried to test it as mu
Feel free to read more about the [features and migration](https://github.com/jest-community/vscode-jest/releases/tag/v4.0.0) or come chat with us in the [release discussion](https://github.com/jest-community/vscode-jest/discussions/693) for general comments or questions about this release.
P.S. We find the new version did made the development of this extension a bit easier (yes, we do eat our own dog food :dog:), hopefully, it will do the same for your project. Happy coding!
+
---
@@ -29,9 +40,17 @@ Content
- [How to use code coverage?](#how-to-use-code-coverage)
- [How to use the extension with monorepo projects?](#how-to-use-the-extension-with-monorepo-projects)
- [How to read the StatusBar?](#how-to-read-the-statusbar)
+ - [How to use the Test Explorer?](#how-to-use-the-test-explorer)
- [Customization](#customization)
- [Settings](#settings)
- [Details](#details)
+ - [jestCommandLine](#jestcommandline)
+ - [rootPath](#rootpath)
+ - [debugCodeLens.showWhenTestStateIn](#debugcodelensshowwhenteststatein)
+ - [coverageFormatter](#coverageformatter)
+ - [coverageColors](#coveragecolors)
+ - [autoRun](#autorun)
+ - [testExplorer](#testexplorer)
- [Debug Config](#debug-config)
- [Commands](#commands)
- [Menu](#menu)
@@ -101,7 +120,7 @@ Feel free to checkout the complete list of available [custom settings](#customiz
### How to trigger the test run?
-By default, users need not do anything, the extension will automatically trigger related test run when needed by running jest in the watch mode. However, this can be easily changed if more granular control is desired. Below shows the execution models supported and how to use [jest.autoRun](#autoRun) to opt into it:
+By default, users need not do anything, the extension will automatically trigger related test run when needed by running jest in the watch mode. However, this can be easily changed if more granular control is desired. Below shows the execution models supported and how to use [jest.autoRun](#autorun) to opt into it:
fully automated
@@ -130,7 +149,7 @@ Note: other than the "off" mode, users can specify the "onStartup" option for an
### How to debug tests?
-A test can be debugged via the debug codeLens appeared above the [debuggable](#showWhenTestStateIn) tests. Simply clicking on the codeLens will launch vscode debugger for the specific test. The extension also supports parameterized tests and allows users to pick the specific parameter set to debug.
+A test can be debugged via the debug codeLens appeared above the [debuggable](#debugcodelensshowwhenteststatein) tests. Simply clicking on the codeLens will launch vscode debugger for the specific test. The extension also supports parameterized tests and allows users to pick the specific parameter set to debug.
The simplest use cases should be supported out-of-the-box. If VS Code displays errors about the attribute `program` or `runtimeExecutable` not being available, you can either use [setup wizard]() to help or create your own debug configuration within `launch.json`. See more details in [Customization - Debug Config](#debug-config).
@@ -197,6 +216,21 @@ shows the active workspace has onSave for test file only, and that the workspace
shows the autoRun will be triggered by either test or source file changes.
+### How to use the Test Explorer?
+Users with `vscode` v1.59 and `vscode-jest` v4.1 and up will start to see tests appearing in the test explorer automatically. Test explorer provides a "test-centric" view (vs. "source-centric" view in the editors), allows users to run/debug tests directly from the explorer (in addition to the inline debug codeLens), and provides a native terminal output experience (with colors!):
+
+![test-explore.png](images/test-explorer.png)
+
+You can further customize the explorer with [jest.testExplorer](#testexplorer) in [settings](#settings).
+
+However, test explorer is new and some features are still work-in-progress or not available yet:
+- can't turn on/off coverage yet (pending on vscode API change)
+- not able to accurately indicate run/debug eligibility on the item level, this means you might not be able to run/debug some items through run/debug buttons. (pending on vscode API change)
+- the tests stats on the top of the explorer might not be accurate, especially for multiroot workspaces. (pending on vscode fix))
+- for watch-mode workspaces, the run button is turned off since tests will be automatically executed.
+- debug can only be executed for the test blocks, not on the file or folder level. (Please let us know if you have an use case otherwise)
+
+
## Customization
### Settings
Users can use the following settings to tailor the extension for their environments.
@@ -210,13 +244,14 @@ Users can use the following settings to tailor the extension for their environme
|**Process**|
|autoEnable :x:|Automatically start Jest for this project|true|Please use `autoRun` instead|
|[jestCommandLine](#jestCommandLine)|The command line to start jest tests|undefined|`"jest.jestCommandLine": "npm test -"` or `"jest.jestCommandLine": "yarn test"` or `"jest.jestCommandLine": "node_modules/.bin/jest --config custom-config.js"`|
-|[autoRun](#autoRun)|Controls when and what tests should be run|undefined|`"jest.autoRun": "off"` or `"jest.autoRun": {"watch": true, "onStartup": ["all-tests"]}` or `"jest.autoRun": false, onSave:"test-only"}`|
+|[autoRun](#autorun)|Controls when and what tests should be run|undefined|`"jest.autoRun": "off"` or `"jest.autoRun": {"watch": true, "onStartup": ["all-tests"]}` or `"jest.autoRun": false, onSave:"test-only"}`|
|pathToJest :x:|The path to the Jest binary, or an npm/yarn command to run tests|undefined|Please use `jestCommandLine` instead|
|pathToConfig :x:|The path to your Jest configuration file"|""|Please use `jestCommandLine` instead|
|[rootPath](#rootPath)|The path to your frontend src folder|""|`"jest.rootPath":"packages/app"` or `"jest.rootPath":"/apps/my-app"`|
|runAllTestsFirst :x:| Run all tests before starting Jest in watch mode|true|Please use `autoRun` instead|
|**Editor**|
-|enableInlineErrorMessages :x:| Whether errors should be reported inline on a file|false|It is recommended not to use the inline error message and in favor of vscode's hovering messages, especially for parameterized tests|
+|enableInlineErrorMessages :x:| Whether errors should be reported inline on a file|--|This is now deprecated in favor of `jest.testExplorer` |
+|[testExplorer](#testexplorer) |Configure jest test explorer|`{"enabled": true}`| `{"enabled": false}`, `{"enabled": true, showClassicStatus: true, showInlineError: true}`|
|**Snapshot**|
|enableSnapshotUpdateMessages|Whether snapshot update messages should show|true|`"jest.enableSnapshotUpdateMessages": false`|
|enableSnapshotPreviews 💼|Whether snapshot previews should show|true|`"jest.enableSnapshotPreviews": false`|
@@ -227,47 +262,43 @@ Users can use the following settings to tailor the extension for their environme
|[coverageColors](#coverageColors)|Coverage indicator color override|undefined|`"jest.coverageColors": { "uncovered": "rgba(255,99,71, 0.2)", "partially-covered": "rgba(255,215,0, 0.2)"}`|
|**Debug**|
|enableCodeLens 💼|Whether codelens for debugging should show|true|`"jest.enableCodeLens": false`|
-|[debugCodeLens.showWhenTestStateIn](#showWhenTestStateIn) 💼|Show the debug CodeLens for the tests with the specified status. (window)|["fail", "unknown"]|`"jest.debugCodeLens.showWhenTestStateIn":["fail", "pass", "unknown"]`|
+|[debugCodeLens.showWhenTestStateIn](#debugcodelensshowwhenteststatein) 💼|Show the debug CodeLens for the tests with the specified status. (window)|["fail", "unknown"]|`"jest.debugCodeLens.showWhenTestStateIn":["fail", "pass", "unknown"]`|
|**Misc**|
|debugMode|Enable debug mode to diagnose plugin issues. (see developer console)|false|`"jest.debugMode": true`|
|disabledWorkspaceFolders 💼|Disabled workspace folders names in multiroot environment|[]|`"jest.disabledWorkspaceFolders": ["package-a", "package-b"]`|
#### Details
-
-jestCommandLine
+##### jestCommandLine
This should be the command users used to kick off the jest tests in the terminal. However, since the extension will append additional options at run time, please make sure the command line can pass along these options, which usually just means if you uses npm, add an additional "--" at the end (e.g. `"npm run test --"`) if you haven't already in your script.
It is recommended not to add the following options as they are managed by the extension: `--watch`, `--watchAll`, `--coverage`
-
-
-rootPath
+##### rootPath
If your project doesn't live in the root of your repository, you may want to customize the `jest.rootPath` setting to enlighten the extension as to where to look. For instance: `"jest.rootPath": "src/client-app"` will direct the extension to use the `src/client-app` folder as the root for Jest.
-
-
-debugCodeLens.showWhenTestStateIn
+##### debugCodeLens.showWhenTestStateIn
Possible status are: `[ "fail", "pass", "skip", "unknown"]`. Please note that this is a window level setting, i.e. its value will apply for all workspaces.
-
-
-coverageFormatter
+##### coverageFormatter
There are 2 formatters to choose from:
- 1. DefaultFormatter: high light uncovered and partially-covered code inlilne as well as on the right overview ruler. (this is the default)
- ![coverage-DefaultFormatter.png](./images/coverage-DefaultFormatter.png)
+ 1. DefaultFormatter: high light uncovered and partially-covered code inlilne as well as on the right overview ruler. (this is the default)
+ ![coverage-DefaultFormatter.png](./images/coverage-DefaultFormatter.png)
+
+
+ 2. GutterFormatter: render coverage status in the gutter as well as the overview ruler.
+ ![coverage-GutterFormatter.png](./images/coverage-GutterFormatter.png)
+
- 2. GutterFormatter: render coverage status in the gutter as well as the overview ruler.
- ![coverage-GutterFormatter.png](./images/coverage-GutterFormatter.png)
_(Note, there is an known issue in vscode (microsoft/vscode#5923) that gutter decorators could interfere with debug breakpoints visibility. Therefore, you probably want to disable coverage before debugging or switch to DefaultFormatter)_
-
+##### coverageColors
+Besides the formatter, user can also customize the color via `jest.coverageColors` to change color for 3 coverage categories: `"uncovered", "covered", or "partially-covered"`,
-coverageColors
-
-Besides the formatter, user can also customize the color via `jest.coverageColors` to change color for 3 coverage categories: `"uncovered", "covered", or "partially-covered"`, for example:
+example
+for example:
```
"jest.coverageColors": {
"uncovered": "rgba(255,99,71, 0.2)",
@@ -283,11 +314,9 @@ Besides the formatter, user can also customize the color via `jest.coverageColor
}
```
-
-autoRun
- - definition:
- ```
+##### autoRun
+ ```json
AutoRun =
| 'off'
| { watch: true; onStartup?: ["all-tests"] }
@@ -297,7 +326,9 @@ Besides the formatter, user can also customize the color via `jest.coverageColor
onSave?: 'test-file' | 'test-src-file';
}
```
- - examples
+
+ example
+
- Turn off auto run, users need to trigger tests run manually via [run commands](#commands-run) and [menus](#context-menu):
```json
"jest.autoRun": "off"
@@ -336,6 +367,17 @@ Besides the formatter, user can also customize the color via `jest.coverageColor
```
+##### testExplorer
+ ```json
+ testExplorer =
+ | {enabled: false}
+ | {enabled: true, showClassicStatus?: boolean, showInlineError?: boolean}
+ ```
+ - `showClassicStatus`: show status symbol (prior to 4.1) in editor gutter, in addition to explorer status symbols. default is false if explorer is enabled.
+ - `showInlineError`: show vscode style inline error and error message viewer, default is false.
+
+default is `"jest.testExplorer": {"enabled": true}`
+>
### Debug Config
This extension looks for `"vscode-jest-tests"` debug config in the workspace `.vscode/launch.json`. If not found, it will attempt to generate a default config that should work for most standard jest or projects bootstrapped by `create-react-app`.
@@ -405,7 +447,7 @@ Sorry you are having trouble with the extension. If your issue did not get resol
It seems to make my vscode sluggish
- By default the extension will run all tests when it is launched followed by a jest watch process. If you have many resource extensive tests or source files that can trigger many tests when changed, this could be the reason. Check out [jest.autoRun](#autoRun) to see how you can change and control when and what tests should be run.
+ By default the extension will run all tests when it is launched followed by a jest watch process. If you have many resource extensive tests or source files that can trigger many tests when changed, this could be the reason. Check out [jest.autoRun](#autorun) to see how you can change and control when and what tests should be run.
diff --git a/__mocks__/vscode.ts b/__mocks__/vscode.ts
index 645c6fc5f..483ceef29 100644
--- a/__mocks__/vscode.ts
+++ b/__mocks__/vscode.ts
@@ -44,6 +44,7 @@ const Uri = {
joinPath: jest.fn(),
};
const Range = jest.fn();
+const Location = jest.fn();
const Position = jest.fn();
const Diagnostic = jest.fn();
const ThemeIcon = jest.fn();
@@ -69,7 +70,26 @@ const QuickInputButtons = {
Back: {},
};
-export {
+const tests = {
+ createTestController: jest.fn(),
+};
+
+const TestRunProfileKind = {
+ Run: 1,
+ Debug: 2,
+ Coverage: 3,
+};
+
+const TestMessage = jest.fn();
+const TestRunRequest = jest.fn();
+
+const EventEmitter = jest.fn().mockImplementation(() => {
+ return {
+ fire: jest.fn(),
+ };
+});
+
+export = {
CodeLens,
languages,
StatusBarAlignment,
@@ -78,6 +98,7 @@ export {
OverviewRulerLane,
Uri,
Range,
+ Location,
Position,
Diagnostic,
ThemeIcon,
@@ -86,4 +107,9 @@ export {
debug,
commands,
QuickInputButtons,
+ tests,
+ TestRunProfileKind,
+ EventEmitter,
+ TestMessage,
+ TestRunRequest,
};
diff --git a/images/test-explorer.png b/images/test-explorer.png
new file mode 100644
index 000000000..3b12f678d
Binary files /dev/null and b/images/test-explorer.png differ
diff --git a/package.json b/package.json
index 19de548c6..8fb556cf1 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.0.3",
+ "version": "4.1.0-rc.3",
"publisher": "Orta",
"engines": {
- "vscode": "^1.45.0"
+ "vscode": "^1.59.0"
},
"author": {
"name": "Orta Therox, ConnectDotz & Sean Poulter",
@@ -20,7 +20,8 @@
"color": "#384357"
},
"categories": [
- "Other"
+ "Other",
+ "Testing"
],
"keywords": [
"jest",
@@ -63,7 +64,7 @@
],
"configuration": {
"type": "object",
- "title": "Jest configuration",
+ "title": "Jest",
"properties": {
"jest.autoEnable": {
"description": "Automatically start Jest for this project",
@@ -97,13 +98,6 @@
"default": "",
"scope": "resource"
},
- "jest.enableInlineErrorMessages": {
- "description": "Whether errors should be reported inline on a file",
- "type": "boolean",
- "default": false,
- "scope": "resource",
- "markdownDeprecationMessage": "**Deprecated**: in favor of vscode hovering message"
- },
"jest.enableSnapshotUpdateMessages": {
"description": "Whether snapshot update messages should show",
"type": "boolean",
@@ -197,6 +191,14 @@
],
"default": null,
"scope": "resource"
+ },
+ "jest.testExplorer": {
+ "markdownDescription": "Configure jest TestExplorer. See valid [formats](https://github.com/jest-community/vscode-jest/blob/master/README.md#testexplorer) or [how to use test explorer](https://github.com/jest-community/vscode-jest/blob/master/README.md#how-to-use-the-test-explorer) for more details",
+ "type": "object",
+ "default": {
+ "enabled": true
+ },
+ "scope": "resource"
}
}
},
@@ -401,7 +403,6 @@
"@types/istanbul-lib-source-maps": "^4.0.1",
"@types/jest": "^26.0.15",
"@types/node": "^8.0.31",
- "@types/vscode": "^1.45.0",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"copy-webpack-plugin": "^8.1.0",
@@ -420,7 +421,7 @@
"rimraf": "^3.0.2",
"ts-jest": "^26.5.2",
"ts-loader": "^7.0.1",
- "typescript": "^4.2.2",
+ "typescript": "^4.3.2",
"vscode-test": "^1.3.0",
"webpack": "^5.30.0",
"webpack-cli": "^4.6.0"
diff --git a/src/Coverage/CoverageOverlay.ts b/src/Coverage/CoverageOverlay.ts
index ded2a781e..dec1de012 100644
--- a/src/Coverage/CoverageOverlay.ts
+++ b/src/Coverage/CoverageOverlay.ts
@@ -3,7 +3,6 @@ import { CoverageMapProvider } from './CoverageMapProvider';
import { DefaultFormatter } from './Formatters/DefaultFormatter';
import { GutterFormatter } from './Formatters/GutterFormatter';
import * as vscode from 'vscode';
-import { hasDocument } from '../editor';
export type CoverageStatus = 'covered' | 'partially-covered' | 'uncovered';
export type CoverageColors = {
@@ -61,7 +60,7 @@ export class CoverageOverlay {
}
update(editor: vscode.TextEditor): void {
- if (!hasDocument(editor)) {
+ if (!editor.document) {
return;
}
diff --git a/src/JestExt/core.ts b/src/JestExt/core.ts
index 24c11d45f..56cdd9f26 100644
--- a/src/JestExt/core.ts
+++ b/src/JestExt/core.ts
@@ -2,7 +2,6 @@ import * as vscode from 'vscode';
import { JestTotalResults } from 'jest-editor-support';
import { TestStatus } from '../decorations/test-status';
-import inlineErrorStyle from '../decorations/inline-error';
import { statusBar, StatusBar, Mode, StatusBarUpdate, SBTestStats } from '../StatusBar';
import {
TestReconciliationState,
@@ -19,18 +18,26 @@ import { updateDiagnostics, updateCurrentDiagnostics, resetDiagnostics } from '.
import { DebugCodeLensProvider, DebugTestIdentifier } from '../DebugCodeLens';
import { DebugConfigurationProvider } from '../DebugConfigurationProvider';
import { DecorationOptions, TestStats } from '../types';
-import { isOpenInMultipleEditors } from '../editor';
import { CoverageOverlay } from '../Coverage/CoverageOverlay';
import { resultsWithoutAnsiEscapeSequence } from '../TestResults/TestResult';
import { CoverageMapData } from 'istanbul-lib-coverage';
import { Logging } from '../logging';
import { createProcessSession, ProcessSession } from './process-session';
-import { JestExtContext, JestExtSessionAware } from './types';
+import {
+ DebugFunction,
+ JestExtContext,
+ JestSessionEvents,
+ JestExtSessionContext,
+ JestRunEvent,
+} from './types';
import * as messaging from '../messaging';
import { SupportedLanguageIds } from '../appGlobals';
import { createJestExtContext, getExtensionResourceSettings, prefixWorkspace } from './helper';
import { PluginResourceSettings } from '../Settings';
import { startWizard, WizardTaskId } from '../setup-wizard';
+import { JestExtExplorerContext } from '../test-provider/types';
+import { JestTestProvider } from '../test-provider';
+import { JestProcessInfo } from '../JestProcessManagement';
interface RunTestPickItem extends vscode.QuickPickItem {
id: DebugTestIdentifier;
@@ -49,8 +56,6 @@ export class JestExt {
// So you can read what's going on
channel: vscode.OutputChannel;
- failingAssertionDecorators: { [fileName: string]: vscode.TextEditorDecorationType[] };
-
private decorations: TestStatus;
// The ability to show fails in the problems section
@@ -63,10 +68,12 @@ export class JestExt {
private status: ReturnType;
private logging: Logging;
- private sessionAwareComponents: JestExtSessionAware[];
private extContext: JestExtContext;
private dirtyFiles: Set = new Set();
+ private testProvider?: JestTestProvider;
+ public events: JestSessionEvents;
+
constructor(
vscodeContext: vscode.ExtensionContext,
workspaceFolder: vscode.WorkspaceFolder,
@@ -80,7 +87,6 @@ export class JestExt {
this.logging = this.extContext.loggingFactory.create('JestExt');
this.channel = vscode.window.createOutputChannel(`Jest (${workspaceFolder.name})`);
- this.failingAssertionDecorators = {};
this.failDiagnostics = vscode.languages.createDiagnosticCollection(
`Jest (${workspaceFolder.name})`
);
@@ -97,7 +103,18 @@ export class JestExt {
pluginSettings.coverageColors
);
- this.testResultProvider = new TestResultProvider(pluginSettings.debugMode ?? false);
+ this.events = {
+ onRunEvent: new vscode.EventEmitter(),
+ onTestSessionStarted: new vscode.EventEmitter(),
+ onTestSessionStopped: new vscode.EventEmitter(),
+ };
+ this.setupRunEvents(this.events);
+
+ this.testResultProvider = new TestResultProvider(
+ this.events,
+ pluginSettings.debugMode ?? false
+ );
+
this.debugConfigurationProvider = debugConfigurationProvider;
this.status = statusBar.bind(workspaceFolder.name);
@@ -108,11 +125,19 @@ export class JestExt {
resetDiagnostics(this.failDiagnostics);
this.processSession = this.createProcessSession();
- this.sessionAwareComponents = [this.testResultProvider];
this.setupStatusBar();
}
+ private getExtExplorerContext(): JestExtExplorerContext {
+ return {
+ ...this.extContext,
+ sessionEvents: this.events,
+ session: this.processSession,
+ testResolveProvider: this.testResultProvider,
+ debugTests: this.debugTests,
+ };
+ }
private setupWizardAction(taskId: WizardTaskId): messaging.MessageAction {
return {
title: 'Run Setup Wizard',
@@ -125,13 +150,53 @@ export class JestExt {
};
}
+ private setupRunEvents(events: JestSessionEvents): void {
+ events.onRunEvent.event((event: JestRunEvent) => {
+ switch (event.type) {
+ case 'scheduled':
+ this.channel.appendLine(`${event.process.id} is scheduled`);
+ break;
+ case 'data':
+ if (event.newLine) {
+ this.channel.appendLine(event.text);
+ } else {
+ this.channel.append(event.text);
+ }
+ if (event.isError) {
+ this.channel.show();
+ }
+ break;
+ case 'start':
+ this.updateStatusBar({ state: 'running' });
+ this.channel.clear();
+ break;
+ case 'end':
+ this.updateStatusBar({ state: 'done' });
+ break;
+ case 'exit':
+ if (event.error) {
+ this.updateStatusBar({ state: 'stopped' });
+ const msg = `${event.error}\n see troubleshooting: ${messaging.TROUBLESHOOTING_URL}`;
+ this.channel.appendLine(msg);
+ this.channel.show();
+ messaging.systemErrorMessage(
+ event.error,
+ messaging.showTroubleshootingAction,
+ this.setupWizardAction('cmdLine')
+ );
+ } else {
+ this.updateStatusBar({ state: 'done' });
+ }
+ break;
+ }
+ });
+ }
+
private createProcessSession(): ProcessSession {
return createProcessSession({
...this.extContext,
- output: this.channel,
- updateStatusBar: this.updateStatusBar.bind(this),
updateWithData: this.updateWithData.bind(this),
- setupWizardAction: this.setupWizardAction.bind(this),
+ onRunEvent: this.events.onRunEvent,
});
}
private toSBStats(stats: TestStats): SBTestStats {
@@ -157,9 +222,15 @@ export class JestExt {
} else {
this.channel.appendLine('Starting Jest Session');
}
+
+ this.testProvider?.dispose();
+ if (this.extContext.settings.testExplorer.enabled) {
+ this.testProvider = new JestTestProvider(this.getExtExplorerContext());
+ }
+
await this.processSession.start();
- this.sessionAwareComponents.forEach((c) => c.onSessionStart?.());
+ this.events.onTestSessionStarted.fire({ ...this.extContext, session: this.processSession });
this.updateTestFileList();
this.channel.appendLine('Jest Session Started');
@@ -168,7 +239,7 @@ export class JestExt {
this.logging('error', `${msg}:`, e);
this.channel.appendLine('Failed to start jest session');
messaging.systemErrorMessage(
- '${msg}...',
+ `${msg}...`,
messaging.showTroubleshootingAction,
this.setupWizardAction('cmdLine')
);
@@ -180,7 +251,10 @@ export class JestExt {
this.channel.appendLine('Stopping Jest Session');
await this.processSession.stop();
- this.sessionAwareComponents.forEach((c) => c.onSessionStop?.());
+ this.testProvider?.dispose();
+ this.testProvider = undefined;
+
+ this.events.onTestSessionStopped.fire();
this.channel.appendLine('Jest Session Stopped');
this.updateStatusBar({ state: 'stopped' });
@@ -203,13 +277,13 @@ export class JestExt {
}
const filePath = editor.document.fileName;
- let testResults: SortedTestResults | undefined;
+ let sortedResults: SortedTestResults | undefined;
try {
- testResults = this.testResultProvider.getSortedResults(filePath);
+ sortedResults = this.testResultProvider.getSortedResults(filePath);
} catch (e) {
this.channel.appendLine(`${filePath}: failed to parse test results: ${e.toString()}`);
// assign an empty result so we can clear the outdated decorators/diagnostics etc
- testResults = {
+ sortedResults = {
fail: [],
skip: [],
success: [],
@@ -217,12 +291,12 @@ export class JestExt {
};
}
- if (!testResults) {
+ if (!sortedResults) {
return;
}
- this.updateDecorators(testResults, editor);
- updateCurrentDiagnostics(testResults.fail, this.failDiagnostics, editor);
+ this.updateDecorators(sortedResults, editor);
+ updateCurrentDiagnostics(sortedResults.fail, this.failDiagnostics, editor);
}
public triggerUpdateActiveEditor(editor: vscode.TextEditor): void {
@@ -259,47 +333,42 @@ export class JestExt {
}
updateDecorators(testResults: SortedTestResults, editor: vscode.TextEditor): void {
- // Status indicators (gutter icons)
- const styleMap = [
- {
- data: testResults.success,
- decorationType: this.decorations.passing,
- state: TestReconciliationState.KnownSuccess,
- },
- {
- data: testResults.fail,
- decorationType: this.decorations.failing,
- state: TestReconciliationState.KnownFail,
- },
- {
- data: testResults.skip,
- decorationType: this.decorations.skip,
- state: TestReconciliationState.KnownSkip,
- },
- {
- data: testResults.unknown,
- decorationType: this.decorations.unknown,
- state: TestReconciliationState.Unknown,
- },
- ];
-
- styleMap.forEach((style) => {
- const decorators = this.generateDotsForItBlocks(style.data, style.state);
- editor.setDecorations(style.decorationType, decorators);
- });
+ if (
+ this.extContext.settings.testExplorer.enabled === false ||
+ this.extContext.settings.testExplorer.showClassicStatus
+ ) {
+ // Status indicators (gutter icons)
+ const styleMap = [
+ {
+ data: testResults.success,
+ decorationType: this.decorations.passing,
+ state: TestReconciliationState.KnownSuccess,
+ },
+ {
+ data: testResults.fail,
+ decorationType: this.decorations.failing,
+ state: TestReconciliationState.KnownFail,
+ },
+ {
+ data: testResults.skip,
+ decorationType: this.decorations.skip,
+ state: TestReconciliationState.KnownSkip,
+ },
+ {
+ data: testResults.unknown,
+ decorationType: this.decorations.unknown,
+ state: TestReconciliationState.Unknown,
+ },
+ ];
+
+ styleMap.forEach((style) => {
+ const decorators = this.generateDotsForItBlocks(style.data, style.state);
+ editor.setDecorations(style.decorationType, decorators);
+ });
+ }
// Debug CodeLens
this.debugCodeLensProvider.didChange();
-
- // Inline error messages
- this.resetInlineErrorDecorators(editor);
- if (this.extContext.settings.enableInlineErrorMessages) {
- const fileName = editor.document.fileName;
- testResults.fail.forEach((a) => {
- const { style, decorator } = this.generateInlineErrorDecorator(fileName, a);
- editor.setDecorations(style, [decorator]);
- });
- }
}
private isSupportedDocument(document: vscode.TextDocument | undefined): boolean {
@@ -324,14 +393,30 @@ export class JestExt {
return true;
}
+ public activate(): void {
+ if (
+ vscode.window.activeTextEditor?.document.uri &&
+ vscode.workspace.getWorkspaceFolder(vscode.window.activeTextEditor.document.uri) ===
+ this.extContext.workspace
+ ) {
+ this.onDidChangeActiveTextEditor(vscode.window.activeTextEditor);
+ }
+ }
public deactivate(): void {
this.stopSession();
this.channel.dispose();
+
+ this.testResultProvider.dispose();
+ this.testProvider?.dispose();
+
+ this.events.onRunEvent.dispose();
+ this.events.onTestSessionStarted.dispose();
+ this.events.onTestSessionStopped.dispose();
}
//** commands */
- public debugTests = async (
- document: vscode.TextDocument,
+ public debugTests: DebugFunction = async (
+ document: vscode.TextDocument | string,
...ids: DebugTestIdentifier[]
): Promise => {
const idString = (type: IdStringType, id: DebugTestIdentifier): string =>
@@ -366,7 +451,7 @@ export class JestExt {
}
this.debugConfigurationProvider.prepareTestRun(
- document.fileName,
+ typeof document === 'string' ? document : document.fileName,
escapeRegExp(idString('full-name', testId))
);
@@ -390,23 +475,29 @@ export class JestExt {
};
public runAllTests(editor?: vscode.TextEditor): void {
if (!editor) {
- this.processSession.scheduleProcess({ type: 'all-tests' });
- this.dirtyFiles.clear();
+ if (this.processSession.scheduleProcess({ type: 'all-tests' })) {
+ this.dirtyFiles.clear();
+ return;
+ }
} else {
const name = editor.document.fileName;
- this.dirtyFiles.delete(name);
- this.processSession.scheduleProcess({
- type: 'by-file',
- testFileNamePattern: name,
- });
+ if (
+ this.processSession.scheduleProcess({
+ type: 'by-file',
+ testFileName: name,
+ })
+ ) {
+ this.dirtyFiles.delete(name);
+ return;
+ }
}
+ this.logging('error', 'failed to schedule the run for', editor?.document.fileName);
}
//** window events handling */
onDidCloseTextDocument(document: vscode.TextDocument): void {
this.removeCachedTestResults(document);
- this.removeCachedDecorationTypes(document);
}
removeCachedTestResults(document: vscode.TextDocument, invalidateResult = false): void {
@@ -422,14 +513,6 @@ export class JestExt {
}
}
- removeCachedDecorationTypes(document: vscode.TextDocument): void {
- if (!document || !document.fileName) {
- return;
- }
-
- delete this.failingAssertionDecorators[document.fileName];
- }
-
onDidChangeActiveTextEditor(editor: vscode.TextEditor): void {
this.triggerUpdateActiveEditor(editor);
}
@@ -445,7 +528,7 @@ export class JestExt {
) {
this.processSession.scheduleProcess({
type: 'by-file',
- testFileNamePattern: document.fileName,
+ testFileName: document.fileName,
});
} else {
this.dirtyFiles.add(document.fileName);
@@ -544,42 +627,6 @@ export class JestExt {
this.triggerUpdateSettings(this.extContext.settings);
}
- private resetInlineErrorDecorators(editor: vscode.TextEditor): void {
- if (!this.failingAssertionDecorators[editor.document.fileName]) {
- this.failingAssertionDecorators[editor.document.fileName] = [];
- return;
- }
-
- if (isOpenInMultipleEditors(editor.document)) {
- return;
- }
-
- this.failingAssertionDecorators[editor.document.fileName].forEach((element) => {
- element.dispose();
- });
- this.failingAssertionDecorators[editor.document.fileName] = [];
- }
-
- private generateInlineErrorDecorator(
- fileName: string,
- test: TestResult
- ): { decorator: vscode.DecorationOptions; style: vscode.TextEditorDecorationType } {
- const errorMessage = test.terseMessage || test.shortMessage;
- // TODO use better typing to indicate for error TestResult, the lineNumberOfError and errorMessage should never be null
- const decorator = {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- range: new vscode.Range(test.lineNumberOfError!, 0, test.lineNumberOfError!, 0),
- };
-
- // We have to make a new style for each unique message, this is
- // why we have to remove off of them beforehand
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const style = inlineErrorStyle(errorMessage!);
- this.failingAssertionDecorators[fileName].push(style);
-
- return { style, decorator };
- }
-
private setupStatusBar(): void {
this.updateStatusBar({ state: 'initial' });
}
@@ -603,12 +650,13 @@ export class JestExt {
this.coverageOverlay.updateVisibleEditors();
});
}
- private updateWithData(data: JestTotalResults): void {
+ private updateWithData(data: JestTotalResults, process: JestProcessInfo): void {
const noAnsiData = resultsWithoutAnsiEscapeSequence(data);
const normalizedData = resultsWithLowerCaseWindowsDriveLetters(noAnsiData);
this._updateCoverageMap(normalizedData.coverageMap);
- const statusList = this.testResultProvider.updateTestResults(normalizedData);
+ const statusList = this.testResultProvider.updateTestResults(normalizedData, process);
+
updateDiagnostics(statusList, this.failDiagnostics);
this.refreshDocumentChange();
diff --git a/src/JestExt/helper.ts b/src/JestExt/helper.ts
index a1c9438c3..67cf95eae 100644
--- a/src/JestExt/helper.ts
+++ b/src/JestExt/helper.ts
@@ -5,7 +5,7 @@ import * as vscode from 'vscode';
import * as path from 'path';
import { ProjectWorkspace } from 'jest-editor-support';
import { JestProcessRequest } from '../JestProcessManagement';
-import { PluginResourceSettings, JestExtAutoRunConfig } from '../Settings';
+import { PluginResourceSettings, JestExtAutoRunConfig, TestExplorerConfig } from '../Settings';
import { AutoRunMode } from '../StatusBar';
import { pathToJest, pathToConfig, toFilePath } from '../helpers';
import { workspaceLogging } from '../logging';
@@ -102,7 +102,6 @@ export const getExtensionResourceSettings = (uri: vscode.Uri): PluginResourceSet
const config = vscode.workspace.getConfiguration('jest', uri);
return {
autoEnable: config.get('autoEnable'),
- enableInlineErrorMessages: config.get('enableInlineErrorMessages'),
enableSnapshotUpdateMessages: config.get('enableSnapshotUpdateMessages'),
pathToConfig: config.get('pathToConfig'),
jestCommandLine: config.get('jestCommandLine'),
@@ -118,6 +117,7 @@ export const getExtensionResourceSettings = (uri: vscode.Uri): PluginResourceSet
debugMode: config.get('debugMode'),
coverageColors: config.get('coverageColors'),
autoRun: config.get('autoRun'),
+ testExplorer: config.get('testExplorer') ?? { enabled: true },
};
};
diff --git a/src/JestExt/process-listeners.ts b/src/JestExt/process-listeners.ts
index f5a09183c..d3dbb6ab3 100644
--- a/src/JestExt/process-listeners.ts
+++ b/src/JestExt/process-listeners.ts
@@ -1,19 +1,21 @@
import * as vscode from 'vscode';
import { JestTotalResults } from 'jest-editor-support';
-import * as messaging from '../messaging';
import { cleanAnsi } from '../helpers';
-import { JestProcess, JestProcessEvent, JestProcessListener } from '../JestProcessManagement';
+import { JestProcess, JestProcessEvent } from '../JestProcessManagement';
import { ListenerSession, ListTestFilesCallback } from './process-session';
import { isWatchRequest, prefixWorkspace } from './helper';
import { Logging } from '../logging';
+import { JestRunEvent } from './types';
-export class AbstractProcessListener implements JestProcessListener {
+export class AbstractProcessListener {
protected session: ListenerSession;
protected readonly logging: Logging;
+ public onRunEvent: vscode.EventEmitter;
constructor(session: ListenerSession) {
this.session = session;
this.logging = session.context.loggingFactory.create(this.name);
+ this.onRunEvent = session.context.onRunEvent;
}
protected get name(): string {
return 'AbstractProcessListener';
@@ -27,7 +29,7 @@ export class AbstractProcessListener implements JestProcessListener {
}
case 'executableStdErr': {
const data = (args[0] as Buffer).toString();
- this.onExecutableStdErr(jestProcess, cleanAnsi(data));
+ this.onExecutableStdErr(jestProcess, cleanAnsi(data), data);
break;
}
case 'executableJSON': {
@@ -35,11 +37,13 @@ export class AbstractProcessListener implements JestProcessListener {
break;
}
case 'executableOutput': {
- this.onExecutableOutput(jestProcess, cleanAnsi(args[0] as string));
+ const str = args[0] as string;
+ this.onExecutableOutput(jestProcess, cleanAnsi(str), str);
break;
}
case 'terminalError': {
- this.onTerminalError(jestProcess, cleanAnsi(args[0] as string));
+ const str = args[0] as string;
+ this.onTerminalError(jestProcess, cleanAnsi(str), str);
break;
}
case 'processClose': {
@@ -62,38 +66,34 @@ export class AbstractProcessListener implements JestProcessListener {
}
protected onProcessStarting(process: JestProcess): void {
- this.session.context.updateStatusBar({ state: 'running' });
+ this.session.context.onRunEvent.fire({ type: 'start', process });
this.logging('debug', `${process.request.type} onProcessStarting`);
}
- protected onExecutableStdErr(process: JestProcess, data: string): void {
+ protected onExecutableStdErr(process: JestProcess, data: string, _raw: string): void {
this.logging('debug', `${process.request.type} onExecutableStdErr:`, data);
}
protected onExecutableJSON(process: JestProcess, data: JestTotalResults): void {
this.logging('debug', `${process.request.type} onExecutableJSON:`, data);
}
- protected onExecutableOutput(process: JestProcess, data: string): void {
+ protected onExecutableOutput(process: JestProcess, data: string, _raw: string): void {
this.logging('debug', `${process.request.type} onExecutableOutput:`, data);
}
- protected onTerminalError(process: JestProcess, data: string): void {
+ protected onTerminalError(process: JestProcess, data: string, _raw: string): void {
this.logging('error', `${process.request.type} onTerminalError:`, data);
}
protected onProcessClose(_process: JestProcess, _code?: number, _signal?: string): void {
// no default behavior...
}
protected onProcessExit(process: JestProcess, code?: number, signal?: string): void {
- this.session.context.updateStatusBar({ state: 'done' });
// code = 1 is general error, usually mean the command emit error, which should already handled by other event processing, for example when jest has failed tests.
// However, error beyond 1, usually means some error outside of the command it is trying to execute, so reporting here for debugging purpose
// see shell error code: https://www.linuxjournal.com/article/10844
if (code && code > 1) {
- this.session.context.updateStatusBar({ state: 'stopped' });
- this.logging(
- 'debug',
- `${process.request.type} onProcessExit: process exit with code=${code}, signal=${signal}:`,
- process.toString()
- );
+ const error = `${process.request.type} onProcessExit: process exit with code=${code}, signal=${signal}`;
+ this.session.context.onRunEvent.fire({ type: 'exit', process, error });
+ this.logging('debug', `${error} :`, process.toString());
} else {
- this.session.context.updateStatusBar({ state: 'done' });
+ this.session.context.onRunEvent.fire({ type: 'exit', process });
}
}
}
@@ -150,7 +150,10 @@ export class RunTestListener extends AbstractProcessListener {
private shouldIgnoreOutput(text: string): boolean {
// this fails when snapshots change - to be revised - returning always false for now
return (
- text.includes('Watch Usage') || text.includes('onRunComplete') || text.includes('onRunStart')
+ text.length <= 0 ||
+ text.includes('Watch Usage') ||
+ text.includes('onRunComplete') ||
+ text.includes('onRunStart')
);
}
@@ -168,7 +171,7 @@ export class RunTestListener extends AbstractProcessListener {
) {
const scope =
process.request.type === 'by-file'
- ? `for file "${process.request.testFileNamePattern}"`
+ ? `for file "${process.request.testFileName}"`
: `for all files in "${this.session.context.workspace.name}"`;
vscode.window
.showInformationMessage(`Would you like to update snapshots ${scope}?`, {
@@ -181,7 +184,12 @@ export class RunTestListener extends AbstractProcessListener {
type: 'update-snapshot',
baseRequest: process.request,
});
- this.session.context.output.appendLine('Updating snapshots...');
+ this.onRunEvent.fire({
+ type: 'data',
+ process,
+ text: 'Updating snapshots...',
+ newLine: true,
+ });
}
});
}
@@ -204,7 +212,7 @@ export class RunTestListener extends AbstractProcessListener {
}
// watch process should not exit unless we request it to be closed
- private handleWatchProcessCrash(process: JestProcess) {
+ private handleWatchProcessCrash(process: JestProcess): string | undefined {
if (
(process.request.type === 'watch-tests' || process.request.type === 'watch-all-tests') &&
process.stopReason !== 'on-demand'
@@ -215,62 +223,53 @@ export class RunTestListener extends AbstractProcessListener {
);
this.logging('warn', msg);
- this.session.context.output.appendLine(
- `${msg}\n see troubleshooting: ${messaging.TROUBLESHOOTING_URL}`
- );
- this.session.context.output.show(true);
- messaging.systemErrorMessage(
- msg,
- messaging.showTroubleshootingAction,
- this.session.context.setupWizardAction('cmdLine')
- );
+ return msg;
}
}
//=== event handlers ===
- protected onProcessStarting(process: JestProcess): void {
- super.onProcessStarting(process);
- this.session.context.output.clear();
- }
-
- protected onExecutableJSON(_process: JestProcess, data: JestTotalResults): void {
- this.session.context.updateWithData(data);
+ protected onExecutableJSON(process: JestProcess, data: JestTotalResults): void {
+ this.session.context.updateWithData(data, process);
}
- protected onExecutableStdErr(process: JestProcess, message: string): void {
+ protected onExecutableStdErr(process: JestProcess, message: string, raw: string): void {
if (this.shouldIgnoreOutput(message)) {
return;
}
- this.session.context.output.append(message);
+ this.onRunEvent.fire({ type: 'data', process, text: message, raw });
this.handleSnapshotTestFailuer(process, message);
this.handleWatchNotSupportedError(process, message);
}
- protected onExecutableOutput(process: JestProcess, output: string): void {
+ protected onExecutableOutput(process: JestProcess, output: string, raw: string): void {
if (output.includes('onRunStart')) {
- this.session.context.updateStatusBar({ state: 'running' });
if (isWatchRequest(process.request)) {
- this.session.context.output.clear();
+ this.onRunEvent.fire({ type: 'start', process });
}
}
if (output.includes('onRunComplete')) {
- this.session.context.updateStatusBar({ state: 'done' });
+ if (isWatchRequest(process.request)) {
+ this.onRunEvent.fire({ type: 'end', process });
+ }
}
if (!this.shouldIgnoreOutput(output)) {
- this.session.context.output.append(output);
+ this.onRunEvent.fire({ type: 'data', process, text: output, raw });
}
}
- protected onTerminalError(_process: JestProcess, data: string): void {
- this.session.context.output.appendLine(`\nException raised: ${data}`);
- this.session.context.output.show(true);
+ protected onTerminalError(process: JestProcess, data: string, raw: string): void {
+ this.onRunEvent.fire({ type: 'data', process, text: data, raw, newLine: true, isError: true });
+ }
+ protected onProcessExit(_process: JestProcess): void {
+ //override parent method so we will fire run event only when process closed
}
protected onProcessClose(process: JestProcess): void {
super.onProcessClose(process);
- this.handleWatchProcessCrash(process);
+ const error = this.handleWatchProcessCrash(process);
+ this.onRunEvent.fire({ type: 'exit', process, error });
}
}
diff --git a/src/JestExt/process-session.ts b/src/JestExt/process-session.ts
index 43f08cb7a..9d9d49c98 100644
--- a/src/JestExt/process-session.ts
+++ b/src/JestExt/process-session.ts
@@ -5,6 +5,7 @@ import {
ScheduleStrategy,
requestString,
QueueType,
+ JestProcessInfo,
} from '../JestProcessManagement';
import { JestTestProcessType } from '../Settings';
import { RunTestListener, ListTestFileListener } from './process-listeners';
@@ -22,7 +23,7 @@ export type InternalRequestBase =
baseRequest: JestProcessRequest;
};
-type JestExtRequestType = JestProcessRequestBase | InternalRequestBase;
+export type JestExtRequestType = JestProcessRequestBase | InternalRequestBase;
const ProcessScheduleStrategy: Record = {
// abort if there is already an pending request
@@ -42,6 +43,14 @@ const ProcessScheduleStrategy: Record = {
queue: 'blocking',
dedup: { filterByStatus: ['pending'], filterByContent: true },
},
+ 'by-file-pattern': {
+ queue: 'blocking',
+ dedup: { filterByStatus: ['pending'] },
+ },
+ 'by-file-test-pattern': {
+ queue: 'blocking',
+ dedup: { filterByStatus: ['pending'], filterByContent: true },
+ },
'not-test': {
queue: 'non-blocking',
dedup: { filterByStatus: ['pending'] },
@@ -51,11 +60,15 @@ const ProcessScheduleStrategy: Record = {
export interface ProcessSession {
start: () => Promise;
stop: () => Promise;
- scheduleProcess: (request: JestExtRequestType) => boolean;
+ scheduleProcess: (
+ request: T
+ ) => JestProcessInfo | undefined;
}
export interface ListenerSession {
context: JestExtProcessContext;
- scheduleProcess: (request: JestExtRequestType) => boolean;
+ scheduleProcess: (
+ request: T
+ ) => JestProcessInfo | undefined;
}
export const createProcessSession = (context: JestExtProcessContext): ProcessSession => {
@@ -67,19 +80,25 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes
* @param type
* @param stoppRunning if true, will stop and remove processes with the same type, default is false
*/
- const scheduleProcess = (request: JestExtRequestType): boolean => {
- context.output.appendLine(`scheduling jest process: ${request.type}`);
+ const scheduleProcess = (
+ request: T
+ ): JestProcessInfo | undefined => {
+ logging('debug', `scheduling jest process: ${request.type}`);
try {
const pRequest = createProcessRequest(request);
- const success = jestProcessManager.scheduleJestProcess(pRequest);
- if (!success) {
+ const process = jestProcessManager.scheduleJestProcess(pRequest);
+ if (!process) {
logging('warn', `request schedule failed: ${requestString(pRequest)}`);
+ return;
}
- return success;
+
+ context.onRunEvent.fire({ type: 'scheduled', process });
+
+ return process;
} catch (e) {
logging('warn', '[scheduleProcess] failed to create/schedule process for ', request);
- return false;
+ return;
}
};
const listenerSession: ListenerSession = { context, scheduleProcess };
@@ -96,6 +115,9 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes
return { type: 'all-tests', updateSnapshot: true };
case 'all-tests':
case 'by-file':
+ case 'by-file-pattern':
+ case 'by-file-test':
+ case 'by-file-test-pattern':
if (baseRequest.updateSnapshot) {
throw new Error(
'schedule a update-snapshot run within an update-snapshot run is not supported'
@@ -108,15 +130,19 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes
};
const createProcessRequest = (request: JestExtRequestType): JestProcessRequest => {
+ const lSession = listenerSession;
switch (request.type) {
case 'all-tests':
case 'watch-all-tests':
case 'watch-tests':
- case 'by-file': {
+ case 'by-file':
+ case 'by-file-pattern':
+ case 'by-file-test':
+ case 'by-file-test-pattern': {
const schedule = ProcessScheduleStrategy[request.type];
return {
...request,
- listener: new RunTestListener(listenerSession),
+ listener: new RunTestListener(lSession),
schedule,
};
}
@@ -129,7 +155,7 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes
return {
...snapshotRequest,
- listener: new RunTestListener(listenerSession),
+ listener: new RunTestListener(lSession),
schedule,
};
}
@@ -139,7 +165,7 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes
...request,
type: 'not-test',
args: ['--listTests', '--json', '--watchAll=false'],
- listener: new ListTestFileListener(listenerSession, request.onResult),
+ listener: new ListTestFileListener(lSession, request.onResult),
schedule,
};
}
@@ -152,7 +178,7 @@ export const createProcessSession = (context: JestExtProcessContext): ProcessSes
*/
const start = async (): Promise => {
if (jestProcessManager.numberOfProcesses() > 0) {
- context.output.appendLine(`${jestProcessManager.numberOfProcesses} queued, stoping all...`);
+ logging('debug', `${jestProcessManager.numberOfProcesses} queued, stoping all...`);
await stop();
}
diff --git a/src/JestExt/types.ts b/src/JestExt/types.ts
index 6b2503cee..ba14df8d0 100644
--- a/src/JestExt/types.ts
+++ b/src/JestExt/types.ts
@@ -1,7 +1,6 @@
import { JestTotalResults, ProjectWorkspace } from 'jest-editor-support';
import * as vscode from 'vscode';
-import * as messaging from '../messaging';
import { LoggingFactory } from '../logging';
import {
JestExtAutoRunConfig,
@@ -9,20 +8,16 @@ import {
OnStartupType,
PluginResourceSettings,
} from '../Settings';
-import { WizardTaskId } from '../setup-wizard';
-import { AutoRunMode, StatusBarUpdate } from '../StatusBar';
+import { AutoRunMode } from '../StatusBar';
+import { ProcessSession } from './process-session';
+import { DebugTestIdentifier } from '../DebugCodeLens';
+import { JestProcessInfo } from '../JestProcessManagement';
export enum WatchMode {
None = 'none',
Watch = 'watch',
WatchAll = 'watchAll',
}
-
-export interface JestExtSessionAware {
- onSessionStart?: () => void;
- onSessionStop?: () => void;
-}
-
export interface AutoRunAccessor {
config: JestExtAutoRunConfig;
isOff: boolean;
@@ -39,10 +34,32 @@ export interface JestExtContext {
autoRun: AutoRunAccessor;
}
+export interface JestExtSessionContext extends JestExtContext {
+ session: ProcessSession;
+}
+export interface RunEventBase {
+ process: JestProcessInfo;
+}
+export type JestRunEvent = RunEventBase &
+ (
+ | { type: 'scheduled' }
+ | { type: 'data'; text: string; raw?: string; newLine?: boolean; isError?: boolean }
+ | { type: 'start' }
+ | { type: 'end' }
+ | { type: 'exit'; error?: string }
+ );
+export interface JestSessionEvents {
+ onRunEvent: vscode.EventEmitter;
+ onTestSessionStarted: vscode.EventEmitter;
+ onTestSessionStopped: vscode.EventEmitter;
+}
export interface JestExtProcessContextRaw extends JestExtContext {
- output: vscode.OutputChannel;
- updateStatusBar: (status: StatusBarUpdate) => void;
- updateWithData: (data: JestTotalResults) => void;
- setupWizardAction: (taskId: WizardTaskId) => messaging.MessageAction;
+ updateWithData: (data: JestTotalResults, process: JestProcessInfo) => void;
+ onRunEvent: vscode.EventEmitter;
}
export type JestExtProcessContext = Readonly;
+
+export type DebugFunction = (
+ document: vscode.TextDocument | string,
+ ...ids: DebugTestIdentifier[]
+) => Promise;
diff --git a/src/JestProcessManagement/JestProcess.ts b/src/JestProcessManagement/JestProcess.ts
index 29d6cd878..6c9bfdaba 100644
--- a/src/JestProcessManagement/JestProcess.ts
+++ b/src/JestProcessManagement/JestProcess.ts
@@ -4,9 +4,9 @@ import { Runner, RunnerEvent, Options } from 'jest-editor-support';
import { JestExtContext, WatchMode } from '../JestExt/types';
import { extensionId } from '../appGlobals';
import { Logging } from '../logging';
-import { JestProcessRequest } from './types';
+import { JestProcessInfo, JestProcessRequest } from './types';
import { requestString } from './helper';
-import { toFilePath, removeSurroundingQuote } from '../helpers';
+import { toFilePath, removeSurroundingQuote, escapeRegExp } from '../helpers';
export const RunnerEvents: RunnerEvent[] = [
'processClose',
@@ -27,29 +27,29 @@ export type StopReason = 'on-demand' | 'process-end';
let SEQ = 0;
-export class JestProcess {
+export class JestProcess implements JestProcessInfo {
static readonly stopHangTimeout = 500;
private task?: RunnerTask;
private extContext: JestExtContext;
private logging: Logging;
private _stopReason?: StopReason;
- private _id: string;
+ public readonly id: string;
+ private desc: string;
public readonly request: JestProcessRequest;
constructor(extContext: JestExtContext, request: JestProcessRequest) {
this.extContext = extContext;
this.request = request;
this.logging = extContext.loggingFactory.create(`JestProcess ${request.type}`);
- this._id = `id: ${SEQ++}, request: ${requestString(request)}`;
+ this.id = `${request.type}-${SEQ++}`;
+ this.desc = `id: ${this.id}, request: ${requestString(request)}`;
}
public get stopReason(): StopReason | undefined {
return this._stopReason;
}
- public get id(): string {
- return this._id;
- }
+
private get watchMode(): WatchMode {
if (this.request.type === 'watch-tests') {
return WatchMode.Watch;
@@ -61,7 +61,7 @@ export class JestProcess {
}
public toString(): string {
- return `JestProcess: ${this.id}; stopReason: ${this.stopReason}`;
+ return `JestProcess: ${this.desc}; stopReason: ${this.stopReason}`;
}
public start(): Promise {
this._stopReason = undefined;
@@ -92,6 +92,9 @@ export class JestProcess {
private quoteFileName(fileName: string): string {
return `"${toFilePath(removeSurroundingQuote(fileName))}"`;
}
+ private quote(aString: string): string {
+ return `"${removeSurroundingQuote(aString)}"`;
+ }
private startRunner(): Promise {
if (this.task) {
this.logging('warn', `the runner task has already started!`);
@@ -99,34 +102,54 @@ export class JestProcess {
}
const options: Options = {
- noColor: true,
+ noColor: false,
reporters: ['default', `"${this.getReporterPath()}"`],
+ args: { args: ['--colors'] },
};
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const args = options.args!.args;
+
switch (this.request.type) {
case 'all-tests':
+ args.push('--watchAll=false');
if (this.request.updateSnapshot) {
- options.args = { args: ['--updateSnapshot', '--watchAll=false'] };
+ args.push('--updateSnapshot');
}
break;
case 'by-file': {
- options.testFileNamePattern = this.quoteFileName(this.request.testFileNamePattern);
- const args: string[] = ['--findRelatedTests', '--watchAll=false'];
+ options.testFileNamePattern = this.quoteFileName(this.request.testFileName);
+ args.push('--findRelatedTests', '--watchAll=false');
+ if (this.request.updateSnapshot) {
+ args.push('--updateSnapshot');
+ }
+ break;
+ }
+ case 'by-file-pattern': {
+ const regex = this.quote(escapeRegExp(this.request.testFileNamePattern));
+ args.push('--watchAll=false', '--testPathPattern', regex);
if (this.request.updateSnapshot) {
args.push('--updateSnapshot');
}
- options.args = { args };
break;
}
case 'by-file-test': {
- options.testFileNamePattern = this.quoteFileName(this.request.testFileNamePattern);
- options.testNamePattern = this.request.testNamePattern;
- const args: string[] = ['--runTestsByPath', '--watchAll=false'];
+ options.testFileNamePattern = this.quoteFileName(this.request.testFileName);
+ options.testNamePattern = this.quote(escapeRegExp(this.request.testNamePattern));
+ args.push('--runTestsByPath', '--watchAll=false');
+ if (this.request.updateSnapshot) {
+ args.push('--updateSnapshot');
+ }
+ break;
+ }
+ case 'by-file-test-pattern': {
+ const regex = this.quote(escapeRegExp(this.request.testFileNamePattern));
+ options.testNamePattern = this.quote(escapeRegExp(this.request.testNamePattern));
+ args.push('--watchAll=false', '--testPathPattern', regex);
if (this.request.updateSnapshot) {
args.push('--updateSnapshot');
}
- options.args = { args };
break;
}
case 'not-test':
diff --git a/src/JestProcessManagement/JestProcessManager.ts b/src/JestProcessManagement/JestProcessManager.ts
index 6a0c35933..9016947ea 100644
--- a/src/JestProcessManagement/JestProcessManager.ts
+++ b/src/JestProcessManagement/JestProcessManager.ts
@@ -1,5 +1,5 @@
import { JestProcess } from './JestProcess';
-import { TaskArrayFunctions, JestProcessRequest, QueueType, Task } from './types';
+import { TaskArrayFunctions, JestProcessRequest, QueueType, Task, JestProcessInfo } from './types';
import { Logging } from '../logging';
import { createTaskQueue, TaskQueue } from './task-queue';
import { isDup, requestString } from './helper';
@@ -41,22 +41,22 @@ export class JestProcessManager implements TaskArrayFunctions {
/**
* schedule a jest process and handle duplication process if dedup is requested.
* @param request
- * @returns a boolean to indicate if the process is scheduled
+ * @returns a jest process id if successfully scheduled, otherwise undefined
*/
- public scheduleJestProcess(request: JestProcessRequest): boolean {
+ public scheduleJestProcess(request: JestProcessRequest): JestProcessInfo | undefined {
if (this.foundDup(request)) {
this.logging(
'debug',
`duplicate request found, process is not scheduled: ${requestString(request)}`
);
- return false;
+ return;
}
const queue = this.getQueue(request.schedule.queue);
const process = new JestProcess(this.extContext, request);
queue.add(process);
this.run(queue);
- return true;
+ return process;
}
// run the first process in the queue
@@ -90,7 +90,7 @@ export class JestProcessManager implements TaskArrayFunctions {
promises = queue.map((t) => t.data.stop());
queue.reset();
}
- await Promise.all(promises);
+ await Promise.allSettled(promises);
return;
}
diff --git a/src/JestProcessManagement/helper.ts b/src/JestProcessManagement/helper.ts
index 461a57f80..1fefd2516 100644
--- a/src/JestProcessManagement/helper.ts
+++ b/src/JestProcessManagement/helper.ts
@@ -4,8 +4,16 @@ import { JestProcessRequest, Task, TaskPredicate } from './types';
export const isRequestEqual = (r1: JestProcessRequest, r2: JestProcessRequest): boolean => {
switch (r1.type) {
case 'by-file':
+ return r2.type === r1.type && r1.testFileName === r2.testFileName;
+ case 'by-file-pattern':
return r2.type === r1.type && r1.testFileNamePattern === r2.testFileNamePattern;
case 'by-file-test':
+ return (
+ r2.type === r1.type &&
+ r1.testFileName === r2.testFileName &&
+ r1.testNamePattern === r2.testNamePattern
+ );
+ case 'by-file-test-pattern':
return (
r2.type === r1.type &&
r1.testFileNamePattern === r2.testFileNamePattern &&
diff --git a/src/JestProcessManagement/types.ts b/src/JestProcessManagement/types.ts
index c2af16174..9b6e7c492 100644
--- a/src/JestProcessManagement/types.ts
+++ b/src/JestProcessManagement/types.ts
@@ -7,6 +7,10 @@ export interface JestProcessListener {
onEvent: (process: JestProcess, event: JestProcessEvent, ...args: unknown[]) => unknown;
}
export type JestProcessStatus = 'pending' | 'running' | 'stopping' | 'stoppped';
+export interface JestProcessInfo {
+ readonly id: string;
+ readonly request: JestProcessRequest;
+}
export type TaskStatus = 'running' | 'pending';
export interface Task {
@@ -49,11 +53,22 @@ export type JestProcessRequestBase =
}
| {
type: Extract;
- testFileNamePattern: string;
+ testFileName: string;
updateSnapshot?: boolean;
}
| {
type: Extract;
+ testFileName: string;
+ testNamePattern: string;
+ updateSnapshot?: boolean;
+ }
+ | {
+ type: Extract;
+ testFileNamePattern: string;
+ updateSnapshot?: boolean;
+ }
+ | {
+ type: Extract;
testFileNamePattern: string;
testNamePattern: string;
updateSnapshot?: boolean;
@@ -62,6 +77,7 @@ export type JestProcessRequestBase =
type: Extract;
args: string[];
};
+
export type JestProcessRequest = JestProcessRequestBase & JestProcessRequestCommon;
export interface TaskArrayFunctions {
diff --git a/src/Settings/index.ts b/src/Settings/index.ts
index 4edd10dd5..a5e11b7aa 100644
--- a/src/Settings/index.ts
+++ b/src/Settings/index.ts
@@ -7,7 +7,9 @@ export type JestTestProcessType =
| 'watch-all-tests'
| 'by-file'
| 'by-file-test'
- | 'not-test';
+ | 'not-test'
+ | 'by-file-test-pattern'
+ | 'by-file-pattern';
export type OnStartupType = Extract[];
export type OnSaveFileType = 'test-file' | 'test-src-file';
@@ -19,9 +21,12 @@ export type JestExtAutoRunConfig =
onStartup?: OnStartupType;
onSave?: OnSaveFileType;
};
+
+export type TestExplorerConfig =
+ | { enabled: false }
+ | { enabled: true; showClassicStatus?: boolean; showInlineError?: boolean };
export interface PluginResourceSettings {
autoEnable?: boolean;
- enableInlineErrorMessages?: boolean;
enableSnapshotUpdateMessages?: boolean;
jestCommandLine?: string;
pathToConfig?: string;
@@ -34,6 +39,7 @@ export interface PluginResourceSettings {
debugMode?: boolean;
coverageColors?: CoverageColors;
autoRun?: JestExtAutoRunConfig;
+ testExplorer: TestExplorerConfig;
}
export interface PluginWindowSettings {
diff --git a/src/StatusBar.ts b/src/StatusBar.ts
index 4935bb99e..5780a53c5 100644
--- a/src/StatusBar.ts
+++ b/src/StatusBar.ts
@@ -65,7 +65,7 @@ const createStatusBarItem = (type: StatusType, priority: number): SpinnableStatu
set text(_text: string) {
item.text = _text;
},
- set tooltip(_tooltip: string | undefined) {
+ set tooltip(_tooltip: string | vscode.MarkdownString | undefined) {
item.tooltip = _tooltip;
},
};
diff --git a/src/TestResults/TestResultProvider.ts b/src/TestResults/TestResultProvider.ts
index 647f051ac..ac3684b9c 100644
--- a/src/TestResults/TestResultProvider.ts
+++ b/src/TestResults/TestResultProvider.ts
@@ -4,23 +4,25 @@ import {
TestFileAssertionStatus,
IParseResults,
parse,
+ TestAssertionStatus,
} from 'jest-editor-support';
import { TestReconciliationState, TestReconciliationStateType } from './TestReconciliationState';
import { TestResult, TestResultStatusInfo } from './TestResult';
import * as match from './match-by-context';
-import { JestExtSessionAware } from '../JestExt';
+import { JestSessionEvents } from '../JestExt';
import { TestStats } from '../types';
import { emptyTestStats } from '../helpers';
+import { createTestResultEvents, TestResultEvents } from './test-result-events';
+import { ContainerNode } from './match-node';
+import { JestProcessInfo } from '../JestProcessManagement';
-interface TestSuiteResult {
+export interface TestSuiteResult {
status: TestReconciliationStateType;
+ message: string;
+ assertionContainer?: ContainerNode;
results?: TestResult[];
sorted?: SortedTestResults;
}
-type TestSuiteResultMap = {
- [filePath: string]: TestSuiteResult;
-};
-
export interface SortedTestResults {
fail: TestResult[];
skip: TestResult[];
@@ -34,25 +36,29 @@ const sortByStatus = (a: TestResult, b: TestResult): number => {
}
return TestResultStatusInfo[a.status].precedence - TestResultStatusInfo[b.status].precedence;
};
-export class TestResultProvider implements JestExtSessionAware {
+export class TestResultProvider {
verbose: boolean;
+ events: TestResultEvents;
private reconciler: TestReconciler;
- private testSuites: TestSuiteResultMap = {};
+ private testSuites: Map;
private testFiles?: string[];
- constructor(verbose = false) {
+ constructor(extEvents: JestSessionEvents, verbose = false) {
this.reconciler = new TestReconciler();
- this.resetCache();
this.verbose = verbose;
+ this.events = createTestResultEvents();
+ this.testSuites = new Map();
+ extEvents.onTestSessionStarted.event(this.onSessionStart.bind(this));
}
- public onSessionStart(): void {
- this.resetCache();
- this.reconciler = new TestReconciler();
+ dispose(): void {
+ this.events.testListUpdated.dispose();
+ this.events.testSuiteChanged.dispose();
}
- resetCache(): void {
- this.testSuites = {};
+ private onSessionStart(): void {
+ this.testSuites.clear();
+ this.reconciler = new TestReconciler();
}
private groupByRange(results: TestResult[]): TestResult[] {
@@ -89,11 +95,19 @@ export class TestResultProvider implements JestExtSessionAware {
this.testFiles = testFiles;
// clear the cache in case we have cached some non-test files prior
- this.testSuites = {};
+ this.testSuites.clear();
+
+ this.events.testListUpdated.fire(testFiles);
+ }
+ getTestList(): string[] {
+ if (this.testFiles && this.testFiles.length > 0) {
+ return this.testFiles;
+ }
+ return Array.from(this.testSuites.keys());
}
isTestFile(fileName: string): 'yes' | 'no' | 'unknown' {
- if (this.testFiles?.includes(fileName) || this.testSuites[fileName] != null) {
+ if (this.testFiles?.includes(fileName) || this.testSuites.get(fileName) != null) {
return 'yes';
}
if (!this.testFiles) {
@@ -102,24 +116,41 @@ export class TestResultProvider implements JestExtSessionAware {
return 'no';
}
- private matchResults(filePath: string, { root, itBlocks }: IParseResults): TestSuiteResult {
- try {
+ public getTestSuiteResult(filePath: string): TestSuiteResult | undefined {
+ const cache = this.testSuites.get(filePath);
+ if (cache && !cache.assertionContainer) {
const assertions = this.reconciler.assertionsForTestFile(filePath);
if (assertions && assertions.length > 0) {
- const status = this.reconciler.stateForTestFile(filePath);
- return {
- status,
- results: this.groupByRange(
- match.matchTestAssertions(filePath, root, assertions, this.verbose)
- ),
- };
+ cache.assertionContainer = match.buildAssertionContainer(assertions);
+ this.testSuites.set(filePath, cache);
}
+ }
+ return cache;
+ }
+ private matchResults(filePath: string, { root, itBlocks }: IParseResults): TestSuiteResult {
+ let error: string | undefined;
+ try {
+ const cache = this.getTestSuiteResult(filePath);
+ if (cache?.assertionContainer) {
+ cache.results = this.groupByRange(
+ match.matchTestAssertions(filePath, root, cache.assertionContainer, this.verbose)
+ );
+ this.events.testSuiteChanged.fire({
+ type: 'result-matched',
+ file: filePath,
+ });
+ return cache;
+ }
+ error = 'no assertion generated for file';
} catch (e) {
console.warn(`failed to match test results for ${filePath}:`, e);
+ error = `encountered internal match error: ${e}`;
}
+
// no need to do groupByRange as the source block will not have blocks under the same location
return {
status: 'Unknown',
+ message: error,
results: itBlocks.map((t) => match.toMatchResult(t, 'no assertion found', 'match-failed')),
};
}
@@ -132,7 +163,7 @@ export class TestResultProvider implements JestExtSessionAware {
* @throws if parsing or matching internal error
*/
getResults(filePath: string): TestResult[] | undefined {
- const results = this.testSuites[filePath]?.results;
+ const results = this.testSuites.get(filePath)?.results;
if (results) {
return results;
}
@@ -141,19 +172,16 @@ export class TestResultProvider implements JestExtSessionAware {
return;
}
- let suiteResult: TestSuiteResult = { status: 'Unknown', results: [] };
try {
const parseResult = parse(filePath);
- suiteResult = this.matchResults(filePath, parseResult);
+ this.testSuites.set(filePath, this.matchResults(filePath, parseResult));
+ return this.testSuites.get(filePath)?.results;
} catch (e) {
- console.warn(`failed to get test results for ${filePath}:`, e);
- suiteResult = { status: 'KnownFail', results: [] };
+ const message = `failed to get test results for ${filePath}`;
+ console.warn(message, e);
+ this.testSuites.set(filePath, { status: 'KnownFail', message, results: [] });
throw e;
- } finally {
- this.testSuites[filePath] = suiteResult;
}
-
- return suiteResult.results;
}
/**
@@ -164,7 +192,7 @@ export class TestResultProvider implements JestExtSessionAware {
*/
getSortedResults(filePath: string): SortedTestResults | undefined {
- const cached = this.testSuites[filePath]?.sorted;
+ const cached = this.testSuites.get(filePath)?.sorted;
if (cached) {
return cached;
}
@@ -198,23 +226,33 @@ export class TestResultProvider implements JestExtSessionAware {
}
}
} finally {
- if (this.testSuites[filePath]) {
- this.testSuites[filePath].sorted = result;
+ const cached = this.testSuites.get(filePath);
+ if (cached) {
+ cached.sorted = result;
}
}
return result;
}
- updateTestResults(data: JestTotalResults): TestFileAssertionStatus[] {
+ updateTestResults(data: JestTotalResults, process: JestProcessInfo): TestFileAssertionStatus[] {
const results = this.reconciler.updateFileWithJestStatus(data);
results?.forEach((r) => {
- this.testSuites[r.file] = { status: r.status };
+ this.testSuites.set(r.file, {
+ status: r.status,
+ message: r.message,
+ assertionContainer: r.assertions ? match.buildAssertionContainer(r.assertions) : undefined,
+ });
+ });
+ this.events.testSuiteChanged.fire({
+ type: 'assertions-updated',
+ files: results.map((r) => r.file),
+ process,
});
return results;
}
removeCachedResults(filePath: string): void {
- delete this.testSuites[filePath];
+ this.testSuites.delete(filePath);
}
invalidateTestResults(filePath: string): void {
this.removeCachedResults(filePath);
@@ -224,7 +262,7 @@ export class TestResultProvider implements JestExtSessionAware {
// test stats
getTestSuiteStats(): TestStats {
const stats = emptyTestStats();
- for (const suite of Object.values(this.testSuites)) {
+ this.testSuites.forEach((suite) => {
if (suite.status === 'KnownSuccess') {
stats.success += 1;
} else if (suite.status === 'KnownFail') {
@@ -232,7 +270,7 @@ export class TestResultProvider implements JestExtSessionAware {
} else {
stats.unknown += 1;
}
- }
+ });
if (this.testFiles) {
if (this.testFiles.length > stats.fail + stats.success + stats.unknown) {
diff --git a/src/TestResults/test-result-events.ts b/src/TestResults/test-result-events.ts
new file mode 100644
index 000000000..18b9d5ed7
--- /dev/null
+++ b/src/TestResults/test-result-events.ts
@@ -0,0 +1,21 @@
+import * as vscode from 'vscode';
+import { JestProcessInfo } from '../JestProcessManagement';
+
+export type TestSuiteChangeReason = 'assertions-updated' | 'result-matched';
+export type TestSuitChangeEvent =
+ | {
+ type: 'assertions-updated';
+ process: JestProcessInfo;
+ files: string[];
+ }
+ | {
+ type: 'result-matched';
+ file: string;
+ };
+
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+export const createTestResultEvents = () => ({
+ testListUpdated: new vscode.EventEmitter(),
+ testSuiteChanged: new vscode.EventEmitter(),
+});
+export type TestResultEvents = ReturnType;
diff --git a/src/decorations/inline-error.ts b/src/decorations/inline-error.ts
deleted file mode 100644
index 7059b4edf..000000000
--- a/src/decorations/inline-error.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { window, TextEditorDecorationType, OverviewRulerLane } from 'vscode';
-
-const inlineError = (text: string): TextEditorDecorationType => {
- return window.createTextEditorDecorationType({
- isWholeLine: true,
- overviewRulerColor: 'red',
- overviewRulerLane: OverviewRulerLane.Left,
- light: {
- before: {
- color: '#FF564B',
- },
- after: {
- color: '#FF564B',
- contentText: ' // ' + text,
- },
- },
- dark: {
- before: {
- color: '#AD322D',
- },
- after: {
- color: '#AD322D',
- contentText: ' // ' + text,
- },
- },
- });
-};
-
-export default inlineError;
diff --git a/src/editor.ts b/src/editor.ts
deleted file mode 100644
index 29ea93988..000000000
--- a/src/editor.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import * as vscode from 'vscode';
-
-export function hasDocument(editor: vscode.TextEditor) {
- return !!editor && !!editor.document;
-}
-
-export function isOpenInMultipleEditors(document: vscode.TextDocument) {
- if (!document || !document.fileName) {
- return false;
- }
-
- let count = 0;
- for (const editor of vscode.window.visibleTextEditors) {
- if (editor && editor.document && editor.document.fileName === document.fileName) {
- count += 1;
- }
-
- if (count > 1) {
- break;
- }
- }
-
- return count > 1;
-}
diff --git a/src/extensionManager.ts b/src/extensionManager.ts
index 29e729028..293079d32 100644
--- a/src/extensionManager.ts
+++ b/src/extensionManager.ts
@@ -66,10 +66,9 @@ export class ExtensionManager {
this.commonPluginSettings = getExtensionWindowSettings();
this.debugConfigurationProvider = new DebugConfigurationProvider();
- this.debugCodeLensProvider = new DebugCodeLensProvider((uri) => this.getByDocUri(uri));
- this.coverageCodeLensProvider = new CoverageCodeLensProvider((uri) => this.getByDocUri(uri));
+ this.debugCodeLensProvider = new DebugCodeLensProvider(this.getByDocUri);
+ this.coverageCodeLensProvider = new CoverageCodeLensProvider(this.getByDocUri);
this.applySettings(getExtensionWindowSettings());
- this.registerAll();
}
applySettings(settings: PluginWindowSettings): void {
this.commonPluginSettings = settings;
@@ -78,6 +77,13 @@ export class ExtensionManager {
? debugCodeLens.showWhenTestStateIn
: [];
settings.disabledWorkspaceFolders.forEach(this.unregisterByName, this);
+
+ //register workspace folder not in the disable list
+ vscode.workspace.workspaceFolders?.forEach((ws) => {
+ if (!this.extByWorkspace.get(ws.name)) {
+ this.register(ws);
+ }
+ });
}
register(workspaceFolder: vscode.WorkspaceFolder): void {
if (!this.shouldStart(workspaceFolder.name)) {
@@ -94,9 +100,7 @@ export class ExtensionManager {
this.extByWorkspace.set(workspaceFolder.name, jestExt);
jestExt.startSession();
}
- registerAll(): void {
- vscode.workspace.workspaceFolders?.forEach(this.register, this);
- }
+
unregister(workspaceFolder: vscode.WorkspaceFolder): void {
this.unregisterByName(workspaceFolder.name);
}
@@ -125,9 +129,9 @@ export class ExtensionManager {
}
return true;
}
- getByName(workspaceFolderName: string): JestExt | undefined {
+ public getByName = (workspaceFolderName: string): JestExt | undefined => {
return this.extByWorkspace.get(workspaceFolderName);
- }
+ };
public getByDocUri: GetJestExtByURI = (uri: vscode.Uri) => {
const workspace = vscode.workspace.getWorkspaceFolder(uri);
if (workspace) {
@@ -262,12 +266,34 @@ export class ExtensionManager {
}, [] as vscode.Uri[]);
this.onFilesChange(files, (ext) => ext.onDidRenameFiles(event));
}
+
activate(): void {
if (vscode.window.activeTextEditor?.document.uri) {
const ext = this.getByDocUri(vscode.window.activeTextEditor.document.uri);
if (ext) {
- ext.onDidChangeActiveTextEditor(vscode.window.activeTextEditor);
+ ext.activate();
}
}
+ this.showReleaseMessage();
+ }
+ private showReleaseMessage(): void {
+ vscode.window
+ .showInformationMessage(
+ `vscode-jest now supports the official vscode test explorer!!`,
+ 'Show Test Explorer',
+ 'See Details'
+ )
+ .then((value) => {
+ if (value === 'Show Test Explorer') {
+ vscode.commands.executeCommand('workbench.view.testing.focus');
+ } else {
+ vscode.commands.executeCommand(
+ 'vscode.open',
+ vscode.Uri.parse(
+ 'https://github.com/jest-community/vscode-jest/blob/master/README.md#how-to-use-the-test-explorer'
+ )
+ );
+ }
+ });
}
}
diff --git a/src/test-provider/index.ts b/src/test-provider/index.ts
new file mode 100644
index 000000000..5aa9994af
--- /dev/null
+++ b/src/test-provider/index.ts
@@ -0,0 +1 @@
+export { JestTestProvider } from './test-provider';
diff --git a/src/test-provider/test-item-data.ts b/src/test-provider/test-item-data.ts
new file mode 100644
index 000000000..fdb58a7d3
--- /dev/null
+++ b/src/test-provider/test-item-data.ts
@@ -0,0 +1,607 @@
+import * as vscode from 'vscode';
+import { extensionId } from '../appGlobals';
+import { JestRunEvent } from '../JestExt';
+import { TestSuiteResult } from '../TestResults';
+import * as path from 'path';
+import { JestExtRequestType } from '../JestExt/process-session';
+import { TestAssertionStatus } from 'jest-editor-support';
+import { DataNode, NodeType, ROOT_NODE_NAME } from '../TestResults/match-node';
+import { Logging } from '../logging';
+import { TestSuitChangeEvent } from '../TestResults/test-result-events';
+import { Debuggable, TestItemData, TestItemRun } from './types';
+import { JestTestProviderContext } from './test-provider-context';
+import { JestProcessInfo } from '../JestProcessManagement';
+
+interface JestRunable {
+ getJestRunRequest: (profile: vscode.TestRunProfile) => JestExtRequestType;
+}
+interface WithUri {
+ uri: vscode.Uri;
+}
+
+type TestItemRunRequest = JestExtRequestType & { itemRun: TestItemRun };
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const isTestItemRunRequest = (arg: any): arg is TestItemRunRequest =>
+ arg.itemRun?.item && arg.itemRun?.run && arg.itemRun?.end;
+
+const deepItemState = (item: vscode.TestItem, setState: (item: vscode.TestItem) => void): void => {
+ setState(item);
+ item.children.forEach((child) => deepItemState(child, setState));
+};
+abstract class TestItemDataBase implements TestItemData, JestRunable, WithUri {
+ item!: vscode.TestItem;
+ log: Logging;
+
+ constructor(public context: JestTestProviderContext, name: string) {
+ this.log = context.ext.loggingFactory.create(name);
+ }
+ get uri(): vscode.Uri {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return this.item.uri!;
+ }
+
+ scheduleTest(run: vscode.TestRun, end: () => void, profile: vscode.TestRunProfile): void {
+ const jestRequest = this.getJestRunRequest(profile);
+ const itemRun: TestItemRun = { item: this.item, run, end };
+ deepItemState(this.item, run.enqueued);
+
+ const process = this.context.ext.session.scheduleProcess({
+ ...jestRequest,
+ itemRun,
+ });
+ if (!process) {
+ const msg = `failed to schedule test for ${this.item.id}`;
+ run.errored(this.item, new vscode.TestMessage(msg));
+ this.context.appendOutput(msg, run, true, 'red');
+ end();
+ }
+ }
+
+ 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;
+ }
+}
+
+/**
+ * Goal of this class is to manage the TestItem hierarchy reflects DocumentRoot path. It is responsible
+ * to create DocumentRoot for each test file by listening to the TestResultEvents.
+ */
+export class WorkspaceRoot extends TestItemDataBase {
+ private testDocuments: Map;
+ private listeners: vscode.Disposable[];
+ private cachedRun: Map;
+
+ constructor(context: JestTestProviderContext) {
+ super(context, 'WorkspaceRoot');
+ this.item = this.createTestItem();
+ this.testDocuments = new Map();
+ this.listeners = [];
+ this.cachedRun = new Map();
+
+ this.registerEvents();
+ }
+ createTestItem(): vscode.TestItem {
+ const item = this.context.createTestItem(
+ `${extensionId}:${this.context.ext.workspace.name}`,
+ this.context.ext.workspace.name,
+ this.context.ext.workspace.uri,
+ this
+ );
+ item.description = `(${this.context.ext.autoRun.mode})`;
+
+ item.canResolveChildren = true;
+ return item;
+ }
+
+ getJestRunRequest(_profile: vscode.TestRunProfile): JestExtRequestType {
+ return { type: 'all-tests' };
+ }
+ discoverTest(run: vscode.TestRun): void {
+ const testList = this.context.ext.testResolveProvider.getTestList();
+ this.onTestListUpdated(testList, run);
+ }
+
+ // test result event handling
+ private registerEvents = (): void => {
+ this.listeners = [
+ this.context.ext.testResolveProvider.events.testListUpdated.event(this.onTestListUpdated),
+ this.context.ext.testResolveProvider.events.testSuiteChanged.event(this.onTestSuiteChanged),
+ this.context.ext.sessionEvents.onRunEvent.event(this.onRunEvent),
+ ];
+ };
+ private unregisterEvents = (): void => {
+ this.listeners.forEach((l) => l.dispose());
+ this.listeners.length = 0;
+ };
+
+ private createRun = (name?: string, item?: vscode.TestItem): vscode.TestRun => {
+ const target = item ?? this.item;
+ return this.context.createTestRun(new vscode.TestRunRequest([target]), name ?? target.id);
+ };
+
+ private addFolder = (parent: FolderData | undefined, folderName: string): FolderData => {
+ const p = parent ?? this;
+ const uri = FolderData.makeUri(p.item, folderName);
+ return (
+ this.context.getChildData(p.item, uri.fsPath) ??
+ new FolderData(this.context, folderName, p.item)
+ );
+ };
+ private addPath = (absoluteFileName: string): FolderData | undefined => {
+ const relativePath = path.relative(this.context.ext.workspace.uri.fsPath, absoluteFileName);
+ const folders = relativePath.split(path.sep).slice(0, -1);
+
+ return folders.reduce(this.addFolder, undefined);
+ };
+ /**
+ * create a test item hierarchy for the given the test file based on its reltive path. If the file is not
+ * a test file, exception will be thrown.
+ */
+ private addTestFile = (
+ absoluteFileName: string,
+ onTestDocument?: (doc: TestDocumentRoot) => void
+ ): TestDocumentRoot => {
+ let docRoot = this.testDocuments.get(absoluteFileName);
+ if (!docRoot) {
+ const parent = this.addPath(absoluteFileName) ?? this;
+ docRoot =
+ this.context.getChildData(parent.item, absoluteFileName) ??
+ new TestDocumentRoot(this.context, vscode.Uri.file(absoluteFileName), parent.item);
+ this.testDocuments.set(absoluteFileName, docRoot);
+ }
+
+ onTestDocument?.(docRoot);
+
+ return docRoot;
+ };
+
+ /**
+ * Wwhen test list updated, rebuild the whole testItem tree for all the test files (DocumentRoot)
+ * Note: this could be optimized to only updat the differences if needed.
+ */
+ private onTestListUpdated = (
+ absoluteFileNames: string[] | undefined,
+ run?: vscode.TestRun
+ ): void => {
+ this.item.children.replace([]);
+ this.testDocuments.clear();
+
+ const aRun = run ?? this.createRun();
+ try {
+ absoluteFileNames?.forEach((f) =>
+ this.addTestFile(f, (testRoot) => testRoot.updateResultState(aRun))
+ );
+ } catch (e) {
+ this.log('error', `[WorkspaceRoot] "${this.item.id}" onTestListUpdated failed:`, e);
+ } finally {
+ aRun.end();
+ }
+ this.item.canResolveChildren = false;
+ };
+
+ /**
+ * invoked when external test result changed, this could be caused by the watch-mode or on-demand test run, includes vscode's runTest.
+ * We will try to find the run based on the event's id, if found, means a vscode runTest initiated such run, will use that run to
+ * ask all touched DocumentRoot to refresh both the test items and their states.
+ *
+ * @param event
+ */
+ private onTestSuiteChanged = (event: TestSuitChangeEvent): void => {
+ switch (event.type) {
+ case 'assertions-updated': {
+ const itemRun = this.getItemRun(event.process);
+ const run = itemRun?.run ?? this.createRun(event.process.id);
+
+ this.context.appendOutput(
+ `update status from run "${event.process.id}": ${event.files.length} files`,
+ run
+ );
+ try {
+ event.files.forEach((f) => this.addTestFile(f, (testRoot) => testRoot.discoverTest(run)));
+ } catch (e) {
+ this.log('error', `"${this.item.id}" onTestSuiteChanged: assertions-updated failed:`, e);
+ } finally {
+ (itemRun ?? run).end();
+ }
+ break;
+ }
+ case 'result-matched': {
+ this.addTestFile(event.file, (testRoot) => testRoot.onTestMatched());
+ break;
+ }
+ }
+ };
+
+ private getItemFromProcess = (process: JestProcessInfo): vscode.TestItem => {
+ let fileName;
+ switch (process.request.type) {
+ case 'watch-tests':
+ case 'watch-all-tests':
+ case 'all-tests':
+ return this.item;
+ case 'by-file':
+ fileName = process.request.testFileName;
+ break;
+ case 'by-file-pattern':
+ fileName = process.request.testFileNamePattern;
+ break;
+ default:
+ throw new Error(`unsupported external process type ${process.request.type}`);
+ }
+
+ const item = this.testDocuments.get(fileName)?.item;
+ if (item) {
+ return item;
+ }
+ throw new Error(`No test file found for ${fileName}`);
+ };
+
+ private createTestItemRun = (event: JestRunEvent): TestItemRun => {
+ const item = this.getItemFromProcess(event.process);
+ const run = this.createRun(`${event.type}:${event.process.id}`, item);
+ const end = () => {
+ this.cachedRun.delete(event.process.id);
+ run.end();
+ };
+ const itemRun: TestItemRun = { item, run, end };
+ this.cachedRun.set(event.process.id, itemRun);
+ return itemRun;
+ };
+ private getItemRun = (process: JestProcessInfo): TestItemRun | undefined =>
+ isTestItemRunRequest(process.request)
+ ? process.request.itemRun
+ : this.cachedRun.get(process.id);
+
+ private onRunEvent = (event: JestRunEvent) => {
+ if (event.process.request.type === 'not-test') {
+ return;
+ }
+
+ let itemRun = this.getItemRun(event.process);
+
+ try {
+ switch (event.type) {
+ case 'scheduled': {
+ if (!itemRun) {
+ itemRun = this.createTestItemRun(event);
+ const text = `Scheduled test run "${event.process.id}" for "${itemRun.item.id}"`;
+ this.context.appendOutput(text, itemRun.run);
+ deepItemState(itemRun.item, itemRun.run.enqueued);
+ }
+
+ break;
+ }
+ case 'data': {
+ itemRun = itemRun ?? this.createTestItemRun(event);
+ const text = event.raw ?? event.text;
+ const color = event.isError ? 'red' : undefined;
+ this.context.appendOutput(text, itemRun.run, event.newLine ?? false, color);
+ break;
+ }
+ case 'start': {
+ itemRun = itemRun ?? this.createTestItemRun(event);
+ deepItemState(itemRun.item, itemRun.run.started);
+ break;
+ }
+ case 'end': {
+ itemRun?.end();
+ break;
+ }
+ case 'exit': {
+ if (event.error) {
+ if (!itemRun || itemRun.run.token.isCancellationRequested) {
+ itemRun = this.createTestItemRun(event);
+ }
+ this.context.appendOutput(event.error, itemRun.run, true, 'red');
+ itemRun.run.errored(itemRun.item, new vscode.TestMessage(event.error));
+ }
+ itemRun?.end();
+ break;
+ }
+ }
+ } catch (err) {
+ this.log('error', ` ${event.type} failed:`, err);
+ }
+ };
+
+ dispose(): void {
+ this.unregisterEvents();
+ this.cachedRun.forEach((run) => run.end());
+ }
+}
+
+export class FolderData extends TestItemDataBase {
+ static makeUri = (parent: vscode.TestItem, folderName: string): vscode.Uri => {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return vscode.Uri.joinPath(parent.uri!, folderName);
+ };
+ constructor(
+ readonly context: JestTestProviderContext,
+ readonly name: string,
+ parent: vscode.TestItem
+ ) {
+ super(context, 'FolderData');
+ this.item = this.createTestItem(name, parent);
+ }
+ private createTestItem(name: string, parent: vscode.TestItem) {
+ const uri = FolderData.makeUri(parent, name);
+ const item = this.context.createTestItem(uri.fsPath, name, uri, this, parent);
+
+ item.canResolveChildren = false;
+ return item;
+ }
+ getJestRunRequest(_profile: vscode.TestRunProfile): JestExtRequestType {
+ return {
+ type: 'by-file-pattern',
+ testFileNamePattern: this.uri.fsPath,
+ };
+ }
+}
+
+const isDataNode = (arg: NodeType): arg is DataNode =>
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (arg as any).data != null;
+
+type AssertNode = NodeType;
+abstract class TestResultData extends TestItemDataBase {
+ constructor(readonly context: JestTestProviderContext, name: string) {
+ super(context, name);
+ }
+
+ updateItemState(
+ run: vscode.TestRun,
+ result?: TestSuiteResult | TestAssertionStatus,
+ errorLocation?: vscode.Location
+ ): void {
+ if (!result) {
+ return;
+ }
+ const status = result.status;
+ switch (status) {
+ case 'KnownSuccess':
+ run.passed(this.item);
+ break;
+ case 'KnownSkip':
+ case 'KnownTodo':
+ run.skipped(this.item);
+ break;
+ case 'KnownFail': {
+ const message = new vscode.TestMessage(result.message);
+ message.location = errorLocation;
+
+ run.failed(this.item, message);
+ break;
+ }
+ }
+ }
+
+ makeTestId(fileUri: vscode.Uri, target?: NodeType, extra?: string): string {
+ const parts = [fileUri.fsPath];
+ if (target && target.name !== ROOT_NODE_NAME) {
+ parts.push(target.fullName);
+ }
+ if (extra) {
+ parts.push(extra);
+ }
+ return parts.join('#');
+ }
+
+ isSameId(id1: string, id2: string): boolean {
+ if (id1 === id2) {
+ return true;
+ }
+ // truncate the last "extra-id" added for duplicate test names before comparing
+ const truncateExtra = (id: string): string => id.replace(/(.*)(#[0-9]+$)/, '$1');
+ return truncateExtra(id1) === truncateExtra(id2);
+ }
+ syncChildNodes(node: AssertNode): void {
+ const testId = this.makeTestId(this.uri, node);
+ if (!this.isSameId(testId, this.item.id)) {
+ this.item.error = 'invalid node';
+ return;
+ }
+ this.item.error = undefined;
+
+ if (!isDataNode(node)) {
+ const idMap = [...node.childContainers, ...node.childData]
+ .flatMap((n) => n.getAll() as AssertNode[])
+ .reduce((map, node) => {
+ const id = this.makeTestId(this.uri, node);
+ map.set(id, map.get(id)?.concat(node) ?? [node]);
+ return map;
+ }, new Map());
+
+ const newItems: vscode.TestItem[] = [];
+ idMap.forEach((nodes, id) => {
+ if (nodes.length > 1) {
+ // duplicate names found, append index to make a unique id: re-create the item with new id
+ nodes.forEach((n, idx) => {
+ newItems.push(new TestData(this.context, this.uri, n, this.item, `${idx}`).item);
+ });
+ return;
+ }
+ let cItem = this.item.children.get(id);
+ if (cItem) {
+ this.context.getData(cItem)?.updateNode(nodes[0]);
+ } else {
+ cItem = new TestData(this.context, this.uri, nodes[0], this.item).item;
+ }
+ newItems.push(cItem);
+ });
+ this.item.children.replace(newItems);
+ } else {
+ this.item.children.replace([]);
+ }
+ }
+
+ createLocation(uri: vscode.Uri, zeroBasedLine = 0): vscode.Location {
+ return new vscode.Location(
+ uri,
+ new vscode.Range(new vscode.Position(zeroBasedLine, 0), new vscode.Position(zeroBasedLine, 0))
+ );
+ }
+}
+export class TestDocumentRoot extends TestResultData {
+ constructor(
+ readonly context: JestTestProviderContext,
+ fileUri: vscode.Uri,
+ parent: vscode.TestItem
+ ) {
+ super(context, 'TestDocumentRoot');
+ this.item = this.createTestItem(fileUri, parent);
+ }
+ private createTestItem(fileUri: vscode.Uri, parent: vscode.TestItem): vscode.TestItem {
+ const item = this.context.createTestItem(
+ this.makeTestId(fileUri),
+ path.basename(fileUri.fsPath),
+ fileUri,
+ this,
+ parent
+ );
+
+ item.canResolveChildren = true;
+ return item;
+ }
+
+ discoverTest = (run: vscode.TestRun): void => {
+ this.createChildItems();
+ this.updateResultState(run);
+ };
+
+ private createChildItems = (): void => {
+ try {
+ const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id);
+ if (!suiteResult || !suiteResult.assertionContainer) {
+ this.item.children.replace([]);
+ } else {
+ this.syncChildNodes(suiteResult.assertionContainer);
+ }
+ } catch (e) {
+ this.log('error', `[TestDocumentRoot] "${this.item.id}" createChildItems failed:`, e);
+ } finally {
+ this.item.canResolveChildren = false;
+ }
+ };
+
+ public updateResultState(run: vscode.TestRun): void {
+ const suiteResult = this.context.ext.testResolveProvider.getTestSuiteResult(this.item.id);
+ this.updateItemState(run, suiteResult);
+
+ this.item.children.forEach((childItem) =>
+ this.context.getData(childItem)?.updateResultState(run)
+ );
+ }
+
+ getJestRunRequest = (_profile: vscode.TestRunProfile): JestExtRequestType => {
+ return {
+ type: 'by-file',
+ testFileName: this.item.id,
+ };
+ };
+
+ public onTestMatched = (): void => {
+ this.item.children.forEach((childItem) =>
+ this.context.getData(childItem)?.onTestMatched()
+ );
+ };
+}
+export class TestData extends TestResultData implements Debuggable {
+ constructor(
+ readonly context: JestTestProviderContext,
+ fileUri: vscode.Uri,
+ private node: AssertNode,
+ parent: vscode.TestItem,
+ extraId?: string
+ ) {
+ super(context, 'TestData');
+ this.item = this.createTestItem(fileUri, parent, extraId);
+ this.updateNode(node);
+ }
+
+ private createTestItem(fileUri: vscode.Uri, parent: vscode.TestItem, extraId?: string) {
+ const item = this.context.createTestItem(
+ this.makeTestId(fileUri, this.node, extraId),
+ this.node.name,
+ fileUri,
+ this,
+ parent
+ );
+
+ item.canResolveChildren = false;
+ return item;
+ }
+
+ getJestRunRequest(_profile: vscode.TestRunProfile): JestExtRequestType {
+ return {
+ type: 'by-file-test-pattern',
+ testFileNamePattern: this.uri.fsPath,
+ testNamePattern: this.node.fullName,
+ };
+ }
+ isDebuggable(): boolean {
+ return true;
+ }
+ getDebugInfo(): { fileName: string; testNamePattern: string } {
+ return { fileName: this.uri.fsPath, testNamePattern: this.node.fullName };
+ }
+ private updateItemRange(): void {
+ if (this.node.attrs.range) {
+ const pos = [
+ this.node.attrs.range.start.line,
+ this.node.attrs.range.start.column,
+ this.node.attrs.range.end.line,
+ this.node.attrs.range.end.column,
+ ];
+ if (pos.every((n) => n >= 0)) {
+ this.item.range = new vscode.Range(pos[0], pos[1], pos[2], pos[3]);
+ return;
+ }
+ }
+ this.item.range = undefined;
+ }
+
+ updateNode(node: NodeType): void {
+ this.node = node;
+ this.updateItemRange();
+ this.syncChildNodes(node);
+ }
+
+ public onTestMatched(): void {
+ // assertion might have picked up source block location
+ this.updateItemRange();
+ this.item.children.forEach((childItem) =>
+ this.context.getData(childItem)?.onTestMatched()
+ );
+ }
+
+ public updateResultState(run: vscode.TestRun): void {
+ if (this.node && isDataNode(this.node)) {
+ const assertion = this.node.data;
+ const errorLine =
+ assertion.line != null &&
+ this.context.ext.settings.testExplorer.enabled &&
+ this.context.ext.settings.testExplorer.showInlineError
+ ? this.createLocation(this.uri, assertion.line - 1)
+ : undefined;
+ this.updateItemState(run, assertion, errorLine);
+ }
+ this.item.children.forEach((childItem) =>
+ this.context.getData(childItem)?.updateResultState(run)
+ );
+ }
+}
diff --git a/src/test-provider/test-provider-context.ts b/src/test-provider/test-provider-context.ts
new file mode 100644
index 000000000..ad685b75e
--- /dev/null
+++ b/src/test-provider/test-provider-context.ts
@@ -0,0 +1,84 @@
+import * as vscode from 'vscode';
+import { JestExtExplorerContext, TestItemData } from './types';
+
+/**
+ * provide context information from JestExt and test provider state:
+ * 1. TestData <-> TestItem
+ *
+ * as well as factory functions to create TestItem and TestRun that could impact the state
+ */
+
+// output color support
+export type OUTPUT_COLOR = 'red' | 'green' | 'yellow';
+const COLORS = {
+ ['red']: '\x1b[0;31m',
+ ['green']: '\x1b[0;32m',
+ ['yellow']: '\x1b[0;33m',
+ ['end']: '\x1b[0m',
+};
+
+export class JestTestProviderContext {
+ private testItemData: WeakMap;
+
+ constructor(
+ public readonly ext: JestExtExplorerContext,
+ private readonly controller: vscode.TestController
+ ) {
+ this.testItemData = new WeakMap();
+ }
+ createTestItem = (
+ id: string,
+ label: string,
+ uri: vscode.Uri,
+ data: TestItemData,
+ parent?: vscode.TestItem
+ ): 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);
+
+ return testItem;
+ };
+
+ /**
+ * check if there is such child in the item, if exists returns the associated data
+ *
+ * @param item
+ * @param childId id of the child item
+ * @returns data of the child item, casting for easy usage but does not guarentee type safety.
+ */
+ getChildData = (
+ item: vscode.TestItem,
+ childId: string
+ ): T | undefined => {
+ const cItem = item.children.get(childId);
+
+ // Note: casting for easy usage but does not guarentee type safety.
+ return cItem && (this.testItemData.get(cItem) as T);
+ };
+
+ /**
+ * get data associated with the item. All item used here should have some data associated with, otherwise
+ * an exception will be thrown
+ *
+ * @returns casting for easy usage but does not guarentee type safety
+ */
+ getData = (item: vscode.TestItem): T | undefined => {
+ // Note: casting for easy usage but does not guarentee type safety.
+ return this.testItemData.get(item) as T | undefined;
+ };
+
+ createTestRun = (request: vscode.TestRunRequest, name: string): vscode.TestRun => {
+ return this.controller.createTestRun(request, name);
+ };
+
+ appendOutput = (msg: string, run: vscode.TestRun, newLine = true, color?: OUTPUT_COLOR): void => {
+ const converted = msg.replace(/\n/g, '\r\n');
+ let text = newLine ? `[${this.ext.workspace.name}]: ${converted}` : converted;
+ if (color) {
+ text = `${COLORS[color]}${text}${COLORS['end']}`;
+ }
+ run.appendOutput(`${text}${newLine ? '\r\n' : ''}`);
+ };
+}
diff --git a/src/test-provider/test-provider.ts b/src/test-provider/test-provider.ts
new file mode 100644
index 000000000..6954c690f
--- /dev/null
+++ b/src/test-provider/test-provider.ts
@@ -0,0 +1,181 @@
+import * as vscode from 'vscode';
+import { JestTestProviderContext } from './test-provider-context';
+import { WorkspaceRoot } from './test-item-data';
+import { Debuggable, JestExtExplorerContext, TestItemData } from './types';
+import { extensionId } from '../appGlobals';
+import { Logging } from '../logging';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const isDebuggable = (arg: any): arg is Debuggable => arg && typeof arg.getDebugInfo === 'function';
+
+export const RunProfileInfo: Record = {
+ [vscode.TestRunProfileKind.Run]: 'run',
+ [vscode.TestRunProfileKind.Debug]: 'debug',
+ [vscode.TestRunProfileKind.Coverage]: 'run with coverage',
+};
+
+export class JestTestProvider {
+ private readonly controller: vscode.TestController;
+ private context: JestTestProviderContext;
+ private workspaceRoot: WorkspaceRoot;
+ private log: Logging;
+
+ constructor(jestContext: JestExtExplorerContext) {
+ this.log = jestContext.loggingFactory.create('JestTestProvider');
+ const wsFolder = jestContext.workspace;
+
+ this.controller = this.createController(wsFolder, jestContext);
+
+ this.context = new JestTestProviderContext(jestContext, this.controller);
+ this.workspaceRoot = new WorkspaceRoot(this.context);
+ }
+
+ private createController = (
+ wsFolder: vscode.WorkspaceFolder,
+ jestContext: JestExtExplorerContext
+ ): 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 discoverTest = (item: vscode.TestItem | undefined): void => {
+ const theItem = item ?? this.workspaceRoot.item;
+ const run = this.context.createTestRun(
+ new vscode.TestRunRequest([theItem]),
+ `disoverTest: ${this.controller.id}`
+ );
+ const data = this.context.getData(theItem);
+ run.appendOutput(
+ `${
+ data ? `resolving children for ${theItem.id}\r\n` : `no data found for item ${theItem.id}`
+ }`
+ );
+ try {
+ data?.discoverTest?.(run);
+ } catch (e) {
+ this.log('error', `[JestTestProvider]: discoverTest error for "${theItem.id}" : `, e);
+ theItem.error = `discoverTest error: ${JSON.stringify(e)}`;
+ } finally {
+ run.end();
+ }
+ };
+
+ private getAllItems = (): vscode.TestItem[] => {
+ const items: vscode.TestItem[] = [];
+ this.controller.items.forEach((item) => items.push(item));
+ return items;
+ };
+
+ /**
+ * invoke JestExt debug function for the given data, handle unexpected exception and set item state accordingly.
+ * should never throw or reject.
+ */
+ debugTest = async (tData: TestItemData, run: vscode.TestRun): Promise => {
+ let error;
+ if (isDebuggable(tData)) {
+ try {
+ const debugInfo = tData.getDebugInfo();
+ this.context.appendOutput(`launching debugger for ${tData.item.id}`, run);
+ await this.context.ext.debugTests(debugInfo.fileName, debugInfo.testNamePattern);
+ return;
+ } catch (e) {
+ error = `item ${tData.item.id} failed to debug: ${JSON.stringify(e)}`;
+ }
+ }
+ error = error ?? `item ${tData.item.id} is not debuggable`;
+ run.errored(tData.item, new vscode.TestMessage(error));
+ this.context.appendOutput(`${error}`, run, true, 'red');
+ return Promise.resolve();
+ };
+
+ runTests = async (
+ request: vscode.TestRunRequest,
+ cancelToken: vscode.CancellationToken
+ ): Promise => {
+ if (!request.profile) {
+ 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) {
+ const tData = this.context.getData(test);
+ if (!tData || cancelToken.isCancellationRequested) {
+ run.skipped(test);
+ continue;
+ }
+ if (!tData.canRun(profile)) {
+ run.skipped(test);
+ notRunnable.push(test.id);
+ continue;
+ }
+ 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);
+ } catch (e) {
+ const msg = `failed to schedule test for ${tData.item.id}: ${JSON.stringify(e)}`;
+ this.log('error', msg, e);
+ run.errored(test, new vscode.TestMessage(msg));
+ reject(msg);
+ }
+ })
+ );
+ }
+ }
+
+ // 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');
+ }
+
+ await Promise.allSettled(promises);
+ run.end();
+ };
+
+ dispose(): void {
+ this.workspaceRoot.dispose();
+ this.controller.dispose();
+ }
+}
diff --git a/src/test-provider/types.ts b/src/test-provider/types.ts
new file mode 100644
index 000000000..3793a72af
--- /dev/null
+++ b/src/test-provider/types.ts
@@ -0,0 +1,34 @@
+import * as vscode from 'vscode';
+import { DebugFunction, JestSessionEvents, JestExtSessionContext } from '../JestExt';
+import { TestResultProvider } from '../TestResults';
+import { WorkspaceRoot, FolderData, TestData, TestDocumentRoot } from './test-item-data';
+import { JestTestProviderContext } from './test-provider-context';
+
+export type TestItemDataType = WorkspaceRoot | FolderData | TestDocumentRoot | TestData;
+
+/** JestExt context exposed to the test explorer */
+export interface JestExtExplorerContext extends JestExtSessionContext {
+ readonly testResolveProvider: TestResultProvider;
+ readonly sessionEvents: JestSessionEvents;
+ debugTests: DebugFunction;
+}
+
+export interface TestItemRun {
+ item: vscode.TestItem;
+ run: vscode.TestRun;
+ end: () => void;
+}
+
+export type RunType = vscode.TestRun | TestItemRun;
+export interface TestItemData {
+ readonly item: vscode.TestItem;
+ readonly uri: vscode.Uri;
+ context: JestTestProviderContext;
+ discoverTest?: (run: vscode.TestRun) => void;
+ scheduleTest: (run: vscode.TestRun, end: () => void, profile: vscode.TestRunProfile) => void;
+ canRun: (profile: vscode.TestRunProfile) => boolean;
+}
+
+export interface Debuggable {
+ getDebugInfo: () => { fileName: string; testNamePattern: string };
+}
diff --git a/tests/Coverage/CoverageOverlay.test.ts b/tests/Coverage/CoverageOverlay.test.ts
index 5e76d215f..14ca2b98f 100644
--- a/tests/Coverage/CoverageOverlay.test.ts
+++ b/tests/Coverage/CoverageOverlay.test.ts
@@ -23,7 +23,6 @@ jest.mock('vscode', () => {
import { CoverageOverlay } from '../../src/Coverage/CoverageOverlay';
import { DefaultFormatter } from '../../src/Coverage/Formatters/DefaultFormatter';
import { GutterFormatter } from '../../src/Coverage/Formatters/GutterFormatter';
-import { hasDocument } from '../../src/editor';
describe('CoverageOverlay', () => {
const coverageMapProvider: any = {};
@@ -134,7 +133,6 @@ describe('CoverageOverlay', () => {
describe('update()', () => {
it('should do nothing if the editor does not have a valid document', () => {
const sut = new CoverageOverlay(null, coverageMapProvider);
- ((hasDocument as unknown) as jest.Mock<{}>).mockReturnValueOnce(false);
const editor: any = {};
sut.update(editor);
@@ -146,9 +144,8 @@ describe('CoverageOverlay', () => {
it('should add the overlay when enabled', () => {
const enabled = true;
const sut = new CoverageOverlay(null, coverageMapProvider, enabled);
- ((hasDocument as unknown) as jest.Mock<{}>).mockReturnValueOnce(true);
- const editor: any = {};
+ const editor: any = { document: {} };
sut.update(editor);
expect(sut.formatter.format).toBeCalledWith(editor);
@@ -157,9 +154,8 @@ describe('CoverageOverlay', () => {
it('should remove the overlay when disabled', () => {
const enabled = false;
const sut = new CoverageOverlay(null, coverageMapProvider, enabled);
- ((hasDocument as unknown) as jest.Mock<{}>).mockReturnValueOnce(true);
- const editor: any = {};
+ const editor: any = { document: {} };
sut.update(editor);
expect(sut.formatter.clear).toBeCalledWith(editor);
diff --git a/tests/DebugCodeLens/DebugCodeLensProvider.test.ts b/tests/DebugCodeLens/DebugCodeLensProvider.test.ts
index 10f531ac0..da98ff1c8 100644
--- a/tests/DebugCodeLens/DebugCodeLensProvider.test.ts
+++ b/tests/DebugCodeLens/DebugCodeLensProvider.test.ts
@@ -49,7 +49,7 @@ jest.mock('vscode', () => {
});
import { DebugCodeLensProvider } from '../../src/DebugCodeLens/DebugCodeLensProvider';
-import { TestResultProvider, TestResult, TestReconciliationState } from '../../src/TestResults';
+import { TestResult, TestReconciliationState } from '../../src/TestResults';
import { DebugCodeLens } from '../../src/DebugCodeLens/DebugCodeLens';
import { extensionName } from '../../src/appGlobals';
import * as vscode from 'vscode';
@@ -57,15 +57,19 @@ import { TestState } from '../../src/DebugCodeLens';
import * as helper from '../test-helper';
describe('DebugCodeLensProvider', () => {
- const testResultProvider = new TestResultProvider();
- const provideJestExt: any = () => ({ testResultProvider });
+ const getResultsMock = jest.fn();
+ const testResultProviderMock: any = { getResults: getResultsMock };
+ const provideJestExt: any = () => ({ testResultProvider: testResultProviderMock });
const allTestStates = [TestState.Fail, TestState.Pass, TestState.Skip, TestState.Unknown];
+ beforeEach(() => {
+ getResultsMock.mockClear();
+ });
describe('constructor()', () => {
it('should set the jest extension provider', () => {
const sut = new DebugCodeLensProvider(provideJestExt, allTestStates);
- expect((sut as any).getJestExt().testResultProvider).toBe(testResultProvider);
+ expect((sut as any).getJestExt().testResultProvider).toBe(testResultProviderMock);
});
it('should set which test states to show the CodeLens above', () => {
@@ -125,7 +129,7 @@ describe('DebugCodeLensProvider', () => {
describe('provideCodeLenses()', () => {
const document = { fileName: 'file.js' } as any;
const token = {} as any;
- const getResults = (testResultProvider.getResults as unknown) as jest.Mock<{}>;
+ const getResults = getResultsMock;
const testResults = [
({
name: 'should fail',
@@ -163,7 +167,7 @@ describe('DebugCodeLensProvider', () => {
getResults.mockReturnValueOnce([]);
sut.provideCodeLenses(document, token);
- expect(testResultProvider.getResults).toBeCalledWith(document.fileName);
+ expect(getResults).toBeCalledWith(document.fileName);
});
it('should not show the CodeLens above failing tests unless configured', () => {
diff --git a/tests/JestExt/core.test.ts b/tests/JestExt/core.test.ts
index d988b117b..0f5ccc595 100644
--- a/tests/JestExt/core.test.ts
+++ b/tests/JestExt/core.test.ts
@@ -1,4 +1,3 @@
-/* eslint-disable jest/no-conditional-expect */
jest.unmock('events');
jest.unmock('../../src/JestExt/core');
jest.unmock('../../src/JestExt/helper');
@@ -12,13 +11,12 @@ jest.mock('os');
jest.mock('../../src/decorations/test-status', () => ({
TestStatus: jest.fn(),
}));
-jest.mock('../../src/decorations/inline-error', () => ({
- default: jest.fn(),
-}));
-const update = jest.fn();
+const sbUpdateMock = jest.fn();
const statusBar = {
- bind: () => ({ update }),
+ bind: () => ({
+ update: sbUpdateMock,
+ }),
};
jest.mock('../../src/StatusBar', () => ({ statusBar }));
jest.mock('jest-editor-support');
@@ -26,12 +24,9 @@ jest.mock('jest-editor-support');
import * as vscode from 'vscode';
import { JestExt } from '../../src/JestExt/core';
import { createProcessSession } from '../../src/JestExt/process-session';
-import { window, workspace, debug, ExtensionContext, TextEditorDecorationType } from 'vscode';
-import { hasDocument, isOpenInMultipleEditors } from '../../src/editor';
import { TestStatus } from '../../src/decorations/test-status';
import { updateCurrentDiagnostics, updateDiagnostics } from '../../src/diagnostics';
import { CoverageMapProvider } from '../../src/Coverage';
-import inlineError from '../../src/decorations/inline-error';
import * as helper from '../../src/helpers';
import { TestIdentifier, resultsWithLowerCaseWindowsDriveLetters } from '../../src/TestResults';
import * as messaging from '../../src/messaging';
@@ -41,6 +36,7 @@ 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"] }] */
const mockHelpers = helper as jest.Mocked;
@@ -54,17 +50,18 @@ const EmptySortedResult = {
const mockGetExtensionResourceSettings = jest.spyOn(extHelper, 'getExtensionResourceSettings');
describe('JestExt', () => {
- const getConfiguration = workspace.getConfiguration as jest.Mock;
+ const getConfiguration = vscode.workspace.getConfiguration as jest.Mock;
const StateDecorationsMock = TestStatus as jest.Mock;
- const context: any = { asAbsolutePath: (text) => text } as ExtensionContext;
+ const context: any = { asAbsolutePath: (text) => text } as vscode.ExtensionContext;
const workspaceFolder = { name: 'test-folder' } as any;
const channelStub = {
appendLine: jest.fn(),
+ append: jest.fn(),
clear: jest.fn(),
show: jest.fn(),
dispose: jest.fn(),
} as any;
- const extensionSettings = { debugCodeLens: {} } as any;
+ const extensionSettings = { debugCodeLens: {}, testExplorer: { enabled: true } } as any;
const debugCodeLensProvider = {} as any;
const debugConfigurationProvider = {
provideDebugConfigurations: jest.fn(),
@@ -105,6 +102,10 @@ describe('JestExt', () => {
};
};
+ const mockTestProvider: any = {
+ dispose: jest.fn(),
+ };
+
beforeEach(() => {
jest.resetAllMocks();
@@ -115,80 +116,9 @@ describe('JestExt', () => {
(createProcessSession as jest.Mocked).mockReturnValue(mockProcessSession);
(ProjectWorkspace as jest.Mocked).mockImplementation(mockProjectWorkspace);
(workspaceLogging as jest.Mocked).mockImplementation(mockWworkspaceLogging);
- });
-
- describe('resetInlineErrorDecorators()', () => {
- let sut: JestExt;
- const editor = mockEditor('file.js', 'javascript');
- const decorationType: any = { dispose: jest.fn() };
-
- beforeEach(() => {
- sut = newJestExt();
-
- sut.debugCodeLensProvider.didChange = jest.fn();
- ((sut.testResultProvider.getSortedResults as unknown) as jest.Mock<{}>).mockReturnValueOnce(
- EmptySortedResult
- );
- });
- it('should initialize the cached decoration types as an empty array', () => {
- expect(sut.failingAssertionDecorators[editor.document.fileName]).toBeUndefined();
- sut.triggerUpdateActiveEditor(editor);
-
- expect(sut.failingAssertionDecorators[editor.document.fileName]).toEqual([]);
- expect(isOpenInMultipleEditors).not.toBeCalled();
- });
-
- it('should not clear the cached decorations types when the document is open more than once', () => {
- ((isOpenInMultipleEditors as unknown) as jest.Mock<{}>).mockReturnValueOnce(true);
-
- sut.failingAssertionDecorators[editor.document.fileName] = {
- forEach: jest.fn(),
- } as any;
- sut.triggerUpdateActiveEditor(editor);
-
- expect(sut.failingAssertionDecorators[editor.document.fileName].forEach).not.toBeCalled();
- });
-
- it('should dispose of each cached decoration type', () => {
- sut.failingAssertionDecorators[editor.document.fileName] = [decorationType];
- sut.triggerUpdateActiveEditor(editor);
-
- expect(decorationType.dispose).toBeCalled();
- });
-
- it('should reset the cached decoration types', () => {
- sut.failingAssertionDecorators[editor.document.fileName] = [decorationType];
- sut.triggerUpdateActiveEditor(editor);
-
- expect(sut.failingAssertionDecorators[editor.document.fileName]).toEqual([]);
- });
- });
-
- describe('generateInlineErrorDecorator()', () => {
- it('should add the decoration type to the cache', () => {
- const settings: any = {
- debugCodeLens: {},
- enableInlineErrorMessages: true,
- };
- const expected = { key: 'value' };
- const failingAssertionStyle = inlineError as jest.Mock;
- failingAssertionStyle.mockReturnValueOnce(expected);
- const sut = newJestExt({ settings });
- const editor = mockEditor('file.ts');
- sut.testResultProvider.getSortedResults = jest.fn().mockReturnValueOnce({
- success: [],
- fail: [
- {
- start: {},
- },
- ],
- skip: [],
- unknown: [],
- });
- sut.debugCodeLensProvider.didChange = jest.fn();
- sut.triggerUpdateActiveEditor(editor);
-
- expect(sut.failingAssertionDecorators[editor.document.fileName]).toEqual([expected]);
+ (JestTestProvider as jest.Mocked).mockImplementation(() => mockTestProvider);
+ (vscode.EventEmitter as jest.Mocked) = jest.fn().mockImplementation(() => {
+ return { fire: jest.fn(), event: jest.fn(), dispose: jest.fn() };
});
});
@@ -204,7 +134,7 @@ describe('JestExt', () => {
const mockShowQuickPick = jest.fn();
let mockConfigurations = [];
beforeEach(() => {
- startDebugging = (debug.startDebugging as unknown) as jest.Mock<{}>;
+ startDebugging = (vscode.debug.startDebugging as unknown) as jest.Mock<{}>;
((startDebugging as unknown) as jest.Mock<{}>).mockImplementation(
async (_folder: any, nameOrConfig: any) => {
// trigger fallback to default configuration
@@ -226,24 +156,29 @@ describe('JestExt', () => {
sut = newJestExt();
});
- it('should run the supplied test', async () => {
- const testNamePattern = 'testNamePattern';
- await sut.debugTests(document, testNamePattern);
- expect(debug.startDebugging).toHaveBeenCalledWith(workspaceFolder, debugConfiguration);
- const configuration = startDebugging.mock.calls[startDebugging.mock.calls.length - 1][1];
- expect(configuration).toBeDefined();
- expect(configuration.type).toBe('dummyconfig');
- expect(sut.debugConfigurationProvider.prepareTestRun).toBeCalledWith(
- fileName,
- testNamePattern
- );
+ describe('should run the supplied test', () => {
+ it.each([[document], ['fileName']])('support document paramter: %s', async (doc) => {
+ const testNamePattern = 'testNamePattern';
+ await sut.debugTests(doc, testNamePattern);
+ expect(vscode.debug.startDebugging).toHaveBeenCalledWith(
+ workspaceFolder,
+ debugConfiguration
+ );
+ const configuration = startDebugging.mock.calls[startDebugging.mock.calls.length - 1][1];
+ expect(configuration).toBeDefined();
+ expect(configuration.type).toBe('dummyconfig');
+ expect(sut.debugConfigurationProvider.prepareTestRun).toBeCalledWith(
+ fileName,
+ testNamePattern
+ );
+ });
});
it('can handle testIdentifier argument', async () => {
const tId = makeIdentifier('test-1', ['d-1', 'd-1-1']);
const fullName = 'd-1 d-1-1 test-1';
mockHelpers.testIdString.mockReturnValue(fullName);
await sut.debugTests(document, tId);
- expect(debug.startDebugging).toHaveBeenCalledWith(workspaceFolder, debugConfiguration);
+ expect(vscode.debug.startDebugging).toHaveBeenCalledWith(workspaceFolder, debugConfiguration);
const configuration = startDebugging.mock.calls[startDebugging.mock.calls.length - 1][1];
expect(configuration).toBeDefined();
expect(configuration.type).toBe('dummyconfig');
@@ -265,14 +200,17 @@ describe('JestExt', () => {
expect(mockHelpers.escapeRegExp).toHaveBeenCalled();
}
if (startDebug) {
- expect(debug.startDebugging).toHaveBeenCalledWith(workspaceFolder, debugConfiguration);
+ expect(vscode.debug.startDebugging).toHaveBeenCalledWith(
+ workspaceFolder,
+ debugConfiguration
+ );
const configuration = startDebugging.mock.calls[startDebugging.mock.calls.length - 1][1];
expect(configuration).toBeDefined();
expect(configuration.type).toBe('dummyconfig');
expect(sut.debugConfigurationProvider.prepareTestRun).toHaveBeenCalled();
} else {
expect(sut.debugConfigurationProvider.prepareTestRun).not.toHaveBeenCalled();
- expect(debug.startDebugging).not.toHaveBeenCalled();
+ expect(vscode.debug.startDebugging).not.toHaveBeenCalled();
}
});
describe('paramerterized test', () => {
@@ -314,7 +252,10 @@ describe('JestExt', () => {
const selected = [tId1, tId2, tId3][selectIdx];
expect(mockHelpers.escapeRegExp).toBeCalledWith(selected);
// verify the actual test to be run is the one we selected: tId2
- expect(debug.startDebugging).toHaveBeenCalledWith(workspaceFolder, debugConfiguration);
+ expect(vscode.debug.startDebugging).toHaveBeenCalledWith(
+ workspaceFolder,
+ debugConfiguration
+ );
const configuration = startDebugging.mock.calls[startDebugging.mock.calls.length - 1][1];
expect(configuration).toBeDefined();
expect(configuration.type).toBe('dummyconfig');
@@ -324,13 +265,13 @@ describe('JestExt', () => {
selectIdx = -1;
await sut.debugTests(document, tId1, tId2, tId3);
expect(mockShowQuickPick).toHaveBeenCalledTimes(1);
- expect(debug.startDebugging).not.toHaveBeenCalled();
+ expect(vscode.debug.startDebugging).not.toHaveBeenCalled();
});
it('if pass zero testId, nothing will be run', async () => {
await sut.debugTests(document);
expect(mockShowQuickPick).not.toHaveBeenCalled();
expect(mockHelpers.testIdString).not.toBeCalled();
- expect(debug.startDebugging).not.toHaveBeenCalled();
+ expect(vscode.debug.startDebugging).not.toHaveBeenCalled();
});
});
});
@@ -355,13 +296,13 @@ describe('JestExt', () => {
expect(startDebugging).toBeCalledTimes(1);
if (shouldShowWarning) {
// debug with generated config
- expect(debug.startDebugging).toHaveBeenLastCalledWith(
+ expect(vscode.debug.startDebugging).toHaveBeenLastCalledWith(
workspaceFolder,
debugConfiguration
);
} else {
// debug with existing config
- expect(debug.startDebugging).toHaveBeenLastCalledWith(workspaceFolder, {
+ expect(vscode.debug.startDebugging).toHaveBeenLastCalledWith(workspaceFolder, {
name: 'vscode-jest-tests',
});
}
@@ -404,11 +345,6 @@ describe('JestExt', () => {
sut.onDidCloseTextDocument(document);
expect(sut.removeCachedTestResults).toBeCalledWith(document);
});
-
- it('should remove the cached decorations', () => {
- sut.onDidCloseTextDocument(document);
- expect(sut.removeCachedDecorationTypes).toBeCalled();
- });
});
describe('removeCachedTestResults()', () => {
@@ -445,30 +381,6 @@ describe('JestExt', () => {
});
});
- describe('removeCachedAnnotations()', () => {
- let sut;
- beforeEach(() => {
- sut = newJestExt();
-
- sut.failingAssertionDecorators = {
- 'file.js': [],
- };
- });
-
- it('should do nothing when the document is falsy', () => {
- sut.onDidCloseTextDocument(null);
-
- expect(sut.failingAssertionDecorators['file.js']).toBeDefined();
- });
-
- it('should remove the annotations for the document', () => {
- const document: any = { fileName: 'file.js' } as any;
- sut.onDidCloseTextDocument(document);
-
- expect(sut.failingAssertionDecorators['file.js']).toBeUndefined();
- });
- });
-
describe('onDidChangeActiveTextEditor()', () => {
const editor: any = {};
let sut;
@@ -480,7 +392,7 @@ describe('JestExt', () => {
});
it('should update the annotations when the editor has a document', () => {
- ((hasDocument as unknown) as jest.Mock<{}>).mockReturnValueOnce(true);
+ editor.document = {};
sut.onDidChangeActiveTextEditor(editor);
expect(sut.triggerUpdateActiveEditor).toBeCalledWith(editor);
@@ -549,17 +461,16 @@ describe('JestExt', () => {
it('should trigger updateActiveEditor', () => {
const editor: any = { document: event.document };
sut.triggerUpdateActiveEditor = jest.fn();
- window.visibleTextEditors = [editor];
+ vscode.window.visibleTextEditors = [editor];
sut.onDidChangeTextDocument(event);
expect(sut.triggerUpdateActiveEditor).toBeCalledWith(editor);
});
it('should update statusBar for stats', () => {
- const updateStatusBarSpy = jest.spyOn(sut as any, 'updateStatusBar');
sut.onDidChangeTextDocument(event);
expect(sut.testResultProvider.getTestSuiteStats).toBeCalled();
- expect(updateStatusBarSpy).toBeCalled();
+ expect(sbUpdateMock).toBeCalled();
});
});
@@ -567,7 +478,7 @@ describe('JestExt', () => {
it.each([[true], [false]])(
'ony invalidate test status if document is dirty: isDirty=%d',
(isDirty) => {
- window.visibleTextEditors = [];
+ vscode.window.visibleTextEditors = [];
const sut: any = newJestExt();
sut.testResultProvider.invalidateTestResults = jest.fn();
const event = {
@@ -612,8 +523,7 @@ describe('JestExt', () => {
fileName,
};
- window.visibleTextEditors = [];
- const updateStatusBarSpy = jest.spyOn(sut as any, 'updateStatusBar');
+ vscode.window.visibleTextEditors = [];
(sut.testResultProvider.isTestFile as jest.Mocked).mockReturnValueOnce(isTestFile);
mockProcessSession.scheduleProcess.mockClear();
@@ -622,14 +532,12 @@ describe('JestExt', () => {
if (shouldSchedule) {
expect(mockProcessSession.scheduleProcess).toBeCalledWith(
- expect.objectContaining({ type: 'by-file', testFileNamePattern: fileName })
+ expect.objectContaining({ type: 'by-file', testFileName: fileName })
);
} else {
expect(mockProcessSession.scheduleProcess).not.toBeCalled();
}
- expect(updateStatusBarSpy).toBeCalledWith(
- expect.objectContaining({ stats: { isDirty } })
- );
+ expect(sbUpdateMock).toBeCalledWith(expect.objectContaining({ stats: { isDirty } }));
}
);
});
@@ -707,6 +615,7 @@ describe('JestExt', () => {
});
it('when failed to get test result, it should report error and clear the decorators and diagnostics', () => {
const sut = newJestExt();
+ sut.debugCodeLensProvider.didChange = jest.fn();
const editor = mockEditor('a');
(sut.testResultProvider.getSortedResults as jest.Mocked).mockImplementation(() => {
throw new Error('force error');
@@ -733,6 +642,7 @@ describe('JestExt', () => {
}
);
updateDecoratorsSpy = jest.spyOn(sut, 'updateDecorators');
+ sut.debugCodeLensProvider.didChange = jest.fn();
});
it.each`
languageId | shouldSkip
@@ -795,7 +705,6 @@ describe('JestExt', () => {
let sut: JestExt;
const mockEditor: any = { document: { uri: { fsPath: `file://a/b/c.js` } } };
const emptyTestResults = { success: [], fail: [], skip: [], unknown: [] };
- const failingAssertionStyle = inlineError as jest.Mock;
const settings: any = {
debugCodeLens: {},
@@ -811,58 +720,62 @@ describe('JestExt', () => {
beforeEach(() => {
StateDecorationsMock.mockImplementation(() => ({
- passing: { key: 'pass' } as TextEditorDecorationType,
- failing: { key: 'fail' } as TextEditorDecorationType,
- skip: { key: 'skip' } as TextEditorDecorationType,
- unknown: { key: 'unknown' } as TextEditorDecorationType,
+ passing: { key: 'pass' } as vscode.TextEditorDecorationType,
+ failing: { key: 'fail' } as vscode.TextEditorDecorationType,
+ skip: { key: 'skip' } as vscode.TextEditorDecorationType,
+ unknown: { key: 'unknown' } as vscode.TextEditorDecorationType,
}));
- sut = newJestExt(settings);
mockEditor.setDecorations = jest.fn();
- sut.debugCodeLensProvider.didChange = jest.fn();
});
- it('will reset decorator if testResults is empty', () => {
- sut.updateDecorators(emptyTestResults, mockEditor);
- expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4);
- for (const args of mockEditor.setDecorations.mock.calls) {
- expect(args[1].length).toBe(0);
- }
- });
- it('will generate dot dectorations for test results', () => {
- const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] };
- sut.updateDecorators(testResults2, mockEditor);
- expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4);
- for (const args of mockEditor.setDecorations.mock.calls) {
- let expectedLength = -1;
- switch (args[0].key) {
- case 'fail':
- case 'pass':
- expectedLength = 1;
- break;
- case 'skip':
- case 'unknown':
- expectedLength = 0;
- break;
+ describe('when "showClassicStatus" is on', () => {
+ beforeEach(() => {
+ sut = newJestExt({
+ settings: { ...settings, testExplorer: { enabled: true, showClassicStatus: true } },
+ });
+ sut.debugCodeLensProvider.didChange = jest.fn();
+ });
+ it('will reset decorator if testResults is empty', () => {
+ sut.updateDecorators(emptyTestResults, mockEditor);
+ expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4);
+ for (const args of mockEditor.setDecorations.mock.calls) {
+ expect(args[1].length).toBe(0);
}
- expect(args[1].length).toBe(expectedLength);
- }
+ });
+ it('will generate dot dectorations for test results', () => {
+ const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] };
+ sut.updateDecorators(testResults2, mockEditor);
+ expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4);
+ for (const args of mockEditor.setDecorations.mock.calls) {
+ let expectedLength = -1;
+ switch (args[0].key) {
+ case 'fail':
+ case 'pass':
+ expectedLength = 1;
+ break;
+ case 'skip':
+ case 'unknown':
+ expectedLength = 0;
+ break;
+ }
+ expect(args[1].length).toBe(expectedLength);
+ }
+ });
});
-
- it('will update inlineError decorator only if setting is enabled', () => {
- const testResults2: any = { success: [], fail: [tr1, tr2], skip: [], unknown: [] };
- const expected = {};
- failingAssertionStyle.mockReturnValueOnce(expected);
- sut.updateDecorators(testResults2, mockEditor);
- expect(failingAssertionStyle).not.toBeCalled();
- expect(mockEditor.setDecorations).toHaveBeenCalledTimes(4);
-
- jest.clearAllMocks();
- settings.enableInlineErrorMessages = true;
- sut = newJestExt({ settings });
- sut.updateDecorators(testResults2, mockEditor);
- expect(failingAssertionStyle).toHaveBeenCalledTimes(2);
- expect(mockEditor.setDecorations).toHaveBeenCalledTimes(6);
+ describe('when showDecorations for "status.classic" is off', () => {
+ it.each([[{ enabled: true }], [{ enabled: true, showClassicStatus: false }]])(
+ 'no dot decorators will be generatred for testExplore config: %s',
+ (testExplorerConfig) => {
+ sut = newJestExt({
+ settings: { ...settings, testExplorer: testExplorerConfig },
+ });
+ sut.debugCodeLensProvider.didChange = jest.fn();
+ const testResults2: any = { success: [tr1], fail: [tr2], skip: [], unknown: [] };
+ sut.updateDecorators(testResults2, mockEditor);
+ expect(mockEditor.setDecorations).toHaveBeenCalledTimes(0);
+ }
+ );
});
});
@@ -874,11 +787,15 @@ describe('JestExt', () => {
};
beforeEach(() => {});
describe('startSession', () => {
- it('starts a new session and notify session aware components', async () => {
+ it('starts a new session and file event', async () => {
const sut = createJestExt();
await sut.startSession();
expect(mockProcessSession.start).toHaveBeenCalled();
- expect(sut.testResultProvider.onSessionStart).toHaveBeenCalled();
+ expect(JestTestProvider).toHaveBeenCalled();
+
+ expect(sut.events.onTestSessionStarted.fire).toHaveBeenCalledWith(
+ expect.objectContaining({ session: mockProcessSession })
+ );
});
it('if failed to start session, show error', async () => {
mockProcessSession.start.mockReturnValueOnce(Promise.reject('forced error'));
@@ -886,6 +803,16 @@ describe('JestExt', () => {
await sut.startSession();
expect(messaging.systemErrorMessage).toBeCalled();
});
+ it('dispose existing jestProvider before creating new one', async () => {
+ expect.hasAssertions();
+ const sut = createJestExt();
+ await sut.startSession();
+ expect(JestTestProvider).toHaveBeenCalledTimes(1);
+
+ await sut.startSession();
+ expect(mockTestProvider.dispose).toBeCalledTimes(1);
+ expect(JestTestProvider).toHaveBeenCalledTimes(2);
+ });
describe('will update test file list', () => {
it.each`
fileNames | error | expectedTestFiles
@@ -898,7 +825,6 @@ describe('JestExt', () => {
async ({ fileNames, error, expectedTestFiles }) => {
expect.hasAssertions();
const sut = createJestExt();
- const updateStatusBarSpy = jest.spyOn(sut as any, 'updateStatusBar');
const stats = { success: 1000, isDirty: false };
sut.testResultProvider.getTestSuiteStats = jest.fn().mockReturnValueOnce(stats);
@@ -914,22 +840,31 @@ describe('JestExt', () => {
// stats will be updated in status baar accordingly
expect(sut.testResultProvider.getTestSuiteStats).toBeCalled();
- expect(updateStatusBarSpy).toBeCalledWith({ stats });
+ expect(sbUpdateMock).toBeCalledWith({ stats });
}
);
});
});
describe('stopSession', () => {
- it('notify session aware components', async () => {
+ it('will fire event', async () => {
const sut = createJestExt();
await sut.stopSession();
expect(mockProcessSession.stop).toHaveBeenCalled();
+ expect(sut.events.onTestSessionStopped.fire).toHaveBeenCalled();
+ });
+ it('dispose existing testProvider', async () => {
+ const sut = createJestExt();
+ await sut.startSession();
+ expect(JestTestProvider).toHaveBeenCalledTimes(1);
+
+ await sut.stopSession();
+ expect(mockTestProvider.dispose).toBeCalledTimes(1);
+ expect(JestTestProvider).toHaveBeenCalledTimes(1);
});
it('updatae statusBar status', async () => {
const sut = createJestExt();
- const mockUpdateStatusBar = jest.spyOn(sut as any, 'updateStatusBar');
await sut.stopSession();
- expect(mockUpdateStatusBar).toHaveBeenCalledWith({ state: 'stopped' });
+ expect(sbUpdateMock).toHaveBeenCalledWith({ state: 'stopped' });
});
it('if failed to stop session, show error', async () => {
mockProcessSession.stop.mockReturnValueOnce(Promise.reject('forced error'));
@@ -965,7 +900,7 @@ describe('JestExt', () => {
sut.runAllTests(editor);
expect(mockProcessSession.scheduleProcess).toBeCalledWith({
type: 'by-file',
- testFileNamePattern: 'whatever',
+ testFileName: 'whatever',
});
});
});
@@ -1025,17 +960,19 @@ describe('JestExt', () => {
});
});
it('will invoke internal components to process test results', () => {
- updateWithData({});
+ updateWithData({}, 'test-all-12');
expect(mockCoverageMapProvider.update).toBeCalled();
- expect(sut.testResultProvider.updateTestResults).toBeCalled();
+ expect(sut.testResultProvider.updateTestResults).toBeCalledWith(
+ expect.anything(),
+ 'test-all-12'
+ );
expect(updateDiagnostics).toBeCalled();
});
it('will calculate stats and update statusBar', () => {
- const updateStatusBarSpy = jest.spyOn(sut as any, 'updateStatusBar');
updateWithData({});
expect(sut.testResultProvider.getTestSuiteStats).toBeCalled();
- expect(updateStatusBarSpy).toBeCalled();
+ expect(sbUpdateMock).toBeCalled();
});
it('will update visible editors for the current workspace', () => {
(vscode.window.visibleTextEditors as any) = [
@@ -1059,5 +996,97 @@ describe('JestExt', () => {
expect(mockProcessSession.stop).toBeCalledTimes(1);
expect(channelStub.dispose).toBeCalledTimes(1);
});
+ it('will dispose test provider if initialized', () => {
+ const sut = newJestExt();
+ sut.deactivate();
+ expect(mockTestProvider.dispose).not.toBeCalledTimes(1);
+ sut.startSession();
+ sut.deactivate();
+ expect(mockTestProvider.dispose).toBeCalledTimes(1);
+ });
+ it('will dispose all events', () => {
+ const sut = newJestExt();
+ sut.deactivate();
+ expect(sut.events.onRunEvent.dispose).toHaveBeenCalled();
+ expect(sut.events.onTestSessionStarted.dispose).toHaveBeenCalled();
+ expect(sut.events.onTestSessionStopped.dispose).toHaveBeenCalled();
+ });
+ });
+ describe('activate', () => {
+ it('will invoke onDidChangeActiveTextEditor for activeTextEditor', () => {
+ const sut = newJestExt();
+ const spy = jest.spyOn(sut, 'onDidChangeActiveTextEditor').mockImplementation(() => {});
+ vscode.window.activeTextEditor = undefined;
+
+ sut.activate();
+ expect(spy).not.toHaveBeenCalled();
+
+ (vscode.window.activeTextEditor as any) = {
+ document: { uri: 'whatever' },
+ };
+ (vscode.workspace.getWorkspaceFolder as jest.Mocked).mockReturnValue(workspaceFolder);
+
+ sut.activate();
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+ describe('runEvents', () => {
+ let sut, onRunEvent, process;
+ beforeEach(() => {
+ sut = newJestExt();
+ onRunEvent = (sut.events.onRunEvent.event as jest.Mocked).mock.calls[0][0];
+ process = { id: 'a process id' };
+ });
+
+ describe('can process run events', () => {
+ it('register onRunEvent listener', () => {
+ expect(sut.events.onRunEvent.event).toBeCalledTimes(1);
+ });
+ it('scheduled event: output to channel', () => {
+ onRunEvent({ type: 'scheduled', process });
+ expect(sut.channel.appendLine).toBeCalledWith(expect.stringContaining(process.id));
+ });
+ it('data event: relay clean-text to channel', () => {
+ onRunEvent({
+ type: 'data',
+ text: 'plain text',
+ raw: 'raw text',
+ newLine: true,
+ isError: true,
+ process,
+ });
+ expect(sut.channel.appendLine).toBeCalledWith(expect.stringContaining('plain text'));
+ expect(sut.channel.show).toBeCalled();
+ sut.channel.show.mockClear();
+
+ onRunEvent({ type: 'data', text: 'plain text 2', raw: 'raw text', process });
+ expect(sut.channel.append).toBeCalledWith(expect.stringContaining('plain text 2'));
+ expect(sut.channel.show).not.toBeCalled();
+ });
+ it('start event: notify status bar and clear channel', () => {
+ onRunEvent({ type: 'start', process });
+ expect(sbUpdateMock).toBeCalledWith({ state: 'running' });
+ expect(sut.channel.clear).toBeCalled();
+ });
+ it('end event: notify status bar', () => {
+ onRunEvent({ type: 'end', process });
+ expect(sbUpdateMock).toBeCalledWith({ state: 'done' });
+ });
+ describe('exit event: notify status bar', () => {
+ it('if no error: status bar done', () => {
+ onRunEvent({ type: 'exit', process });
+ expect(sbUpdateMock).toBeCalledWith({ state: 'done' });
+ });
+ it('if error: status bar stopped and show error', () => {
+ onRunEvent({ type: 'exit', error: 'something is wrong', process });
+ expect(sbUpdateMock).toBeCalledWith({ state: 'stopped' });
+ expect(messaging.systemErrorMessage).toHaveBeenCalled();
+ });
+ });
+ });
+ it('events are disposed when extensioin deactivated', () => {
+ sut.deactivate();
+ expect(sut.events.onRunEvent.dispose).toBeCalled();
+ });
});
});
diff --git a/tests/JestExt/helper.test.ts b/tests/JestExt/helper.test.ts
index 430b998b9..f4caf7a3f 100644
--- a/tests/JestExt/helper.test.ts
+++ b/tests/JestExt/helper.test.ts
@@ -140,7 +140,6 @@ describe('getExtensionResourceSettings()', () => {
expect(getExtensionResourceSettings(uri)).toEqual({
autoEnable: true,
coverageFormatter: 'DefaultFormatter',
- enableInlineErrorMessages: false,
enableSnapshotUpdateMessages: true,
pathToConfig: '',
pathToJest: null,
@@ -152,6 +151,7 @@ describe('getExtensionResourceSettings()', () => {
debugMode: false,
coverageColors: null,
autoRun: null,
+ testExplorer: { enabled: true },
});
});
});
diff --git a/tests/JestExt/process-listeners.test.ts b/tests/JestExt/process-listeners.test.ts
index 9550db9f8..d6c88cea3 100644
--- a/tests/JestExt/process-listeners.test.ts
+++ b/tests/JestExt/process-listeners.test.ts
@@ -23,12 +23,12 @@ describe('jest process listeners', () => {
context: {
settings: {},
autoRun: {},
- updateStatusBar: jest.fn(),
workspace: {},
setupWizardAction: jest.fn(),
loggingFactory: {
create: jest.fn(() => mockLogging),
},
+ onRunEvent: { fire: jest.fn() },
},
};
mockProcess = { request: { type: 'watch' } };
@@ -104,44 +104,57 @@ describe('jest process listeners', () => {
show: jest.fn(),
};
mockSession.context.updateWithData = jest.fn();
- mockSession.context.updateStatusBar = jest.fn();
mockProcess = { request: { type: 'watch-tests' } };
});
- it('can handle test result', () => {
- expect.hasAssertions();
- const listener = new RunTestListener(mockSession);
- const mockData = {};
- listener.onEvent(mockProcess, 'executableJSON', mockData);
- expect(mockSession.context.updateWithData).toBeCalledWith(mockData);
+ describe('can handle test result', () => {
+ it.each([[true], [false]])('with full output implementation: %s', (fullOutput) => {
+ if (!fullOutput) {
+ delete mockSession.context.output.clear;
+ delete mockSession.context.output.show;
+ }
+ expect.hasAssertions();
+ const listener = new RunTestListener(mockSession);
+ const mockData = {};
+ mockProcess = { id: 'mock-id' };
+ listener.onEvent(mockProcess, 'executableJSON', mockData);
+ expect(mockSession.context.updateWithData).toBeCalledWith(mockData, mockProcess);
+ });
});
describe.each`
- output | stdout | stderr | error
- ${'whatever'} | ${true} | ${true} | ${true}
- ${'onRunStart'} | ${false} | ${false} | ${true}
- ${'onRunComplete'} | ${false} | ${false} | ${true}
- ${'Watch Usage'} | ${false} | ${false} | ${true}
- `('propagate process output: $output', ({ output, stdout, stderr, error }) => {
- it('from stdout: show=$stdout', () => {
+ output | stdout | stderr | error
+ ${'whatever'} | ${'data'} | ${'data'} | ${'data'}
+ ${'onRunStart'} | ${'start'} | ${undefined} | ${'data'}
+ ${'onRunComplete'} | ${'end'} | ${undefined} | ${'data'}
+ ${'Watch Usage'} | ${undefined} | ${undefined} | ${'data'}
+ `('propagate run events: $output', ({ output, stdout, stderr, error }) => {
+ it('from stdout: eventType=$stdout', () => {
expect.hasAssertions();
const listener = new RunTestListener(mockSession);
// stdout
listener.onEvent(mockProcess, 'executableOutput', output);
if (stdout) {
- expect(mockSession.context.output.append).toBeCalledWith(output);
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({
+ type: stdout,
+ })
+ );
} else {
- expect(mockSession.context.output.append).not.toBeCalled();
+ expect(mockSession.context.onRunEvent.fire).not.toBeCalled();
}
});
- it('from stderr: show=$stderr', () => {
+ it('from stderr: eventyType=$stderr', () => {
expect.hasAssertions();
const listener = new RunTestListener(mockSession);
- // stdout
listener.onEvent(mockProcess, 'executableStdErr', Buffer.from(output));
if (stderr) {
- expect(mockSession.context.output.append).toBeCalledWith(output);
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({
+ type: stderr,
+ })
+ );
} else {
- expect(mockSession.context.output.append).not.toBeCalled();
+ expect(mockSession.context.onRunEvent.fire).not.toBeCalled();
}
});
it('from error event: show=$error', () => {
@@ -150,53 +163,52 @@ describe('jest process listeners', () => {
// stdout
listener.onEvent(mockProcess, 'terminalError', output);
if (error) {
- expect(mockSession.context.output.appendLine).toBeCalledWith(
- expect.stringContaining(output)
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({
+ type: error,
+ })
);
} else {
- expect(mockSession.context.output.appendLine).not.toBeCalled();
+ expect(mockSession.context.onRunEvent.fire).not.toBeCalled();
}
});
});
- describe('can clear output and manage running state', () => {
+ describe('can notify start/end events', () => {
it('when process start and end', () => {
expect.hasAssertions();
const listener = new RunTestListener(mockSession);
// stdout
listener.onEvent(mockProcess, 'processStarting');
- expect(mockSession.context.updateStatusBar).toHaveBeenLastCalledWith({ state: 'running' });
- expect(mockSession.context.output.clear).toBeCalledTimes(1);
+ expect(mockSession.context.onRunEvent.fire).toBeCalledTimes(1);
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({ type: 'start' })
+ );
+ mockSession.context.onRunEvent.fire.mockClear();
listener.onEvent(mockProcess, 'processExit');
- expect(mockSession.context.updateStatusBar).toHaveBeenLastCalledWith({ state: 'done' });
- expect(mockSession.context.output.clear).toBeCalledTimes(1);
+ expect(mockSession.context.onRunEvent.fire).not.toBeCalled();
+
+ listener.onEvent(mockProcess, 'processClose');
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({ type: 'exit' })
+ );
+ });
+ it('when reporters reports start/end', () => {
+ expect.hasAssertions();
+ const listener = new RunTestListener(mockSession);
+
+ // stdout
+ listener.onEvent(mockProcess, 'executableOutput', 'onRunStart');
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({ type: 'start' })
+ );
+
+ listener.onEvent(mockProcess, 'executableOutput', 'onRunComplete');
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({ type: 'end' })
+ );
});
- it.each`
- procType | clearCount
- ${'watch-tests'} | ${1}
- ${'watch-all-tests'} | ${1}
- ${'all-tests'} | ${0}
- ${'by-file'} | ${0}
- `(
- 'for watch mode runs, where process do not stop, perform cleaning/reporting also from content',
- ({ procType, clearCount }) => {
- expect.hasAssertions();
- const listener = new RunTestListener(mockSession);
- mockProcess.request.type = procType;
-
- // stdout
- listener.onEvent(mockProcess, 'executableOutput', 'onRunStart');
- expect(mockSession.context.updateStatusBar).toHaveBeenLastCalledWith({
- state: 'running',
- });
- expect(mockSession.context.output.clear).toBeCalledTimes(clearCount);
-
- listener.onEvent(mockProcess, 'executableOutput', 'onRunComplete');
- expect(mockSession.context.updateStatusBar).toHaveBeenLastCalledWith({ state: 'done' });
- expect(mockSession.context.output.clear).toBeCalledTimes(clearCount);
- }
- );
});
describe('when snapshot test failed', () => {
it.each`
@@ -309,15 +321,19 @@ describe('jest process listeners', () => {
beforeEach(() => {
mockSession.context.workspace = { name: 'workspace-xyz' };
mockProcess.request = { type: 'watch-tests' };
- (messaging as jest.Mocked).systemErrorMessage = jest.fn();
});
- it('will show error message with help', () => {
+ it('will fire exit with error', () => {
expect.hasAssertions();
const listener = new RunTestListener(mockSession);
listener.onEvent(mockProcess, 'processClose', 1);
- expect(messaging.systemErrorMessage).toBeCalled();
+ expect(mockSession.context.onRunEvent.fire).toBeCalledWith(
+ expect.objectContaining({
+ type: 'exit',
+ error: expect.anything(),
+ })
+ );
});
it('in single-root env, folder name will not be shown in the message', () => {
expect.hasAssertions();
@@ -327,9 +343,10 @@ describe('jest process listeners', () => {
const listener = new RunTestListener(mockSession);
listener.onEvent(mockProcess, 'processClose', 1);
- expect(messaging.systemErrorMessage).toBeCalled();
- const [msg] = (messaging.systemErrorMessage as jest.Mocked).mock.calls[0];
- expect(msg).not.toContain('workspace-xyz');
+ expect(mockSession.context.onRunEvent.fire).toBeCalled();
+
+ const event = mockSession.context.onRunEvent.fire.mock.calls[0][0];
+ expect(event.error).not.toContain('workspace-xyz');
});
it('in multi-root env, folder name will be shown in the message', () => {
expect.hasAssertions();
@@ -339,9 +356,10 @@ describe('jest process listeners', () => {
const listener = new RunTestListener(mockSession);
listener.onEvent(mockProcess, 'processClose', 1);
- expect(messaging.systemErrorMessage).toBeCalled();
- const [msg] = (messaging.systemErrorMessage as jest.Mocked).mock.calls[0];
- expect(msg).toContain('workspace-xyz');
+ expect(mockSession.context.onRunEvent.fire).toBeCalled();
+
+ const event = mockSession.context.onRunEvent.fire.mock.calls[0][0];
+ expect(event.error).toContain('workspace-xyz');
});
});
});
diff --git a/tests/JestExt/process-session.test.ts b/tests/JestExt/process-session.test.ts
index baf5a3355..977bbff1b 100644
--- a/tests/JestExt/process-session.test.ts
+++ b/tests/JestExt/process-session.test.ts
@@ -10,103 +10,137 @@ import { mockJestProcessContext } from '../test-helper';
const mockProcessManager = JestProcessManager as jest.Mocked;
+let SEQ = 1;
describe('ProcessSession', () => {
let context;
- const mockScheduleJestProcess = jest.fn();
- const mockNumberOfProcesses = jest.fn();
- const mockStopAll = jest.fn();
+ let processManagerMock;
beforeEach(() => {
jest.resetAllMocks();
- mockProcessManager.mockReturnValue({
- scheduleJestProcess: mockScheduleJestProcess,
- numberOfProcesses: mockNumberOfProcesses,
- stopAll: mockStopAll,
- });
+ processManagerMock = {
+ scheduleJestProcess: jest.fn().mockImplementation(() => ({
+ id: SEQ++,
+ })),
+ numberOfProcesses: jest.fn(),
+ stopAll: jest.fn(),
+ };
+ mockProcessManager.mockReturnValue(processManagerMock);
context = mockJestProcessContext();
});
- 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'} | ${{ testFileNamePattern: 'abc' }} | ${{ queue: 'blocking', dedup: { filterByStatus: ['pending'] } }} | ${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 }) => {
- expect.hasAssertions();
+ describe('scheduleProcess', () => {
+ it('will fire event for successful schedule', () => {
const sm = createProcessSession(context);
- expect(mockProcessManager).toHaveBeenCalledTimes(1);
- sm.scheduleProcess({ type, ...(inputProperty ?? {}) });
- expect(mockScheduleJestProcess).toHaveBeenCalledTimes(1);
- const request = mockScheduleJestProcess.mock.calls[0][0];
- expect(request.schedule).toEqual(expectedSchedule);
- if (inputProperty) {
- expect(request).toMatchObject(inputProperty);
- }
- if (expectedExtraProperty) {
- expect(request).toMatchObject(expectedExtraProperty);
- } else {
- expect(request.type).toEqual(type);
+ processManagerMock.scheduleJestProcess.mockReturnValueOnce(undefined);
+ let process = sm.scheduleProcess({ type: 'all-tests' });
+ expect(process).toBeUndefined();
+ expect(context.onRunEvent.fire).not.toBeCalled();
+
+ const p = { id: 'whatever' };
+ processManagerMock.scheduleJestProcess.mockReturnValueOnce(p);
+ process = sm.scheduleProcess({ type: 'all-tests' });
+ expect(process).toEqual(p);
+ 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'] }}
+ `(
+ "can schedule '$type' request with ProcessManager",
+ ({ type, inputProperty, expectedSchedule, expectedExtraProperty }) => {
+ expect.hasAssertions();
+ const sm = createProcessSession(context);
+ expect(mockProcessManager).toHaveBeenCalledTimes(1);
+
+ const process = sm.scheduleProcess({ type, ...(inputProperty ?? {}) });
+ expect(process).not.toBeUndefined();
+ expect(processManagerMock.scheduleJestProcess).toHaveBeenCalledTimes(1);
+ const request = processManagerMock.scheduleJestProcess.mock.calls[0][0];
+ expect(request.schedule).toEqual(expectedSchedule);
+ if (inputProperty) {
+ expect(request).toMatchObject(inputProperty);
+ }
+ if (expectedExtraProperty) {
+ expect(request).toMatchObject(expectedExtraProperty);
+ } else {
+ expect(request.type).toEqual(type);
+ }
}
- }
- );
- it.each`
- baseRequest | snapshotRequest
- ${{ type: 'watch-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }}
- ${{ type: 'watch-all-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }}
- ${{ type: 'all-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }}
- ${{ type: 'by-file', testFileNamePattern: 'abc' }} | ${{ type: 'by-file', testFileNamePattern: 'abc', updateSnapshot: true }}
- ${{ type: 'by-file', testFileNamePattern: 'abc', updateSnapshot: true }} | ${undefined}
- `(
- 'can schedule update-snapshot request with ProcessManager for process: $request',
- async ({ baseRequest, snapshotRequest }) => {
- expect.hasAssertions();
- const sm = createProcessSession(context);
- expect(mockProcessManager).toHaveBeenCalledTimes(1);
+ );
+ it.each`
+ baseRequest | snapshotRequest
+ ${{ type: 'watch-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }}
+ ${{ type: 'watch-all-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }}
+ ${{ type: 'all-tests' }} | ${{ type: 'all-tests', updateSnapshot: true }}
+ ${{ type: 'by-file', testFileName: 'abc' }} | ${{ type: 'by-file', testFileName: 'abc', updateSnapshot: true }}
+ ${{ type: 'by-file', testFileName: 'abc', updateSnapshot: true }} | ${undefined}
+ ${{ type: 'by-file-pattern', testFileNamePattern: 'abc' }} | ${{ type: 'by-file-pattern', testFileNamePattern: 'abc', updateSnapshot: true }}
+ ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: 'a test' }} | ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: 'a test', updateSnapshot: true }}
+ ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: 'a test' }} | ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: 'a test', updateSnapshot: true }}
+ `(
+ 'can schedule update-snapshot request: $baseRequest',
+ async ({ baseRequest, snapshotRequest }) => {
+ expect.hasAssertions();
+ const sm = createProcessSession(context);
+ expect(mockProcessManager).toHaveBeenCalledTimes(1);
- sm.scheduleProcess({ type: 'update-snapshot', baseRequest });
+ sm.scheduleProcess({ type: 'update-snapshot', baseRequest });
- if (snapshotRequest) {
- expect(mockScheduleJestProcess).toHaveBeenCalledWith(
- expect.objectContaining(snapshotRequest)
- );
- } else {
- expect(mockScheduleJestProcess).not.toHaveBeenCalled();
+ if (snapshotRequest) {
+ expect(processManagerMock.scheduleJestProcess).toHaveBeenCalledWith(
+ expect.objectContaining(snapshotRequest)
+ );
+ } else {
+ expect(processManagerMock.scheduleJestProcess).not.toHaveBeenCalled();
+ }
}
- }
- );
- it.each([['not-test', 'by-file-test']])(
- 'currently does not support "%s" request scheduling',
- (type) => {
+ );
+
+ it.each([['not-test']])('currently does not support "%s" request scheduling', (type) => {
expect.hasAssertions();
const sm = createProcessSession(context);
expect(mockProcessManager).toHaveBeenCalledTimes(1);
const requestType = type as any;
- expect(sm.scheduleProcess({ type: requestType })).toEqual(false);
- }
- );
- describe.each`
- type | inputProperty | defaultListener
- ${'all-tests'} | ${undefined} | ${listeners.RunTestListener}
- ${'watch-tests'} | ${undefined} | ${listeners.RunTestListener}
- ${'watch-all-tests'} | ${undefined} | ${listeners.RunTestListener}
- ${'by-file'} | ${{ testFileNamePattern: 'abc' }} | ${listeners.RunTestListener}
- ${'list-test-files'} | ${undefined} | ${listeners.ListTestFileListener}
- ${'update-snapshot'} | ${{ baseRequest: { type: 'all-tests' } }} | ${listeners.RunTestListener}
- `('schedule $type', ({ type, inputProperty, defaultListener }) => {
- it('with default listener', () => {
- expect.hasAssertions();
- const sm = createProcessSession(context);
+ sm.scheduleProcess({ type: requestType });
+ expect(processManagerMock.scheduleJestProcess).not.toHaveBeenCalled();
+ });
+ describe.each`
+ type | inputProperty | defaultListener
+ ${'all-tests'} | ${undefined} | ${listeners.RunTestListener}
+ ${'watch-tests'} | ${undefined} | ${listeners.RunTestListener}
+ ${'watch-all-tests'} | ${undefined} | ${listeners.RunTestListener}
+ ${'by-file'} | ${{ testFileNamePattern: 'abc' }} | ${listeners.RunTestListener}
+ ${'list-test-files'} | ${undefined} | ${listeners.ListTestFileListener}
+ ${'update-snapshot'} | ${{ baseRequest: { type: 'all-tests' } }} | ${listeners.RunTestListener}
+ `('schedule $type', ({ type, inputProperty, defaultListener }) => {
+ it('with default listener', () => {
+ expect.hasAssertions();
+ const sm = createProcessSession(context);
- sm.scheduleProcess({ type, ...(inputProperty ?? {}) });
- expect(mockScheduleJestProcess).toHaveBeenCalledTimes(1);
- const request = mockScheduleJestProcess.mock.calls[0][0];
- expect(request.listener).not.toBeUndefined();
- expect(defaultListener).toHaveBeenCalledTimes(1);
+ sm.scheduleProcess({ type, ...(inputProperty ?? {}) });
+ expect(processManagerMock.scheduleJestProcess).toHaveBeenCalledTimes(1);
+ const request = processManagerMock.scheduleJestProcess.mock.calls[0][0];
+ expect(request.listener).not.toBeUndefined();
+ expect(defaultListener).toHaveBeenCalledTimes(1);
+ });
+ });
+ it('can pass custom requet', () => {
+ const sm = createProcessSession(context);
+ expect(mockProcessManager).toHaveBeenCalledTimes(1);
+ const extraInfo = 'whatever';
+ sm.scheduleProcess({ type: 'all-tests', extraInfo });
+ expect(processManagerMock.scheduleJestProcess).toHaveBeenCalled();
+ expect(processManagerMock.scheduleJestProcess).toHaveBeenCalledWith(
+ expect.objectContaining({ extraInfo })
+ );
});
});
@@ -123,11 +157,13 @@ describe('ProcessSession', () => {
expect.hasAssertions();
const settings: any = { autoRun };
context.autoRun = AutoRun(settings);
- mockNumberOfProcesses.mockReturnValue(0);
+ processManagerMock.numberOfProcesses.mockReturnValue(0);
const session = createProcessSession(context);
await session.start();
- const requestTypes = mockScheduleJestProcess.mock.calls.map((c) => c[0].type);
+ const requestTypes = processManagerMock.scheduleJestProcess.mock.calls.map(
+ (c) => c[0].type
+ );
expect(requestTypes).toEqual(expectedRequests);
}
);
@@ -135,21 +171,21 @@ describe('ProcessSession', () => {
expect.hasAssertions();
const settings: any = { autoRun: { watch: true } };
context.autoRun = AutoRun(settings);
- mockNumberOfProcesses.mockReturnValue(1);
+ processManagerMock.numberOfProcesses.mockReturnValue(1);
const session = createProcessSession(context);
await session.start();
- expect(mockStopAll).toBeCalledTimes(1);
- expect(mockScheduleJestProcess).toBeCalledTimes(1);
+ expect(processManagerMock.stopAll).toBeCalledTimes(1);
+ expect(processManagerMock.scheduleJestProcess).toBeCalledTimes(1);
});
});
describe('stop', () => {
it('will stop all processes in the queues', async () => {
expect.hasAssertions();
context.pluginSettings = { autoEnable: true };
- mockNumberOfProcesses.mockReturnValue(1);
+ processManagerMock.numberOfProcesses.mockReturnValue(1);
const session = createProcessSession(context);
await session.stop();
- expect(mockStopAll).toBeCalledTimes(1);
+ expect(processManagerMock.stopAll).toBeCalledTimes(1);
});
});
});
diff --git a/tests/JestProcessManagement/JestProcess.test.ts b/tests/JestProcessManagement/JestProcess.test.ts
index 80f7244d0..f6c1ea547 100644
--- a/tests/JestProcessManagement/JestProcess.test.ts
+++ b/tests/JestProcessManagement/JestProcess.test.ts
@@ -54,7 +54,7 @@ describe('JestProcess', () => {
jestProcess = new JestProcess(extContext, request);
expect(`${jestProcess}`).toEqual(jestProcess.toString());
expect(jestProcess.toString()).toMatchInlineSnapshot(
- `"JestProcess: id: 0, request: {\\"type\\":\\"all-tests\\",\\"schedule\\":{\\"queue\\":\\"blocking\\"},\\"listener\\":\\"function\\"}; stopReason: undefined"`
+ `"JestProcess: id: all-tests-0, request: {\\"type\\":\\"all-tests\\",\\"schedule\\":{\\"queue\\":\\"blocking\\"},\\"listener\\":\\"function\\"}; stopReason: undefined"`
);
});
describe('when creating', () => {
@@ -133,13 +133,15 @@ describe('JestProcess', () => {
);
it.each`
- type | extraProperty | startArgs | includeReporter | extraRunnerOptions
- ${'all-tests'} | ${undefined} | ${[false, false]} | ${true} | ${undefined}
- ${'watch-tests'} | ${undefined} | ${[true, false]} | ${true} | ${undefined}
- ${'watch-all-tests'} | ${undefined} | ${[true, true]} | ${true} | ${undefined}
- ${'by-file'} | ${{ testFileNamePattern: '"c:\\a\\b.ts"' }} | ${[false, false]} | ${true} | ${{ args: { args: ['--findRelatedTests', '--watchAll=false'] }, testFileNamePattern: '"C:\\a\\b.ts"' }}
- ${'by-file-test'} | ${{ testFileNamePattern: '"/a/b.js"', testNamePattern: 'test' }} | ${[false, false]} | ${true} | ${{ args: { args: ['--runTestsByPath', '--watchAll=false'] }, testFileNamePattern: '"/a/b.js"' }}
- ${'not-test'} | ${{ args: ['--listTests', '--watchAll=false'] }} | ${[false, false]} | ${false} | ${{ args: { args: ['--listTests', '--watchAll=false'], replace: true } }}
+ type | extraProperty | startArgs | includeReporter | extraRunnerOptions
+ ${'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: ['--findRelatedTests'] }, testFileNamePattern: '"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"' }}
+ ${'not-test'} | ${{ args: ['--listTests', '--watchAll=false'] }} | ${[false, false]} | ${false} | ${{ args: { args: ['--listTests'], replace: true } }}
`(
'supports jest process request: $type',
async ({ type, extraProperty, startArgs, includeReporter, extraRunnerOptions }) => {
@@ -157,20 +159,63 @@ describe('JestProcess', () => {
expect(options.reporters).toBeUndefined();
}
- expect(options).toEqual(expect.objectContaining(extraRunnerOptions ?? extraProperty ?? {}));
+ if (extraRunnerOptions) {
+ const { args, ...restOptions } = extraRunnerOptions;
+ expect(options).toEqual(expect.objectContaining(restOptions));
+ const { args: flags, replace } = args;
+ expect(options.args.replace).toEqual(replace);
+ expect(options.args.args).toEqual(expect.arrayContaining(flags));
+ }
expect(mockRunner.start).toBeCalledWith(...startArgs);
closeRunner();
await p;
}
);
+ describe('common flags', () => {
+ it.each`
+ type | extraProperty | excludeWatch | withColors
+ ${'all-tests'} | ${undefined} | ${true} | ${true}
+ ${'watch-tests'} | ${undefined} | ${false} | ${true}
+ ${'watch-all-tests'} | ${undefined} | ${false} | ${true}
+ ${'by-file'} | ${{ testFileName: '"c:\\a\\b.ts"' }} | ${true} | ${true}
+ ${'by-file-test'} | ${{ testFileName: '"/a/b.js"', testNamePattern: 'a test' }} | ${true} | ${true}
+ ${'by-file-pattern'} | ${{ testFileNamePattern: '"c:\\a\\b.ts"' }} | ${true} | ${true}
+ ${'by-file-test-pattern'} | ${{ testFileNamePattern: '/a/b.js', testNamePattern: 'a test' }} | ${true} | ${true}
+ ${'not-test'} | ${{ args: ['--listTests', '--watchAll=false'] }} | ${true} | ${false}
+ `(
+ 'request $type: excludeWatch:$excludeWatch, withColors:$withColors',
+ async ({ type, extraProperty, excludeWatch, withColors }) => {
+ expect.hasAssertions();
+ const request = mockRequest(type, extraProperty);
+ jestProcess = new JestProcess(extContext, request);
+ const p = jestProcess.start();
+ closeRunner();
+ await p;
+
+ const [, options] = RunnerClassMock.mock.calls[0];
+ if (withColors) {
+ expect(options.args.args).toContain('--colors');
+ } else {
+ expect(options.args.args).not.toContain('--colors');
+ }
+ if (excludeWatch) {
+ expect(options.args.args).toContain('--watchAll=false');
+ } else {
+ expect(options.args.args).not.toContain('--watchAll=false');
+ }
+ }
+ );
+ });
it.each`
- request | expectUpdate
- ${{ type: 'all-tests', updateSnapshot: true }} | ${true}
- ${{ type: 'all-tests', updateSnapshot: false }} | ${false}
- ${{ type: 'by-file', updateSnapshot: true, testFileNamePattern: 'abc' }} | ${true}
- ${{ type: 'by-file-test', updateSnapshot: true, testFileNamePattern: 'abc', testNamePattern: 'xyz' }} | ${true}
- ${{ type: 'watch-tests', updateSnapshot: true }} | ${false}
- ${{ type: 'watch-all-tests', updateSnapshot: true }} | ${false}
+ request | expectUpdate
+ ${{ type: 'all-tests', updateSnapshot: true }} | ${true}
+ ${{ type: 'all-tests', updateSnapshot: false }} | ${false}
+ ${{ type: 'by-file', updateSnapshot: true, testFileName: 'abc' }} | ${true}
+ ${{ type: 'by-file-pattern', updateSnapshot: true, testFileNamePattern: 'abc' }} | ${true}
+ ${{ type: 'by-file-test', updateSnapshot: true, testFileName: 'abc', testNamePattern: 'xyz' }} | ${true}
+ ${{ type: 'by-file-test-pattern', updateSnapshot: true, testFileNamePattern: 'abc', testNamePattern: 'xyz' }} | ${true}
+ ${{ type: 'watch-tests', updateSnapshot: true }} | ${false}
+ ${{ type: 'watch-all-tests', updateSnapshot: true }} | ${false}
`('can update snapshot with request $request', ({ request, expectUpdate }) => {
expect.hasAssertions();
const _request = mockRequest(request.type, request);
@@ -180,7 +225,7 @@ describe('JestProcess', () => {
if (expectUpdate) {
expect(options.args.args).toContain('--updateSnapshot');
} else {
- expect(options.args).toBeUndefined();
+ expect(options.args.args).not.toContain('--updateSnapshot');
}
});
it('starting on a running process does nothing but returns the same promise', async () => {
diff --git a/tests/JestProcessManagement/JestProcessManager.test.ts b/tests/JestProcessManagement/JestProcessManager.test.ts
index 0e3741ff5..85ae5cdf6 100644
--- a/tests/JestProcessManagement/JestProcessManager.test.ts
+++ b/tests/JestProcessManagement/JestProcessManager.test.ts
@@ -26,6 +26,7 @@ const getState = (pm: JestProcessManager, process: JestProcess): ProcessState =>
return state;
};
+let SEQ = 1;
describe('JestProcessManager', () => {
let extContext;
@@ -39,6 +40,7 @@ describe('JestProcessManager', () => {
reject = _reject;
});
const mockProcess = {
+ id: `${request.type}-${SEQ++}`,
request,
start: jest.fn().mockReturnValueOnce(promise),
stop: jest.fn().mockImplementation(() => resolve('requested to stop')),
@@ -90,7 +92,8 @@ describe('JestProcessManager', () => {
it('can run process after scheduling', () => {
expect.hasAssertions();
- pm.scheduleJestProcess(request);
+ const process = pm.scheduleJestProcess(request);
+ expect(process.id).toEqual(expect.stringContaining(request.type));
expect(jestProcessMock).toBeCalledTimes(1);
expect(mockProcess.start).toBeCalledTimes(1);
@@ -161,12 +164,12 @@ describe('JestProcessManager', () => {
// submit first request
let scheduled = pm.scheduleJestProcess(requests[0]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[0].type);
expect(getState(pm, processes[0])).toEqual({ inQ: true, started: true, qSize: 1 });
// schedule 2nd request while first one is still running
scheduled = pm.scheduleJestProcess(requests[1]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[1].type);
expect(getState(pm, processes[0])).toEqual({ inQ: true, started: true, qSize: 2 });
expect(getState(pm, processes[1])).toEqual({ inQ: true, started: false, qSize: 2 });
@@ -177,7 +180,7 @@ describe('JestProcessManager', () => {
// schedule the 3rd request
scheduled = pm.scheduleJestProcess(requests[2]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[2].type);
expect(getState(pm, processes[1])).toEqual({ inQ: true, started: true, qSize: 2 });
// getState(pm, processes[1], { 'in-queue': true, started: true, 'queue-length': 1 })
@@ -188,9 +191,9 @@ describe('JestProcessManager', () => {
describe('can run jest process in parallel', () => {
const schedule: ScheduleStrategy = { queue: 'non-blocking' };
const requests = [
- mockProcessRequest('by-file', { testFileNamePattern: 'file-1', schedule }),
+ mockProcessRequest('by-file', { testFileName: 'file-1', schedule }),
mockProcessRequest('not-test', { args: ['--listFiles'], schedule }),
- mockProcessRequest('by-file', { testFileNamePattern: 'file-2', schedule }),
+ mockProcessRequest('by-file', { testFileName: 'file-2', schedule }),
mockProcessRequest('not-test', { args: ['--listFiles'], schedule }),
];
it('works for scheduling all requests up front', async () => {
@@ -232,20 +235,20 @@ describe('JestProcessManager', () => {
// submit first request
let scheduled = pm.scheduleJestProcess(requests[0]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[0].type);
expect(getState(pm, processes[0])).toEqual({ inQ: true, started: true, qSize: 1 });
// schedule 2nd request while first one is still running
scheduled = pm.scheduleJestProcess(requests[1]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[1].type);
expect(getState(pm, processes[0])).toEqual({ inQ: true, started: true, qSize: 2 });
expect(getState(pm, processes[1])).toEqual({ inQ: true, started: true, qSize: 2 });
// schedule 3rd and 4th requests
scheduled = pm.scheduleJestProcess(requests[2]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[2].type);
scheduled = pm.scheduleJestProcess(requests[3]);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(requests[3].type);
expect(getState(pm, processes[0])).toEqual({ inQ: true, started: true, qSize: 4 });
expect(getState(pm, processes[1])).toEqual({ inQ: true, started: true, qSize: 4 });
expect(getState(pm, processes[2])).toEqual({ inQ: true, started: true, qSize: 4 });
@@ -274,13 +277,13 @@ describe('JestProcessManager', () => {
// schedule the first process
let scheduled = pm.scheduleJestProcess(request);
// the process is running
- expect(scheduled).toEqual(true);
+ expect(scheduled).toEqual(process);
expect(getState(pm, process)).toEqual({ inQ: true, started: true, qSize: 1 });
// schedule the 2nd process which should failed because the process is already running
const process2 = mockJestProcess(request);
scheduled = pm.scheduleJestProcess(request);
- expect(scheduled).toEqual(false);
+ expect(scheduled).toBeUndefined();
expect(getState(pm, process2)).toEqual({ inQ: false, qSize: 1 });
});
it('will not schedule if there is pending process', () => {
@@ -302,13 +305,13 @@ describe('JestProcessManager', () => {
// schedule the 2nd process which should succeed because there is no pending process
const process2 = mockJestProcess(request);
scheduled = pm.scheduleJestProcess(request);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(request.type);
expect(getState(pm, process2)).toEqual({ inQ: true, started: false, qSize: 2 });
// but the 3rd one would fail because the 2nd process is pending and thus can not add any more
const process3 = mockJestProcess(request);
scheduled = pm.scheduleJestProcess(request);
- expect(scheduled).toEqual(false);
+ expect(scheduled).toBeUndefined();
expect(getState(pm, process)).toEqual({ inQ: true, started: true, qSize: 2 });
expect(getState(pm, process2)).toEqual({ inQ: true, started: false, qSize: 2 });
expect(getState(pm, process3)).toEqual({ inQ: false, qSize: 2 });
@@ -320,11 +323,11 @@ describe('JestProcessManager', () => {
dedup: { filterByStatus: ['pending'], filterByContent: true },
};
const request1 = mockProcessRequest('by-file', {
- testFileNamePattern: '/file/1',
+ testFileName: '/file/1',
schedule,
});
const request2 = mockProcessRequest('by-file', {
- testFileNamePattern: '/file/2',
+ testFileName: '/file/2',
schedule,
});
@@ -333,25 +336,25 @@ describe('JestProcessManager', () => {
// schedule the first process: no problem
const process1 = mockJestProcess(request1);
let scheduled = await pm.scheduleJestProcess(request1);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(request1.type);
expect(getState(pm, process1)).toEqual({ inQ: true, started: true, qSize: 1 });
// schedule the 2nd process with request1, fine because process1 is running, not pending
const process2 = mockJestProcess(request1);
scheduled = await pm.scheduleJestProcess(request1);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(request1.type);
expect(getState(pm, process2)).toEqual({ inQ: true, started: false, qSize: 2 });
// schedule the 3rd one with different request2, should be fine, no dup
const process3 = mockJestProcess(request2);
scheduled = await pm.scheduleJestProcess(request2);
- expect(scheduled).toEqual(true);
+ expect(scheduled.id).toContain(request2.type);
expect(getState(pm, process3)).toEqual({ inQ: true, started: false, qSize: 3 });
// schedule the 4th one with request1, should be rejected as there is already one request pending
const process4 = mockJestProcess(request1);
scheduled = await pm.scheduleJestProcess(request1);
- expect(scheduled).toEqual(false);
+ expect(scheduled).toBeUndefined();
expect(getState(pm, process4)).toEqual({ inQ: false, qSize: 3 });
});
});
diff --git a/tests/JestProcessManagement/helper.test.ts b/tests/JestProcessManagement/helper.test.ts
index cd7812201..1e8a819ac 100644
--- a/tests/JestProcessManagement/helper.test.ts
+++ b/tests/JestProcessManagement/helper.test.ts
@@ -4,21 +4,25 @@ jest.unmock('../../src/JestProcessManagement/helper');
describe('isRequestEqual', () => {
it.each`
- r1 | r2 | isEqual
- ${{ type: 'all-tests' }} | ${{ type: 'all-tests' }} | ${true}
- ${{ type: 'all-tests' }} | ${{ type: 'watch-tests' }} | ${false}
- ${{ type: 'watch-tests' }} | ${{ type: 'watch-all-tests' }} | ${false}
- ${{ type: 'by-file', testFileNamePattern: 'abc' }} | ${{ type: 'by-file', testFileNamePattern: 'abc' }} | ${true}
- ${{ type: 'by-file', testFileNamePattern: 'abc' }} | ${{ type: 'by-file', testFileNamePattern: 'abc', extra: 'whatever' }} | ${true}
- ${{ type: 'by-file', testFileNamePattern: 'abc' }} | ${{ type: 'by-file', testFileNamePattern: 'def' }} | ${false}
- ${{ type: 'by-file', testFileNamePattern: undefined }} | ${{ type: 'by-file', testFileNamePattern: null }} | ${false}
- ${{ type: 'by-file', testFileNamePattern: 'abc' }} | ${{ type: 'by-file' }} | ${false}
- ${{ type: 'by-file-test', testFileNamePattern: 'abc', testNamePattern: '123' }} | ${{ type: 'by-file-test', testFileNamePattern: 'abc', testNamePattern: '123' }} | ${true}
- ${{ type: 'by-file-test', testFileNamePattern: 'abc' }} | ${{ type: 'by-file-test', testFileNamePattern: 'abc', testNamePattern: '123' }} | ${false}
- ${{ type: 'by-file-test', testFileNamePattern: 'abc' }} | ${{ type: 'by-file-test', testFileNamePattern: 'abc' }} | ${true}
- ${{ type: 'not-test', args: ['abc', 'xyz'] }} | ${{ type: 'not-test', args: ['abc', 'xyz'] }} | ${true}
- ${{ type: 'not-test', args: ['abc', 'xyz'] }} | ${{ type: 'not-test', args: ['abc'] }} | ${false}
- ${{ type: 'not-test', args: [] }} | ${{ type: 'not-test', args: ['abc'] }} | ${false}
+ r1 | r2 | isEqual
+ ${{ type: 'all-tests' }} | ${{ type: 'all-tests' }} | ${true}
+ ${{ type: 'all-tests' }} | ${{ type: 'watch-tests' }} | ${false}
+ ${{ type: 'watch-tests' }} | ${{ type: 'watch-all-tests' }} | ${false}
+ ${{ type: 'by-file', testFileName: 'abc' }} | ${{ type: 'by-file', testFileName: 'abc' }} | ${true}
+ ${{ type: 'by-file', testFileName: 'abc' }} | ${{ type: 'by-file', testFileName: 'abc', extra: 'whatever' }} | ${true}
+ ${{ type: 'by-file', testFileName: 'abc' }} | ${{ type: 'by-file', testFileName: 'def' }} | ${false}
+ ${{ type: 'by-file', testFileName: undefined }} | ${{ type: 'by-file', testFileName: null }} | ${false}
+ ${{ type: 'by-file', testFileName: 'abc' }} | ${{ type: 'by-file' }} | ${false}
+ ${{ type: 'by-file-pattern', testFileNamePattern: 'abc' }} | ${{ type: 'by-file-pattern', testFileNamePattern: 'abc' }} | ${true}
+ ${{ type: 'by-file-pattern', testFileNamePattern: 'abc' }} | ${{ type: 'by-file-pattern', testFileNamePattern: 'Abc' }} | ${false}
+ ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: '123' }} | ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: '123' }} | ${true}
+ ${{ type: 'by-file-test', testFileName: 'abc' }} | ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: '123' }} | ${false}
+ ${{ type: 'by-file-test', testFileName: 'abc' }} | ${{ type: 'by-file-test', testFileName: 'abc' }} | ${true}
+ ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: '123' }} | ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: '123' }} | ${true}
+ ${{ type: 'by-file-test-pattern', testFileNamePattern: 'abc', testNamePattern: '123' }} | ${{ type: 'by-file-test', testFileName: 'abc', testNamePattern: '123' }} | ${false}
+ ${{ type: 'not-test', args: ['abc', 'xyz'] }} | ${{ type: 'not-test', args: ['abc', 'xyz'] }} | ${true}
+ ${{ type: 'not-test', args: ['abc', 'xyz'] }} | ${{ type: 'not-test', args: ['abc'] }} | ${false}
+ ${{ type: 'not-test', args: [] }} | ${{ type: 'not-test', args: ['abc'] }} | ${false}
`('$r1 ?== $r2 ? $isEqual', ({ r1, r2, isEqual }) => {
expect(isRequestEqual(r1, r2)).toEqual(isEqual);
});
@@ -61,23 +65,23 @@ describe('isDup', () => {
expect(isDup(task, request)).toEqual(expected);
});
it.each`
- type | status | testFileNamePattern | expected
- ${'by-file'} | ${['running']} | ${'abc'} | ${false}
- ${'by-file'} | ${['pending']} | ${'abc'} | ${true}
- ${'by-file'} | ${['pending']} | ${'def'} | ${false}
- ${'by-file'} | ${['pending']} | ${undefined} | ${false}
- ${'by-file'} | ${undefined} | ${'abc'} | ${true}
- ${'watch-all-tests'} | ${['pending']} | ${'abc'} | ${false}
+ type | status | testFileName | expected
+ ${'by-file'} | ${['running']} | ${'abc'} | ${false}
+ ${'by-file'} | ${['pending']} | ${'abc'} | ${true}
+ ${'by-file'} | ${['pending']} | ${'def'} | ${false}
+ ${'by-file'} | ${['pending']} | ${undefined} | ${false}
+ ${'by-file'} | ${undefined} | ${'abc'} | ${true}
+ ${'watch-all-tests'} | ${['pending']} | ${'abc'} | ${false}
`(
'can check by $type, $status, filterByContent=$filterByContent, isDup = $expected',
- ({ type, status, testFileNamePattern, expected }) => {
+ ({ type, status, testFileName, expected }) => {
const task: any = {
- data: { request: { type: 'by-file', testFileNamePattern: 'abc' } },
+ data: { request: { type: 'by-file', testFileName: 'abc' } },
status: 'pending',
};
const request: any = {
type,
- testFileNamePattern,
+ testFileName,
schedule: { dedup: { filterByStatus: status, filterByContent: true } },
};
diff --git a/tests/TestResults/TestResultProvider.test.ts b/tests/TestResults/TestResultProvider.test.ts
index 8c60f4d5f..fe0ff1ec3 100644
--- a/tests/TestResults/TestResultProvider.test.ts
+++ b/tests/TestResults/TestResultProvider.test.ts
@@ -1,4 +1,6 @@
+/* eslint-disable @typescript-eslint/no-non-null-assertion */
jest.unmock('../../src/TestResults/TestResultProvider');
+jest.unmock('../../src/TestResults/test-result-events');
jest.unmock('../../src/TestResults/match-node');
jest.unmock('../../src/TestResults/match-by-context');
jest.unmock('../../src/helpers');
@@ -8,7 +10,6 @@ const mockTestReconciler = jest.fn();
const mockReconciler = {
updateFileWithJestStatus: jest.fn(),
assertionsForTestFile: jest.fn(),
- stateForTestFile: jest.fn(),
removeTestFile: jest.fn(),
};
@@ -33,19 +34,22 @@ jest.mock('path', () => {
return path;
});
+import * as vscode from 'vscode';
import { TestResultProvider } from '../../src/TestResults/TestResultProvider';
import { TestReconciliationState } from '../../src/TestResults';
import * as helper from '../test-helper';
-import { ItBlock } from 'jest-editor-support';
+import { ItBlock, TestAssertionStatus, TestReconcilationState } from 'jest-editor-support';
+import * as match from '../../src/TestResults/match-by-context';
+import { mockJestExtEvents } from '../test-helper';
-const mockmockParse = (itBlocks: ItBlock[]) => {
+const setupMockParse = (itBlocks: ItBlock[]) => {
mockParse.mockReturnValue({
root: helper.makeRoot(itBlocks),
itBlocks,
});
};
-const setupJestEditorSupport = () => {
+const createDataSet = (): [ItBlock[], TestAssertionStatus[]] => {
const testBlocks = [
helper.makeItBlock('test 1', [2, 3, 4, 5]),
helper.makeItBlock('test 2', [12, 13, 14, 15]),
@@ -60,10 +64,60 @@ const setupJestEditorSupport = () => {
helper.makeAssertion('test 4', TestReconciliationState.Unknown, undefined, [32, 0]),
helper.makeAssertion('test 5', TestReconciliationState.KnownSuccess, undefined, [42, 0]),
];
- mockmockParse(testBlocks);
- mockReconciler.assertionsForTestFile.mockReturnValue(assertions);
+ return [testBlocks, assertions];
};
+interface TestData {
+ itBlocks: ItBlock[];
+ assertions: TestAssertionStatus[];
+ file: string;
+ fStatus: TestReconcilationState;
+ message?: string;
+}
+
+const makeData = (
+ itBlocks: ItBlock[],
+ assertions: TestAssertionStatus[],
+ file: string,
+ fStatus: TestReconcilationState = 'Unknown',
+ message?: string
+): TestData => ({
+ itBlocks,
+ assertions,
+ file,
+ fStatus,
+ message,
+});
+
+const eventsMock: any = mockJestExtEvents();
+
+const newProviderWithData = (testData: TestData[]): TestResultProvider => {
+ mockParse.mockImplementation((file) => {
+ const data = testData.find((data) => data.file === file);
+ if (data) {
+ return {
+ root: helper.makeRoot(data.itBlocks),
+ itBlocks: data.itBlocks,
+ };
+ }
+ });
+ mockReconciler.assertionsForTestFile.mockImplementation((file) => {
+ const data = testData.find((data) => data.file === file);
+ return data?.assertions;
+ });
+ mockReconciler.updateFileWithJestStatus.mockReturnValueOnce(
+ testData.map((data) => ({
+ file: data.file,
+ status: data.fStatus,
+ message: data.message,
+ assertions: data.assertions,
+ }))
+ );
+ const sut = new TestResultProvider(eventsMock);
+ // warn up cache
+ sut.updateTestResults({} as any, {} as any);
+ return sut;
+};
describe('TestResultProvider', () => {
const filePath = 'file.js';
const testBlock = helper.makeItBlock('test name', [2, 3, 4, 5]);
@@ -83,30 +137,33 @@ describe('TestResultProvider', () => {
throw new Error('forced error');
});
};
- const forceMatchError = (sut: any) => {
+ const forceMatchError = () => {
+ jest.spyOn(match, 'matchTestAssertions').mockImplementation(() => {
+ throw new Error('forced error');
+ });
+ };
+ const forceMatchResultError = (sut: any) => {
sut.matchResults = jest.fn(() => {
throw new Error('forced error');
});
};
beforeEach(() => {
jest.resetAllMocks();
+ jest.restoreAllMocks();
mockTestReconciler.mockReturnValue(mockReconciler);
+ (vscode.EventEmitter as jest.Mocked) = jest.fn().mockImplementation(helper.mockEvent);
});
describe('getResults()', () => {
it('should return the cached results if possible', () => {
- const sut = new TestResultProvider();
- mockmockParse([]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([]);
+ const sut = newProviderWithData([makeData([], [], filePath)]);
const expected = sut.getResults(filePath);
expect(sut.getResults(filePath)).toBe(expected);
});
it('should re-index the line and column number to zero-based', () => {
- const sut = new TestResultProvider();
- mockmockParse([testBlock]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([assertion]);
+ const sut = newProviderWithData([makeData([testBlock], [assertion], filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
@@ -122,22 +179,18 @@ describe('TestResultProvider', () => {
});
it('if context are the same, test will match even if name does not', () => {
- const sut = new TestResultProvider();
- mockmockParse([testBlock]);
const assertionC = { ...assertion };
assertionC.title = 'xxx';
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([assertionC]);
+ const sut = newProviderWithData([makeData([testBlock], [assertionC], filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
expect(actual[0].status).toBe(TestReconciliationState.KnownFail);
});
it('should look up the test result by test name', () => {
- const sut = new TestResultProvider();
- mockmockParse([testBlock]);
const assertionC = { ...assertion };
assertionC.line = undefined;
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([assertionC]);
+ const sut = newProviderWithData([makeData([testBlock], [assertionC], filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
@@ -157,9 +210,7 @@ describe('TestResultProvider', () => {
});
it('unmatched test should report the reason', () => {
- const sut = new TestResultProvider();
- mockmockParse([testBlock]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([]);
+ const sut = newProviderWithData([makeData([testBlock], [], filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
@@ -167,17 +218,23 @@ describe('TestResultProvider', () => {
expect(actual[0].shortMessage).not.toBeUndefined();
expect(actual[0].terseMessage).toBeUndefined();
});
+ it('fire testSuiteChanged event for newly matched result', () => {
+ const sut = newProviderWithData([makeData([testBlock], [], filePath)]);
+ sut.getResults(filePath);
+ expect(sut.events.testSuiteChanged.fire).toBeCalledWith({
+ type: 'result-matched',
+ file: filePath,
+ });
+ });
describe('duplicate test names', () => {
const testBlock2 = helper.makeItBlock(testBlock.name, [5, 3, 7, 5]);
beforeEach(() => {});
it('can resolve as long as they have the same context structure', () => {
- mockmockParse([testBlock, testBlock2]);
-
- const sut = new TestResultProvider();
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([
+ const assertions = [
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownFail, [], [1, 0]),
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownSuccess, [], [10, 0]),
- ]);
+ ];
+ const sut = newProviderWithData([makeData([testBlock, testBlock2], assertions, filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(2);
@@ -185,9 +242,9 @@ describe('TestResultProvider', () => {
expect(actual[1].status).toBe(TestReconciliationState.KnownSuccess);
});
it('however when context structures are different, duplicate names within the same layer can not be resolved.', () => {
- mockmockParse([testBlock, testBlock2]);
+ setupMockParse([testBlock, testBlock2]);
- const sut = new TestResultProvider();
+ const sut = new TestResultProvider(eventsMock);
// note: these 2 assertions have the same line number, therefore will be merge
// into a group-node, which made the context difference: source: 2 nodes, assertion: 1 node.
// but since the 2 assertions' name matched the testBlock, it will still be considered as 1-to-many match
@@ -204,15 +261,14 @@ describe('TestResultProvider', () => {
});
it('should only mark error line number if it is within the right itBlock', () => {
- const sut = new TestResultProvider();
const testBlock2 = helper.makeItBlock('test2', [5, 3, 7, 5]);
- mockmockParse([testBlock, testBlock2]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([
+ const assertions = [
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownSuccess, [], [1, 1]),
helper.makeAssertion(testBlock2.name, TestReconciliationState.KnownFail, [], [2, 2], {
line: 3,
}),
- ]);
+ ];
+ const sut = newProviderWithData([makeData([testBlock, testBlock2], assertions, filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(2);
@@ -227,7 +283,6 @@ describe('TestResultProvider', () => {
});
it('can handle template literal in the context', () => {
- const sut = new TestResultProvider();
const testBlock2 = helper.makeItBlock('template literal I got ${str}', [6, 0, 7, 20], {
nameType: 'TemplateLiteral',
});
@@ -237,7 +292,6 @@ describe('TestResultProvider', () => {
{ nameType: 'TemplateLiteral' }
);
- mockmockParse([testBlock, testBlock3, testBlock2]);
const assertions = [
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownSuccess, [], [1, 0]),
helper.makeAssertion(
@@ -253,7 +307,9 @@ describe('TestResultProvider', () => {
[3, 0]
),
];
- mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
+ const sut = newProviderWithData([
+ makeData([testBlock, testBlock3, testBlock2], assertions, filePath),
+ ]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(3);
expect(actual.map((a) => a.name)).toEqual([
@@ -279,19 +335,21 @@ describe('TestResultProvider', () => {
});
describe('safe-guard warnings', () => {
- const consoleWarning = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ let consoleWarning;
+ beforeEach(() => {
+ consoleWarning = jest.spyOn(console, 'warn').mockImplementation(() => {});
+ });
it('report warning if match failed', () => {
- const sut = new TestResultProvider();
- mockmockParse([testBlock]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([
+ const assertions = [
helper.makeAssertion(
'another name',
TestReconciliationState.KnownSuccess,
['d-1'],
[20, 25]
),
- ]);
+ ];
+ const sut = newProviderWithData([makeData([testBlock], assertions, filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
expect(actual[0].status).toBe(TestReconciliationState.Unknown);
@@ -299,12 +357,13 @@ describe('TestResultProvider', () => {
expect(consoleWarning).toHaveBeenCalled();
});
it('1-many match (jest.each) detected', () => {
- const sut = new TestResultProvider();
- mockmockParse([{ ...testBlock, lastProperty: 'each' }]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([
+ const assertions = [
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownSuccess, [], [1, 12]),
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownSuccess, [], [1, 12]),
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownSuccess, [], [1, 12]),
+ ];
+ const sut = newProviderWithData([
+ makeData([{ ...testBlock, lastProperty: 'each' }], assertions, filePath),
]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
@@ -313,12 +372,11 @@ describe('TestResultProvider', () => {
expect(consoleWarning).not.toHaveBeenCalled();
});
it('when all goes according to plan, no warning but can still log debug message', () => {
- const sut = new TestResultProvider();
- sut.verbose = true;
- mockmockParse([testBlock]);
- mockReconciler.assertionsForTestFile.mockReturnValueOnce([
+ const assertions = [
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownFail, [], [1, 12]),
- ]);
+ ];
+ const sut = newProviderWithData([makeData([testBlock], assertions, filePath)]);
+ sut.verbose = true;
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
expect(actual[0].status).toBe(TestReconciliationState.KnownFail);
@@ -327,14 +385,10 @@ describe('TestResultProvider', () => {
});
});
describe('parameterized tests', () => {
- let sut: TestResultProvider;
const testBlock2 = helper.makeItBlock('p-test-$status', [8, 0, 20, 20], {
lastProperty: 'each',
});
- beforeEach(() => {
- sut = new TestResultProvider();
- mockmockParse([testBlock, testBlock2]);
- });
+
it('test results shared the same range will be grouped', () => {
const assertions = [
helper.makeAssertion(testBlock.name, TestReconciliationState.KnownFail, [], [1, 12]),
@@ -342,7 +396,7 @@ describe('TestResultProvider', () => {
helper.makeAssertion('p-test-fail-1', TestReconciliationState.KnownFail, [], [8, 20]),
helper.makeAssertion('p-test-fail-2', TestReconciliationState.KnownFail, [], [8, 20]),
];
- mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
+ const sut = newProviderWithData([makeData([testBlock, testBlock2], assertions, filePath)]);
const actual = sut.getResults(filePath);
// should only have 2 test results returned, as the last 3 assertions match to the same test block
@@ -370,7 +424,8 @@ describe('TestResultProvider', () => {
helper.makeAssertion('p-test-fail', TestReconciliationState.KnownFail, [], [8, 20]),
helper.makeAssertion('p-test-unknown', TestReconciliationState.Unknown, [], [8, 20]),
];
- mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
+ const sut = newProviderWithData([makeData([testBlock, testBlock2], assertions, filePath)]);
+
const actual = sut.getResults(filePath);
// should only have 2 test results returned, as the last 4 assertions match to the same test block
@@ -392,7 +447,7 @@ describe('TestResultProvider', () => {
helper.makeAssertion('p-test-skip', TestReconciliationState.KnownSkip, [], [8, 20]),
helper.makeAssertion('p-test-unknown', TestReconciliationState.Unknown, [], [8, 20]),
];
- mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
+ const sut = newProviderWithData([makeData([testBlock, testBlock2], assertions, filePath)]);
const actual = sut.getResults(filePath);
// should only have 2 test results returned, as the last 4 assertions match to the same test block
@@ -419,7 +474,7 @@ describe('TestResultProvider', () => {
[8, 20]
),
];
- mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
+ const sut = newProviderWithData([makeData([testBlock, testBlock2], assertions, filePath)]);
const actual = sut.getResults(filePath);
// should only have 2 test results returned, as the last 4 assertions match to the same test block
@@ -432,15 +487,10 @@ describe('TestResultProvider', () => {
});
});
describe('paramertized describes', () => {
- let sut: TestResultProvider;
const tBlock = helper.makeItBlock('p-test-$count', [8, 0, 20, 20], { lastProperty: 'each' });
const dBlock = helper.makeDescribeBlock('p-describe-scount', [tBlock], {
lastProperty: 'each',
});
- beforeEach(() => {
- sut = new TestResultProvider();
- mockmockParse([dBlock]);
- });
it('test from different parameter block can still be grouped', () => {
const assertions = [
helper.makeAssertion(
@@ -468,7 +518,7 @@ describe('TestResultProvider', () => {
[8, 20]
),
];
- mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
+ const sut = newProviderWithData([makeData([dBlock], assertions, filePath)]);
const actual = sut.getResults(filePath);
expect(actual).toHaveLength(1);
@@ -490,8 +540,8 @@ describe('TestResultProvider', () => {
let sut: TestResultProvider;
const tBlock = helper.makeItBlock('a test', [8, 0, 20, 20]);
beforeEach(() => {
- sut = new TestResultProvider();
- mockmockParse([tBlock]);
+ sut = new TestResultProvider(eventsMock);
+ setupMockParse([tBlock]);
});
it.each([[[]], [undefined]])('for assertions = %s', (assertions) => {
mockReconciler.assertionsForTestFile.mockReturnValueOnce(assertions);
@@ -504,32 +554,40 @@ describe('TestResultProvider', () => {
});
});
describe('error handling', () => {
+ let itBlocks, assertions;
beforeEach(() => {
- setupJestEditorSupport();
+ [itBlocks, assertions] = createDataSet();
});
const setupForNonTest = (sut: any) => {
sut.updateTestFileList(['test-file']);
};
it.each`
- desc | setup | expectedResults | isFail
- ${'parse failed'} | ${forceParseError} | ${'throw error'} | ${true}
- ${'match failed'} | ${forceMatchError} | ${'throw error'} | ${true}
- ${'file is not a test file'} | ${setupForNonTest} | ${undefined} | ${false}
+ desc | setup | expectedResults | statsChange
+ ${'parse failed'} | ${forceParseError} | ${'throw error'} | ${'fail'}
+ ${'matchResult failed'} | ${forceMatchResultError} | ${'throw error'} | ${'fail'}
+ ${'match failed'} | ${forceMatchError} | ${'Unknown'} | ${'unknown'}
+ ${'file is not a test file'} | ${setupForNonTest} | ${undefined} | ${undefined}
`(
- 'when $desc => returns $expectedResults, stats.fail = $isFail',
- ({ setup, expectedResults, isFail }) => {
- const sut = new TestResultProvider();
+ 'when $desc => returns $expectedResults, stats changed: $statsChange',
+ ({ setup, expectedResults, statsChange }) => {
+ const sut = newProviderWithData([makeData(itBlocks, assertions, 'whatever')]);
setup(sut);
const stats = sut.getTestSuiteStats();
if (expectedResults === 'throw error') {
expect(() => sut.getResults('whatever')).toThrow();
+ } else if (expectedResults === 'Unknown') {
+ expect(
+ sut.getResults('whatever').every((r) => r.status === expectedResults)
+ ).toBeTruthy();
} else {
expect(sut.getResults('whatever')).toEqual(expectedResults);
}
- if (isFail) {
- expect(sut.getTestSuiteStats()).toEqual({ ...stats, fail: stats.fail + 1 });
+ if (statsChange === 'fail') {
+ expect(sut.getTestSuiteStats()).toEqual({ ...stats, fail: stats.fail + 1, unknown: 0 });
+ } else if (statsChange === 'unknown') {
+ expect(sut.getTestSuiteStats()).toEqual({ ...stats, unknown: 1 });
} else {
expect(sut.getTestSuiteStats()).toEqual(stats);
}
@@ -540,12 +598,13 @@ describe('TestResultProvider', () => {
describe('getSortedResults()', () => {
const filePath = 'file.js';
+ let sut;
beforeEach(() => {
- setupJestEditorSupport();
+ const [itBlocks, assertions] = createDataSet();
+ sut = newProviderWithData([makeData(itBlocks, assertions, filePath)]);
});
it('should return cached results if possible', () => {
- const sut = new TestResultProvider();
const getResultSpy = jest.spyOn(sut, 'getResults');
const expected = sut.getSortedResults(filePath);
expect(getResultSpy).toBeCalledTimes(1);
@@ -555,7 +614,6 @@ describe('TestResultProvider', () => {
});
it('should sort the test results', () => {
- const sut = new TestResultProvider();
const sorted = sut.getSortedResults(filePath);
expect(sorted.fail.map((t) => t.name)).toEqual(['test 2']);
expect(sorted.success.map((t) => t.name)).toEqual(['test 1', 'test 5']);
@@ -563,13 +621,19 @@ describe('TestResultProvider', () => {
expect(sorted.unknown.map((t) => t.name)).toEqual(['test 4']);
});
it('returns undefined for non-test file', () => {
- const sut = new TestResultProvider();
sut.updateTestFileList(['test-file']);
expect(sut.getSortedResults('source file')).toBeUndefined();
});
+ it('returns undefined if no result for the file yet', () => {
+ const getResultSpy = jest.spyOn(sut, 'getResults');
+ getResultSpy.mockImplementation(() => {
+ return undefined;
+ });
+ sut.updateTestFileList(['test-file']);
+ expect(sut.getSortedResults('test-file')).toBeUndefined();
+ });
it('can throw for internal error for once', () => {
forceParseError();
- const sut = new TestResultProvider();
expect(() => sut.getSortedResults(filePath)).toThrow();
//2nd time will just return empty result
@@ -583,11 +647,12 @@ describe('TestResultProvider', () => {
});
describe('updateTestResults()', () => {
- beforeEach(() => {
- setupJestEditorSupport();
- });
it('should only reset the cache for files in result', () => {
- const sut = new TestResultProvider();
+ const [itBlocks, assertions] = createDataSet();
+ const sut = newProviderWithData([
+ makeData(itBlocks, assertions, 'file 1'),
+ makeData(itBlocks, assertions, 'file 2'),
+ ]);
expect(mockReconciler.assertionsForTestFile).toBeCalledTimes(0);
// warm up the cache
@@ -595,14 +660,12 @@ describe('TestResultProvider', () => {
const results2 = sut.getResults('file 2');
expect(results1).toHaveLength(5);
expect(results2).toHaveLength(5);
- expect(mockReconciler.assertionsForTestFile).toBeCalledTimes(2);
- mockReconciler.assertionsForTestFile.mockClear();
// now let's update "file 1"
mockReconciler.updateFileWithJestStatus.mockReturnValueOnce([
{ file: 'file 1', status: 'KnownSuceess' },
]);
- sut.updateTestResults({} as any);
+ sut.updateTestResults({} as any, {} as any);
// to get result from "file 1" should trigger mockReconciler.assertionsForTestFile
const r1 = sut.getResults('file 1');
@@ -620,10 +683,10 @@ describe('TestResultProvider', () => {
const expected: any = [];
mockReconciler.updateFileWithJestStatus.mockReturnValueOnce(expected);
- const sut = new TestResultProvider();
+ const sut = new TestResultProvider(eventsMock);
const results: any = {};
- expect(sut.updateTestResults(results)).toBe(expected);
+ expect(sut.updateTestResults(results, {} as any)).toBe(expected);
expect(mockReconciler.updateFileWithJestStatus).toBeCalledWith(results);
});
it('should updated the stats', () => {
@@ -633,18 +696,34 @@ describe('TestResultProvider', () => {
];
mockReconciler.updateFileWithJestStatus.mockReturnValueOnce(results);
- const sut = new TestResultProvider();
- sut.updateTestResults({} as any);
+ const sut = new TestResultProvider(eventsMock);
+ sut.updateTestResults({} as any, {} as any);
const stats = sut.getTestSuiteStats();
expect(stats).toEqual({ success: 1, fail: 1, unknown: 0 });
});
+ it('should fire testSuiteChanged event', () => {
+ const results: any = [
+ { file: 'a', status: 'KnownSuccess' },
+ { file: 'b', status: 'KnownFail' },
+ ];
+ mockReconciler.updateFileWithJestStatus.mockReturnValueOnce(results);
+
+ const sut = new TestResultProvider(eventsMock);
+ const process: any = { id: 'a-process' };
+ sut.updateTestResults({} as any, process);
+ expect(sut.events.testSuiteChanged.fire).toBeCalledWith({
+ type: 'assertions-updated',
+ files: ['a', 'b'],
+ process,
+ });
+ });
});
it('removeCachedResults', () => {
- mockmockParse([]);
+ setupMockParse([]);
mockReconciler.assertionsForTestFile.mockReturnValue([]);
- const sut = new TestResultProvider();
+ const sut = new TestResultProvider(eventsMock);
sut.getResults('whatever');
expect(mockParse).toHaveBeenCalledTimes(1);
@@ -659,23 +738,23 @@ describe('TestResultProvider', () => {
expect(mockParse).toHaveBeenCalledTimes(2);
});
describe('testFile list', () => {
- beforeEach(() => {
- jest.resetAllMocks();
- });
it('when available, can optimize to only parse file in the list', () => {
- mockmockParse([]);
- mockReconciler.assertionsForTestFile.mockReturnValue([]);
- const sut = new TestResultProvider();
+ const sut = newProviderWithData([makeData([], [], 'file1')]);
sut.updateTestFileList(['file1']);
sut.getResults('whatever');
expect(mockParse).not.toHaveBeenCalled();
sut.getResults('file1');
expect(mockParse).toHaveBeenCalled();
});
+ it('fire testListUpdated event', () => {
+ const sut = newProviderWithData([makeData([], [], 'file1')]);
+ sut.updateTestFileList(['file1']);
+ expect(sut.events.testListUpdated.fire).toHaveBeenCalledWith(['file1']);
+ });
it('if not available, revert to the legacy behavior: parse any file requested', () => {
- mockmockParse([]);
+ setupMockParse([]);
mockReconciler.assertionsForTestFile.mockReturnValue([]);
- const sut = new TestResultProvider();
+ const sut = new TestResultProvider(eventsMock);
sut.updateTestFileList(['file1']);
sut.getResults('whatever');
expect(mockParse).not.toHaveBeenCalled();
@@ -684,69 +763,92 @@ describe('TestResultProvider', () => {
sut.getResults('whatever');
expect(mockParse).toHaveBeenCalled();
});
- });
- describe('JestExtSessionAware', () => {
- beforeEach(() => {
- jest.resetAllMocks();
+ describe('getTestList', () => {
+ it('returns testFiles if available', () => {
+ const sut = new TestResultProvider(eventsMock);
+ expect(sut.getTestList()).toEqual([]);
+
+ sut.updateTestFileList(['file1']);
+ expect(sut.getTestList()).toEqual(['file1']);
+
+ mockReconciler.updateFileWithJestStatus.mockReturnValueOnce([{ file: 'file2' }]);
+ sut.updateTestResults({} as any, {} as any);
+ expect(sut.getTestList()).toEqual(['file1']);
+
+ sut.updateTestFileList([]);
+ expect(sut.getTestList()).toEqual([]);
+ });
+ it('otherwise returns cached result file list', () => {
+ const sut = new TestResultProvider(eventsMock);
+ expect(sut.getTestList()).toEqual([]);
+
+ mockReconciler.updateFileWithJestStatus.mockReturnValueOnce([{ file: 'file2' }]);
+ sut.updateTestResults({} as any, {} as any);
+ expect(sut.getTestList()).toEqual(['file2']);
+ });
});
- it('when session start, cache and reconciler will be reset', () => {
- mockmockParse([]);
- mockReconciler.assertionsForTestFile.mockReturnValue([]);
- const sut = new TestResultProvider();
- expect(mockTestReconciler).toHaveBeenCalledTimes(1);
+ });
+ describe('events', () => {
+ describe('listen to session events', () => {
+ it('when session start, cache and reconciler will be reset', () => {
+ const sut = newProviderWithData([makeData([], [], 'whatever')]);
+ expect(eventsMock.onTestSessionStarted.event).toBeCalled();
+ expect(mockTestReconciler).toHaveBeenCalledTimes(1);
- const spyResetCache = jest.spyOn(sut, 'resetCache');
- sut.onSessionStart();
+ sut.getResults('whatever');
+ sut.getResults('whatever');
+ expect(mockParse).toHaveBeenCalledTimes(1);
- expect(spyResetCache).toHaveBeenCalled();
- expect(mockTestReconciler).toHaveBeenCalledTimes(2);
+ const sessionStartListener = eventsMock.onTestSessionStarted.event.mock.calls[0][0];
+ sessionStartListener({} as any);
+
+ expect(mockTestReconciler).toHaveBeenCalledTimes(2);
+ });
+ });
+ it('will dispose result events', () => {
+ const sut = new TestResultProvider(eventsMock);
+ sut.dispose();
+ expect(sut.events.testListUpdated.dispose).toBeCalled();
+ expect(sut.events.testSuiteChanged.dispose).toBeCalled();
});
});
describe('invalidateTestResults', () => {
it('remove cached results means getResult() will returns nothing', () => {
- setupJestEditorSupport();
- const sut = new TestResultProvider();
- // fill something in cache
- sut.getResults('file 1');
- sut.getResults('file 2');
- expect(mockReconciler.assertionsForTestFile).toBeCalledTimes(2);
- mockReconciler.assertionsForTestFile.mockClear();
+ const [iteBlocks, assertions] = createDataSet();
+ const sut = newProviderWithData([
+ makeData(iteBlocks, assertions, 'file 1', 'KnownSuccess'),
+ makeData(iteBlocks, assertions, 'file 2', 'KnownFail'),
+ ]);
+
+ expect(sut.getTestSuiteResult('file 1')).not.toBeUndefined();
+ expect(sut.getTestSuiteResult('file 2')).not.toBeUndefined();
//invalidate "file 1"
sut.invalidateTestResults('file 1');
// reconciler's test should be removed
expect(mockReconciler.removeTestFile).toBeCalled();
-
//internal cache for "file 1" should also be removed
- sut.getResults('file 1');
- expect(mockReconciler.assertionsForTestFile).toBeCalledTimes(1);
- mockReconciler.assertionsForTestFile.mockClear();
+ expect(sut.getTestSuiteResult('file 1')).toBeUndefined();
- // "file 2" should still come from cache
- sut.getResults('file 2');
- expect(mockReconciler.assertionsForTestFile).toBeCalledTimes(0);
+ // should not impact "file 2"
+ expect(sut.getTestSuiteResult('file 2')).not.toBeUndefined();
});
});
describe('getTestSuiteStats', () => {
let sut;
const testFiles = ['file 1', 'file 2', 'file 3', 'file 4', 'file 5'];
beforeEach(() => {
- setupJestEditorSupport();
- sut = new TestResultProvider();
- sut.updateTestFileList(testFiles);
- const fileStats = {
- ['file 1']: 'KnownSuccess',
- ['file 2']: 'KnownFail',
- ['file 3']: 'KnownSuccess',
- ['file 4']: 'KnownSkip',
- ['file 5']: 'Unknown',
- };
- mockReconciler.stateForTestFile.mockImplementation((file) => fileStats[file]);
+ const [itBlocks, assertions] = createDataSet();
+ sut = newProviderWithData([
+ makeData(itBlocks, assertions, 'file 1', 'KnownSuccess'),
+ makeData(itBlocks, assertions, 'file 2', 'KnownFail'),
+ makeData(itBlocks, assertions, 'file 3', 'KnownSuccess'),
+ makeData(itBlocks, assertions, 'file 4', 'KnownSkip'),
+ makeData(itBlocks, assertions, 'file 5', 'Unknown'),
+ ]);
});
it('calculate stats based on the cached results', () => {
- // add all test into the cache
- testFiles.forEach((file) => sut.getResults(file));
const stats = sut.getTestSuiteStats();
expect(stats).toEqual({
success: 2,
@@ -755,34 +857,24 @@ describe('TestResultProvider', () => {
});
});
it('if there are tests not in the cache, they will be marked as "unknown"', () => {
- sut.getResults('file 1');
+ // cache will be cleared
+ sut.updateTestFileList(testFiles);
const stats = sut.getTestSuiteStats();
expect(stats).toEqual({
- success: 1,
+ success: 0,
fail: 0,
- unknown: 4,
+ unknown: 5,
});
});
});
describe('updateTestFileList', () => {
it('will reset file cache', () => {
- setupJestEditorSupport();
- const sut = new TestResultProvider();
- sut.getResults('file 1');
- expect(mockReconciler.assertionsForTestFile).toHaveBeenCalledTimes(1);
- mockReconciler.assertionsForTestFile.mockClear();
+ const [itBlocks, assertions] = createDataSet();
+ const sut = newProviderWithData([makeData(itBlocks, assertions, 'file 1')]);
+ expect(sut.getTestSuiteResult('file 1')).not.toBeUndefined();
- // subsequent call will come from cache
- sut.getResults('file 1');
- expect(mockReconciler.assertionsForTestFile).toHaveBeenCalledTimes(0);
- mockReconciler.assertionsForTestFile.mockClear();
-
- // update test file list
sut.updateTestFileList(['file 1', 'file 2']);
-
- // when we get file 1 again, cache is clean, so will ask for reconciler again
- sut.getResults('file 1');
- expect(mockReconciler.assertionsForTestFile).toHaveBeenCalledTimes(1);
+ expect(sut.getTestSuiteResult('file 1')).toBeUndefined();
});
});
describe('isTestFile', () => {
@@ -804,14 +896,14 @@ describe('TestResultProvider', () => {
${['file-2']} | ${['file-1']} | ${'yes'}
${['file-2']} | ${['file-2']} | ${'no'}
`('$testFiles, $testResults => $expected', ({ testFiles, testResults, expected }) => {
- const sut = new TestResultProvider();
+ const sut = new TestResultProvider(eventsMock);
if (testFiles) {
sut.updateTestFileList(testFiles);
}
if (testResults) {
const mockResults = testResults.map((file) => ({ file, status: 'KnownSuceess' }));
mockReconciler.updateFileWithJestStatus.mockReturnValueOnce(mockResults);
- sut.updateTestResults({} as any);
+ sut.updateTestResults({} as any, {} as any);
}
expect(sut.isTestFile(target)).toEqual(expected);
diff --git a/tests/decorations/inline-error.test.ts b/tests/decorations/inline-error.test.ts
deleted file mode 100644
index 42069bd50..000000000
--- a/tests/decorations/inline-error.test.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import * as vscode from 'vscode';
-import inlineError from '../../src/decorations/inline-error';
-
-jest.unmock('../../src/decorations/inline-error');
-
-describe('inlineError', () => {
- it('should create text editor decoration', () => {
- const mock = (vscode.window.createTextEditorDecorationType as unknown) as jest.Mock;
- mock.mockReset();
- const decoration = inlineError('');
-
- expect(mock).toHaveBeenCalledTimes(1);
- expect(mock.mock.calls[0][0].overviewRulerLane).toBe(vscode.OverviewRulerLane.Left);
- expect(decoration).toBe(mock({}));
- });
-
- it('should add text to decoration', () => {
- const mock = (vscode.window.createTextEditorDecorationType as unknown) as jest.Mock;
- mock.mockReset();
- inlineError('test text');
-
- expect(mock.mock.calls[0][0].light.after.contentText).toBe(' // test text');
- expect(mock.mock.calls[0][0].dark.after.contentText).toBe(' // test text');
- });
-});
diff --git a/tests/editor.test.ts b/tests/editor.test.ts
deleted file mode 100644
index 8d590ad9c..000000000
--- a/tests/editor.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-jest.unmock('../src/editor');
-
-const vscodeProperties = {
- window: {
- activeTextEditor: jest.fn(),
- visibleTextEditors: jest.fn(),
- },
-};
-jest.mock('vscode', () => {
- const vscode = {
- window: {},
- };
- Object.defineProperty(vscode.window, 'visibleTextEditors', {
- get: () => vscodeProperties.window.visibleTextEditors(),
- });
- return vscode;
-});
-
-import { hasDocument, isOpenInMultipleEditors } from '../src/editor';
-
-describe('editor', () => {
- describe('hasDocument()', () => {
- it('should return false when the editor is falsy', () => {
- const editor: any = undefined;
- expect(hasDocument(editor)).toBe(false);
- });
-
- it('should return false when the document is falsy', () => {
- const editor: any = {};
- expect(hasDocument(editor)).toBe(false);
- });
-
- it('should return true when the document is defined', () => {
- const editor: any = { document: {} };
- expect(hasDocument(editor)).toBe(true);
- });
- });
-
- describe('isOpenInMultipleEditors()', () => {
- const document: any = { fileName: 'fileName' };
-
- it('should return false when the document is falsy', () => {
- expect(isOpenInMultipleEditors(undefined)).toBe(false);
- });
-
- it('should return false when the document is not matched', () => {
- vscodeProperties.window.visibleTextEditors.mockReturnValueOnce([]);
- expect(isOpenInMultipleEditors(document)).toBe(false);
- });
-
- it('should return false when the document is open once', () => {
- vscodeProperties.window.visibleTextEditors.mockReturnValueOnce([{ document }]);
- expect(isOpenInMultipleEditors(document)).toBe(false);
- });
-
- it('should return true when the document is open more than once', () => {
- vscodeProperties.window.visibleTextEditors.mockReturnValueOnce([{ document }, { document }]);
- expect(isOpenInMultipleEditors(document)).toBe(true);
- });
- });
-});
diff --git a/tests/extensionManager.test.ts b/tests/extensionManager.test.ts
index ac02a5646..b50534efd 100644
--- a/tests/extensionManager.test.ts
+++ b/tests/extensionManager.test.ts
@@ -30,6 +30,7 @@ vscode.workspace.getConfiguration = jest.fn().mockImplementation((section) => {
const makeJestExt = (workspace: vscode.WorkspaceFolder): any => {
return {
deactivate: jest.fn(),
+ activate: jest.fn(),
onDidCloseTextDocument: jest.fn(),
onDidChangeActiveTextEditor: jest.fn(),
onDidChangeTextDocument: jest.fn(),
@@ -124,9 +125,7 @@ describe('ExtensionManager', () => {
});
it('should respect disabledWorkspaceFolders', () => {
- registerInstance('workspaceFolder1');
registerInstance('workspaceFolder2');
-
expect(extensionManager.getByName('workspaceFolder1')).toBeDefined();
expect(extensionManager.getByName('workspaceFolder2')).toBeDefined();
const newSettings: PluginWindowSettings = {
@@ -140,6 +139,24 @@ describe('ExtensionManager', () => {
expect(extensionManager.getByName('workspaceFolder1')).toBeUndefined();
expect(extensionManager.getByName('workspaceFolder2')).toBeDefined();
});
+ it('will register workspace not in disable list', () => {
+ expect(extensionManager.getByName('workspaceFolder1')).not.toBeUndefined();
+
+ const newSettings: PluginWindowSettings = {
+ debugCodeLens: {
+ enabled: true,
+ showWhenTestStateIn: [],
+ },
+ disabledWorkspaceFolders: ['workspaceFolder1'],
+ };
+ extensionManager.applySettings(newSettings);
+ expect(extensionManager.getByName('workspaceFolder1')).toBeUndefined();
+
+ newSettings.disabledWorkspaceFolders = [];
+ extensionManager.applySettings(newSettings);
+
+ expect(extensionManager.getByName('workspaceFolder1')).not.toBeUndefined();
+ });
});
describe('register()', () => {
@@ -568,13 +585,16 @@ describe('ExtensionManager', () => {
extensionManager = createExtensionManager(['ws-1', 'ws-2']);
ext1 = extensionManager.getByName('ws-1');
ext2 = extensionManager.getByName('ws-2');
+ (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue(
+ Promise.resolve('')
+ );
});
it('with active editor => can trigger active extension to render it', () => {
const document: any = { document: { uri: 'ws-2' } };
(vscode.window.activeTextEditor as any) = document;
extensionManager.activate();
- expect(ext1.onDidChangeActiveTextEditor).not.toBeCalled();
- expect(ext2.onDidChangeActiveTextEditor).toBeCalledWith(document);
+ expect(ext1.activate).not.toBeCalled();
+ expect(ext2.activate).toBeCalled();
});
it('without active editor => do nothing', () => {
(vscode.window.activeTextEditor as any) = undefined;
@@ -589,5 +609,30 @@ describe('ExtensionManager', () => {
expect(ext1.onDidChangeActiveTextEditor).not.toBeCalled();
expect(ext2.onDidChangeActiveTextEditor).not.toBeCalled();
});
+ describe('can show test explore information', () => {
+ beforeEach(() => {
+ (vscode.window.activeTextEditor as any) = undefined;
+ });
+ it('can reveal test explore view', async () => {
+ (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue(
+ Promise.resolve('Show Test Explorer')
+ );
+ await extensionManager.activate();
+ expect(vscode.commands.executeCommand).toBeCalledWith('workbench.view.testing.focus');
+ });
+ it('can show README document', async () => {
+ (vscode.window.showInformationMessage as jest.Mocked).mockReturnValue(
+ Promise.resolve('See Details')
+ );
+ (vscode.Uri.parse as jest.Mocked).mockImplementation((uri) => uri);
+ await extensionManager.activate();
+ expect(vscode.commands.executeCommand).toBeCalledWith(
+ 'vscode.open',
+ expect.stringContaining(
+ 'jest-community/vscode-jest/blob/master/README.md#how-to-use-the-test-explorer'
+ )
+ );
+ });
+ });
});
});
diff --git a/tests/test-helper.ts b/tests/test-helper.ts
index 3e4adc237..e48d25cef 100644
--- a/tests/test-helper.ts
+++ b/tests/test-helper.ts
@@ -136,6 +136,17 @@ export const mockProjectWorkspace = (...args: any[]): any => {
export const mockWworkspaceLogging = (): any => ({ create: () => jest.fn() });
+export const mockEvent = () => ({
+ event: jest.fn().mockReturnValue({ dispose: jest.fn() }),
+ fire: jest.fn(),
+ dispose: jest.fn(),
+});
+export const mockJestExtEvents: any = () => ({
+ onRunEvent: mockEvent(),
+ onTestSessionStarted: mockEvent(),
+ onTestSessionStopped: mockEvent(),
+});
+
export const mockJestExtContext = (autoRun?: AutoRunAccessor): any => {
return {
workspace: jest.fn(),
@@ -143,14 +154,14 @@ export const mockJestExtContext = (autoRun?: AutoRunAccessor): any => {
settings: jest.fn(),
loggingFactory: { create: jest.fn(() => jest.fn()) },
autoRun: autoRun ?? jest.fn(),
+ events: mockJestExtEvents(),
};
};
export const mockJestProcessContext = (): any => {
return {
...mockJestExtContext(),
- output: { appendLine: jest.fn(), clear: jest.fn() },
- updateStatusBar: jest.fn(),
+ onRunEvent: mockEvent(),
updateWithData: jest.fn(),
};
};
diff --git a/tests/test-provider/test-helper.ts b/tests/test-provider/test-helper.ts
new file mode 100644
index 000000000..f25b91879
--- /dev/null
+++ b/tests/test-provider/test-helper.ts
@@ -0,0 +1,85 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
+import * as vscode from 'vscode';
+import { mockJestExtEvents } from '../test-helper';
+
+export class TestItemCollectionMock {
+ constructor(public parent?: vscode.TestItem) {}
+ private items: vscode.TestItem[] = [];
+ get size(): number {
+ return this.items.length;
+ }
+ replace = (list: vscode.TestItem[]): void => {
+ this.items = list;
+ };
+ get = (id: string): vscode.TestItem | undefined => this.items.find((i) => i.id === id);
+ add = (item: vscode.TestItem): void => {
+ this.items.push(item);
+ (item as any).parent = this.parent;
+ };
+ delete = (id: string): void => {
+ this.items = this.items.filter((i) => i.id !== id);
+ };
+ forEach = (f: (item: vscode.TestItem) => void): void => {
+ this.items.forEach(f);
+ };
+}
+
+export const mockExtExplorerContext = (wsName = 'ws-1', override: any = {}): any => {
+ return {
+ loggingFactory: { create: jest.fn().mockReturnValue(jest.fn()) },
+ autoRun: {},
+ session: { scheduleProcess: jest.fn() },
+ workspace: { name: wsName, uri: { fsPath: `/${wsName}` } },
+ testResolveProvider: {
+ events: {
+ testListUpdated: { event: jest.fn().mockReturnValue({ dispose: jest.fn() }) },
+ testSuiteChanged: { event: jest.fn().mockReturnValue({ dispose: jest.fn() }) },
+ },
+ getTestList: jest.fn().mockReturnValue([]),
+ isTestFile: jest.fn().mockReturnValue('yes'),
+ getTestSuiteResult: jest.fn().mockReturnValue({}),
+ },
+ debugTests: jest.fn(),
+ sessionEvents: mockJestExtEvents(),
+ ...override,
+ };
+};
+
+export const mockRun = (request?: any, name?: any): any => ({
+ request,
+ name,
+ started: jest.fn(),
+ passed: jest.fn(),
+ skipped: jest.fn(),
+ errored: jest.fn(),
+ failed: jest.fn(),
+ enqueued: jest.fn(),
+ appendOutput: jest.fn(),
+ end: jest.fn(),
+ token: { onCancellationRequested: jest.fn() },
+});
+export const mockController = (): any => {
+ const runMocks = [];
+ return {
+ runMocks,
+ lastRunMock: () => (runMocks.length > 0 ? runMocks[runMocks.length - 1] : undefined),
+ createTestRun: jest.fn().mockImplementation((r, n) => {
+ const run = mockRun(r, n);
+ runMocks.push(run);
+ return run;
+ }),
+ dispose: jest.fn(),
+ createRunProfile: jest.fn(),
+ createTestItem: jest.fn().mockImplementation((id, label, uri) => {
+ const item: any = {
+ id,
+ label,
+ uri,
+ errored: jest.fn(),
+ };
+ item.children = new TestItemCollectionMock(item);
+ return item;
+ }),
+ items: new TestItemCollectionMock(),
+ };
+};
diff --git a/tests/test-provider/test-item-data.test.ts b/tests/test-provider/test-item-data.test.ts
new file mode 100644
index 000000000..6d4986691
--- /dev/null
+++ b/tests/test-provider/test-item-data.test.ts
@@ -0,0 +1,1149 @@
+jest.unmock('../../src/test-provider/test-item-data');
+jest.unmock('../../src/test-provider/test-provider-context');
+jest.unmock('../../src/appGlobals');
+jest.unmock('../../src/TestResults/match-node');
+jest.unmock('../../src/TestResults/match-by-context');
+jest.unmock('../test-helper');
+jest.unmock('./test-helper');
+
+jest.mock('path', () => {
+ let sep = '/';
+ return {
+ relative: (p1, p2) => {
+ const p = p2.split(p1)[1];
+ if (p[0] === sep) {
+ return p.slice(1);
+ }
+ return p;
+ },
+ basename: (p) => p.split(sep).slice(-1),
+ sep,
+ setSep: (newSep: string) => {
+ sep = newSep;
+ },
+ };
+});
+
+import * as vscode from 'vscode';
+import {
+ FolderData,
+ TestData,
+ TestDocumentRoot,
+ WorkspaceRoot,
+} from '../../src/test-provider/test-item-data';
+import * as helper from '../test-helper';
+import { JestTestProviderContext } from '../../src/test-provider/test-provider-context';
+import { buildAssertionContainer } from '../../src/TestResults/match-by-context';
+import * as path from 'path';
+import { mockController, mockExtExplorerContext } from './test-helper';
+
+const mockPathSep = (newSep: string) => {
+ (path as jest.Mocked).setSep(newSep);
+ (path as jest.Mocked).sep = newSep;
+};
+
+const getChildItem = (item: vscode.TestItem, partialId: string): vscode.TestItem | undefined => {
+ let found;
+ item.children.forEach((child) => {
+ if (!found && child.id.includes(partialId)) {
+ found = child;
+ }
+ });
+ return found;
+};
+
+const mockScheduleProcess = (context) => {
+ const process = { id: 'whatever', request: { type: 'all-tests' } };
+ context.ext.session.scheduleProcess.mockImplementation((request) => {
+ process.request = request;
+ return process;
+ });
+ return process;
+};
+describe('test-item-data', () => {
+ let context;
+ let profile;
+ let runMock;
+ let controllerMock;
+ let resolveMock;
+
+ beforeEach(() => {
+ controllerMock = mockController();
+ context = new JestTestProviderContext(mockExtExplorerContext('ws-1'), controllerMock);
+ runMock = context.createTestRun();
+ profile = { kind: vscode.TestRunProfileKind.Run };
+ resolveMock = jest.fn();
+
+ vscode.Uri.joinPath = jest
+ .fn()
+ .mockImplementation((uri, p) => ({ fsPath: `${uri.fsPath}/${p}` }));
+ vscode.Uri.file = jest.fn().mockImplementation((f) => ({ fsPath: f }));
+ });
+
+ describe('discover children', () => {
+ describe('WorkspaceRoot', () => {
+ it('create test document tree for testFiles list', () => {
+ const testFiles = [
+ '/ws-1/src/a.test.ts',
+ '/ws-1/src/b.test.ts',
+ '/ws-1/src/app/app.test.ts',
+ ];
+ context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles);
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+
+ // verify tree structure
+ expect(wsRoot.item.children.size).toEqual(1);
+ wsRoot.item.children.forEach((child) => {
+ expect(child.id).toEqual(expect.stringContaining('src'));
+ expect(context.getData(child) instanceof FolderData).toBeTruthy();
+ expect(child.children.size).toEqual(3);
+
+ // app.test.ts
+ const appItem = getChildItem(child, 'app');
+ const aItem = getChildItem(child, 'a.test.ts');
+ const bItem = getChildItem(child, 'b.test.ts');
+
+ expect(context.getData(appItem) instanceof FolderData).toBeTruthy();
+ expect(appItem.children.size).toEqual(1);
+ const appFileItem = getChildItem(appItem, 'app.test.ts');
+ expect(context.getData(appFileItem) instanceof TestDocumentRoot).toBeTruthy();
+ expect(appFileItem.children.size).toEqual(0);
+
+ [aItem, bItem].forEach((fItem) => {
+ expect(context.getData(fItem) instanceof TestDocumentRoot).toBeTruthy();
+ expect(fItem.children.size).toEqual(0);
+ });
+ });
+
+ //verify state after the discovery
+ expect(wsRoot.item.canResolveChildren).toBe(false);
+ });
+ it('if no testFiles yet, should still turn off canResolveChildren', () => {
+ context.ext.testResolveProvider.getTestList.mockReturnValue([]);
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+ expect(wsRoot.item.children.size).toEqual(0);
+ expect(wsRoot.item.canResolveChildren).toBe(false);
+ });
+ it('will only discover up to the test file level', () => {
+ const a1 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]);
+ const assertionContainer = buildAssertionContainer([a1]);
+ const testFiles = ['/ws-1/a.test.ts'];
+ context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownSuccess',
+ assertionContainer,
+ });
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+ const docItem = wsRoot.item.children.get(testFiles[0]);
+ expect(docItem.children.size).toEqual(0);
+ });
+ it('will remove folder item if no test file exist any more', () => {
+ const a1 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]);
+ const assertionContainer = buildAssertionContainer([a1]);
+ const testFiles = ['/ws-1/tests1/a.test.ts', '/ws-1/tests2/b.test.ts'];
+ context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownSuccess',
+ assertionContainer,
+ });
+ const wsRoot = new WorkspaceRoot(context);
+
+ // first discover all test files and build the tree
+ wsRoot.discoverTest(runMock);
+ expect(wsRoot.item.children.size).toEqual(2);
+ let folderItem = wsRoot.item.children.get('/ws-1/tests1');
+ let docItem = folderItem.children.get(testFiles[0]);
+ expect(docItem).not.toBeUndefined();
+ folderItem = wsRoot.item.children.get('/ws-1/tests2');
+ docItem = folderItem.children.get(testFiles[1]);
+ expect(docItem).not.toBeUndefined();
+
+ // now remove '/ws-1/tests2/b.test.ts' and rediscover
+ testFiles.length = 1;
+ wsRoot.discoverTest(runMock);
+ expect(wsRoot.item.children.size).toEqual(1);
+ folderItem = wsRoot.item.children.get('/ws-1/tests2');
+ expect(folderItem).toBeUndefined();
+ folderItem = wsRoot.item.children.get('/ws-1/tests1');
+ docItem = folderItem.children.get(testFiles[0]);
+ expect(docItem).not.toBeUndefined();
+ });
+
+ describe('external events can trigger test tree changes', () => {
+ beforeEach(() => {
+ (vscode.Range as jest.Mocked).mockImplementation((n1, n2, n3, n4) => ({
+ args: [n1, n2, n3, n4],
+ }));
+ (vscode.TestMessage as jest.Mocked).mockImplementation((message) => ({
+ message,
+ }));
+ });
+ it('register for jest session run events', () => {
+ new WorkspaceRoot(context);
+ expect(context.ext.sessionEvents.onRunEvent.event).toHaveBeenCalled();
+ });
+ it('register for test result events', () => {
+ new WorkspaceRoot(context);
+ expect(context.ext.testResolveProvider.events.testListUpdated.event).toHaveBeenCalled();
+ expect(context.ext.testResolveProvider.events.testSuiteChanged.event).toHaveBeenCalled();
+ });
+ it('unregister events upon dispose', () => {
+ const wsRoot = new WorkspaceRoot(context);
+
+ const listeners = [
+ context.ext.testResolveProvider.events.testListUpdated.event.mock.results[0].value,
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.results[0].value,
+ context.ext.sessionEvents.onRunEvent.event.mock.results[0].value,
+ ];
+ wsRoot.dispose();
+ listeners.forEach((l) => expect(l.dispose).toBeCalled());
+ });
+ describe('when testFile list is changed', () => {
+ it('testListUpdated event will be fired', () => {
+ const wsRoot = new WorkspaceRoot(context);
+ context.ext.testResolveProvider.getTestList.mockReturnValueOnce([]);
+ wsRoot.discoverTest(runMock);
+ expect(wsRoot.item.children.size).toBe(0);
+
+ // invoke testListUpdated event listener
+ context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0]([
+ '/ws-1/a.test.ts',
+ ]);
+ // should have created a new run
+ const runMock2 = controllerMock.lastRunMock();
+ expect(runMock2).not.toBe(runMock);
+
+ expect(wsRoot.item.children.size).toBe(1);
+ const docItem = getChildItem(wsRoot.item, 'a.test.ts');
+ expect(docItem).not.toBeUndefined();
+ expect(runMock2.end).toBeCalled();
+ });
+ });
+ describe('when testSuiteChanged.assertions-updated event filed', () => {
+ it('all item data will be updated accordingly', () => {
+ context.ext.testResolveProvider.getTestList.mockReturnValueOnce([]);
+
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+
+ expect(wsRoot.item.children.size).toBe(0);
+
+ // assertions are available now
+ const a1 = helper.makeAssertion('test-a', 'KnownFail', [], [1, 0], {
+ message: 'test error',
+ });
+ const assertionContainer = buildAssertionContainer([a1]);
+ const testSuiteResult: any = {
+ status: 'KnownFail',
+ message: 'test file failed',
+ assertionContainer,
+ };
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(testSuiteResult);
+
+ // triggers testSuiteChanged event listener
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'assertions-updated',
+ process: { id: 'whatever', request: {} },
+ files: ['/ws-1/a.test.ts'],
+ });
+ const runMock2 = controllerMock.lastRunMock();
+ expect(wsRoot.item.children.size).toBe(1);
+ const docItem = getChildItem(wsRoot.item, 'a.test.ts');
+ expect(docItem).not.toBeUndefined();
+ expect(runMock2.failed).toHaveBeenCalledWith(docItem, {
+ message: testSuiteResult.message,
+ });
+
+ expect(docItem.children.size).toEqual(1);
+ const tItem = getChildItem(docItem, 'test-a');
+ expect(tItem).not.toBeUndefined();
+ expect(runMock2.failed).toHaveBeenCalledWith(tItem, { message: a1.message });
+ expect(tItem.range).toEqual({ args: [1, 0, 1, 0] });
+
+ expect(runMock2.end).toBeCalled();
+ });
+ });
+ describe('when testSuiteChanged.result-matched event fired', () => {
+ it('test data range will be updated accordingly', () => {
+ // assertion should be discovered prior
+ context.ext.testResolveProvider.getTestList.mockReturnValueOnce(['/ws-1/a.test.ts']);
+
+ const a1 = helper.makeAssertion('test-a', 'KnownFail', ['desc-1'], [1, 0]);
+ const assertionContainer = buildAssertionContainer([a1]);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownFail',
+ assertionContainer,
+ });
+
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+ expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalledTimes(1);
+
+ expect(wsRoot.item.children.size).toBe(1);
+ const docItem = getChildItem(wsRoot.item, 'a.test.ts');
+ expect(docItem.children.size).toEqual(0);
+
+ // after jest test run, result suite should be updated and test block should be populated
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'assertions-updated',
+ process: { id: 'whatever', request: {} },
+ files: ['/ws-1/a.test.ts'],
+ });
+ expect(docItem.children.size).toEqual(1);
+ const dItem = getChildItem(docItem, 'desc-1');
+ expect(dItem.range).toEqual({ args: [1, 0, 1, 0] });
+ const tItem = getChildItem(dItem, 'test-a');
+ expect(tItem.range).toEqual({ args: [1, 0, 1, 0] });
+
+ expect(context.ext.testResolveProvider.getTestSuiteResult).toHaveBeenCalled();
+ controllerMock.createTestRun.mockClear();
+ context.ext.testResolveProvider.getTestSuiteResult.mockClear();
+
+ // after match, the assertion nodes would have updated range
+ const descNode = assertionContainer.childContainers[0];
+ descNode.attrs.range = {
+ start: { line: 1, column: 2 },
+ end: { line: 13, column: 4 },
+ };
+ const testNode = descNode.childData[0];
+ testNode.attrs.range = {
+ start: { line: 2, column: 2 },
+ end: { line: 10, column: 4 },
+ };
+
+ // triggers testSuiteChanged event listener
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'result-matched',
+ file: '/ws-1/a.test.ts',
+ });
+
+ // no run should be created as we are not changing any test item tree
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ expect(context.ext.testResolveProvider.getTestSuiteResult).not.toHaveBeenCalled();
+
+ // expect the item's range has picked up the updated nodes
+ expect(dItem.range).toEqual({
+ args: [
+ descNode.attrs.range.start.line,
+ descNode.attrs.range.start.column,
+ descNode.attrs.range.end.line,
+ descNode.attrs.range.end.column,
+ ],
+ });
+ expect(tItem.range).toEqual({
+ args: [
+ testNode.attrs.range.start.line,
+ testNode.attrs.range.start.column,
+ testNode.attrs.range.end.line,
+ testNode.attrs.range.end.column,
+ ],
+ });
+ });
+ });
+ });
+ });
+ describe('TestDocumentRoot', () => {
+ it('will discover all tests within the file', () => {
+ const a1 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]);
+ const assertionContainer = buildAssertionContainer([a1]);
+ const uri: any = { fsPath: '/ws-1/a.test.ts' };
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownSuccess',
+ assertionContainer,
+ });
+ const parentItem: any = controllerMock.createTestItem('ws-1', 'ws-1', uri);
+ const docRoot = new TestDocumentRoot(context, uri, parentItem);
+ docRoot.discoverTest(runMock);
+ expect(docRoot.item.children.size).toEqual(1);
+ const tData = context.getData(getChildItem(docRoot.item, 'test-1'));
+ expect(tData instanceof TestData).toBeTruthy();
+ expect(runMock.passed).toBeCalledWith(tData.item);
+ });
+ it('if no test suite result yet, children list is empty', () => {
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(undefined);
+ const uri: any = { fsPath: '/ws-1/a.test.ts' };
+ const parentItem: any = controllerMock.createTestItem('ws-1', 'ws-1', uri);
+ const docRoot = new TestDocumentRoot(context, uri, parentItem);
+ docRoot.discoverTest(runMock);
+ expect(docRoot.item.children.size).toEqual(0);
+ });
+ });
+ it('FolderData do not support discoverTest', () => {
+ controllerMock.createTestRun.mockClear();
+ const parentItem: any = controllerMock.createTestItem('parent', 'parent', {});
+ const folder = new FolderData(context, 'whatever', parentItem);
+ expect(folder.item.canResolveChildren).toBe(false);
+ expect((folder as any).discoverTest).toBeUndefined();
+ });
+ it('TestData do not support discoverTest', () => {
+ const parentItem: any = controllerMock.createTestItem('parent', 'parent', {});
+ const node: any = { fullName: 'a test', attrs: {}, data: {} };
+
+ const test = new TestData(context, { fsPath: 'whatever' } as any, node, parentItem);
+ expect(test.item.canResolveChildren).toBe(false);
+ expect((test as any).discoverTest).toBeUndefined();
+ });
+ });
+ describe('when TestExplorer triggered runTest', () => {
+ describe('Each item data can schedule a test run within the session', () => {
+ beforeEach(() => {
+ context.ext.session.scheduleProcess.mockReturnValue({ id: 'pid' });
+ });
+ describe('run request', () => {
+ it('WorkspaceRoot runs all tests in the workspace', () => {
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.scheduleTest(runMock, resolveMock, profile);
+ expect(context.ext.session.scheduleProcess).toBeCalledWith(
+ expect.objectContaining({ type: 'all-tests' })
+ );
+ });
+ 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);
+ expect(context.ext.session.scheduleProcess).toBeCalledWith(
+ expect.objectContaining({
+ type: 'by-file-pattern',
+ testFileNamePattern: '/ws-1/folder',
+ })
+ );
+ });
+ it('DocumentRoot runs all tests in the test file', () => {
+ 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
+ );
+ docRoot.scheduleTest(runMock, resolveMock, profile);
+ expect(context.ext.session.scheduleProcess).toBeCalledWith(
+ expect.objectContaining({
+ type: 'by-file',
+ testFileName: '/ws-1/a.test.ts',
+ })
+ );
+ });
+ it('TestData runs the specific test pattern', () => {
+ const uri: any = { fsPath: '/ws-1/a.test.ts' };
+ 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);
+ expect(context.ext.session.scheduleProcess).toBeCalledWith(
+ expect.objectContaining({
+ type: 'by-file-test-pattern',
+ testFileNamePattern: uri.fsPath,
+ testNamePattern: 'a test',
+ })
+ );
+ });
+ });
+ it('reports error if failed to schedule test', () => {
+ 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(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);
+ const request = context.ext.session.scheduleProcess.mock.calls[0][0];
+
+ expect(request.itemRun.run).toEqual(runMock);
+ expect(request.itemRun.item).toEqual(folderData.item);
+ });
+ });
+
+ describe('when test result is ready', () => {
+ describe('WorkspaceRoot will receive testSuiteChanged event to update item status', () => {
+ const file = '/ws-1/a.test.ts';
+ let wsRoot;
+ beforeEach(() => {
+ context.ext.testResolveProvider.getTestList.mockReturnValueOnce([file]);
+ wsRoot = new WorkspaceRoot(context);
+
+ // mocking test results
+ const a1 = helper.makeAssertion('test-a', 'KnownSuccess', [], [1, 0]);
+ const a2 = helper.makeAssertion('test-b', 'KnownFail', [], [10, 0], { line: 13 });
+ const assertionContainer = buildAssertionContainer([a1, a2]);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownFail',
+ assertionContainer,
+ });
+ controllerMock.createTestRun.mockClear();
+ });
+ it('for extension-managed runs, the run will be closed after processing the result', () => {
+ // simulate an external run has been scheduled
+ const process = { id: 'whatever', request: { type: 'all-tests' } };
+ const onRunEvent = context.ext.sessionEvents.onRunEvent.event.mock.calls[0][0];
+ onRunEvent({ type: 'scheduled', process });
+ expect(controllerMock.createTestRun).toHaveBeenCalledTimes(1);
+
+ // triggers testSuiteChanged event listener
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'assertions-updated',
+ process,
+ files: [file],
+ });
+
+ // no new run should be created the previous scheduled run should be used to update state
+ expect(controllerMock.createTestRun).toHaveBeenCalledTimes(1);
+ const runMock = controllerMock.lastRunMock();
+
+ const dItem = getChildItem(wsRoot.item, 'a.test.ts');
+ expect(dItem.children.size).toBe(2);
+ const tItem = getChildItem(dItem, 'test-a');
+ expect(runMock.passed).toBeCalledWith(tItem);
+ expect(runMock.end).toBeCalledTimes(1);
+ });
+ it('for exporer-triggered runs, only the resolve function will be invoked', () => {
+ // simulate an internal run has been scheduled
+ const process = mockScheduleProcess(context);
+
+ const runMock = context.createTestRun();
+ const resolve = jest.fn();
+ controllerMock.createTestRun.mockClear();
+
+ wsRoot.scheduleTest(runMock, resolve, {});
+
+ expect(controllerMock.createTestRun).not.toHaveBeenCalled();
+
+ const onRunEvent = context.ext.sessionEvents.onRunEvent.event.mock.calls[0][0];
+ onRunEvent({ type: 'scheduled', process });
+ expect(controllerMock.createTestRun).toHaveBeenCalledTimes(0);
+
+ // triggers testSuiteChanged event listener
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'assertions-updated',
+ process,
+ files: [file],
+ });
+
+ // no new run should be created the previous scheduled run should be used to update state
+ expect(controllerMock.createTestRun).toHaveBeenCalledTimes(0);
+
+ const dItem = getChildItem(wsRoot.item, 'a.test.ts');
+ expect(dItem.children.size).toBe(2);
+ const tItem = getChildItem(dItem, 'test-a');
+ expect(runMock.passed).toBeCalledWith(tItem);
+ expect(runMock.end).not.toBeCalled();
+ expect(resolve).toBeCalled();
+ });
+ it.each`
+ config | hasLocation
+ ${{ enabled: false }} | ${false}
+ ${{ enabled: true }} | ${false}
+ ${{ enabled: true, showInlineError: false }} | ${false}
+ ${{ enabled: true, showInlineError: true }} | ${true}
+ `(
+ 'testExplore config $config, will show inline error? $hasLocation',
+ ({ config, hasLocation }) => {
+ context.ext.settings = { testExplorer: config };
+ const process = mockScheduleProcess(context);
+
+ controllerMock.createTestRun.mockClear();
+
+ wsRoot.scheduleTest(runMock, resolveMock, {});
+
+ // triggers testSuiteChanged event listener
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'assertions-updated',
+ process,
+ files: [file],
+ });
+
+ // no new run should be created the previous scheduled run should be used to update state
+ expect(controllerMock.createTestRun).toHaveBeenCalledTimes(0);
+
+ const dItem = getChildItem(wsRoot.item, 'a.test.ts');
+ const tItem = getChildItem(dItem, 'test-b');
+ expect(runMock.failed).toBeCalledWith(tItem, expect.anything());
+ if (hasLocation) {
+ expect(vscode.Location).toHaveBeenCalledWith(tItem.uri, expect.anything());
+ expect(vscode.Position).toBeCalledTimes(2);
+ expect(vscode.Position).toBeCalledWith(12, 0);
+ } else {
+ expect(vscode.Location).not.toBeCalled();
+ }
+ }
+ );
+ });
+ });
+ });
+
+ describe('sync test item tree with testFile list', () => {
+ describe('works in windows', () => {
+ beforeEach(() => {
+ mockPathSep('\\');
+ context.ext.workspace = { name: 'ws-1', uri: { fsPath: 'c:\\ws-1' } };
+ vscode.Uri.joinPath = jest
+ .fn()
+ .mockImplementation((uri, p) => ({ fsPath: `${uri.fsPath}\\${p}` }));
+ });
+ afterEach(() => {
+ mockPathSep('/');
+ });
+
+ it('can create folders testItems', () => {
+ const testFiles = [
+ 'c:\\ws-1\\src\\a.test.ts',
+ 'c:\\ws-1\\src\\b.test.ts',
+ 'c:\\ws-1\\src\\app\\app.test.ts',
+ ];
+ context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles);
+ const wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+
+ // verify tree structure
+ expect(wsRoot.item.children.size).toEqual(1);
+ wsRoot.item.children.forEach((child) => {
+ expect(child.id).toEqual(expect.stringContaining('src'));
+ expect(context.getData(child) instanceof FolderData).toBeTruthy();
+ expect(child.children.size).toEqual(3);
+
+ // app.test.ts
+ const appItem = getChildItem(child, 'app');
+ const aItem = getChildItem(child, 'a.test.ts');
+ const bItem = getChildItem(child, 'b.test.ts');
+
+ expect(context.getData(appItem) instanceof FolderData).toBeTruthy();
+ expect(appItem.children.size).toEqual(1);
+ const appFileItem = getChildItem(appItem, 'app.test.ts');
+ expect(context.getData(appFileItem) instanceof TestDocumentRoot).toBeTruthy();
+ expect(appFileItem.children.size).toEqual(0);
+
+ [aItem, bItem].forEach((fItem) => {
+ expect(context.getData(fItem) instanceof TestDocumentRoot).toBeTruthy();
+ expect(fItem.children.size).toEqual(0);
+ });
+ });
+ });
+ });
+ describe('when testFile list changed', () => {
+ let wsRoot;
+ let testFiles;
+ beforeEach(() => {
+ // establish baseline with 3 test files
+ testFiles = ['/ws-1/src/a.test.ts', '/ws-1/src/b.test.ts', '/ws-1/src/app/app.test.ts'];
+ context.ext.testResolveProvider.getTestList.mockReturnValue(testFiles);
+ wsRoot = new WorkspaceRoot(context);
+ wsRoot.discoverTest(runMock);
+ });
+ it('add', () => {
+ // add 2 new files
+ const withNewTestFiles = [...testFiles, '/ws-1/tests/d.test.ts', '/ws-1/src/c.test.ts'];
+
+ // trigger event
+ context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0](
+ withNewTestFiles
+ );
+
+ //should see the new files in the tree
+ expect(wsRoot.item.children.size).toEqual(2);
+ const testsFolder = getChildItem(wsRoot.item, 'tests');
+ const d = getChildItem(testsFolder, 'd.test.ts');
+ expect(d.id).toEqual(expect.stringContaining('d.test.ts'));
+ const srcFolder = getChildItem(wsRoot.item, 'src');
+ expect(srcFolder.children.size).toBe(4);
+ const c = getChildItem(srcFolder, 'c.test.ts');
+ expect(c.id).toEqual(expect.stringContaining('c.test.ts'));
+ });
+ it('delete', () => {
+ // delete app test file
+ const withoutAppFiles = [testFiles[0], testFiles[1]];
+
+ // trigger event
+ context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0](
+ withoutAppFiles
+ );
+
+ //should see the new files in the tree
+ expect(wsRoot.item.children.size).toEqual(1);
+ const srcFolder = getChildItem(wsRoot.item, 'src');
+ expect(srcFolder.children.size).toBe(2);
+ const a = getChildItem(srcFolder, 'a.test.ts');
+ expect(a).not.toBeUndefined();
+ const b = getChildItem(srcFolder, 'b.test.ts');
+ expect(b).not.toBeUndefined();
+ const app = getChildItem(srcFolder, 'app');
+ expect(app).toBeUndefined();
+ });
+ it('rename', () => {
+ // rename src/a.test.ts to c.test.ts
+ const withRenamed = ['/ws-1/c.test.ts', testFiles[1], testFiles[2]];
+
+ // trigger event
+ context.ext.testResolveProvider.events.testListUpdated.event.mock.calls[0][0](withRenamed);
+
+ //should see the new files in the tree
+ expect(wsRoot.item.children.size).toEqual(2);
+ const c = getChildItem(wsRoot.item, 'c.test.ts');
+ expect(c).not.toBeUndefined();
+
+ const srcFolder = getChildItem(wsRoot.item, 'src');
+ expect(srcFolder.children.size).toBe(2);
+ const b = getChildItem(srcFolder, 'b.test.ts');
+ expect(b).not.toBeUndefined();
+ const app = getChildItem(srcFolder, 'app');
+ expect(app).not.toBeUndefined();
+ });
+ });
+ });
+ describe('syncChildNode', () => {
+ let docRoot, a1;
+ beforeEach(() => {
+ // setup baseline with 1 describe block and 1 test
+ const parent: any = controllerMock.createTestItem('ws-1', 'ws-1', { fsPath: '/ws-1' });
+ a1 = helper.makeAssertion('test-1', 'KnownSuccess', ['desc-1'], [1, 0]);
+ const assertionContainer = buildAssertionContainer([a1]);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValueOnce({
+ status: 'KnownSuccess',
+ assertionContainer,
+ });
+ docRoot = new TestDocumentRoot(context, { fsPath: '/ws-1/a.test.ts' } as any, parent);
+ docRoot.discoverTest(runMock);
+ });
+ it('add', () => {
+ // add test-2 under existing desc-1 and a new desc-2/test-3
+ const a2 = helper.makeAssertion('test-2', 'KnownFail', ['desc-1'], [5, 0]);
+ const a3 = helper.makeAssertion('test-3', 'KnownSuccess', ['desc-2'], [10, 0]);
+ const a4 = helper.makeAssertion('test-4', 'KnownTodo', ['desc-2'], [15, 0]);
+ const assertionContainer = buildAssertionContainer([a1, a2, a3, a4]);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownFail',
+ assertionContainer,
+ });
+ docRoot.discoverTest(runMock);
+ expect(docRoot.item.children.size).toEqual(2);
+ expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything());
+
+ const desc1 = getChildItem(docRoot.item, 'desc-1');
+ expect(desc1.children.size).toEqual(2);
+
+ const t1 = getChildItem(desc1, 'desc-1 test-1');
+ expect(t1).not.toBeUndefined();
+ expect(runMock.passed).toBeCalledWith(t1);
+
+ const t2 = getChildItem(desc1, 'desc-1 test-2');
+ expect(t2).not.toBeUndefined();
+ expect(runMock.failed).toBeCalledWith(t2, expect.anything());
+
+ const desc2 = getChildItem(docRoot.item, 'desc-2');
+ const t3 = getChildItem(desc2, 'desc-2 test-3');
+ expect(t3).not.toBeUndefined();
+ expect(runMock.passed).toBeCalledWith(t3);
+
+ const t4 = getChildItem(desc2, 'desc-2 test-4');
+ expect(t4).not.toBeUndefined();
+ expect(runMock.skipped).toBeCalledWith(t4);
+ });
+ it('delete', () => {
+ // delete the only test -1
+ const assertionContainer = buildAssertionContainer([]);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValueOnce({
+ status: 'Unknown',
+ assertionContainer,
+ });
+ docRoot.discoverTest(runMock);
+ expect(docRoot.item.children.size).toEqual(0);
+ });
+ it('rename', () => {
+ const a2 = helper.makeAssertion('test-2', 'KnownFail', [], [1, 0]);
+ const assertionContainer = buildAssertionContainer([a2]);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownFail',
+ assertionContainer,
+ });
+
+ docRoot.discoverTest(runMock);
+ expect(docRoot.item.children.size).toEqual(1);
+ expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything());
+ const t2 = getChildItem(docRoot.item, 'test-2');
+ expect(t2).not.toBeUndefined();
+ expect(runMock.failed).toBeCalledWith(t2, expect.anything());
+ });
+ describe('duplicate test names', () => {
+ const setup = (assertions) => {
+ runMock.passed.mockClear();
+ runMock.failed.mockClear();
+
+ const assertionContainer = buildAssertionContainer(assertions);
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue({
+ status: 'KnownFail',
+ assertionContainer,
+ });
+ };
+ it('can still be inserted to test tree with unique ids', () => {
+ const a2 = helper.makeAssertion('test-1', 'KnownFail', [], [1, 0]);
+ const a3 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]);
+ setup([a2, a3]);
+ docRoot.discoverTest(runMock);
+ expect(docRoot.item.children.size).toEqual(2);
+ expect(runMock.failed).toBeCalledWith(docRoot.item, expect.anything());
+ const items = [];
+ docRoot.item.children.forEach((item) => items.push(item));
+ expect(items[0].id).not.toEqual(items[1].id);
+ items.forEach((item) => expect(item.id).toEqual(expect.stringContaining('test-1')));
+
+ expect(runMock.failed).toBeCalledTimes(2);
+ expect(runMock.passed).toBeCalledTimes(1);
+ });
+ it('can still sync with test results', () => {
+ const a2 = helper.makeAssertion('test-1', 'KnownFail', [], [1, 0]);
+ const a3 = helper.makeAssertion('test-1', 'KnownSuccess', [], [1, 0]);
+ setup([a2, a3]);
+ docRoot.discoverTest(runMock);
+ expect(runMock.failed).toBeCalledTimes(2);
+ expect(runMock.passed).toBeCalledTimes(1);
+
+ //update a2 status
+ a2.status = 'KnownSuccess';
+ setup([a2, a3]);
+ docRoot.discoverTest(runMock);
+ expect(runMock.failed).toBeCalledTimes(1);
+ expect(runMock.passed).toBeCalledTimes(2);
+ });
+ });
+ });
+ 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();
+ });
+ 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', {});
+ 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 });
+ });
+ 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();
+ });
+ });
+ describe('WorkspaceRoot listens to jest run events', () => {
+ it('register and dispose event listeners', () => {
+ const wsRoot = new WorkspaceRoot(context);
+ expect(context.ext.sessionEvents.onRunEvent.event).toBeCalled();
+ wsRoot.dispose();
+ const listener = context.ext.sessionEvents.onRunEvent.event.mock.results[0].value;
+ expect(listener.dispose).toBeCalled();
+ });
+ it('can adapt raw output to terminal output', () => {
+ const coloredText = '[2K[1G[1myarn run v1.22.5[22m\n';
+ const converted = '[2K[1G[1myarn run v1.22.5[22m\r\n';
+ context.appendOutput(coloredText, runMock);
+ expect(runMock.appendOutput).toBeCalledWith(expect.stringContaining(converted));
+ });
+ describe('handle run event to set item status and show output', () => {
+ const file = '/ws-1/tests/a.test.ts';
+ let wsRoot, folder, testFile, testBlock, onRunEvent;
+ beforeEach(() => {
+ context.ext.testResolveProvider.getTestList.mockReturnValueOnce([file]);
+ wsRoot = new WorkspaceRoot(context);
+ onRunEvent = context.ext.sessionEvents.onRunEvent.event.mock.calls[0][0];
+
+ // build out the test item tree
+ const a1 = helper.makeAssertion('test-a', 'KnownFail', [], [1, 0], {
+ message: 'test error',
+ });
+ const assertionContainer = buildAssertionContainer([a1]);
+ const testSuiteResult: any = {
+ status: 'KnownFail',
+ message: 'test file failed',
+ assertionContainer,
+ };
+ context.ext.testResolveProvider.getTestSuiteResult.mockReturnValue(testSuiteResult);
+
+ // triggers testSuiteChanged event listener
+ context.ext.testResolveProvider.events.testSuiteChanged.event.mock.calls[0][0]({
+ type: 'assertions-updated',
+ process: { id: 'whatever', request: {} },
+ files: [file],
+ });
+
+ folder = getChildItem(wsRoot.item, 'tests');
+ testFile = getChildItem(folder, 'a.test.ts');
+ testBlock = getChildItem(testFile, 'test-a');
+ });
+ describe('explorer-triggered runs', () => {
+ const setup = (type: string) => {
+ const getItem = () => {
+ switch (type) {
+ case 'workspace':
+ return wsRoot.item;
+ case 'folder':
+ return folder;
+ case 'testFile':
+ return testFile;
+ case 'testBlock':
+ return testBlock;
+ }
+ };
+ const item = getItem();
+ const data = context.getData(item);
+ data.scheduleTest(runMock, resolveMock, profile);
+ controllerMock.createTestRun.mockClear();
+
+ return item;
+ };
+ let process;
+ beforeEach(() => {
+ process = mockScheduleProcess(context);
+ });
+ describe.each`
+ itemType
+ ${'workspace'}
+ ${'folder'}
+ ${'testFile'}
+ ${'testBlock'}
+ `('will use run passed from explorer throughout for $targetItem item', ({ itemType }) => {
+ it('item will be enqueued after schedule', () => {
+ const item = setup(itemType);
+ expect(process.request.itemRun.run.enqueued).toBeCalledWith(item);
+ });
+ it('item will show started when jest run started', () => {
+ const item = setup(itemType);
+
+ process.request.itemRun.run.enqueued.mockClear();
+
+ // scheduled event has no effect
+ onRunEvent({ type: 'scheduled', process });
+ expect(process.request.itemRun.run.enqueued).not.toBeCalled();
+
+ // starting the process
+ onRunEvent({ type: 'start', process });
+ expect(process.request.itemRun.item).toBe(item);
+ expect(process.request.itemRun.run.started).toBeCalledWith(item);
+
+ //will not create new run
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ });
+ it.each`
+ text | raw | newLine | isError | outputText | outputNewLine | outputColor
+ ${'text'} | ${'raw'} | ${true} | ${false} | ${'raw'} | ${true} | ${undefined}
+ ${'text'} | ${'raw'} | ${false} | ${undefined} | ${'raw'} | ${false} | ${undefined}
+ ${'text'} | ${'raw'} | ${undefined} | ${undefined} | ${'raw'} | ${false} | ${undefined}
+ ${'text'} | ${'raw'} | ${true} | ${true} | ${'raw'} | ${true} | ${'red'}
+ ${'text'} | ${undefined} | ${true} | ${true} | ${'text'} | ${true} | ${'red'}
+ `(
+ 'can output process data: $text, $raw, $newLine, $isError',
+ ({ text, raw, newLine, isError, outputText, outputNewLine, outputColor }) => {
+ setup(itemType);
+ const appendOutput = jest.spyOn(context, 'appendOutput');
+
+ onRunEvent({ type: 'start', process });
+ onRunEvent({ type: 'data', process, text, raw, newLine, isError });
+
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ expect(appendOutput).toBeCalledWith(
+ outputText,
+ process.request.itemRun.run,
+ outputNewLine,
+ outputColor
+ );
+ }
+ );
+ it.each([['end'], ['exit']])(
+ "will only resolve the promise and not close the run for event '%s'",
+ (eventType) => {
+ setup(itemType);
+ onRunEvent({ type: 'start', process });
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ expect(process.request.itemRun.run.started).toBeCalled();
+
+ onRunEvent({ type: eventType, process });
+ expect(process.request.itemRun.run.end).not.toBeCalled();
+ expect(resolveMock).toBeCalled();
+ }
+ );
+ it('can report exit error even if run is ended', () => {
+ setup(itemType);
+
+ onRunEvent({ type: 'start', process });
+ onRunEvent({ type: 'end', process });
+
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ expect(process.request.itemRun.run.end).not.toBeCalled();
+ expect(resolveMock).toBeCalled();
+
+ const error = 'something is wrong';
+ onRunEvent({ type: 'exit', error, process });
+
+ // no new run need to be created
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ expect(process.request.itemRun.run.appendOutput).toBeCalledWith(
+ expect.stringContaining(error)
+ );
+ });
+ });
+ });
+ describe('extension-managed runs', () => {
+ beforeEach(() => {
+ controllerMock.createTestRun.mockClear();
+ });
+ describe.each`
+ request | withFile
+ ${{ type: 'watch-tests' }} | ${false}
+ ${{ type: 'watch-all-tests' }} | ${false}
+ ${{ type: 'all-tests' }} | ${false}
+ ${{ type: 'by-file', testFileName: file }} | ${true}
+ ${{ type: 'by-file-pattern', testFileNamePattern: file }} | ${true}
+ `('will create a new run and use it throughout: $request', ({ request, withFile }) => {
+ it('if run starts before schedule returns: no enqueue', () => {
+ const process = { id: 'whatever', request };
+ const item = withFile ? testFile : wsRoot.item;
+
+ // starting the process
+ onRunEvent({ type: 'start', process });
+ const runMock = controllerMock.lastRunMock();
+ expect(runMock.started).toBeCalledWith(item);
+
+ //followed by scheduled
+ onRunEvent({ type: 'scheduled', process });
+ // run has already started, do nothing,
+ expect(runMock.enqueued).not.toBeCalled();
+
+ //will create 1 new run
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ });
+ it('if run starts after schedule: show enqueue then start', () => {
+ const process = { id: 'whatever', request };
+ const item = withFile ? testFile : wsRoot.item;
+
+ //scheduled
+ onRunEvent({ type: 'scheduled', process });
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ const runMock = controllerMock.lastRunMock();
+ expect(runMock.enqueued).toBeCalledWith(item);
+
+ // followed by starting process
+ onRunEvent({ type: 'start', process });
+ expect(runMock.started).toBeCalledWith(item);
+
+ //will create 1 new run
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ });
+ it.each`
+ text | raw | newLine | isError | outputText | outputNewLine | outputColor
+ ${'text'} | ${'raw'} | ${true} | ${false} | ${'raw'} | ${true} | ${undefined}
+ ${'text'} | ${'raw'} | ${false} | ${undefined} | ${'raw'} | ${false} | ${undefined}
+ ${'text'} | ${'raw'} | ${undefined} | ${undefined} | ${'raw'} | ${false} | ${undefined}
+ ${'text'} | ${'raw'} | ${true} | ${true} | ${'raw'} | ${true} | ${'red'}
+ ${'text'} | ${undefined} | ${true} | ${true} | ${'text'} | ${true} | ${'red'}
+ `(
+ 'can output process data: ($text, $raw, $newLine, $isError)',
+ ({ text, raw, newLine, isError, outputText, outputNewLine, outputColor }) => {
+ const process = { id: 'whatever', request };
+ const appendOutput = jest.spyOn(context, 'appendOutput');
+
+ onRunEvent({ type: 'start', process });
+ onRunEvent({ type: 'data', process, text, raw, newLine, isError });
+
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ const runMock = controllerMock.lastRunMock();
+
+ expect(appendOutput).toBeCalledWith(outputText, runMock, outputNewLine, outputColor);
+ }
+ );
+ it.each([['end'], ['exit']])("close the run on event '%s'", (eventType) => {
+ const process = { id: 'whatever', request: { type: 'all-tests' } };
+ onRunEvent({ type: 'start', process });
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ const runMock = controllerMock.lastRunMock();
+ expect(runMock.started).toBeCalled();
+ expect(runMock.end).not.toBeCalled();
+
+ onRunEvent({ type: eventType, process });
+ expect(runMock.end).toBeCalled();
+ });
+ it('can report exit error even if run is ended', () => {
+ const appendOutput = jest.spyOn(context, 'appendOutput');
+ const process = { id: 'whatever', request: { type: 'all-tests' } };
+ onRunEvent({ type: 'start', process });
+ onRunEvent({ type: 'end', process });
+
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ const runMock = controllerMock.lastRunMock();
+ expect(runMock.end).toBeCalled();
+
+ const error = 'something is wrong';
+ onRunEvent({ type: 'exit', error, process });
+
+ expect(controllerMock.createTestRun).toBeCalledTimes(2);
+ const runMock2 = controllerMock.lastRunMock();
+
+ expect(appendOutput).toBeCalledWith(
+ error,
+ runMock2,
+ expect.anything(),
+ expect.anything()
+ );
+ expect(runMock2.errored).toBeCalled();
+ expect(runMock2.end).toBeCalled();
+ });
+ it('if WorkspaceRoot is disposed before process end, all pending run will be closed', () => {
+ const process = { id: 'whatever', request: { type: 'all-tests' } };
+ onRunEvent({ type: 'start', process });
+
+ expect(controllerMock.createTestRun).toBeCalledTimes(1);
+ const runMock = controllerMock.lastRunMock();
+
+ wsRoot.dispose();
+ expect(runMock.end).toBeCalled();
+ });
+ });
+ describe('request not supported', () => {
+ it.each`
+ request
+ ${{ type: 'not-test' }}
+ ${{ type: 'by-file-test', testFileName: file, testNamePattern: 'whatever' }}
+ ${{ type: 'by-file-test-pattern', testFileNamePattern: file, testNamePattern: 'whatever' }}
+ `('$request', ({ request }) => {
+ const process = { id: 'whatever', request };
+
+ // starting the process
+ onRunEvent({ type: 'start', process });
+ const runMock = controllerMock.lastRunMock();
+ expect(runMock.started).not.toBeCalled();
+
+ //will not create any run
+ expect(controllerMock.createTestRun).not.toBeCalled();
+ });
+ });
+ });
+ it('scheduled and start events will do deep item status update', () => {
+ const process = mockScheduleProcess(context);
+ const testFileData = context.getData(testFile);
+
+ testFileData.scheduleTest(runMock, resolveMock, profile);
+ expect(runMock.enqueued).toBeCalledTimes(2);
+ [testFile, testBlock].forEach((t) => expect(runMock.enqueued).toBeCalledWith(t));
+
+ onRunEvent({ type: 'start', process });
+ expect(runMock.started).toBeCalledTimes(2);
+ [testFile, testBlock].forEach((t) => expect(runMock.started).toBeCalledWith(t));
+ });
+ });
+ });
+});
diff --git a/tests/test-provider/test-provider.test.ts b/tests/test-provider/test-provider.test.ts
new file mode 100644
index 000000000..1470d4a72
--- /dev/null
+++ b/tests/test-provider/test-provider.test.ts
@@ -0,0 +1,584 @@
+jest.unmock('../../src/test-provider/test-provider');
+jest.unmock('../../src/test-provider/test-provider-context');
+jest.unmock('./test-helper');
+jest.unmock('../../src/appGlobals');
+
+import * as vscode from 'vscode';
+import { JestTestProvider } from '../../src/test-provider/test-provider';
+import { WorkspaceRoot } from '../../src/test-provider/test-item-data';
+import { JestTestProviderContext } from '../../src/test-provider/test-provider-context';
+import { extensionId } from '../../src/appGlobals';
+import { mockController, mockExtExplorerContext } from './test-helper';
+
+const throwError = () => {
+ throw new Error('debug error');
+};
+
+describe('JestTestProvider', () => {
+ const makeItemData = (debuggable = true) => {
+ const data: any = {
+ discoverTest: jest.fn(),
+ scheduleTest: jest.fn(),
+ dispose: jest.fn(),
+ canRun: jest.fn().mockReturnValue(true),
+ };
+ if (debuggable) {
+ data.getDebugInfo = jest.fn();
+ }
+ return data;
+ };
+
+ const setupTestItemData = (
+ id: string,
+ debuggable = true,
+ context?: JestTestProviderContext
+ ): any => {
+ const data = makeItemData(debuggable);
+ data.item = context?.createTestItem(id, id, {} as any, data) ?? { id };
+ return data;
+ };
+
+ let controllerMock;
+ let extExplorerContextMock;
+ let workspaceRootMock;
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+
+ extExplorerContextMock = mockExtExplorerContext();
+
+ controllerMock = mockController();
+ (vscode.tests.createTestController as jest.Mocked).mockImplementation((id, label) => {
+ controllerMock.id = id;
+ controllerMock.label = label;
+ return controllerMock;
+ });
+
+ (vscode.workspace.getWorkspaceFolder as jest.Mocked).mockImplementation((uri) => ({
+ name: uri,
+ }));
+
+ (WorkspaceRoot as jest.Mocked).mockImplementation((context) => {
+ workspaceRootMock = setupTestItemData('workspace-root', false, context);
+ workspaceRootMock.context = context;
+ return workspaceRootMock;
+ });
+ });
+
+ describe('upon creation', () => {
+ it('will setup controller and WorkspaceRoot', () => {
+ new JestTestProvider(extExplorerContextMock);
+
+ expect(controllerMock.resolveHandler).not.toBeUndefined();
+ expect(vscode.tests.createTestController).toHaveBeenCalledWith(
+ `${extensionId}:TestProvider:ws-1`,
+ expect.stringContaining('ws-1')
+ );
+ expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(3);
+ [
+ vscode.TestRunProfileKind.Run,
+ vscode.TestRunProfileKind.Debug,
+ vscode.TestRunProfileKind.Coverage,
+ ].forEach((kind) => {
+ expect(controllerMock.createRunProfile).toHaveBeenCalledWith(
+ expect.anything(),
+ kind,
+ expect.anything(),
+ true
+ );
+ });
+
+ 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);
+ }
+
+ expect(controllerMock.createRunProfile).toHaveBeenCalledTimes(kinds.length);
+ kinds.forEach((kind) => {
+ expect(controllerMock.createRunProfile).toHaveBeenCalledWith(
+ expect.anything(),
+ kind,
+ expect.anything(),
+ true
+ );
+ });
+ }
+ );
+ });
+
+ describe('can discover tests', () => {
+ describe('when no test item is requested', () => {
+ it('will resolve the whole workspace via workspaceRoot', () => {
+ new JestTestProvider(extExplorerContextMock);
+ controllerMock.resolveHandler();
+ expect(controllerMock.createTestRun).toBeCalled();
+ expect(workspaceRootMock.discoverTest).toBeCalledTimes(1);
+ expect(workspaceRootMock.discoverTest).toBeCalledWith(controllerMock.lastRunMock());
+ // run will be created with the controller's id
+ expect(controllerMock.lastRunMock().name).toEqual(
+ expect.stringContaining(controllerMock.id)
+ );
+ // run will be closed
+ expect(controllerMock.lastRunMock().end).toBeCalled();
+ });
+ });
+ describe('when specific item is requested', () => {
+ it('will forward the request to the item', () => {
+ new JestTestProvider(extExplorerContextMock);
+ const data = setupTestItemData('whatever', true, workspaceRootMock.context);
+ controllerMock.resolveHandler(data.item);
+ expect(controllerMock.createTestRun).toBeCalled();
+ expect(data.discoverTest).toBeCalledWith(controllerMock.lastRunMock());
+ // run will be created with the controller's id
+ expect(controllerMock.lastRunMock().name).toEqual(
+ expect.stringContaining(controllerMock.id)
+ );
+ // run will be closed
+ expect(controllerMock.lastRunMock().end).toBeCalled();
+ });
+ it('should not crash if item not found in the item-data map', () => {
+ new JestTestProvider(extExplorerContextMock);
+ const data = makeItemData(true);
+ controllerMock.resolveHandler({});
+ expect(controllerMock.createTestRun).toBeCalled();
+ expect(data.discoverTest).not.toBeCalled();
+ expect(workspaceRootMock.discoverTest).not.toBeCalled();
+ // run will be created with the controller's id
+ expect(controllerMock.lastRunMock().name).toEqual(
+ expect.stringContaining(controllerMock.id)
+ );
+ // run will be closed
+ expect(controllerMock.lastRunMock().end).toBeCalled();
+ });
+ });
+ describe('if discover failed', () => {
+ it('error will be reported', () => {
+ new JestTestProvider(extExplorerContextMock);
+ workspaceRootMock.discoverTest.mockImplementation(() => {
+ throw new Error('forced crash');
+ });
+ controllerMock.resolveHandler();
+ expect(workspaceRootMock.item.error).toEqual(expect.stringContaining('discoverTest error'));
+
+ // run will be created with the controller's id
+ expect(controllerMock.lastRunMock().name).toEqual(
+ expect.stringContaining(controllerMock.id)
+ );
+ // run will be closed
+ expect(controllerMock.lastRunMock().end).toBeCalled();
+ });
+ });
+ });
+ describe('upon dispose', () => {
+ it('vscode.TestController will be disposed', () => {
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ testProvider.dispose();
+ expect(controllerMock.dispose).toBeCalled();
+ expect(workspaceRootMock.dispose).toBeCalled();
+ });
+ });
+ describe('supports explorer UI run and debug request', () => {
+ let cancelToken;
+ const setupItemData = (context, items = [1, 2, 3]) => {
+ const itemDataList = items.map((n) => setupTestItemData(`item-${n}`, true, context));
+ itemDataList.forEach((d) => {
+ d.context = { workspace: { name: 'whatever' } };
+ d.getDebugInfo = jest.fn().mockReturnValueOnce({});
+ d.canRun = jest.fn().mockReturnValue(true);
+ });
+ return itemDataList;
+ };
+ beforeEach(() => {
+ cancelToken = { onCancellationRequested: jest.fn() };
+ });
+ describe('debug tests', () => {
+ let debugDone;
+ const finishDebug = async () => {
+ debugDone();
+ // flush system promise job queue
+ await Promise.resolve();
+ };
+ const controlled = () =>
+ new Promise((resolve) => {
+ 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}
+ `(
+ "invoke debug test async: debugInfo = '$debugInfo' when resultContextMock.debugTests = $resultContextMock.debugTests => error? $hasError",
+ async ({ debugInfo, debugTests, hasError }) => {
+ expect.hasAssertions();
+ extExplorerContextMock.debugTests = jest.fn(() => {
+ if (debugTests) {
+ return debugTests();
+ }
+ });
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+
+ const itemDataList = setupItemData(workspaceRootMock.context, [1]);
+ itemDataList.forEach((d) => {
+ d.canRun.mockReturnValue(true);
+ if (debugInfo) {
+ d.getDebugInfo.mockReturnValueOnce(debugInfo);
+ } else {
+ d.getDebugInfo = undefined;
+ }
+ });
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Debug },
+ };
+
+ await expect(testProvider.runTests(request, cancelToken)).resolves.toBe(undefined);
+
+ if (hasError) {
+ expect(controllerMock.lastRunMock().errored).toBeCalledWith(
+ itemDataList[0].item,
+ expect.anything()
+ );
+ expect(vscode.TestMessage).toBeCalledTimes(1);
+ }
+ }
+ );
+ it('debug tests are done in serial', async () => {
+ expect.hasAssertions();
+
+ extExplorerContextMock.debugTests.mockImplementation(controlled);
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context);
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Debug },
+ };
+
+ const p = testProvider.runTests(request, cancelToken);
+
+ // a run is created
+ expect(controllerMock.createTestRun).toBeCalled();
+
+ // verify seerial execution
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(1);
+
+ await finishDebug();
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(2);
+
+ await finishDebug();
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(3);
+
+ await finishDebug();
+ await p;
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(3);
+
+ // the run will be closed
+ expect(controllerMock.lastRunMock().end).toBeCalled();
+ });
+ it('cancellation means skip the rest of tests', async () => {
+ expect.hasAssertions();
+
+ extExplorerContextMock.debugTests.mockImplementation(controlled);
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context);
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Debug },
+ };
+
+ const p = testProvider.runTests(request, cancelToken);
+
+ // a run is created
+ expect(controllerMock.createTestRun).toBeCalled();
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(1);
+
+ const runMock = controllerMock.lastRunMock();
+ await finishDebug();
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(2);
+
+ // cancel the run during 2nd debug, the 3rd one should be skipped
+ cancelToken.isCancellationRequested = true;
+
+ await finishDebug();
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(2);
+ await p;
+
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(2);
+ expect(runMock.skipped).toBeCalledWith(request.include[2]);
+
+ // the run will be closed
+ expect(runMock.end).toBeCalledTimes(1);
+ });
+ it('can handle exception', async () => {
+ expect.hasAssertions();
+
+ extExplorerContextMock.debugTests
+ .mockImplementationOnce(() => Promise.resolve())
+ .mockImplementationOnce(() => Promise.reject())
+ .mockImplementationOnce(throwError);
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context);
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Debug },
+ };
+
+ await testProvider.runTests(request, cancelToken);
+ const runMock = controllerMock.lastRunMock();
+
+ expect(extExplorerContextMock.debugTests).toBeCalledTimes(3);
+ expect(runMock.errored).toBeCalledTimes(2);
+ expect(runMock.errored).toBeCalledWith(request.include[1], expect.anything());
+ expect(runMock.errored).toBeCalledWith(request.include[2], expect.anything());
+ expect(runMock.end).toBeCalled();
+ });
+ });
+ describe('run tests', () => {
+ const resolveSchedule = (_r, resolve) => {
+ resolve();
+ };
+ it.each`
+ scheduleTest | isCancelled | state
+ ${resolveSchedule} | ${false} | ${undefined}
+ ${resolveSchedule} | ${true} | ${'skipped'}
+ ${throwError} | ${false} | ${'errored'}
+ `(
+ 'run test should always resolve: schedule test pid = $pid, isCancelled=$isCancelled => state? $state',
+ async ({ scheduleTest, isCancelled, state }) => {
+ expect.hasAssertions();
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context, [1]);
+ itemDataList.forEach((d) => d.scheduleTest.mockImplementation(scheduleTest));
+ const tData = itemDataList[0];
+
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Run },
+ };
+
+ cancelToken.isCancellationRequested = isCancelled;
+ const p = testProvider.runTests(request, cancelToken);
+
+ const runMock = controllerMock.lastRunMock();
+
+ if (isCancelled) {
+ expect(tData.scheduleTest).not.toBeCalled();
+ } else {
+ expect(tData.scheduleTest).toBeCalled();
+ }
+
+ await expect(p).resolves.toBe(undefined);
+ expect(runMock.end).toBeCalled();
+
+ switch (state) {
+ case 'errored':
+ expect(runMock.errored).toBeCalledWith(tData.item, expect.anything());
+ expect(vscode.TestMessage).toBeCalledTimes(1);
+ break;
+ case 'skipped':
+ expect(runMock.skipped).toBeCalledWith(tData.item);
+ expect(vscode.TestMessage).not.toBeCalled();
+ break;
+ case undefined:
+ break;
+ default:
+ expect('unhandled state type').toBeUndefined();
+ break;
+ }
+ }
+ );
+ it('running tests in parallel', async () => {
+ expect.hasAssertions();
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context);
+ // itemDataList.forEach((d, idx) => d.scheduleTest.mockReturnValueOnce(`pid-${idx}`));
+
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Run },
+ };
+
+ const p = testProvider.runTests(request, cancelToken);
+
+ // a run is created
+ expect(controllerMock.createTestRun).toBeCalled();
+ const runMock = controllerMock.lastRunMock();
+
+ itemDataList.forEach((d) => {
+ expect(d.scheduleTest).toBeCalled();
+ const [run, resolve, profile] = d.scheduleTest.mock.calls[0];
+ expect(run).toBe(runMock);
+ expect(profile).toBe(request.profile);
+ // close the schedule
+ resolve();
+ });
+
+ await p;
+ expect(runMock.end).toBeCalled();
+ });
+ it('cancellation is passed to the itemData to handle', async () => {
+ expect.hasAssertions();
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context);
+ itemDataList.forEach((d, idx) => d.scheduleTest.mockReturnValueOnce(`pid-${idx}`));
+
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Run },
+ };
+ const p = testProvider.runTests(request, cancelToken);
+
+ const runMock = controllerMock.lastRunMock();
+ // cacnel after run
+ cancelToken.isCancellationRequested = true;
+
+ // a run is already created
+ expect(controllerMock.createTestRun).toBeCalled();
+
+ itemDataList.forEach((d) => {
+ expect(d.scheduleTest).toBeCalled();
+ const [run, resolve, profile] = d.scheduleTest.mock.calls[0];
+ expect(run).toBe(runMock);
+ expect(profile).toBe(request.profile);
+ // close the schedule
+ resolve();
+ });
+
+ await p;
+ expect(runMock.end).toBeCalledTimes(1);
+ });
+ it('can handle exception', async () => {
+ expect.hasAssertions();
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const itemDataList = setupItemData(workspaceRootMock.context);
+ itemDataList.forEach((d, idx) => {
+ if (idx === 1) {
+ d.scheduleTest.mockImplementation(() => {
+ throw new Error('error scheduling test');
+ });
+ } else {
+ d.scheduleTest.mockReturnValueOnce(`pid-${idx}`);
+ }
+ });
+ const request: any = {
+ include: itemDataList.map((d) => d.item),
+ profile: { kind: vscode.TestRunProfileKind.Run },
+ };
+ const p = testProvider.runTests(request, cancelToken);
+
+ // cacnel after run
+ cancelToken.isCancellationRequested = true;
+
+ // a run is already created
+ expect(controllerMock.createTestRun).toBeCalled();
+ const runMock = controllerMock.lastRunMock();
+
+ itemDataList.forEach((d, idx) => {
+ expect(d.scheduleTest).toBeCalled();
+ const [run, resolve, profile] = d.scheduleTest.mock.calls[0];
+ expect(run).toBe(runMock);
+ expect(profile).toBe(request.profile);
+
+ /* eslint-disable jest/no-conditional-expect */
+ if (idx === 1) {
+ expect(run.errored).toBeCalledWith(d.item, expect.anything());
+ } else {
+ expect(run.errored).not.toBeCalledWith(d.item, expect.anything());
+ // close the schedule
+ resolve();
+ }
+ });
+
+ await p;
+ expect(runMock.end).toBeCalled();
+ });
+ it('if no item in request, will run test for the whole workplace', async () => {
+ expect.hasAssertions();
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+
+ const request: any = {
+ profile: { kind: vscode.TestRunProfileKind.Run },
+ };
+
+ const p = testProvider.runTests(request, cancelToken);
+ const runMock = controllerMock.lastRunMock();
+ expect(workspaceRootMock.scheduleTest).toBeCalledTimes(1);
+ const [run, resolve, profile] = workspaceRootMock.scheduleTest.mock.calls[0];
+ expect(run).toBe(runMock);
+ expect(profile).toBe(request.profile);
+ resolve();
+
+ await p;
+ expect(runMock.end).toBeCalled();
+ });
+ it('will reject run request without profile', async () => {
+ expect.hasAssertions();
+
+ const testProvider = new JestTestProvider(extExplorerContextMock);
+ const request: any = {};
+
+ await expect(testProvider.runTests(request, cancelToken)).rejects.not.toThrow();
+ expect(workspaceRootMock.scheduleTest).toBeCalledTimes(0);
+ 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/tests/tsconfig.json b/tests/tsconfig.json
index 41cbb223a..6f2706aea 100644
--- a/tests/tsconfig.json
+++ b/tests/tsconfig.json
@@ -1,8 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
- "strict": false,
- "alwaysStrict": true,
+ "strict": true,
+ "noImplicitAny": false,
+ "strictNullChecks": false,
},
- "include": ["**/*.ts", "../custom.d.ts"]
+ "exclude": ["../__mocks__"]
}
diff --git a/tsconfig.json b/tsconfig.json
index f0eba0d7e..b1d176ca8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,7 @@
"noUnusedParameters": true,
"target": "es6",
"outDir": "out",
- "lib": ["es6", "es7", "es2019"],
+ "lib": ["es6", "es7", "es2020"],
"sourceMap": true,
"rootDir": ".",
"plugins": [
diff --git a/custom.d.ts b/typings/custom.d.ts
similarity index 100%
rename from custom.d.ts
rename to typings/custom.d.ts
diff --git a/typings/vscode.d.ts b/typings/vscode.d.ts
new file mode 100644
index 000000000..3b1a5cbf7
--- /dev/null
+++ b/typings/vscode.d.ts
@@ -0,0 +1,14318 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+declare module 'vscode' {
+
+ /**
+ * The version of the editor.
+ */
+ export const version: string;
+
+ /**
+ * Represents a reference to a command. Provides a title which
+ * will be used to represent a command in the UI and, optionally,
+ * an array of arguments which will be passed to the command handler
+ * function when invoked.
+ */
+ export interface Command {
+ /**
+ * Title of the command, like `save`.
+ */
+ title: string;
+
+ /**
+ * The identifier of the actual command handler.
+ * @see {@link commands.registerCommand}
+ */
+ command: string;
+
+ /**
+ * A tooltip for the command, when represented in the UI.
+ */
+ tooltip?: string;
+
+ /**
+ * Arguments that the command handler should be
+ * invoked with.
+ */
+ arguments?: any[];
+ }
+
+ /**
+ * Represents a line of text, such as a line of source code.
+ *
+ * TextLine objects are __immutable__. When a {@link TextDocument document} changes,
+ * previously retrieved lines will not represent the latest state.
+ */
+ export interface TextLine {
+
+ /**
+ * The zero-based line number.
+ */
+ readonly lineNumber: number;
+
+ /**
+ * The text of this line without the line separator characters.
+ */
+ readonly text: string;
+
+ /**
+ * The range this line covers without the line separator characters.
+ */
+ readonly range: Range;
+
+ /**
+ * The range this line covers with the line separator characters.
+ */
+ readonly rangeIncludingLineBreak: Range;
+
+ /**
+ * The offset of the first character which is not a whitespace character as defined
+ * by `/\s/`. **Note** that if a line is all whitespace the length of the line is returned.
+ */
+ readonly firstNonWhitespaceCharacterIndex: number;
+
+ /**
+ * Whether this line is whitespace only, shorthand
+ * for {@link TextLine.firstNonWhitespaceCharacterIndex} === {@link TextLine.text TextLine.text.length}.
+ */
+ readonly isEmptyOrWhitespace: boolean;
+ }
+
+ /**
+ * Represents a text document, such as a source file. Text documents have
+ * {@link TextLine lines} and knowledge about an underlying resource like a file.
+ */
+ export interface TextDocument {
+
+ /**
+ * The associated uri for this document.
+ *
+ * *Note* that most documents use the `file`-scheme, which means they are files on disk. However, **not** all documents are
+ * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk.
+ *
+ * @see {@link FileSystemProvider}
+ * @see {@link TextDocumentContentProvider}
+ */
+ readonly uri: Uri;
+
+ /**
+ * The file system path of the associated resource. Shorthand
+ * notation for {@link TextDocument.uri TextDocument.uri.fsPath}. Independent of the uri scheme.
+ */
+ readonly fileName: string;
+
+ /**
+ * Is this document representing an untitled file which has never been saved yet. *Note* that
+ * this does not mean the document will be saved to disk, use {@linkcode Uri.scheme}
+ * to figure out where a document will be {@link FileSystemProvider saved}, e.g. `file`, `ftp` etc.
+ */
+ readonly isUntitled: boolean;
+
+ /**
+ * The identifier of the language associated with this document.
+ */
+ readonly languageId: string;
+
+ /**
+ * The version number of this document (it will strictly increase after each
+ * change, including undo/redo).
+ */
+ readonly version: number;
+
+ /**
+ * `true` if there are unpersisted changes.
+ */
+ readonly isDirty: boolean;
+
+ /**
+ * `true` if the document has been closed. A closed document isn't synchronized anymore
+ * and won't be re-used when the same resource is opened again.
+ */
+ readonly isClosed: boolean;
+
+ /**
+ * Save the underlying file.
+ *
+ * @return A promise that will resolve to true when the file
+ * has been saved. If the file was not dirty or the save failed,
+ * will return false.
+ */
+ save(): Thenable;
+
+ /**
+ * The {@link EndOfLine end of line} sequence that is predominately
+ * used in this document.
+ */
+ readonly eol: EndOfLine;
+
+ /**
+ * The number of lines in this document.
+ */
+ readonly lineCount: number;
+
+ /**
+ * Returns a text line denoted by the line number. Note
+ * that the returned object is *not* live and changes to the
+ * document are not reflected.
+ *
+ * @param line A line number in [0, lineCount).
+ * @return A {@link TextLine line}.
+ */
+ lineAt(line: number): TextLine;
+
+ /**
+ * Returns a text line denoted by the position. Note
+ * that the returned object is *not* live and changes to the
+ * document are not reflected.
+ *
+ * The position will be {@link TextDocument.validatePosition adjusted}.
+ *
+ * @see {@link TextDocument.lineAt}
+ *
+ * @param position A position.
+ * @return A {@link TextLine line}.
+ */
+ lineAt(position: Position): TextLine;
+
+ /**
+ * Converts the position to a zero-based offset.
+ *
+ * The position will be {@link TextDocument.validatePosition adjusted}.
+ *
+ * @param position A position.
+ * @return A valid zero-based offset.
+ */
+ offsetAt(position: Position): number;
+
+ /**
+ * Converts a zero-based offset to a position.
+ *
+ * @param offset A zero-based offset.
+ * @return A valid {@link Position}.
+ */
+ positionAt(offset: number): Position;
+
+ /**
+ * Get the text of this document. A substring can be retrieved by providing
+ * a range. The range will be {@link TextDocument.validateRange adjusted}.
+ *
+ * @param range Include only the text included by the range.
+ * @return The text inside the provided range or the entire text.
+ */
+ getText(range?: Range): string;
+
+ /**
+ * Get a word-range at the given position. By default words are defined by
+ * common separators, like space, -, _, etc. In addition, per language custom
+ * [word definitions} can be defined. It
+ * is also possible to provide a custom regular expression.
+ *
+ * * *Note 1:* A custom regular expression must not match the empty string and
+ * if it does, it will be ignored.
+ * * *Note 2:* A custom regular expression will fail to match multiline strings
+ * and in the name of speed regular expressions should not match words with
+ * spaces. Use {@linkcode TextLine.text} for more complex, non-wordy, scenarios.
+ *
+ * The position will be {@link TextDocument.validatePosition adjusted}.
+ *
+ * @param position A position.
+ * @param regex Optional regular expression that describes what a word is.
+ * @return A range spanning a word, or `undefined`.
+ */
+ getWordRangeAtPosition(position: Position, regex?: RegExp): Range | undefined;
+
+ /**
+ * Ensure a range is completely contained in this document.
+ *
+ * @param range A range.
+ * @return The given range or a new, adjusted range.
+ */
+ validateRange(range: Range): Range;
+
+ /**
+ * Ensure a position is contained in the range of this document.
+ *
+ * @param position A position.
+ * @return The given position or a new, adjusted position.
+ */
+ validatePosition(position: Position): Position;
+ }
+
+ /**
+ * Represents a line and character position, such as
+ * the position of the cursor.
+ *
+ * Position objects are __immutable__. Use the {@link Position.with with} or
+ * {@link Position.translate translate} methods to derive new positions
+ * from an existing position.
+ */
+ export class Position {
+
+ /**
+ * The zero-based line value.
+ */
+ readonly line: number;
+
+ /**
+ * The zero-based character value.
+ */
+ readonly character: number;
+
+ /**
+ * @param line A zero-based line value.
+ * @param character A zero-based character value.
+ */
+ constructor(line: number, character: number);
+
+ /**
+ * Check if this position is before `other`.
+ *
+ * @param other A position.
+ * @return `true` if position is on a smaller line
+ * or on the same line on a smaller character.
+ */
+ isBefore(other: Position): boolean;
+
+ /**
+ * Check if this position is before or equal to `other`.
+ *
+ * @param other A position.
+ * @return `true` if position is on a smaller line
+ * or on the same line on a smaller or equal character.
+ */
+ isBeforeOrEqual(other: Position): boolean;
+
+ /**
+ * Check if this position is after `other`.
+ *
+ * @param other A position.
+ * @return `true` if position is on a greater line
+ * or on the same line on a greater character.
+ */
+ isAfter(other: Position): boolean;
+
+ /**
+ * Check if this position is after or equal to `other`.
+ *
+ * @param other A position.
+ * @return `true` if position is on a greater line
+ * or on the same line on a greater or equal character.
+ */
+ isAfterOrEqual(other: Position): boolean;
+
+ /**
+ * Check if this position is equal to `other`.
+ *
+ * @param other A position.
+ * @return `true` if the line and character of the given position are equal to
+ * the line and character of this position.
+ */
+ isEqual(other: Position): boolean;
+
+ /**
+ * Compare this to `other`.
+ *
+ * @param other A position.
+ * @return A number smaller than zero if this position is before the given position,
+ * a number greater than zero if this position is after the given position, or zero when
+ * this and the given position are equal.
+ */
+ compareTo(other: Position): number;
+
+ /**
+ * Create a new position relative to this position.
+ *
+ * @param lineDelta Delta value for the line value, default is `0`.
+ * @param characterDelta Delta value for the character value, default is `0`.
+ * @return A position which line and character is the sum of the current line and
+ * character and the corresponding deltas.
+ */
+ translate(lineDelta?: number, characterDelta?: number): Position;
+
+ /**
+ * Derived a new position relative to this position.
+ *
+ * @param change An object that describes a delta to this position.
+ * @return A position that reflects the given delta. Will return `this` position if the change
+ * is not changing anything.
+ */
+ translate(change: { lineDelta?: number; characterDelta?: number; }): Position;
+
+ /**
+ * Create a new position derived from this position.
+ *
+ * @param line Value that should be used as line value, default is the {@link Position.line existing value}
+ * @param character Value that should be used as character value, default is the {@link Position.character existing value}
+ * @return A position where line and character are replaced by the given values.
+ */
+ with(line?: number, character?: number): Position;
+
+ /**
+ * Derived a new position from this position.
+ *
+ * @param change An object that describes a change to this position.
+ * @return A position that reflects the given change. Will return `this` position if the change
+ * is not changing anything.
+ */
+ with(change: { line?: number; character?: number; }): Position;
+ }
+
+ /**
+ * A range represents an ordered pair of two positions.
+ * It is guaranteed that {@link Range.start start}.isBeforeOrEqual({@link Range.end end})
+ *
+ * Range objects are __immutable__. Use the {@link Range.with with},
+ * {@link Range.intersection intersection}, or {@link Range.union union} methods
+ * to derive new ranges from an existing range.
+ */
+ export class Range {
+
+ /**
+ * The start position. It is before or equal to {@link Range.end end}.
+ */
+ readonly start: Position;
+
+ /**
+ * The end position. It is after or equal to {@link Range.start start}.
+ */
+ readonly end: Position;
+
+ /**
+ * Create a new range from two positions. If `start` is not
+ * before or equal to `end`, the values will be swapped.
+ *
+ * @param start A position.
+ * @param end A position.
+ */
+ constructor(start: Position, end: Position);
+
+ /**
+ * Create a new range from number coordinates. It is a shorter equivalent of
+ * using `new Range(new Position(startLine, startCharacter), new Position(endLine, endCharacter))`
+ *
+ * @param startLine A zero-based line value.
+ * @param startCharacter A zero-based character value.
+ * @param endLine A zero-based line value.
+ * @param endCharacter A zero-based character value.
+ */
+ constructor(startLine: number, startCharacter: number, endLine: number, endCharacter: number);
+
+ /**
+ * `true` if `start` and `end` are equal.
+ */
+ isEmpty: boolean;
+
+ /**
+ * `true` if `start.line` and `end.line` are equal.
+ */
+ isSingleLine: boolean;
+
+ /**
+ * Check if a position or a range is contained in this range.
+ *
+ * @param positionOrRange A position or a range.
+ * @return `true` if the position or range is inside or equal
+ * to this range.
+ */
+ contains(positionOrRange: Position | Range): boolean;
+
+ /**
+ * Check if `other` equals this range.
+ *
+ * @param other A range.
+ * @return `true` when start and end are {@link Position.isEqual equal} to
+ * start and end of this range.
+ */
+ isEqual(other: Range): boolean;
+
+ /**
+ * Intersect `range` with this range and returns a new range or `undefined`
+ * if the ranges have no overlap.
+ *
+ * @param range A range.
+ * @return A range of the greater start and smaller end positions. Will
+ * return undefined when there is no overlap.
+ */
+ intersection(range: Range): Range | undefined;
+
+ /**
+ * Compute the union of `other` with this range.
+ *
+ * @param other A range.
+ * @return A range of smaller start position and the greater end position.
+ */
+ union(other: Range): Range;
+
+ /**
+ * Derived a new range from this range.
+ *
+ * @param start A position that should be used as start. The default value is the {@link Range.start current start}.
+ * @param end A position that should be used as end. The default value is the {@link Range.end current end}.
+ * @return A range derived from this range with the given start and end position.
+ * If start and end are not different `this` range will be returned.
+ */
+ with(start?: Position, end?: Position): Range;
+
+ /**
+ * Derived a new range from this range.
+ *
+ * @param change An object that describes a change to this range.
+ * @return A range that reflects the given change. Will return `this` range if the change
+ * is not changing anything.
+ */
+ with(change: { start?: Position, end?: Position }): Range;
+ }
+
+ /**
+ * Represents a text selection in an editor.
+ */
+ export class Selection extends Range {
+
+ /**
+ * The position at which the selection starts.
+ * This position might be before or after {@link Selection.active active}.
+ */
+ anchor: Position;
+
+ /**
+ * The position of the cursor.
+ * This position might be before or after {@link Selection.anchor anchor}.
+ */
+ active: Position;
+
+ /**
+ * Create a selection from two positions.
+ *
+ * @param anchor A position.
+ * @param active A position.
+ */
+ constructor(anchor: Position, active: Position);
+
+ /**
+ * Create a selection from four coordinates.
+ *
+ * @param anchorLine A zero-based line value.
+ * @param anchorCharacter A zero-based character value.
+ * @param activeLine A zero-based line value.
+ * @param activeCharacter A zero-based character value.
+ */
+ constructor(anchorLine: number, anchorCharacter: number, activeLine: number, activeCharacter: number);
+
+ /**
+ * A selection is reversed if {@link Selection.active active}.isBefore({@link Selection.anchor anchor}).
+ */
+ isReversed: boolean;
+ }
+
+ /**
+ * Represents sources that can cause {@link window.onDidChangeTextEditorSelection selection change events}.
+ */
+ export enum TextEditorSelectionChangeKind {
+ /**
+ * Selection changed due to typing in the editor.
+ */
+ Keyboard = 1,
+ /**
+ * Selection change due to clicking in the editor.
+ */
+ Mouse = 2,
+ /**
+ * Selection changed because a command ran.
+ */
+ Command = 3
+ }
+
+ /**
+ * Represents an event describing the change in a {@link TextEditor.selections text editor's selections}.
+ */
+ export interface TextEditorSelectionChangeEvent {
+ /**
+ * The {@link TextEditor text editor} for which the selections have changed.
+ */
+ readonly textEditor: TextEditor;
+ /**
+ * The new value for the {@link TextEditor.selections text editor's selections}.
+ */
+ readonly selections: readonly Selection[];
+ /**
+ * The {@link TextEditorSelectionChangeKind change kind} which has triggered this
+ * event. Can be `undefined`.
+ */
+ readonly kind?: TextEditorSelectionChangeKind;
+ }
+
+ /**
+ * Represents an event describing the change in a {@link TextEditor.visibleRanges text editor's visible ranges}.
+ */
+ export interface TextEditorVisibleRangesChangeEvent {
+ /**
+ * The {@link TextEditor text editor} for which the visible ranges have changed.
+ */
+ readonly textEditor: TextEditor;
+ /**
+ * The new value for the {@link TextEditor.visibleRanges text editor's visible ranges}.
+ */
+ readonly visibleRanges: readonly Range[];
+ }
+
+ /**
+ * Represents an event describing the change in a {@link TextEditor.options text editor's options}.
+ */
+ export interface TextEditorOptionsChangeEvent {
+ /**
+ * The {@link TextEditor text editor} for which the options have changed.
+ */
+ readonly textEditor: TextEditor;
+ /**
+ * The new value for the {@link TextEditor.options text editor's options}.
+ */
+ readonly options: TextEditorOptions;
+ }
+
+ /**
+ * Represents an event describing the change of a {@link TextEditor.viewColumn text editor's view column}.
+ */
+ export interface TextEditorViewColumnChangeEvent {
+ /**
+ * The {@link TextEditor text editor} for which the view column has changed.
+ */
+ readonly textEditor: TextEditor;
+ /**
+ * The new value for the {@link TextEditor.viewColumn text editor's view column}.
+ */
+ readonly viewColumn: ViewColumn;
+ }
+
+ /**
+ * Rendering style of the cursor.
+ */
+ export enum TextEditorCursorStyle {
+ /**
+ * Render the cursor as a vertical thick line.
+ */
+ Line = 1,
+ /**
+ * Render the cursor as a block filled.
+ */
+ Block = 2,
+ /**
+ * Render the cursor as a thick horizontal line.
+ */
+ Underline = 3,
+ /**
+ * Render the cursor as a vertical thin line.
+ */
+ LineThin = 4,
+ /**
+ * Render the cursor as a block outlined.
+ */
+ BlockOutline = 5,
+ /**
+ * Render the cursor as a thin horizontal line.
+ */
+ UnderlineThin = 6
+ }
+
+ /**
+ * Rendering style of the line numbers.
+ */
+ export enum TextEditorLineNumbersStyle {
+ /**
+ * Do not render the line numbers.
+ */
+ Off = 0,
+ /**
+ * Render the line numbers.
+ */
+ On = 1,
+ /**
+ * Render the line numbers with values relative to the primary cursor location.
+ */
+ Relative = 2
+ }
+
+ /**
+ * Represents a {@link TextEditor text editor}'s {@link TextEditor.options options}.
+ */
+ export interface TextEditorOptions {
+
+ /**
+ * The size in spaces a tab takes. This is used for two purposes:
+ * - the rendering width of a tab character;
+ * - the number of spaces to insert when {@link TextEditorOptions.insertSpaces insertSpaces} is true.
+ *
+ * When getting a text editor's options, this property will always be a number (resolved).
+ * When setting a text editor's options, this property is optional and it can be a number or `"auto"`.
+ */
+ tabSize?: number | string;
+
+ /**
+ * When pressing Tab insert {@link TextEditorOptions.tabSize n} spaces.
+ * When getting a text editor's options, this property will always be a boolean (resolved).
+ * When setting a text editor's options, this property is optional and it can be a boolean or `"auto"`.
+ */
+ insertSpaces?: boolean | string;
+
+ /**
+ * The rendering style of the cursor in this editor.
+ * When getting a text editor's options, this property will always be present.
+ * When setting a text editor's options, this property is optional.
+ */
+ cursorStyle?: TextEditorCursorStyle;
+
+ /**
+ * Render relative line numbers w.r.t. the current line number.
+ * When getting a text editor's options, this property will always be present.
+ * When setting a text editor's options, this property is optional.
+ */
+ lineNumbers?: TextEditorLineNumbersStyle;
+ }
+
+ /**
+ * Represents a handle to a set of decorations
+ * sharing the same {@link DecorationRenderOptions styling options} in a {@link TextEditor text editor}.
+ *
+ * To get an instance of a `TextEditorDecorationType` use
+ * {@link window.createTextEditorDecorationType createTextEditorDecorationType}.
+ */
+ export interface TextEditorDecorationType {
+
+ /**
+ * Internal representation of the handle.
+ */
+ readonly key: string;
+
+ /**
+ * Remove this decoration type and all decorations on all text editors using it.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * Represents different {@link TextEditor.revealRange reveal} strategies in a text editor.
+ */
+ export enum TextEditorRevealType {
+ /**
+ * The range will be revealed with as little scrolling as possible.
+ */
+ Default = 0,
+ /**
+ * The range will always be revealed in the center of the viewport.
+ */
+ InCenter = 1,
+ /**
+ * If the range is outside the viewport, it will be revealed in the center of the viewport.
+ * Otherwise, it will be revealed with as little scrolling as possible.
+ */
+ InCenterIfOutsideViewport = 2,
+ /**
+ * The range will always be revealed at the top of the viewport.
+ */
+ AtTop = 3
+ }
+
+ /**
+ * Represents different positions for rendering a decoration in an {@link DecorationRenderOptions.overviewRulerLane overview ruler}.
+ * The overview ruler supports three lanes.
+ */
+ export enum OverviewRulerLane {
+ Left = 1,
+ Center = 2,
+ Right = 4,
+ Full = 7
+ }
+
+ /**
+ * Describes the behavior of decorations when typing/editing at their edges.
+ */
+ export enum DecorationRangeBehavior {
+ /**
+ * The decoration's range will widen when edits occur at the start or end.
+ */
+ OpenOpen = 0,
+ /**
+ * The decoration's range will not widen when edits occur at the start of end.
+ */
+ ClosedClosed = 1,
+ /**
+ * The decoration's range will widen when edits occur at the start, but not at the end.
+ */
+ OpenClosed = 2,
+ /**
+ * The decoration's range will widen when edits occur at the end, but not at the start.
+ */
+ ClosedOpen = 3
+ }
+
+ /**
+ * Represents options to configure the behavior of showing a {@link TextDocument document} in an {@link TextEditor editor}.
+ */
+ export interface TextDocumentShowOptions {
+ /**
+ * An optional view column in which the {@link TextEditor editor} should be shown.
+ * The default is the {@link ViewColumn.Active active}, other values are adjusted to
+ * be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is
+ * not adjusted. Use {@linkcode ViewColumn.Beside} to open the
+ * editor to the side of the currently active one.
+ */
+ viewColumn?: ViewColumn;
+
+ /**
+ * An optional flag that when `true` will stop the {@link TextEditor editor} from taking focus.
+ */
+ 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.
+ */
+ preview?: boolean;
+
+ /**
+ * An optional selection to apply for the document in the {@link TextEditor editor}.
+ */
+ selection?: Range;
+ }
+
+ /**
+ * A reference to one of the workbench colors as defined in https://code.visualstudio.com/docs/getstarted/theme-color-reference.
+ * Using a theme color is preferred over a custom color as it gives theme authors and users the possibility to change the color.
+ */
+ export class ThemeColor {
+
+ /**
+ * Creates a reference to a theme color.
+ * @param id of the color. The available colors are listed in https://code.visualstudio.com/docs/getstarted/theme-color-reference.
+ */
+ constructor(id: string);
+ }
+
+ /**
+ * A reference to a named icon. Currently, {@link ThemeIcon.File File}, {@link ThemeIcon.Folder Folder},
+ * and [ThemeIcon ids](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing) are supported.
+ * Using a theme icon is preferred over a custom icon as it gives product theme authors the possibility to change the icons.
+ *
+ * *Note* that theme icons can also be rendered inside labels and descriptions. Places that support theme icons spell this out
+ * and they use the `$()`-syntax, for instance `quickPick.label = "Hello World $(globe)"`.
+ */
+ export class ThemeIcon {
+ /**
+ * Reference to an icon representing a file. The icon is taken from the current file icon theme or a placeholder icon is used.
+ */
+ static readonly File: ThemeIcon;
+
+ /**
+ * Reference to an icon representing a folder. The icon is taken from the current file icon theme or a placeholder icon is used.
+ */
+ static readonly Folder: ThemeIcon;
+
+ /**
+ * The id of the icon. The available icons are listed in https://code.visualstudio.com/api/references/icons-in-labels#icon-listing.
+ */
+ readonly id: string;
+
+ /**
+ * The optional ThemeColor of the icon. The color is currently only used in {@link TreeItem}.
+ */
+ readonly color?: ThemeColor;
+
+ /**
+ * Creates a reference to a theme icon.
+ * @param id id of the icon. The available icons are listed in https://code.visualstudio.com/api/references/icons-in-labels#icon-listing.
+ * @param color optional `ThemeColor` for the icon. The color is currently only used in {@link TreeItem}.
+ */
+ constructor(id: string, color?: ThemeColor);
+ }
+
+ /**
+ * Represents theme specific rendering styles for a {@link TextEditorDecorationType text editor decoration}.
+ */
+ export interface ThemableDecorationRenderOptions {
+ /**
+ * Background color of the decoration. Use rgba() and define transparent background colors to play well with other decorations.
+ * Alternatively a color from the color registry can be {@link ThemeColor referenced}.
+ */
+ backgroundColor?: string | ThemeColor;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ outline?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'outline' for setting one or more of the individual outline properties.
+ */
+ outlineColor?: string | ThemeColor;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'outline' for setting one or more of the individual outline properties.
+ */
+ outlineStyle?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'outline' for setting one or more of the individual outline properties.
+ */
+ outlineWidth?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ border?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'border' for setting one or more of the individual border properties.
+ */
+ borderColor?: string | ThemeColor;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'border' for setting one or more of the individual border properties.
+ */
+ borderRadius?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'border' for setting one or more of the individual border properties.
+ */
+ borderSpacing?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'border' for setting one or more of the individual border properties.
+ */
+ borderStyle?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ * Better use 'border' for setting one or more of the individual border properties.
+ */
+ borderWidth?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ fontStyle?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ fontWeight?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ textDecoration?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ cursor?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ color?: string | ThemeColor;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ opacity?: string;
+
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ letterSpacing?: string;
+
+ /**
+ * An **absolute path** or an URI to an image to be rendered in the gutter.
+ */
+ gutterIconPath?: string | Uri;
+
+ /**
+ * Specifies the size of the gutter icon.
+ * Available values are 'auto', 'contain', 'cover' and any percentage value.
+ * For further information: https://msdn.microsoft.com/en-us/library/jj127316(v=vs.85).aspx
+ */
+ gutterIconSize?: string;
+
+ /**
+ * The color of the decoration in the overview ruler. Use rgba() and define transparent colors to play well with other decorations.
+ */
+ overviewRulerColor?: string | ThemeColor;
+
+ /**
+ * Defines the rendering options of the attachment that is inserted before the decorated text.
+ */
+ before?: ThemableDecorationAttachmentRenderOptions;
+
+ /**
+ * Defines the rendering options of the attachment that is inserted after the decorated text.
+ */
+ after?: ThemableDecorationAttachmentRenderOptions;
+ }
+
+ export interface ThemableDecorationAttachmentRenderOptions {
+ /**
+ * Defines a text content that is shown in the attachment. Either an icon or a text can be shown, but not both.
+ */
+ contentText?: string;
+ /**
+ * An **absolute path** or an URI to an image to be rendered in the attachment. Either an icon
+ * or a text can be shown, but not both.
+ */
+ contentIconPath?: string | Uri;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ border?: string;
+ /**
+ * CSS styling property that will be applied to text enclosed by a decoration.
+ */
+ borderColor?: string | ThemeColor;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ fontStyle?: string;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ fontWeight?: string;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ textDecoration?: string;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ color?: string | ThemeColor;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ backgroundColor?: string | ThemeColor;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ margin?: string;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ width?: string;
+ /**
+ * CSS styling property that will be applied to the decoration attachment.
+ */
+ height?: string;
+ }
+
+ /**
+ * Represents rendering styles for a {@link TextEditorDecorationType text editor decoration}.
+ */
+ export interface DecorationRenderOptions extends ThemableDecorationRenderOptions {
+ /**
+ * Should the decoration be rendered also on the whitespace after the line text.
+ * Defaults to `false`.
+ */
+ isWholeLine?: boolean;
+
+ /**
+ * Customize the growing behavior of the decoration when edits occur at the edges of the decoration's range.
+ * Defaults to `DecorationRangeBehavior.OpenOpen`.
+ */
+ rangeBehavior?: DecorationRangeBehavior;
+
+ /**
+ * The position in the overview ruler where the decoration should be rendered.
+ */
+ overviewRulerLane?: OverviewRulerLane;
+
+ /**
+ * Overwrite options for light themes.
+ */
+ light?: ThemableDecorationRenderOptions;
+
+ /**
+ * Overwrite options for dark themes.
+ */
+ dark?: ThemableDecorationRenderOptions;
+ }
+
+ /**
+ * Represents options for a specific decoration in a {@link TextEditorDecorationType decoration set}.
+ */
+ export interface DecorationOptions {
+
+ /**
+ * Range to which this decoration is applied. The range must not be empty.
+ */
+ range: Range;
+
+ /**
+ * A message that should be rendered when hovering over the decoration.
+ */
+ hoverMessage?: MarkdownString | MarkedString | Array;
+
+ /**
+ * Render options applied to the current decoration. For performance reasons, keep the
+ * number of decoration specific options small, and use decoration types wherever possible.
+ */
+ renderOptions?: DecorationInstanceRenderOptions;
+ }
+
+ export interface ThemableDecorationInstanceRenderOptions {
+ /**
+ * Defines the rendering options of the attachment that is inserted before the decorated text.
+ */
+ before?: ThemableDecorationAttachmentRenderOptions;
+
+ /**
+ * Defines the rendering options of the attachment that is inserted after the decorated text.
+ */
+ after?: ThemableDecorationAttachmentRenderOptions;
+ }
+
+ export interface DecorationInstanceRenderOptions extends ThemableDecorationInstanceRenderOptions {
+ /**
+ * Overwrite options for light themes.
+ */
+ light?: ThemableDecorationInstanceRenderOptions;
+
+ /**
+ * Overwrite options for dark themes.
+ */
+ dark?: ThemableDecorationInstanceRenderOptions;
+ }
+
+ /**
+ * Represents an editor that is attached to a {@link TextDocument document}.
+ */
+ export interface TextEditor {
+
+ /**
+ * The document associated with this text editor. The document will be the same for the entire lifetime of this text editor.
+ */
+ readonly document: TextDocument;
+
+ /**
+ * The primary selection on this text editor. Shorthand for `TextEditor.selections[0]`.
+ */
+ selection: Selection;
+
+ /**
+ * The selections in this text editor. The primary selection is always at index 0.
+ */
+ selections: Selection[];
+
+ /**
+ * The current visible ranges in the editor (vertically).
+ * This accounts only for vertical scrolling, and not for horizontal scrolling.
+ */
+ readonly visibleRanges: Range[];
+
+ /**
+ * Text editor options.
+ */
+ options: TextEditorOptions;
+
+ /**
+ * The column in which this editor shows. Will be `undefined` in case this
+ * isn't one of the main editors, e.g. an embedded editor, or when the editor
+ * column is larger than three.
+ */
+ readonly viewColumn?: ViewColumn;
+
+ /**
+ * Perform an edit on the document associated with this text editor.
+ *
+ * The given callback-function is invoked with an {@link TextEditorEdit edit-builder} which must
+ * be used to make edits. Note that the edit-builder is only valid while the
+ * callback executes.
+ *
+ * @param callback A function which can create edits using an {@link TextEditorEdit edit-builder}.
+ * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit.
+ * @return A promise that resolves with a value indicating if the edits could be applied.
+ */
+ edit(callback: (editBuilder: TextEditorEdit) => void, options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable;
+
+ /**
+ * Insert a {@link SnippetString snippet} and put the editor into snippet mode. "Snippet mode"
+ * means the editor adds placeholders and additional cursors so that the user can complete
+ * or accept the snippet.
+ *
+ * @param snippet The snippet to insert in this edit.
+ * @param location Position or range at which to insert the snippet, defaults to the current editor selection or selections.
+ * @param options The undo/redo behavior around this edit. By default, undo stops will be created before and after this edit.
+ * @return A promise that resolves with a value indicating if the snippet could be inserted. Note that the promise does not signal
+ * that the snippet is completely filled-in or accepted.
+ */
+ insertSnippet(snippet: SnippetString, location?: Position | Range | readonly Position[] | readonly Range[], options?: { undoStopBefore: boolean; undoStopAfter: boolean; }): Thenable;
+
+ /**
+ * Adds a set of decorations to the text editor. If a set of decorations already exists with
+ * the given {@link TextEditorDecorationType decoration type}, they will be replaced. If
+ * `rangesOrOptions` is empty, the existing decorations with the given {@link TextEditorDecorationType decoration type}
+ * will be removed.
+ *
+ * @see {@link window.createTextEditorDecorationType createTextEditorDecorationType}.
+ *
+ * @param decorationType A decoration type.
+ * @param rangesOrOptions Either {@link Range ranges} or more detailed {@link DecorationOptions options}.
+ */
+ setDecorations(decorationType: TextEditorDecorationType, rangesOrOptions: readonly Range[] | readonly DecorationOptions[]): void;
+
+ /**
+ * Scroll as indicated by `revealType` in order to reveal the given range.
+ *
+ * @param range A range.
+ * @param revealType The scrolling strategy for revealing `range`.
+ */
+ revealRange(range: Range, revealType?: TextEditorRevealType): void;
+
+ /**
+ * Show the text editor.
+ *
+ * @deprecated Use {@link window.showTextDocument} instead.
+ *
+ * @param column The {@link ViewColumn column} in which to show this editor.
+ * This method shows unexpected behavior and will be removed in the next major update.
+ */
+ show(column?: ViewColumn): void;
+
+ /**
+ * Hide the text editor.
+ *
+ * @deprecated Use the command `workbench.action.closeActiveEditor` instead.
+ * This method shows unexpected behavior and will be removed in the next major update.
+ */
+ hide(): void;
+ }
+
+ /**
+ * Represents an end of line character sequence in a {@link TextDocument document}.
+ */
+ export enum EndOfLine {
+ /**
+ * The line feed `\n` character.
+ */
+ LF = 1,
+ /**
+ * The carriage return line feed `\r\n` sequence.
+ */
+ CRLF = 2
+ }
+
+ /**
+ * A complex edit that will be applied in one transaction on a TextEditor.
+ * This holds a description of the edits and if the edits are valid (i.e. no overlapping regions, document was not changed in the meantime, etc.)
+ * they can be applied on a {@link TextDocument document} associated with a {@link TextEditor text editor}.
+ */
+ export interface TextEditorEdit {
+ /**
+ * Replace a certain text region with a new value.
+ * You can use \r\n or \n in `value` and they will be normalized to the current {@link TextDocument document}.
+ *
+ * @param location The range this operation should remove.
+ * @param value The new text this operation should insert after removing `location`.
+ */
+ replace(location: Position | Range | Selection, value: string): void;
+
+ /**
+ * Insert text at a location.
+ * You can use \r\n or \n in `value` and they will be normalized to the current {@link TextDocument document}.
+ * Although the equivalent text edit can be made with {@link TextEditorEdit.replace replace}, `insert` will produce a different resulting selection (it will get moved).
+ *
+ * @param location The position where the new text should be inserted.
+ * @param value The new text this operation should insert.
+ */
+ insert(location: Position, value: string): void;
+
+ /**
+ * Delete a certain text region.
+ *
+ * @param location The range this operation should remove.
+ */
+ delete(location: Range | Selection): void;
+
+ /**
+ * Set the end of line sequence.
+ *
+ * @param endOfLine The new end of line for the {@link TextDocument document}.
+ */
+ setEndOfLine(endOfLine: EndOfLine): void;
+ }
+
+ /**
+ * A universal resource identifier representing either a file on disk
+ * or another resource, like untitled resources.
+ */
+ export class Uri {
+
+ /**
+ * Create an URI from a string, e.g. `http://www.msft.com/some/path`,
+ * `file:///usr/home`, or `scheme:with/path`.
+ *
+ * *Note* that for a while uris without a `scheme` were accepted. That is not correct
+ * as all uris should have a scheme. To avoid breakage of existing code the optional
+ * `strict`-argument has been added. We *strongly* advise to use it, e.g. `Uri.parse('my:uri', true)`
+ *
+ * @see {@link Uri.toString}
+ * @param value The string value of an Uri.
+ * @param strict Throw an error when `value` is empty or when no `scheme` can be parsed.
+ * @return A new Uri instance.
+ */
+ static parse(value: string, strict?: boolean): Uri;
+
+ /**
+ * Create an URI from a file system path. The {@link Uri.scheme scheme}
+ * will be `file`.
+ *
+ * The *difference* between {@link Uri.parse} and {@link Uri.file} is that the latter treats the argument
+ * as path, not as stringified-uri. E.g. `Uri.file(path)` is *not* the same as
+ * `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';
+ ```
+ *
+ * @param path A file system or UNC path.
+ * @return A new Uri instance.
+ */
+ static file(path: string): Uri;
+
+ /**
+ * Create a new uri which path is the result of joining
+ * the path of the base uri with the provided path segments.
+ *
+ * - Note 1: `joinPath` only affects the path component
+ * and all other components (scheme, authority, query, and fragment) are
+ * left as they are.
+ * - Note 2: The base uri must have a path; an error is thrown otherwise.
+ *
+ * The path segments are normalized in the following ways:
+ * - sequences of path separators (`/` or `\`) are replaced with a single separator
+ * - for `file`-uris on windows, the backslash-character (`\`) is considered a path-separator
+ * - the `..`-segment denotes the parent segment, the `.` denotes the current segment
+ * - paths have a root which always remains, for instance on windows drive-letters are roots
+ * so that is true: `joinPath(Uri.file('file:///c:/root'), '../../other').fsPath === 'c:/other'`
+ *
+ * @param base An uri. Must have a path.
+ * @param pathSegments One more more path fragments
+ * @returns A new uri which path is joined with the given fragments
+ */
+ static joinPath(base: Uri, ...pathSegments: string[]): Uri;
+
+ /**
+ * Create an URI from its component parts
+ *
+ * @see {@link Uri.toString}
+ * @param components The component parts of an Uri.
+ * @return A new Uri instance.
+ */
+ static from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri;
+
+ /**
+ * Use the `file` and `parse` factory functions to create new `Uri` objects.
+ */
+ private constructor(scheme: string, authority: string, path: string, query: string, fragment: string);
+
+ /**
+ * Scheme is the `http` part of `http://www.msft.com/some/path?query#fragment`.
+ * The part before the first colon.
+ */
+ readonly scheme: string;
+
+ /**
+ * Authority is the `www.msft.com` part of `http://www.msft.com/some/path?query#fragment`.
+ * The part between the first double slashes and the next slash.
+ */
+ readonly authority: string;
+
+ /**
+ * Path is the `/some/path` part of `http://www.msft.com/some/path?query#fragment`.
+ */
+ readonly path: string;
+
+ /**
+ * Query is the `query` part of `http://www.msft.com/some/path?query#fragment`.
+ */
+ readonly query: string;
+
+ /**
+ * Fragment is the `fragment` part of `http://www.msft.com/some/path?query#fragment`.
+ */
+ readonly fragment: string;
+
+ /**
+ * The string representing the corresponding file system path of this Uri.
+ *
+ * Will handle UNC paths and normalize windows drive letters to lower-case. Also
+ * uses the platform specific path separator.
+ *
+ * * Will *not* validate the path for invalid characters and semantics.
+ * * Will *not* look at the scheme of this Uri.
+ * * The resulting string shall *not* be used for display purposes but
+ * for disk operations, like `readFile` et al.
+ *
+ * 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'
+ ```
+ */
+ readonly fsPath: string;
+
+ /**
+ * Derive a new Uri from this Uri.
+ *
+ * ```ts
+ * let file = Uri.parse('before:some/file/path');
+ * let other = file.with({ scheme: 'after' });
+ * assert.ok(other.toString() === 'after:some/file/path');
+ * ```
+ *
+ * @param change An object that describes a change to this Uri. To unset components use `null` or
+ * the empty string.
+ * @return A new Uri that reflects the given change. Will return `this` Uri if the change
+ * is not changing anything.
+ */
+ with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): Uri;
+
+ /**
+ * Returns a string representation of this Uri. The representation and normalization
+ * of a URI depends on the scheme.
+ *
+ * * The resulting string can be safely used with {@link Uri.parse}.
+ * * The resulting string shall *not* be used for display purposes.
+ *
+ * *Note* that the implementation will encode _aggressive_ which often leads to unexpected,
+ * but not incorrect, results. For instance, colons are encoded to `%3A` which might be unexpected
+ * in file-uri. Also `&` and `=` will be encoded which might be unexpected for http-uris. For stability
+ * reasons this cannot be changed anymore. If you suffer from too aggressive encoding you should use
+ * the `skipEncoding`-argument: `uri.toString(true)`.
+ *
+ * @param skipEncoding Do not percentage-encode the result, defaults to `false`. Note that
+ * the `#` and `?` characters occurring in the path will always be encoded.
+ * @returns A string representation of this Uri.
+ */
+ toString(skipEncoding?: boolean): string;
+
+ /**
+ * Returns a JSON representation of this Uri.
+ *
+ * @return An object.
+ */
+ toJSON(): any;
+ }
+
+ /**
+ * A cancellation token is passed to an asynchronous or long running
+ * operation to request cancellation, like cancelling a request
+ * for completion items because the user continued to type.
+ *
+ * To get an instance of a `CancellationToken` use a
+ * {@link CancellationTokenSource}.
+ */
+ export interface CancellationToken {
+
+ /**
+ * Is `true` when the token has been cancelled, `false` otherwise.
+ */
+ isCancellationRequested: boolean;
+
+ /**
+ * An {@link Event} which fires upon cancellation.
+ */
+ onCancellationRequested: Event;
+ }
+
+ /**
+ * A cancellation source creates and controls a {@link CancellationToken cancellation token}.
+ */
+ export class CancellationTokenSource {
+
+ /**
+ * The cancellation token of this source.
+ */
+ token: CancellationToken;
+
+ /**
+ * Signal cancellation on the token.
+ */
+ cancel(): void;
+
+ /**
+ * Dispose object and free resources.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * An error type that should be used to signal cancellation of an operation.
+ *
+ * This type can be used in response to a {@link CancellationToken cancellation token}
+ * being cancelled or when an operation is being cancelled by the
+ * executor of that operation.
+ */
+ export class CancellationError extends Error {
+
+ /**
+ * Creates a new cancellation error.
+ */
+ constructor();
+ }
+
+ /**
+ * Represents a type which can release resources, such
+ * as event listening or a timer.
+ */
+ export class Disposable {
+
+ /**
+ * Combine many disposable-likes into one. Use this method
+ * when having objects with a dispose function which are not
+ * instances of Disposable.
+ *
+ * @param disposableLikes Objects that have at least a `dispose`-function member.
+ * @return Returns a new disposable which, upon dispose, will
+ * dispose all provided disposables.
+ */
+ static from(...disposableLikes: { dispose: () => any }[]): Disposable;
+
+ /**
+ * Creates a new Disposable calling the provided function
+ * on dispose.
+ * @param callOnDispose Function that disposes something.
+ */
+ constructor(callOnDispose: Function);
+
+ /**
+ * Dispose this object.
+ */
+ dispose(): any;
+ }
+
+ /**
+ * Represents a typed event.
+ *
+ * A function that represents an event to which you subscribe by calling it with
+ * a listener function as argument.
+ *
+ * @example
+ * item.onDidChange(function(event) { console.log("Event happened: " + event); });
+ */
+ export interface Event {
+
+ /**
+ * A function that represents an event to which you subscribe by calling it with
+ * a listener function as argument.
+ *
+ * @param listener The listener function will be called when the event happens.
+ * @param thisArgs The `this`-argument which will be used when calling the event listener.
+ * @param disposables An array to which a {@link Disposable} will be added.
+ * @return A disposable which unsubscribes the event listener.
+ */
+ (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]): Disposable;
+ }
+
+ /**
+ * An event emitter can be used to create and manage an {@link Event} for others
+ * to subscribe to. One emitter always owns one event.
+ *
+ * Use this class if you want to provide event from within your extension, for instance
+ * inside a {@link TextDocumentContentProvider} or when providing
+ * API to other extensions.
+ */
+ export class EventEmitter {
+
+ /**
+ * The event listeners can subscribe to.
+ */
+ event: Event;
+
+ /**
+ * Notify all subscribers of the {@link EventEmitter.event event}. Failure
+ * of one or more listener will not fail this function call.
+ *
+ * @param data The event object.
+ */
+ fire(data: T): void;
+
+ /**
+ * Dispose this object and free resources.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * A file system watcher notifies about changes to files and folders
+ * on disk or from other {@link FileSystemProvider FileSystemProviders}.
+ *
+ * To get an instance of a `FileSystemWatcher` use
+ * {@link workspace.createFileSystemWatcher createFileSystemWatcher}.
+ */
+ export interface FileSystemWatcher extends Disposable {
+
+ /**
+ * true if this file system watcher has been created such that
+ * it ignores creation file system events.
+ */
+ ignoreCreateEvents: boolean;
+
+ /**
+ * true if this file system watcher has been created such that
+ * it ignores change file system events.
+ */
+ ignoreChangeEvents: boolean;
+
+ /**
+ * true if this file system watcher has been created such that
+ * it ignores delete file system events.
+ */
+ ignoreDeleteEvents: boolean;
+
+ /**
+ * An event which fires on file/folder creation.
+ */
+ onDidCreate: Event;
+
+ /**
+ * An event which fires on file/folder change.
+ */
+ onDidChange: Event;
+
+ /**
+ * An event which fires on file/folder deletion.
+ */
+ onDidDelete: Event;
+ }
+
+ /**
+ * A text document content provider allows to add readonly documents
+ * to the editor, such as source from a dll or generated html from md.
+ *
+ * Content providers are {@link workspace.registerTextDocumentContentProvider registered}
+ * for a {@link Uri.scheme uri-scheme}. When a uri with that scheme is to
+ * be {@link workspace.openTextDocument loaded} the content provider is
+ * asked.
+ */
+ export interface TextDocumentContentProvider {
+
+ /**
+ * An event to signal a resource has changed.
+ */
+ onDidChange?: Event;
+
+ /**
+ * Provide textual content for a given uri.
+ *
+ * The editor will use the returned string-content to create a readonly
+ * {@link TextDocument document}. Resources allocated should be released when
+ * the corresponding document has been {@link workspace.onDidCloseTextDocument closed}.
+ *
+ * **Note**: The contents of the created {@link TextDocument document} might not be
+ * identical to the provided text due to end-of-line-sequence normalization.
+ *
+ * @param uri An uri which scheme matches the scheme this provider was {@link workspace.registerTextDocumentContentProvider registered} for.
+ * @param token A cancellation token.
+ * @return A string or a thenable that resolves to such.
+ */
+ provideTextDocumentContent(uri: Uri, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Represents an item that can be selected from
+ * a list of items.
+ */
+ export interface QuickPickItem {
+
+ /**
+ * A human-readable string which is rendered prominent. Supports rendering of {@link ThemeIcon theme icons} via
+ * the `$()`-syntax.
+ */
+ label: string;
+
+ /**
+ * A human-readable string which is rendered less prominent in the same line. Supports rendering of
+ * {@link ThemeIcon theme icons} via the `$()`-syntax.
+ */
+ description?: string;
+
+ /**
+ * A human-readable string which is rendered less prominent in a separate line. Supports rendering of
+ * {@link ThemeIcon theme icons} via the `$()`-syntax.
+ */
+ detail?: string;
+
+ /**
+ * Optional flag indicating if this item is picked initially.
+ * (Only honored when the picker allows multiple selections.)
+ *
+ * @see {@link QuickPickOptions.canPickMany}
+ */
+ picked?: boolean;
+
+ /**
+ * Always show this item.
+ */
+ alwaysShow?: boolean;
+ }
+
+ /**
+ * Options to configure the behavior of the quick pick UI.
+ */
+ export interface QuickPickOptions {
+
+ /**
+ * An optional string that represents the title of the quick pick.
+ */
+ title?: string;
+
+ /**
+ * An optional flag to include the description when filtering the picks.
+ */
+ matchOnDescription?: boolean;
+
+ /**
+ * An optional flag to include the detail when filtering the picks.
+ */
+ matchOnDetail?: boolean;
+
+ /**
+ * An optional string to show as placeholder in the input box to guide the user what to pick on.
+ */
+ placeHolder?: string;
+
+ /**
+ * Set to `true` to keep the picker open when focus moves to another part of the editor or to another window.
+ * This setting is ignored on iPad and is always false.
+ */
+ ignoreFocusOut?: boolean;
+
+ /**
+ * An optional flag to make the picker accept multiple selections, if true the result is an array of picks.
+ */
+ canPickMany?: boolean;
+
+ /**
+ * An optional function that is invoked whenever an item is selected.
+ */
+ onDidSelectItem?(item: QuickPickItem | string): any;
+ }
+
+ /**
+ * Options to configure the behaviour of the {@link WorkspaceFolder workspace folder} pick UI.
+ */
+ export interface WorkspaceFolderPickOptions {
+
+ /**
+ * An optional string to show as placeholder in the input box to guide the user what to pick on.
+ */
+ placeHolder?: string;
+
+ /**
+ * Set to `true` to keep the picker open when focus moves to another part of the editor or to another window.
+ * This setting is ignored on iPad and is always false.
+ */
+ ignoreFocusOut?: boolean;
+ }
+
+ /**
+ * Options to configure the behaviour of a file open dialog.
+ *
+ * * Note 1: On Windows and Linux, a file dialog cannot be both a file selector and a folder selector, so if you
+ * set both `canSelectFiles` and `canSelectFolders` to `true` on these platforms, a folder selector will be shown.
+ * * Note 2: Explicitly setting `canSelectFiles` and `canSelectFolders` to `false` is futile
+ * and the editor then silently adjusts the options to select files.
+ */
+ export interface OpenDialogOptions {
+ /**
+ * The resource the dialog shows when opened.
+ */
+ defaultUri?: Uri;
+
+ /**
+ * A human-readable string for the open button.
+ */
+ openLabel?: string;
+
+ /**
+ * Allow to select files, defaults to `true`.
+ */
+ canSelectFiles?: boolean;
+
+ /**
+ * Allow to select folders, defaults to `false`.
+ */
+ canSelectFolders?: boolean;
+
+ /**
+ * Allow to select many files or folders.
+ */
+ canSelectMany?: boolean;
+
+ /**
+ * A set of file filters that are used by the dialog. Each entry is a human-readable label,
+ * like "TypeScript", and an array of extensions, e.g.
+ * ```ts
+ * {
+ * 'Images': ['png', 'jpg']
+ * 'TypeScript': ['ts', 'tsx']
+ * }
+ * ```
+ */
+ filters?: { [name: string]: string[] };
+
+ /**
+ * Dialog title.
+ *
+ * This parameter might be ignored, as not all operating systems display a title on open dialogs
+ * (for example, macOS).
+ */
+ title?: string;
+ }
+
+ /**
+ * Options to configure the behaviour of a file save dialog.
+ */
+ export interface SaveDialogOptions {
+ /**
+ * The resource the dialog shows when opened.
+ */
+ defaultUri?: Uri;
+
+ /**
+ * A human-readable string for the save button.
+ */
+ saveLabel?: string;
+
+ /**
+ * A set of file filters that are used by the dialog. Each entry is a human-readable label,
+ * like "TypeScript", and an array of extensions, e.g.
+ * ```ts
+ * {
+ * 'Images': ['png', 'jpg']
+ * 'TypeScript': ['ts', 'tsx']
+ * }
+ * ```
+ */
+ filters?: { [name: string]: string[] };
+
+ /**
+ * Dialog title.
+ *
+ * This parameter might be ignored, as not all operating systems display a title on save dialogs
+ * (for example, macOS).
+ */
+ title?: string;
+ }
+
+ /**
+ * Represents an action that is shown with an information, warning, or
+ * error message.
+ *
+ * @see {@link window.showInformationMessage showInformationMessage}
+ * @see {@link window.showWarningMessage showWarningMessage}
+ * @see {@link window.showErrorMessage showErrorMessage}
+ */
+ export interface MessageItem {
+
+ /**
+ * A short title like 'Retry', 'Open Log' etc.
+ */
+ title: string;
+
+ /**
+ * A hint for modal dialogs that the item should be triggered
+ * when the user cancels the dialog (e.g. by pressing the ESC
+ * key).
+ *
+ * Note: this option is ignored for non-modal messages.
+ */
+ isCloseAffordance?: boolean;
+ }
+
+ /**
+ * Options to configure the behavior of the message.
+ *
+ * @see {@link window.showInformationMessage showInformationMessage}
+ * @see {@link window.showWarningMessage showWarningMessage}
+ * @see {@link window.showErrorMessage showErrorMessage}
+ */
+ export interface MessageOptions {
+
+ /**
+ * Indicates that this message should be modal.
+ */
+ modal?: boolean;
+
+ /**
+ * Human-readable detail message that is rendered less prominent. _Note_ that detail
+ * is only shown for {@link MessageOptions.modal modal} messages.
+ */
+ detail?: string;
+ }
+
+ /**
+ * Options to configure the behavior of the input box UI.
+ */
+ export interface InputBoxOptions {
+
+ /**
+ * An optional string that represents the title of the input box.
+ */
+ title?: string;
+
+ /**
+ * The value to prefill in the input box.
+ */
+ value?: string;
+
+ /**
+ * Selection of the prefilled {@linkcode InputBoxOptions.value value}. Defined as tuple of two number where the
+ * first is the inclusive start index and the second the exclusive end index. When `undefined` the whole
+ * word will be selected, when empty (start equals end) only the cursor will be set,
+ * otherwise the defined range will be selected.
+ */
+ valueSelection?: [number, number];
+
+ /**
+ * The text to display underneath the input box.
+ */
+ prompt?: string;
+
+ /**
+ * An optional string to show as placeholder in the input box to guide the user what to type.
+ */
+ placeHolder?: string;
+
+ /**
+ * Controls if a password input is shown. Password input hides the typed text.
+ */
+ password?: boolean;
+
+ /**
+ * Set to `true` to keep the input box open when focus moves to another part of the editor or to another window.
+ * This setting is ignored on iPad and is always false.
+ */
+ ignoreFocusOut?: boolean;
+
+ /**
+ * An optional function that will be called to validate input and to give a hint
+ * to the user.
+ *
+ * @param value The current value of the input box.
+ * @return A human-readable string which is presented as diagnostic message.
+ * Return `undefined`, `null`, or the empty string when 'value' is valid.
+ */
+ validateInput?(value: string): string | undefined | null | Thenable;
+ }
+
+ /**
+ * A relative pattern is a helper to construct glob patterns that are matched
+ * relatively to a base file path. The base path can either be an absolute file
+ * path as string or uri or a {@link WorkspaceFolder workspace folder}, which is the
+ * preferred way of creating the relative pattern.
+ */
+ export class RelativePattern {
+
+ /**
+ * A base file path to which this pattern will be matched against relatively.
+ */
+ base: string;
+
+ /**
+ * A file glob pattern like `*.{ts,js}` that will be matched on file paths
+ * relative to the base path.
+ *
+ * Example: Given a base of `/home/work/folder` and a file path of `/home/work/folder/index.js`,
+ * the file glob pattern will match on `index.js`.
+ */
+ pattern: string;
+
+ /**
+ * Creates a new relative pattern object with a base file path and pattern to match. This pattern
+ * will be matched on file paths relative to the base.
+ *
+ * Example:
+ * ```ts
+ * const folder = vscode.workspace.workspaceFolders?.[0];
+ * if (folder) {
+ *
+ * // Match any TypeScript file in the root of this workspace folder
+ * const pattern1 = new vscode.RelativePattern(folder, '*.ts');
+ *
+ * // Match any TypeScript file in `someFolder` inside this workspace folder
+ * const pattern2 = new vscode.RelativePattern(folder, 'someFolder/*.ts');
+ * }
+ * ```
+ *
+ * @param base A base to which this pattern will be matched against relatively. It is recommended
+ * to pass in a {@link WorkspaceFolder workspace folder} if the pattern should match inside the workspace.
+ * Otherwise, a uri or string should only be used if the pattern is for a file path outside the workspace.
+ * @param pattern A file glob pattern like `*.{ts,js}` that will be matched on paths relative to the base.
+ */
+ constructor(base: WorkspaceFolder | Uri | string, pattern: string)
+ }
+
+ /**
+ * A file glob pattern to match file paths against. This can either be a glob pattern string
+ * (like `**​/*.{ts,js}` or `*.{ts,js}`) or a {@link RelativePattern relative pattern}.
+ *
+ * Glob patterns can have the following syntax:
+ * * `*` to match one or more characters in a path segment
+ * * `?` to match on one character in a path segment
+ * * `**` to match any number of path segments, including none
+ * * `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ * * `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ * * `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ *
+ * Note: a backslash (`\`) is not valid within a glob pattern. If you have an existing file
+ * path to match against, consider to use the {@link RelativePattern relative pattern} support
+ * that takes care of converting any backslash into slash. Otherwise, make sure to convert
+ * any backslash to slash when creating the glob pattern.
+ */
+ export type GlobPattern = string | RelativePattern;
+
+ /**
+ * A document filter denotes a document by different properties like
+ * the {@link TextDocument.languageId language}, the {@link Uri.scheme scheme} of
+ * its resource, or a glob-pattern that is applied to the {@link TextDocument.fileName path}.
+ *
+ * @example A language filter that applies to typescript files on disk
+ * { language: 'typescript', scheme: 'file' }
+ *
+ * @example A language filter that applies to all package.json paths
+ * { language: 'json', pattern: '**​/package.json' }
+ */
+ export interface DocumentFilter {
+
+ /**
+ * A language id, like `typescript`.
+ */
+ readonly language?: string;
+
+ /**
+ * A Uri {@link Uri.scheme scheme}, like `file` or `untitled`.
+ */
+ readonly scheme?: string;
+
+ /**
+ * A {@link GlobPattern glob pattern} that is matched on the absolute path of the document. Use a {@link RelativePattern relative pattern}
+ * to filter documents to a {@link WorkspaceFolder workspace folder}.
+ */
+ readonly pattern?: GlobPattern;
+ }
+
+ /**
+ * A language selector is the combination of one or many language identifiers
+ * and {@link DocumentFilter language filters}.
+ *
+ * *Note* that a document selector that is just a language identifier selects *all*
+ * documents, even those that are not saved on disk. Only use such selectors when
+ * a feature works without further context, e.g. without the need to resolve related
+ * 'files'.
+ *
+ * @example
+ * let sel:DocumentSelector = { scheme: 'file', language: 'typescript' };
+ */
+ export type DocumentSelector = DocumentFilter | string | ReadonlyArray;
+
+ /**
+ * A provider result represents the values a provider, like the {@linkcode HoverProvider},
+ * may return. For once this is the actual result type `T`, like `Hover`, or a thenable that resolves
+ * to that type `T`. In addition, `null` and `undefined` can be returned - either directly or from a
+ * thenable.
+ *
+ * The snippets below are all valid implementations of the {@linkcode HoverProvider}:
+ *
+ * ```ts
+ * let a: HoverProvider = {
+ * provideHover(doc, pos, token): ProviderResult {
+ * return new Hover('Hello World');
+ * }
+ * }
+ *
+ * let b: HoverProvider = {
+ * provideHover(doc, pos, token): ProviderResult {
+ * return new Promise(resolve => {
+ * resolve(new Hover('Hello World'));
+ * });
+ * }
+ * }
+ *
+ * let c: HoverProvider = {
+ * provideHover(doc, pos, token): ProviderResult {
+ * return; // undefined
+ * }
+ * }
+ * ```
+ */
+ export type ProviderResult = T | undefined | null | Thenable;
+
+ /**
+ * Kind of a code action.
+ *
+ * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`.
+ *
+ * Code action kinds are used by the editor for UI elements such as the refactoring context menu. Users
+ * can also trigger code actions with a specific kind with the `editor.action.codeAction` command.
+ */
+ export class CodeActionKind {
+ /**
+ * Empty kind.
+ */
+ static readonly Empty: CodeActionKind;
+
+ /**
+ * Base kind for quickfix actions: `quickfix`.
+ *
+ * Quick fix actions address a problem in the code and are shown in the normal code action context menu.
+ */
+ static readonly QuickFix: CodeActionKind;
+
+ /**
+ * Base kind for refactoring actions: `refactor`
+ *
+ * Refactoring actions are shown in the refactoring context menu.
+ */
+ static readonly Refactor: CodeActionKind;
+
+ /**
+ * Base kind for refactoring extraction actions: `refactor.extract`
+ *
+ * Example extract actions:
+ *
+ * - Extract method
+ * - Extract function
+ * - Extract variable
+ * - Extract interface from class
+ * - ...
+ */
+ static readonly RefactorExtract: CodeActionKind;
+
+ /**
+ * Base kind for refactoring inline actions: `refactor.inline`
+ *
+ * Example inline actions:
+ *
+ * - Inline function
+ * - Inline variable
+ * - Inline constant
+ * - ...
+ */
+ static readonly RefactorInline: CodeActionKind;
+
+ /**
+ * Base kind for refactoring rewrite actions: `refactor.rewrite`
+ *
+ * Example rewrite actions:
+ *
+ * - Convert JavaScript function to class
+ * - Add or remove parameter
+ * - Encapsulate field
+ * - Make method static
+ * - Move method to base class
+ * - ...
+ */
+ static readonly RefactorRewrite: CodeActionKind;
+
+ /**
+ * Base kind for source actions: `source`
+ *
+ * Source code actions apply to the entire file. They must be explicitly requested and will not show in the
+ * normal [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) menu. Source actions
+ * can be run on save using `editor.codeActionsOnSave` and are also shown in the `source` context menu.
+ */
+ static readonly Source: CodeActionKind;
+
+ /**
+ * Base kind for an organize imports source action: `source.organizeImports`.
+ */
+ static readonly SourceOrganizeImports: CodeActionKind;
+
+ /**
+ * Base kind for auto-fix source actions: `source.fixAll`.
+ *
+ * Fix all actions automatically fix errors that have a clear fix that do not require user input.
+ * They should not suppress errors or perform unsafe fixes such as generating new types or classes.
+ */
+ static readonly SourceFixAll: CodeActionKind;
+
+ private constructor(value: string);
+
+ /**
+ * String value of the kind, e.g. `"refactor.extract.function"`.
+ */
+ readonly value: string;
+
+ /**
+ * Create a new kind by appending a more specific selector to the current kind.
+ *
+ * Does not modify the current kind.
+ */
+ append(parts: string): CodeActionKind;
+
+ /**
+ * Checks if this code action kind intersects `other`.
+ *
+ * The kind `"refactor.extract"` for example intersects `refactor`, `"refactor.extract"` and ``"refactor.extract.function"`,
+ * but not `"unicorn.refactor.extract"`, or `"refactor.extractAll"`.
+ *
+ * @param other Kind to check.
+ */
+ intersects(other: CodeActionKind): boolean;
+
+ /**
+ * Checks if `other` is a sub-kind of this `CodeActionKind`.
+ *
+ * The kind `"refactor.extract"` for example contains `"refactor.extract"` and ``"refactor.extract.function"`,
+ * but not `"unicorn.refactor.extract"`, or `"refactor.extractAll"` or `refactor`.
+ *
+ * @param other Kind to check.
+ */
+ contains(other: CodeActionKind): boolean;
+ }
+
+ /**
+ * The reason why code actions were requested.
+ */
+ export enum CodeActionTriggerKind {
+ /**
+ * Code actions were explicitly requested by the user or by an extension.
+ */
+ Invoke = 1,
+
+ /**
+ * Code actions were requested automatically.
+ *
+ * This typically happens when current selection in a file changes, but can
+ * also be triggered when file content changes.
+ */
+ Automatic = 2,
+ }
+
+ /**
+ * Contains additional diagnostic information about the context in which
+ * a {@link CodeActionProvider.provideCodeActions code action} is run.
+ */
+ export interface CodeActionContext {
+ /**
+ * The reason why code actions were requested.
+ */
+ readonly triggerKind: CodeActionTriggerKind;
+
+ /**
+ * An array of diagnostics.
+ */
+ readonly diagnostics: readonly Diagnostic[];
+
+ /**
+ * Requested kind of actions to return.
+ *
+ * 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;
+ }
+
+ /**
+ * A code action represents a change that can be performed in code, e.g. to fix a problem or
+ * to refactor code.
+ *
+ * A CodeAction must set either {@linkcode CodeAction.edit edit} and/or a {@linkcode CodeAction.command command}. If both are supplied, the `edit` is applied first, then the command is executed.
+ */
+ export class CodeAction {
+
+ /**
+ * A short, human-readable, title for this code action.
+ */
+ title: string;
+
+ /**
+ * A {@link WorkspaceEdit workspace edit} this code action performs.
+ */
+ edit?: WorkspaceEdit;
+
+ /**
+ * {@link Diagnostic Diagnostics} that this code action resolves.
+ */
+ diagnostics?: Diagnostic[];
+
+ /**
+ * A {@link Command} this code action executes.
+ *
+ * If this command throws an exception, the editor displays the exception message to users in the editor at the
+ * current cursor position.
+ */
+ command?: Command;
+
+ /**
+ * {@link CodeActionKind Kind} of the code action.
+ *
+ * Used to filter code actions.
+ */
+ kind?: CodeActionKind;
+
+ /**
+ * Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted
+ * by keybindings.
+ *
+ * A quick fix should be marked preferred if it properly addresses the underlying error.
+ * A refactoring should be marked preferred if it is the most reasonable choice of actions to take.
+ */
+ isPreferred?: boolean;
+
+ /**
+ * Marks that the code action cannot currently be applied.
+ *
+ * - Disabled code actions are not shown in automatic [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action)
+ * code action menu.
+ *
+ * - Disabled actions are shown as faded out in the code action menu when the user request a more specific type
+ * of code action, such as refactorings.
+ *
+ * - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions)
+ * that auto applies a code action and only a disabled code actions are returned, the editor will show the user an
+ * error message with `reason` in the editor.
+ */
+ disabled?: {
+ /**
+ * Human readable description of why the code action is currently disabled.
+ *
+ * This is displayed in the code actions UI.
+ */
+ readonly reason: string;
+ };
+
+ /**
+ * Creates a new code action.
+ *
+ * A code action must have at least a {@link CodeAction.title title} and {@link CodeAction.edit edits}
+ * and/or a {@link CodeAction.command command}.
+ *
+ * @param title The title of the code action.
+ * @param kind The kind of the code action.
+ */
+ constructor(title: string, kind?: CodeActionKind);
+ }
+
+ /**
+ * The code action interface defines the contract between extensions and
+ * the [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) feature.
+ *
+ * A code action can be any command that is {@link commands.getCommands known} to the system.
+ */
+ export interface CodeActionProvider {
+ /**
+ * Provide commands for the given document and range.
+ *
+ * @param document The document in which the command was invoked.
+ * @param range The selector or range for which the command was invoked. This will always be a selection if
+ * there is a currently active editor.
+ * @param context Context carrying additional information.
+ * @param token A cancellation token.
+ *
+ * @return An array of code actions, such as quick fixes or refactorings. The lack of a result can be signaled
+ * by returning `undefined`, `null`, or an empty array.
+ *
+ * We also support returning `Command` for legacy reasons, however all new extensions should return
+ * `CodeAction` object instead.
+ */
+ provideCodeActions(document: TextDocument, range: Range | Selection, context: CodeActionContext, token: CancellationToken): ProviderResult<(Command | T)[]>;
+
+ /**
+ * Given a code action fill in its {@linkcode CodeAction.edit edit}-property. Changes to
+ * all other properties, like title, are ignored. A code action that has an edit
+ * will not be resolved.
+ *
+ * *Note* that a code action provider that returns commands, not code actions, cannot successfully
+ * implement this function. Returning commands is deprecated and instead code actions should be
+ * returned.
+ *
+ * @param codeAction A code action.
+ * @param token A cancellation token.
+ * @return The resolved code action or a thenable that resolves to such. It is OK to return the given
+ * `item`. When no result is returned, the given `item` will be used.
+ */
+ resolveCodeAction?(codeAction: T, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Metadata about the type of code actions that a {@link CodeActionProvider} provides.
+ */
+ export interface CodeActionProviderMetadata {
+ /**
+ * List of {@link CodeActionKind CodeActionKinds} that a {@link CodeActionProvider} may return.
+ *
+ * This list is used to determine if a given `CodeActionProvider` should be invoked or not.
+ * To avoid unnecessary computation, every `CodeActionProvider` should list use `providedCodeActionKinds`. The
+ * list of kinds may either be generic, such as `[CodeActionKind.Refactor]`, or list out every kind provided,
+ * such as `[CodeActionKind.Refactor.Extract.append('function'), CodeActionKind.Refactor.Extract.append('constant'), ...]`.
+ */
+ readonly providedCodeActionKinds?: readonly CodeActionKind[];
+
+ /**
+ * Static documentation for a class of code actions.
+ *
+ * Documentation from the provider is shown in the code actions menu if either:
+ *
+ * - Code actions of `kind` are requested by the editor. In this case, the editor will show the documentation that
+ * most closely matches the requested code action kind. For example, if a provider has documentation for
+ * both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`,
+ * the editor will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`.
+ *
+ * - Any code actions of `kind` are returned by the provider.
+ *
+ * At most one documentation entry will be shown per provider.
+ */
+ readonly documentation?: ReadonlyArray<{
+ /**
+ * The kind of the code action being documented.
+ *
+ * If the kind is generic, such as `CodeActionKind.Refactor`, the documentation will be shown whenever any
+ * refactorings are returned. If the kind if more specific, such as `CodeActionKind.RefactorExtract`, the
+ * documentation will only be shown when extract refactoring code actions are returned.
+ */
+ readonly kind: CodeActionKind;
+
+ /**
+ * Command that displays the documentation to the user.
+ *
+ * This can display the documentation directly in the editor or open a website using {@linkcode env.openExternal};
+ *
+ * The title of this documentation code action is taken from {@linkcode Command.title}
+ */
+ readonly command: Command;
+ }>;
+ }
+
+ /**
+ * A code lens represents a {@link Command} that should be shown along with
+ * source text, like the number of references, a way to run tests, etc.
+ *
+ * A code lens is _unresolved_ when no command is associated to it. For performance
+ * reasons the creation of a code lens and resolving should be done to two stages.
+ *
+ * @see {@link CodeLensProvider.provideCodeLenses}
+ * @see {@link CodeLensProvider.resolveCodeLens}
+ */
+ export class CodeLens {
+
+ /**
+ * The range in which this code lens is valid. Should only span a single line.
+ */
+ range: Range;
+
+ /**
+ * The command this code lens represents.
+ */
+ command?: Command;
+
+ /**
+ * `true` when there is a command associated.
+ */
+ readonly isResolved: boolean;
+
+ /**
+ * Creates a new code lens object.
+ *
+ * @param range The range to which this code lens applies.
+ * @param command The command associated to this code lens.
+ */
+ constructor(range: Range, command?: Command);
+ }
+
+ /**
+ * A code lens provider adds {@link Command commands} to source text. The commands will be shown
+ * as dedicated horizontal lines in between the source text.
+ */
+ export interface CodeLensProvider {
+
+ /**
+ * An optional event to signal that the code lenses from this provider have changed.
+ */
+ onDidChangeCodeLenses?: Event;
+
+ /**
+ * Compute a list of {@link CodeLens lenses}. This call should return as fast as possible and if
+ * computing the commands is expensive implementors should only return code lens objects with the
+ * range set and implement {@link CodeLensProvider.resolveCodeLens resolve}.
+ *
+ * @param document The document in which the command was invoked.
+ * @param token A cancellation token.
+ * @return An array of code lenses or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult;
+
+ /**
+ * This function will be called for each visible code lens, usually when scrolling and after
+ * calls to {@link CodeLensProvider.provideCodeLenses compute}-lenses.
+ *
+ * @param codeLens Code lens that must be resolved.
+ * @param token A cancellation token.
+ * @return The given, resolved code lens or thenable that resolves to such.
+ */
+ resolveCodeLens?(codeLens: T, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Information about where a symbol is defined.
+ *
+ * Provides additional metadata over normal {@link Location} definitions, including the range of
+ * the defining symbol
+ */
+ export type DefinitionLink = LocationLink;
+
+ /**
+ * The definition of a symbol represented as one or many {@link Location locations}.
+ * For most programming languages there is only one location at which a symbol is
+ * defined.
+ */
+ export type Definition = Location | Location[];
+
+ /**
+ * The definition provider interface defines the contract between extensions and
+ * the [go to definition](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-definition)
+ * and peek definition features.
+ */
+ export interface DefinitionProvider {
+
+ /**
+ * Provide the definition of the symbol at the given position and document.
+ *
+ * @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.
+ * @return A definition or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The implementation provider interface defines the contract between extensions and
+ * the go to implementation feature.
+ */
+ export interface ImplementationProvider {
+
+ /**
+ * Provide the implementations of the symbol at the given position and document.
+ *
+ * @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.
+ * @return A definition or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideImplementation(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The type definition provider defines the contract between extensions and
+ * the go to type definition feature.
+ */
+ export interface TypeDefinitionProvider {
+
+ /**
+ * Provide the type definition of the symbol at the given position and document.
+ *
+ * @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.
+ * @return A definition or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideTypeDefinition(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The declaration of a symbol representation as one or many {@link Location locations}
+ * or {@link LocationLink location links}.
+ */
+ export type Declaration = Location | Location[] | LocationLink[];
+
+ /**
+ * The declaration provider interface defines the contract between extensions and
+ * the go to declaration feature.
+ */
+ export interface DeclarationProvider {
+
+ /**
+ * Provide the declaration of the symbol at the given position and document.
+ *
+ * @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.
+ * @return A declaration or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideDeclaration(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The MarkdownString represents human-readable text that supports formatting via the
+ * markdown syntax. Standard markdown is supported, also tables, but no embedded html.
+ *
+ * Rendering of {@link ThemeIcon theme icons} via the `$()`-syntax is supported
+ * when the {@linkcode MarkdownString.supportThemeIcons supportThemeIcons} is set to `true`.
+ */
+ export class MarkdownString {
+
+ /**
+ * The markdown string.
+ */
+ value: string;
+
+ /**
+ * Indicates that this markdown string is from a trusted source. Only *trusted*
+ * markdown supports links that execute commands, e.g. `[Run it](command:myCommandId)`.
+ */
+ isTrusted?: boolean;
+
+ /**
+ * Indicates that this markdown string can contain {@link ThemeIcon ThemeIcons}, e.g. `$(zap)`.
+ */
+ supportThemeIcons?: boolean;
+
+ /**
+ * Creates a new markdown string with the given value.
+ *
+ * @param value Optional, initial value.
+ * @param supportThemeIcons Optional, Specifies whether {@link ThemeIcon ThemeIcons} are supported within the {@linkcode MarkdownString}.
+ */
+ constructor(value?: string, supportThemeIcons?: boolean);
+
+ /**
+ * Appends and escapes the given string to this markdown string.
+ * @param value Plain text.
+ */
+ appendText(value: string): MarkdownString;
+
+ /**
+ * Appends the given string 'as is' to this markdown string. When {@linkcode MarkdownString.supportThemeIcons supportThemeIcons} is `true`, {@link ThemeIcon ThemeIcons} in the `value` will be iconified.
+ * @param value Markdown string.
+ */
+ appendMarkdown(value: string): MarkdownString;
+
+ /**
+ * Appends the given string as codeblock using the provided language.
+ * @param value A code snippet.
+ * @param language An optional {@link languages.getLanguages language identifier}.
+ */
+ appendCodeblock(value: string, language?: string): MarkdownString;
+ }
+
+ /**
+ * MarkedString can be used to render human-readable text. It is either a markdown string
+ * or a code-block that provides a language and a code snippet. Note that
+ * markdown strings will be sanitized - that means html will be escaped.
+ *
+ * @deprecated This type is deprecated, please use {@linkcode MarkdownString} instead.
+ */
+ export type MarkedString = string | { language: string; value: string };
+
+ /**
+ * A hover represents additional information for a symbol or word. Hovers are
+ * rendered in a tooltip-like widget.
+ */
+ export class Hover {
+
+ /**
+ * The contents of this hover.
+ */
+ contents: Array;
+
+ /**
+ * The range to which this hover applies. When missing, the
+ * editor will use the range at the current position or the
+ * current position itself.
+ */
+ range?: Range;
+
+ /**
+ * Creates a new hover object.
+ *
+ * @param contents The contents of the hover.
+ * @param range The range to which the hover applies.
+ */
+ constructor(contents: MarkdownString | MarkedString | Array, range?: Range);
+ }
+
+ /**
+ * The hover provider interface defines the contract between extensions and
+ * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature.
+ */
+ export interface HoverProvider {
+
+ /**
+ * Provide a hover for the given position and document. Multiple hovers at the same
+ * position will be merged by the editor. A hover can have a range which defaults
+ * to the word range at the position when omitted.
+ *
+ * @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.
+ * @return A hover or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideHover(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * An EvaluatableExpression represents an expression in a document that can be evaluated by an active debugger or runtime.
+ * The result of this evaluation is shown in a tooltip-like widget.
+ * If only a range is specified, the expression will be extracted from the underlying document.
+ * An optional expression can be used to override the extracted expression.
+ * In this case the range is still used to highlight the range in the document.
+ */
+ export class EvaluatableExpression {
+
+ /*
+ * The range is used to extract the evaluatable expression from the underlying document and to highlight it.
+ */
+ readonly range: Range;
+
+ /*
+ * If specified the expression overrides the extracted expression.
+ */
+ readonly expression?: string;
+
+ /**
+ * Creates a new evaluatable expression object.
+ *
+ * @param range The range in the underlying document from which the evaluatable expression is extracted.
+ * @param expression If specified overrides the extracted expression.
+ */
+ constructor(range: Range, expression?: string);
+ }
+
+ /**
+ * The evaluatable expression provider interface defines the contract between extensions and
+ * the debug hover. In this contract the provider returns an evaluatable expression for a given position
+ * in a document and the editor evaluates this expression in the active debug session and shows the result in a debug hover.
+ */
+ export interface EvaluatableExpressionProvider {
+
+ /**
+ * Provide an evaluatable expression for the given document and position.
+ * The editor will evaluate this expression in the active debug session and will show the result in the debug hover.
+ * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression.
+ *
+ * @param document The document for which the debug hover is about to appear.
+ * @param position The line and character position in the document where the debug hover is about to appear.
+ * @param token A cancellation token.
+ * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideEvaluatableExpression(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Provide inline value as text.
+ */
+ export class InlineValueText {
+ /**
+ * The document range for which the inline value applies.
+ */
+ readonly range: Range;
+ /**
+ * The text of the inline value.
+ */
+ readonly text: string;
+ /**
+ * Creates a new InlineValueText object.
+ *
+ * @param range The document line where to show the inline value.
+ * @param text The value to be shown for the line.
+ */
+ constructor(range: Range, text: string);
+ }
+
+ /**
+ * Provide inline value through a variable lookup.
+ * If only a range is specified, the variable name will be extracted from the underlying document.
+ * An optional variable name can be used to override the extracted name.
+ */
+ export class InlineValueVariableLookup {
+ /**
+ * The document range for which the inline value applies.
+ * The range is used to extract the variable name from the underlying document.
+ */
+ readonly range: Range;
+ /**
+ * If specified the name of the variable to look up.
+ */
+ readonly variableName?: string;
+ /**
+ * How to perform the lookup.
+ */
+ readonly caseSensitiveLookup: boolean;
+ /**
+ * Creates a new InlineValueVariableLookup object.
+ *
+ * @param range The document line where to show the inline value.
+ * @param variableName The name of the variable to look up.
+ * @param caseSensitiveLookup How to perform the lookup. If missing lookup is case sensitive.
+ */
+ constructor(range: Range, variableName?: string, caseSensitiveLookup?: boolean);
+ }
+
+ /**
+ * Provide an inline value through an expression evaluation.
+ * If only a range is specified, the expression will be extracted from the underlying document.
+ * An optional expression can be used to override the extracted expression.
+ */
+ export class InlineValueEvaluatableExpression {
+ /**
+ * The document range for which the inline value applies.
+ * The range is used to extract the evaluatable expression from the underlying document.
+ */
+ readonly range: Range;
+ /**
+ * If specified the expression overrides the extracted expression.
+ */
+ readonly expression?: string;
+ /**
+ * Creates a new InlineValueEvaluatableExpression object.
+ *
+ * @param range The range in the underlying document from which the evaluatable expression is extracted.
+ * @param expression If specified overrides the extracted expression.
+ */
+ constructor(range: Range, expression?: string);
+ }
+
+ /**
+ * Inline value information can be provided by different means:
+ * - directly as a text value (class InlineValueText).
+ * - as a name to use for a variable lookup (class InlineValueVariableLookup)
+ * - as an evaluatable expression (class InlineValueEvaluatableExpression)
+ * The InlineValue types combines all inline value types into one type.
+ */
+ export type InlineValue = InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression;
+
+ /**
+ * A value-object that contains contextual information when requesting inline values from a InlineValuesProvider.
+ */
+ export interface InlineValueContext {
+
+ /**
+ * The stack frame (as a DAP Id) where the execution has stopped.
+ */
+ readonly frameId: number;
+
+ /**
+ * The document range where execution has stopped.
+ * Typically the end position of the range denotes the line where the inline values are shown.
+ */
+ readonly stoppedLocation: Range;
+ }
+
+ /**
+ * The inline values provider interface defines the contract between extensions and the editor's debugger inline values feature.
+ * In this contract the provider returns inline value information for a given document range
+ * and the editor shows this information in the editor at the end of lines.
+ */
+ export interface InlineValuesProvider {
+
+ /**
+ * An optional event to signal that inline values have changed.
+ * @see {@link EventEmitter}
+ */
+ onDidChangeInlineValues?: Event | undefined;
+
+ /**
+ * Provide "inline value" information for a given document and range.
+ * The editor calls this method whenever debugging stops in the given document.
+ * The returned inline values information is rendered in the editor at the end of lines.
+ *
+ * @param document The document for which the inline values information is needed.
+ * @param viewPort The visible document range for which inline values should be computed.
+ * @param context A bag containing contextual information like the current location.
+ * @param token A cancellation token.
+ * @return An array of InlineValueDescriptors or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A document highlight kind.
+ */
+ export enum DocumentHighlightKind {
+
+ /**
+ * A textual occurrence.
+ */
+ Text = 0,
+
+ /**
+ * Read-access of a symbol, like reading a variable.
+ */
+ Read = 1,
+
+ /**
+ * Write-access of a symbol, like writing to a variable.
+ */
+ Write = 2
+ }
+
+ /**
+ * A document highlight is a range inside a text document which deserves
+ * special attention. Usually a document highlight is visualized by changing
+ * the background color of its range.
+ */
+ export class DocumentHighlight {
+
+ /**
+ * The range this highlight applies to.
+ */
+ range: Range;
+
+ /**
+ * The highlight kind, default is {@link DocumentHighlightKind.Text text}.
+ */
+ kind?: DocumentHighlightKind;
+
+ /**
+ * Creates a new document highlight object.
+ *
+ * @param range The range the highlight applies to.
+ * @param kind The highlight kind, default is {@link DocumentHighlightKind.Text text}.
+ */
+ constructor(range: Range, kind?: DocumentHighlightKind);
+ }
+
+ /**
+ * The document highlight provider interface defines the contract between extensions and
+ * the word-highlight-feature.
+ */
+ export interface DocumentHighlightProvider {
+
+ /**
+ * Provide a set of document highlights, like all occurrences of a variable or
+ * all exit-points of a function.
+ *
+ * @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.
+ * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideDocumentHighlights(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A symbol kind.
+ */
+ export enum SymbolKind {
+ File = 0,
+ Module = 1,
+ Namespace = 2,
+ Package = 3,
+ Class = 4,
+ Method = 5,
+ Property = 6,
+ Field = 7,
+ Constructor = 8,
+ Enum = 9,
+ Interface = 10,
+ Function = 11,
+ Variable = 12,
+ Constant = 13,
+ String = 14,
+ Number = 15,
+ Boolean = 16,
+ Array = 17,
+ Object = 18,
+ Key = 19,
+ Null = 20,
+ EnumMember = 21,
+ Struct = 22,
+ Event = 23,
+ Operator = 24,
+ TypeParameter = 25
+ }
+
+ /**
+ * Symbol tags are extra annotations that tweak the rendering of a symbol.
+ */
+ export enum SymbolTag {
+
+ /**
+ * Render a symbol as obsolete, usually using a strike-out.
+ */
+ Deprecated = 1
+ }
+
+ /**
+ * Represents information about programming constructs like variables, classes,
+ * interfaces etc.
+ */
+ export class SymbolInformation {
+
+ /**
+ * The name of this symbol.
+ */
+ name: string;
+
+ /**
+ * The name of the symbol containing this symbol.
+ */
+ containerName: string;
+
+ /**
+ * The kind of this symbol.
+ */
+ kind: SymbolKind;
+
+ /**
+ * Tags for this symbol.
+ */
+ tags?: readonly SymbolTag[];
+
+ /**
+ * The location of this symbol.
+ */
+ location: Location;
+
+ /**
+ * Creates a new symbol information object.
+ *
+ * @param name The name of the symbol.
+ * @param kind The kind of the symbol.
+ * @param containerName The name of the symbol containing the symbol.
+ * @param location The location of the symbol.
+ */
+ constructor(name: string, kind: SymbolKind, containerName: string, location: Location);
+
+ /**
+ * Creates a new symbol information object.
+ *
+ * @deprecated Please use the constructor taking a {@link Location} object.
+ *
+ * @param name The name of the symbol.
+ * @param kind The kind of the symbol.
+ * @param range The range of the location of the symbol.
+ * @param uri The resource of the location of symbol, defaults to the current document.
+ * @param containerName The name of the symbol containing the symbol.
+ */
+ constructor(name: string, kind: SymbolKind, range: Range, uri?: Uri, containerName?: string);
+ }
+
+ /**
+ * Represents programming constructs like variables, classes, interfaces etc. that appear in a document. Document
+ * symbols can be hierarchical and they have two ranges: one that encloses its definition and one that points to
+ * its most interesting range, e.g. the range of an identifier.
+ */
+ export class DocumentSymbol {
+
+ /**
+ * The name of this symbol.
+ */
+ name: string;
+
+ /**
+ * More detail for this symbol, e.g. the signature of a function.
+ */
+ detail: string;
+
+ /**
+ * The kind of this symbol.
+ */
+ kind: SymbolKind;
+
+ /**
+ * Tags for this symbol.
+ */
+ tags?: readonly SymbolTag[];
+
+ /**
+ * 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 reveal when this symbol is being picked, e.g. the name of a function.
+ * Must be contained by the {@linkcode DocumentSymbol.range range}.
+ */
+ selectionRange: Range;
+
+ /**
+ * Children of this symbol, e.g. properties of a class.
+ */
+ children: DocumentSymbol[];
+
+ /**
+ * Creates a new document symbol.
+ *
+ * @param name The name of the symbol.
+ * @param detail Details for the symbol.
+ * @param kind The kind of the symbol.
+ * @param range The full range of the symbol.
+ * @param selectionRange The range that should be reveal.
+ */
+ constructor(name: string, detail: string, kind: SymbolKind, range: Range, selectionRange: Range);
+ }
+
+ /**
+ * The document symbol provider interface defines the contract between extensions and
+ * the [go to symbol](https://code.visualstudio.com/docs/editor/editingevolved#_go-to-symbol)-feature.
+ */
+ export interface DocumentSymbolProvider {
+
+ /**
+ * Provide symbol information for the given document.
+ *
+ * @param document The document in which the command was invoked.
+ * @param token A cancellation token.
+ * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideDocumentSymbols(document: TextDocument, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Metadata about a document symbol provider.
+ */
+ export interface DocumentSymbolProviderMetadata {
+ /**
+ * A human-readable string that is shown when multiple outlines trees show for one document.
+ */
+ label?: string;
+ }
+
+ /**
+ * The workspace symbol provider interface defines the contract between extensions and
+ * the [symbol search](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name)-feature.
+ */
+ export interface WorkspaceSymbolProvider {
+
+ /**
+ * Project-wide search for a symbol matching the given query string.
+ *
+ * The `query`-parameter should be interpreted in a *relaxed way* as the editor will apply its own highlighting
+ * and scoring on the results. A good rule of thumb is to match case-insensitive and to simply check that the
+ * characters of *query* appear in their order in a candidate symbol. Don't use prefix, substring, or similar
+ * strict matching.
+ *
+ * To improve performance implementors can implement `resolveWorkspaceSymbol` and then provide symbols with partial
+ * {@link SymbolInformation.location location}-objects, without a `range` defined. The editor will then call
+ * `resolveWorkspaceSymbol` for selected symbols only, e.g. when opening a workspace symbol.
+ *
+ * @param query A query string, can be the empty string in which case all symbols should be returned.
+ * @param token A cancellation token.
+ * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult;
+
+ /**
+ * Given a symbol fill in its {@link SymbolInformation.location location}. This method is called whenever a symbol
+ * is selected in the UI. Providers can implement this method and return incomplete symbols from
+ * {@linkcode WorkspaceSymbolProvider.provideWorkspaceSymbols provideWorkspaceSymbols} which often helps to improve
+ * performance.
+ *
+ * @param symbol The symbol that is to be resolved. Guaranteed to be an instance of an object returned from an
+ * earlier call to `provideWorkspaceSymbols`.
+ * @param token A cancellation token.
+ * @return The resolved symbol or a thenable that resolves to that. When no result is returned,
+ * the given `symbol` is used.
+ */
+ resolveWorkspaceSymbol?(symbol: T, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Value-object that contains additional information when
+ * requesting references.
+ */
+ export interface ReferenceContext {
+
+ /**
+ * Include the declaration of the current symbol.
+ */
+ includeDeclaration: boolean;
+ }
+
+ /**
+ * The reference provider interface defines the contract between extensions and
+ * the [find references](https://code.visualstudio.com/docs/editor/editingevolved#_peek)-feature.
+ */
+ export interface ReferenceProvider {
+
+ /**
+ * Provide a set of project-wide references for the given position and document.
+ *
+ * @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.
+ *
+ * @return An array of locations or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideReferences(document: TextDocument, position: Position, context: ReferenceContext, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A text edit represents edits that should be applied
+ * to a document.
+ */
+ export class TextEdit {
+
+ /**
+ * Utility to create a replace edit.
+ *
+ * @param range A range.
+ * @param newText A string.
+ * @return A new text edit object.
+ */
+ static replace(range: Range, newText: string): TextEdit;
+
+ /**
+ * Utility to create an insert edit.
+ *
+ * @param position A position, will become an empty range.
+ * @param newText A string.
+ * @return A new text edit object.
+ */
+ static insert(position: Position, newText: string): TextEdit;
+
+ /**
+ * Utility to create a delete edit.
+ *
+ * @param range A range.
+ * @return A new text edit object.
+ */
+ static delete(range: Range): TextEdit;
+
+ /**
+ * Utility to create an eol-edit.
+ *
+ * @param eol An eol-sequence
+ * @return A new text edit object.
+ */
+ static setEndOfLine(eol: EndOfLine): TextEdit;
+
+ /**
+ * The range this edit applies to.
+ */
+ range: Range;
+
+ /**
+ * The string this edit will insert.
+ */
+ newText: string;
+
+ /**
+ * The eol-sequence used in the document.
+ *
+ * *Note* that the eol-sequence will be applied to the
+ * whole document.
+ */
+ newEol?: EndOfLine;
+
+ /**
+ * Create a new TextEdit.
+ *
+ * @param range A range.
+ * @param newText A string.
+ */
+ constructor(range: Range, newText: string);
+ }
+
+ /**
+ * Additional data for entries of a workspace edit. Supports to label entries and marks entries
+ * as needing confirmation by the user. The editor groups edits with equal labels into tree nodes,
+ * for instance all edits labelled with "Changes in Strings" would be a tree node.
+ */
+ export interface WorkspaceEditEntryMetadata {
+
+ /**
+ * A flag which indicates that user confirmation is needed.
+ */
+ needsConfirmation: boolean;
+
+ /**
+ * A human-readable string which is rendered prominent.
+ */
+ label: string;
+
+ /**
+ * A human-readable string which is rendered less prominent on the same line.
+ */
+ description?: string;
+
+ /**
+ * The icon path or {@link ThemeIcon} for the edit.
+ */
+ iconPath?: Uri | { light: Uri; dark: Uri } | ThemeIcon;
+ }
+
+ /**
+ * A workspace edit is a collection of textual and files changes for
+ * multiple resources and documents.
+ *
+ * Use the {@link workspace.applyEdit applyEdit}-function to apply a workspace edit.
+ */
+ export class WorkspaceEdit {
+
+ /**
+ * The number of affected resources of textual or resource changes.
+ */
+ readonly size: number;
+
+ /**
+ * Replace the given range with given text for the given resource.
+ *
+ * @param uri A resource identifier.
+ * @param range A range.
+ * @param newText A string.
+ * @param metadata Optional metadata for the entry.
+ */
+ replace(uri: Uri, range: Range, newText: string, metadata?: WorkspaceEditEntryMetadata): void;
+
+ /**
+ * Insert the given text at the given position.
+ *
+ * @param uri A resource identifier.
+ * @param position A position.
+ * @param newText A string.
+ * @param metadata Optional metadata for the entry.
+ */
+ insert(uri: Uri, position: Position, newText: string, metadata?: WorkspaceEditEntryMetadata): void;
+
+ /**
+ * Delete the text at the given range.
+ *
+ * @param uri A resource identifier.
+ * @param range A range.
+ * @param metadata Optional metadata for the entry.
+ */
+ delete(uri: Uri, range: Range, metadata?: WorkspaceEditEntryMetadata): void;
+
+ /**
+ * Check if a text edit for a resource exists.
+ *
+ * @param uri A resource identifier.
+ * @return `true` if the given resource will be touched by this edit.
+ */
+ has(uri: Uri): boolean;
+
+ /**
+ * Set (and replace) text edits for a resource.
+ *
+ * @param uri A resource identifier.
+ * @param edits An array of text edits.
+ */
+ set(uri: Uri, edits: TextEdit[]): void;
+
+ /**
+ * Get the text edits for a resource.
+ *
+ * @param uri A resource identifier.
+ * @return An array of text edits.
+ */
+ get(uri: Uri): TextEdit[];
+
+ /**
+ * Create a regular file.
+ *
+ * @param uri Uri of the new file..
+ * @param options Defines if an existing file should be overwritten or be
+ * ignored. When overwrite and ignoreIfExists are both set overwrite wins.
+ * When both are unset and when the file already exists then the edit cannot
+ * be applied successfully.
+ * @param metadata Optional metadata for the entry.
+ */
+ createFile(uri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void;
+
+ /**
+ * Delete a file or folder.
+ *
+ * @param uri The uri of the file that is to be deleted.
+ * @param metadata Optional metadata for the entry.
+ */
+ deleteFile(uri: Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void;
+
+ /**
+ * Rename a file or folder.
+ *
+ * @param oldUri The existing file.
+ * @param newUri The new location.
+ * @param options Defines if existing files should be overwritten or be
+ * ignored. When overwrite and ignoreIfExists are both set overwrite wins.
+ * @param metadata Optional metadata for the entry.
+ */
+ renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void;
+
+ /**
+ * Get all text edits grouped by resource.
+ *
+ * @return A shallow copy of `[Uri, TextEdit[]]`-tuples.
+ */
+ entries(): [Uri, TextEdit[]][];
+ }
+
+ /**
+ * A snippet string is a template which allows to insert text
+ * and to control the editor cursor when insertion happens.
+ *
+ * A snippet can define tab stops and placeholders with `$1`, `$2`
+ * and `${3:foo}`. `$0` defines the final tab stop, it defaults to
+ * the end of the snippet. Variables are defined with `$name` and
+ * `${name:default value}`. The full snippet syntax is documented
+ * [here](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_creating-your-own-snippets).
+ */
+ export class SnippetString {
+
+ /**
+ * The snippet string.
+ */
+ value: string;
+
+ constructor(value?: string);
+
+ /**
+ * Builder-function that appends the given string to
+ * the {@linkcode SnippetString.value value} of this snippet string.
+ *
+ * @param string A value to append 'as given'. The string will be escaped.
+ * @return This snippet string.
+ */
+ appendText(string: string): SnippetString;
+
+ /**
+ * Builder-function that appends a tabstop (`$1`, `$2` etc) to
+ * the {@linkcode SnippetString.value value} of this snippet string.
+ *
+ * @param number The number of this tabstop, defaults to an auto-increment
+ * value starting at 1.
+ * @return This snippet string.
+ */
+ appendTabstop(number?: number): SnippetString;
+
+ /**
+ * Builder-function that appends a placeholder (`${1:value}`) to
+ * the {@linkcode SnippetString.value value} of this snippet string.
+ *
+ * @param value The value of this placeholder - either a string or a function
+ * with which a nested snippet can be created.
+ * @param number The number of this tabstop, defaults to an auto-increment
+ * value starting at 1.
+ * @return This snippet string.
+ */
+ appendPlaceholder(value: string | ((snippet: SnippetString) => any), number?: number): SnippetString;
+
+ /**
+ * Builder-function that appends a choice (`${1|a,b,c|}`) to
+ * the {@linkcode SnippetString.value value} of this snippet string.
+ *
+ * @param values The values for choices - the array of strings
+ * @param number The number of this tabstop, defaults to an auto-increment
+ * value starting at 1.
+ * @return This snippet string.
+ */
+ appendChoice(values: string[], number?: number): SnippetString;
+
+ /**
+ * Builder-function that appends a variable (`${VAR}`) to
+ * the {@linkcode SnippetString.value value} of this snippet string.
+ *
+ * @param name The name of the variable - excluding the `$`.
+ * @param defaultValue The default value which is used when the variable name cannot
+ * be resolved - either a string or a function with which a nested snippet can be created.
+ * @return This snippet string.
+ */
+ appendVariable(name: string, defaultValue: string | ((snippet: SnippetString) => any)): SnippetString;
+ }
+
+ /**
+ * The rename provider interface defines the contract between extensions and
+ * the [rename](https://code.visualstudio.com/docs/editor/editingevolved#_rename-symbol)-feature.
+ */
+ export interface RenameProvider {
+
+ /**
+ * Provide an edit that describes changes that have to be made to one
+ * or many resources to rename a symbol to a different name.
+ *
+ * @param document The document in which the command was invoked.
+ * @param position The position at which the command was invoked.
+ * @param newName The new name of the symbol. If the given name is not valid, the provider must return a rejected promise.
+ * @param token A cancellation token.
+ * @return A workspace edit or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideRenameEdits(document: TextDocument, position: Position, newName: string, token: CancellationToken): ProviderResult;
+
+ /**
+ * Optional function for resolving and validating a position *before* running rename. The result can
+ * be a range or a range and a placeholder text. The placeholder text should be the identifier of the symbol
+ * which is being renamed - when omitted the text in the returned range is used.
+ *
+ * *Note: * This function should throw an error or return a rejected thenable when the provided location
+ * doesn't allow for a rename.
+ *
+ * @param document The document in which rename will be invoked.
+ * @param position The position at which rename will be invoked.
+ * @param token A cancellation token.
+ * @return The range or range and placeholder text of the identifier that is to be renamed. The lack of a result can signaled by returning `undefined` or `null`.
+ */
+ prepareRename?(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A semantic tokens legend contains the needed information to decipher
+ * the integer encoded representation of semantic tokens.
+ */
+ export class SemanticTokensLegend {
+ /**
+ * The possible token types.
+ */
+ readonly tokenTypes: string[];
+ /**
+ * The possible token modifiers.
+ */
+ readonly tokenModifiers: string[];
+
+ constructor(tokenTypes: string[], tokenModifiers?: string[]);
+ }
+
+ /**
+ * A semantic tokens builder can help with creating a `SemanticTokens` instance
+ * which contains delta encoded semantic tokens.
+ */
+ export class SemanticTokensBuilder {
+
+ constructor(legend?: SemanticTokensLegend);
+
+ /**
+ * Add another token.
+ *
+ * @param line The token start line number (absolute value).
+ * @param char The token start character (absolute value).
+ * @param length The token length in characters.
+ * @param tokenType The encoded token type.
+ * @param tokenModifiers The encoded token modifiers.
+ */
+ push(line: number, char: number, length: number, tokenType: number, tokenModifiers?: number): void;
+
+ /**
+ * Add another token. Use only when providing a legend.
+ *
+ * @param range The range of the token. Must be single-line.
+ * @param tokenType The token type.
+ * @param tokenModifiers The token modifiers.
+ */
+ push(range: Range, tokenType: string, tokenModifiers?: string[]): void;
+
+ /**
+ * Finish and create a `SemanticTokens` instance.
+ */
+ build(resultId?: string): SemanticTokens;
+ }
+
+ /**
+ * Represents semantic tokens, either in a range or in an entire document.
+ * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format.
+ * @see {@link SemanticTokensBuilder} for a helper to create an instance.
+ */
+ export class SemanticTokens {
+ /**
+ * The result id of the tokens.
+ *
+ * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented).
+ */
+ readonly resultId?: string;
+ /**
+ * The actual tokens data.
+ * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens} for an explanation of the format.
+ */
+ readonly data: Uint32Array;
+
+ constructor(data: Uint32Array, resultId?: string);
+ }
+
+ /**
+ * Represents edits to semantic tokens.
+ * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits provideDocumentSemanticTokensEdits} for an explanation of the format.
+ */
+ export class SemanticTokensEdits {
+ /**
+ * The result id of the tokens.
+ *
+ * This is the id that will be passed to `DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits` (if implemented).
+ */
+ readonly resultId?: string;
+ /**
+ * The edits to the tokens data.
+ * All edits refer to the initial data state.
+ */
+ readonly edits: SemanticTokensEdit[];
+
+ constructor(edits: SemanticTokensEdit[], resultId?: string);
+ }
+
+ /**
+ * Represents an edit to semantic tokens.
+ * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokensEdits provideDocumentSemanticTokensEdits} for an explanation of the format.
+ */
+ export class SemanticTokensEdit {
+ /**
+ * The start offset of the edit.
+ */
+ readonly start: number;
+ /**
+ * The count of elements to remove.
+ */
+ readonly deleteCount: number;
+ /**
+ * The elements to insert.
+ */
+ readonly data?: Uint32Array;
+
+ constructor(start: number, deleteCount: number, data?: Uint32Array);
+ }
+
+ /**
+ * The document semantic tokens provider interface defines the contract between extensions and
+ * semantic tokens.
+ */
+ export interface DocumentSemanticTokensProvider {
+ /**
+ * An optional event to signal that the semantic tokens from this provider have changed.
+ */
+ onDidChangeSemanticTokens?: Event;
+
+ /**
+ * Tokens in a file are represented as an array of integers. The position of each token is expressed relative to
+ * the token before it, because most tokens remain stable relative to each other when edits are made in a file.
+ *
+ * ---
+ * In short, each token takes 5 integers to represent, so a specific token `i` in the file consists of the following array indices:
+ * - at index `5*i` - `deltaLine`: token line number, relative to the previous token
+ * - at index `5*i+1` - `deltaStart`: token start character, relative to the previous token (relative to 0 or the previous token's start if they are on the same line)
+ * - at index `5*i+2` - `length`: the length of the token. A token cannot be multiline.
+ * - at index `5*i+3` - `tokenType`: will be looked up in `SemanticTokensLegend.tokenTypes`. We currently ask that `tokenType` < 65536.
+ * - at index `5*i+4` - `tokenModifiers`: each set bit will be looked up in `SemanticTokensLegend.tokenModifiers`
+ *
+ * ---
+ * ### How to encode tokens
+ *
+ * Here is an example for encoding a file with 3 tokens in a uint32 array:
+ * ```
+ * { line: 2, startChar: 5, length: 3, tokenType: "property", tokenModifiers: ["private", "static"] },
+ * { line: 2, startChar: 10, length: 4, tokenType: "type", tokenModifiers: [] },
+ * { line: 5, startChar: 2, length: 7, tokenType: "class", tokenModifiers: [] }
+ * ```
+ *
+ * 1. First of all, a legend must be devised. This legend must be provided up-front and capture all possible token types.
+ * For this example, we will choose the following legend which must be passed in when registering the provider:
+ * ```
+ * tokenTypes: ['property', 'type', 'class'],
+ * tokenModifiers: ['private', 'static']
+ * ```
+ *
+ * 2. The first transformation step is to encode `tokenType` and `tokenModifiers` as integers using the legend. Token types are looked
+ * up by index, so a `tokenType` value of `1` means `tokenTypes[1]`. Multiple token modifiers can be set by using bit flags,
+ * so a `tokenModifier` value of `3` is first viewed as binary `0b00000011`, which means `[tokenModifiers[0], tokenModifiers[1]]` because
+ * bits 0 and 1 are set. Using this legend, the tokens now are:
+ * ```
+ * { line: 2, startChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 },
+ * { line: 2, startChar: 10, length: 4, tokenType: 1, tokenModifiers: 0 },
+ * { line: 5, startChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }
+ * ```
+ *
+ * 3. The next step is to represent each token relative to the previous token in the file. In this case, the second token
+ * is on the same line as the first token, so the `startChar` of the second token is made relative to the `startChar`
+ * of the first token, so it will be `10 - 5`. The third token is on a different line than the second token, so the
+ * `startChar` of the third token will not be altered:
+ * ```
+ * { deltaLine: 2, deltaStartChar: 5, length: 3, tokenType: 0, tokenModifiers: 3 },
+ * { deltaLine: 0, deltaStartChar: 5, length: 4, tokenType: 1, tokenModifiers: 0 },
+ * { deltaLine: 3, deltaStartChar: 2, length: 7, tokenType: 2, tokenModifiers: 0 }
+ * ```
+ *
+ * 4. Finally, the last step is to inline each of the 5 fields for a token in a single array, which is a memory friendly representation:
+ * ```
+ * // 1st token, 2nd token, 3rd token
+ * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]
+ * ```
+ *
+ * @see {@link SemanticTokensBuilder} for a helper to encode tokens as integers.
+ * *NOTE*: When doing edits, it is possible that multiple edits occur until the editor decides to invoke the semantic tokens provider.
+ * *NOTE*: If the provider cannot temporarily compute semantic tokens, it can indicate this by throwing an error with the message 'Busy'.
+ */
+ provideDocumentSemanticTokens(document: TextDocument, token: CancellationToken): ProviderResult;
+
+ /**
+ * Instead of always returning all the tokens in a file, it is possible for a `DocumentSemanticTokensProvider` to implement
+ * this method (`provideDocumentSemanticTokensEdits`) and then return incremental updates to the previously provided semantic tokens.
+ *
+ * ---
+ * ### How tokens change when the document changes
+ *
+ * Suppose that `provideDocumentSemanticTokens` has previously returned the following semantic tokens:
+ * ```
+ * // 1st token, 2nd token, 3rd token
+ * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]
+ * ```
+ *
+ * Also suppose that after some edits, the new semantic tokens in a file are:
+ * ```
+ * // 1st token, 2nd token, 3rd token
+ * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ]
+ * ```
+ * It is possible to express these new tokens in terms of an edit applied to the previous tokens:
+ * ```
+ * [ 2,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // old tokens
+ * [ 3,5,3,0,3, 0,5,4,1,0, 3,2,7,2,0 ] // new tokens
+ *
+ * edit: { start: 0, deleteCount: 1, data: [3] } // replace integer at offset 0 with 3
+ * ```
+ *
+ * *NOTE*: If the provider cannot compute `SemanticTokensEdits`, it can "give up" and return all the tokens in the document again.
+ * *NOTE*: All edits in `SemanticTokensEdits` contain indices in the old integers array, so they all refer to the previous result state.
+ */
+ provideDocumentSemanticTokensEdits?(document: TextDocument, previousResultId: string, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The document range semantic tokens provider interface defines the contract between extensions and
+ * semantic tokens.
+ */
+ export interface DocumentRangeSemanticTokensProvider {
+ /**
+ * @see {@link DocumentSemanticTokensProvider.provideDocumentSemanticTokens provideDocumentSemanticTokens}.
+ */
+ provideDocumentRangeSemanticTokens(document: TextDocument, range: Range, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Value-object describing what options formatting should use.
+ */
+ export interface FormattingOptions {
+
+ /**
+ * Size of a tab in spaces.
+ */
+ tabSize: number;
+
+ /**
+ * Prefer spaces over tabs.
+ */
+ insertSpaces: boolean;
+
+ /**
+ * Signature for further properties.
+ */
+ [key: string]: boolean | number | string;
+ }
+
+ /**
+ * The document formatting provider interface defines the contract between extensions and
+ * the formatting-feature.
+ */
+ export interface DocumentFormattingEditProvider {
+
+ /**
+ * Provide formatting edits for a whole document.
+ *
+ * @param document The document in which the command was invoked.
+ * @param options Options controlling formatting.
+ * @param token A cancellation token.
+ * @return A set of text edits or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideDocumentFormattingEdits(document: TextDocument, options: FormattingOptions, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The document formatting provider interface defines the contract between extensions and
+ * the formatting-feature.
+ */
+ export interface DocumentRangeFormattingEditProvider {
+
+ /**
+ * Provide formatting edits for a range in a document.
+ *
+ * The given range is a hint and providers can decide to format a smaller
+ * or larger range. Often this is done by adjusting the start and end
+ * of the range to full syntax nodes.
+ *
+ * @param document The document in which the command was invoked.
+ * @param range The range which should be formatted.
+ * @param options Options controlling formatting.
+ * @param token A cancellation token.
+ * @return A set of text edits or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideDocumentRangeFormattingEdits(document: TextDocument, range: Range, options: FormattingOptions, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * The document formatting provider interface defines the contract between extensions and
+ * the formatting-feature.
+ */
+ export interface OnTypeFormattingEditProvider {
+
+ /**
+ * Provide formatting edits after a character has been typed.
+ *
+ * The given position and character should hint to the provider
+ * what range the position to expand to, like find the matching `{`
+ * when `}` has been entered.
+ *
+ * @param document The document in which the command was invoked.
+ * @param position The position at which the command was invoked.
+ * @param ch The character that has been typed.
+ * @param options Options controlling formatting.
+ * @param token A cancellation token.
+ * @return A set of text edits or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Represents a parameter of a callable-signature. A parameter can
+ * have a label and a doc-comment.
+ */
+ export class ParameterInformation {
+
+ /**
+ * The label of this signature.
+ *
+ * Either a string or inclusive start and exclusive end offsets within its containing
+ * {@link SignatureInformation.label signature label}. *Note*: A label of type string must be
+ * a substring of its containing signature information's {@link SignatureInformation.label label}.
+ */
+ label: string | [number, number];
+
+ /**
+ * The human-readable doc-comment of this signature. Will be shown
+ * in the UI but can be omitted.
+ */
+ documentation?: string | MarkdownString;
+
+ /**
+ * Creates a new parameter information object.
+ *
+ * @param label A label string or inclusive start and exclusive end offsets within its containing signature label.
+ * @param documentation A doc string.
+ */
+ constructor(label: string | [number, number], documentation?: string | MarkdownString);
+ }
+
+ /**
+ * Represents the signature of something callable. A signature
+ * can have a label, like a function-name, a doc-comment, and
+ * a set of parameters.
+ */
+ export class SignatureInformation {
+
+ /**
+ * The label of this signature. Will be shown in
+ * the UI.
+ */
+ label: string;
+
+ /**
+ * The human-readable doc-comment of this signature. Will be shown
+ * in the UI but can be omitted.
+ */
+ documentation?: string | MarkdownString;
+
+ /**
+ * The parameters of this signature.
+ */
+ parameters: ParameterInformation[];
+
+ /**
+ * The index of the active parameter.
+ *
+ * If provided, this is used in place of {@linkcode SignatureHelp.activeSignature}.
+ */
+ activeParameter?: number;
+
+ /**
+ * Creates a new signature information object.
+ *
+ * @param label A label string.
+ * @param documentation A doc string.
+ */
+ constructor(label: string, documentation?: string | MarkdownString);
+ }
+
+ /**
+ * Signature help represents the signature of something
+ * callable. There can be multiple signatures but only one
+ * active and only one active parameter.
+ */
+ export class SignatureHelp {
+
+ /**
+ * One or more signatures.
+ */
+ signatures: SignatureInformation[];
+
+ /**
+ * The active signature.
+ */
+ activeSignature: number;
+
+ /**
+ * The active parameter of the active signature.
+ */
+ activeParameter: number;
+ }
+
+ /**
+ * How a {@linkcode SignatureHelpProvider} was triggered.
+ */
+ export enum SignatureHelpTriggerKind {
+ /**
+ * Signature help was invoked manually by the user or by a command.
+ */
+ Invoke = 1,
+
+ /**
+ * Signature help was triggered by a trigger character.
+ */
+ TriggerCharacter = 2,
+
+ /**
+ * Signature help was triggered by the cursor moving or by the document content changing.
+ */
+ ContentChange = 3,
+ }
+
+ /**
+ * Additional information about the context in which a
+ * {@linkcode SignatureHelpProvider.provideSignatureHelp SignatureHelpProvider} was triggered.
+ */
+ export interface SignatureHelpContext {
+ /**
+ * Action that caused signature help to be triggered.
+ */
+ readonly triggerKind: SignatureHelpTriggerKind;
+
+ /**
+ * Character that caused signature help to be triggered.
+ *
+ * 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;
+
+ /**
+ * `true` if signature help was already showing when it was triggered.
+ *
+ * Retriggers occur when the signature help is already active and can be caused by actions such as
+ * typing a trigger character, a cursor move, or document content changes.
+ */
+ readonly isRetrigger: boolean;
+
+ /**
+ * The currently active {@linkcode SignatureHelp}.
+ *
+ * The `activeSignatureHelp` has its [`SignatureHelp.activeSignature`] field updated based on
+ * the user arrowing through available signatures.
+ */
+ readonly activeSignatureHelp?: SignatureHelp;
+ }
+
+ /**
+ * The signature help provider interface defines the contract between extensions and
+ * the [parameter hints](https://code.visualstudio.com/docs/editor/intellisense)-feature.
+ */
+ export interface SignatureHelpProvider {
+
+ /**
+ * Provide help for the signature at the given position and document.
+ *
+ * @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.
+ * @param context Information about how signature help was triggered.
+ *
+ * @return Signature help or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken, context: SignatureHelpContext): ProviderResult;
+ }
+
+ /**
+ * Metadata about a registered {@linkcode SignatureHelpProvider}.
+ */
+ export interface SignatureHelpProviderMetadata {
+ /**
+ * List of characters that trigger signature help.
+ */
+ readonly triggerCharacters: readonly string[];
+
+ /**
+ * List of characters that re-trigger signature help.
+ *
+ * These trigger characters are only active when signature help is already showing. All trigger characters
+ * are also counted as re-trigger characters.
+ */
+ readonly retriggerCharacters: readonly string[];
+ }
+
+ /**
+ * A structured label for a {@link CompletionItem completion item}.
+ */
+ export interface CompletionItemLabel {
+
+ /**
+ * The label of this completion item.
+ *
+ * By default this is also the text that is inserted when this completion is selected.
+ */
+ label: string;
+
+ /**
+ * An optional string which is rendered less prominently directly after {@link CompletionItemLabel.label label},
+ * without any spacing. Should be used for function signatures or type annotations.
+ */
+ detail?: string;
+
+ /**
+ * An optional string which is rendered less prominently after {@link CompletionItemLabel.detail}. Should be used
+ * for fully qualified names or file path.
+ */
+ description?: string;
+ }
+
+ /**
+ * Completion item kinds.
+ */
+ export enum CompletionItemKind {
+ Text = 0,
+ Method = 1,
+ Function = 2,
+ Constructor = 3,
+ Field = 4,
+ Variable = 5,
+ Class = 6,
+ Interface = 7,
+ Module = 8,
+ Property = 9,
+ Unit = 10,
+ Value = 11,
+ Enum = 12,
+ Keyword = 13,
+ Snippet = 14,
+ Color = 15,
+ Reference = 17,
+ File = 16,
+ Folder = 18,
+ EnumMember = 19,
+ Constant = 20,
+ Struct = 21,
+ Event = 22,
+ Operator = 23,
+ TypeParameter = 24,
+ User = 25,
+ Issue = 26,
+ }
+
+ /**
+ * Completion item tags are extra annotations that tweak the rendering of a completion
+ * item.
+ */
+ export enum CompletionItemTag {
+ /**
+ * Render a completion as obsolete, usually using a strike-out.
+ */
+ Deprecated = 1
+ }
+
+ /**
+ * A completion item represents a text snippet that is proposed to complete text that is being typed.
+ *
+ * It is sufficient to create a completion item from just a {@link CompletionItem.label label}. In that
+ * case the completion item will replace the {@link TextDocument.getWordRangeAtPosition word}
+ * until the cursor with the given label or {@link CompletionItem.insertText insertText}. Otherwise the
+ * given {@link CompletionItem.textEdit edit} is used.
+ *
+ * When selecting a completion item in the editor its defined or synthesized text edit will be applied
+ * to *all* cursors/selections whereas {@link CompletionItem.additionalTextEdits additionalTextEdits} will be
+ * applied as provided.
+ *
+ * @see {@link CompletionItemProvider.provideCompletionItems}
+ * @see {@link CompletionItemProvider.resolveCompletionItem}
+ */
+ export class CompletionItem {
+
+ /**
+ * The label of this completion item. By default
+ * this is also the text that is inserted when selecting
+ * this completion.
+ */
+ label: string | CompletionItemLabel;
+
+ /**
+ * The kind of this completion item. Based on the kind
+ * an icon is chosen by the editor.
+ */
+ kind?: CompletionItemKind;
+
+ /**
+ * Tags for this completion item.
+ */
+ tags?: readonly CompletionItemTag[];
+
+ /**
+ * A human-readable string with additional information
+ * about this item, like type or symbol information.
+ */
+ detail?: string;
+
+ /**
+ * A human-readable string that represents a doc-comment.
+ */
+ documentation?: string | MarkdownString;
+
+ /**
+ * A string that should be used when comparing this item
+ * with other items. When `falsy` the {@link CompletionItem.label label}
+ * is used.
+ *
+ * Note that `sortText` is only used for the initial ordering of completion
+ * items. When having a leading word (prefix) ordering is based on how
+ * well completions match that prefix and the initial ordering is only used
+ * when completions match equally well. The prefix is defined by the
+ * {@linkcode CompletionItem.range range}-property and can therefore be different
+ * for each completion.
+ */
+ sortText?: string;
+
+ /**
+ * A string that should be used when filtering a set of
+ * completion items. When `falsy` the {@link CompletionItem.label label}
+ * is used.
+ *
+ * Note that the filter text is matched against the leading word (prefix) which is defined
+ * by the {@linkcode CompletionItem.range range}-property.
+ */
+ filterText?: string;
+
+ /**
+ * Select this item when showing. *Note* that only one completion item can be selected and
+ * that the editor decides which item that is. The rule is that the *first* item of those
+ * that match best is selected.
+ */
+ preselect?: boolean;
+
+ /**
+ * A string or snippet that should be inserted in a document when selecting
+ * this completion. When `falsy` the {@link CompletionItem.label label}
+ * is used.
+ */
+ insertText?: string | SnippetString;
+
+ /**
+ * A range or a insert and replace range selecting the text that should be replaced by this completion item.
+ *
+ * When omitted, the range of the {@link TextDocument.getWordRangeAtPosition current word} is used as replace-range
+ * and as insert-range the start of the {@link TextDocument.getWordRangeAtPosition current word} to the
+ * current position is used.
+ *
+ * *Note 1:* A range must be a {@link Range.isSingleLine single line} and it must
+ * {@link Range.contains contain} the position at which completion has been {@link CompletionItemProvider.provideCompletionItems requested}.
+ * *Note 2:* A insert range must be a prefix of a replace range, that means it must be contained and starting at the same position.
+ */
+ range?: Range | { inserting: Range; replacing: Range; };
+
+ /**
+ * An optional set of characters that when pressed while this completion is active will accept it first and
+ * then type that character. *Note* that all commit characters should have `length=1` and that superfluous
+ * characters will be ignored.
+ */
+ commitCharacters?: string[];
+
+ /**
+ * Keep whitespace of the {@link CompletionItem.insertText insertText} as is. By default, the editor adjusts leading
+ * whitespace of new lines so that they match the indentation of the line for which the item is accepted - setting
+ * this to `true` will prevent that.
+ */
+ keepWhitespace?: boolean;
+
+ /**
+ * @deprecated Use `CompletionItem.insertText` and `CompletionItem.range` instead.
+ *
+ * An {@link TextEdit edit} which is applied to a document when selecting
+ * this completion. When an edit is provided the value of
+ * {@link CompletionItem.insertText insertText} is ignored.
+ *
+ * The {@link Range} of the edit must be single-line and on the same
+ * line completions were {@link CompletionItemProvider.provideCompletionItems requested} at.
+ */
+ textEdit?: TextEdit;
+
+ /**
+ * An optional array of additional {@link TextEdit text edits} that are applied when
+ * selecting this completion. Edits must not overlap with the main {@link CompletionItem.textEdit edit}
+ * nor with themselves.
+ */
+ additionalTextEdits?: TextEdit[];
+
+ /**
+ * An optional {@link Command} that is executed *after* inserting this completion. *Note* that
+ * additional modifications to the current document should be described with the
+ * {@link CompletionItem.additionalTextEdits additionalTextEdits}-property.
+ */
+ command?: Command;
+
+ /**
+ * Creates a new completion item.
+ *
+ * Completion items must have at least a {@link CompletionItem.label label} which then
+ * will be used as insert text as well as for sorting and filtering.
+ *
+ * @param label The label of the completion.
+ * @param kind The {@link CompletionItemKind kind} of the completion.
+ */
+ constructor(label: string | CompletionItemLabel, kind?: CompletionItemKind);
+ }
+
+ /**
+ * Represents a collection of {@link CompletionItem completion items} to be presented
+ * in the editor.
+ */
+ export class CompletionList {
+
+ /**
+ * This list is not complete. Further typing should result in recomputing
+ * this list.
+ */
+ isIncomplete?: boolean;
+
+ /**
+ * The completion items.
+ */
+ items: T[];
+
+ /**
+ * Creates a new completion list.
+ *
+ * @param items The completion items.
+ * @param isIncomplete The list is not complete.
+ */
+ constructor(items?: T[], isIncomplete?: boolean);
+ }
+
+ /**
+ * How a {@link CompletionItemProvider completion provider} was triggered
+ */
+ export enum CompletionTriggerKind {
+ /**
+ * Completion was triggered normally.
+ */
+ Invoke = 0,
+ /**
+ * Completion was triggered by a trigger character.
+ */
+ TriggerCharacter = 1,
+ /**
+ * Completion was re-triggered as current completion list is incomplete
+ */
+ TriggerForIncompleteCompletions = 2
+ }
+
+ /**
+ * Contains additional information about the context in which
+ * {@link CompletionItemProvider.provideCompletionItems completion provider} is triggered.
+ */
+ export interface CompletionContext {
+ /**
+ * How the completion was triggered.
+ */
+ readonly triggerKind: CompletionTriggerKind;
+
+ /**
+ * Character that triggered the completion item provider.
+ *
+ * `undefined` if provider was not triggered by a character.
+ *
+ * The trigger character is already in the document when the completion provider is triggered.
+ */
+ readonly triggerCharacter?: string;
+ }
+
+ /**
+ * The completion item provider interface defines the contract between extensions and
+ * [IntelliSense](https://code.visualstudio.com/docs/editor/intellisense).
+ *
+ * Providers can delay the computation of the {@linkcode CompletionItem.detail detail}
+ * and {@linkcode CompletionItem.documentation documentation} properties by implementing the
+ * {@linkcode CompletionItemProvider.resolveCompletionItem resolveCompletionItem}-function. However, properties that
+ * are needed for the initial sorting and filtering, like `sortText`, `filterText`, `insertText`, and `range`, must
+ * not be changed during resolve.
+ *
+ * Providers are asked for completions either explicitly by a user gesture or -depending on the configuration-
+ * implicitly when typing words or trigger characters.
+ */
+ export interface CompletionItemProvider {
+
+ /**
+ * Provide completion items for the given position and document.
+ *
+ * @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.
+ * @param context How the completion was triggered.
+ *
+ * @return An array of completions, a {@link CompletionList completion list}, or a thenable that resolves to either.
+ * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult>;
+
+ /**
+ * Given a completion item fill in more data, like {@link CompletionItem.documentation doc-comment}
+ * or {@link CompletionItem.detail details}.
+ *
+ * The editor will only resolve a completion item once.
+ *
+ * *Note* that this function is called when completion items are already showing in the UI or when an item has been
+ * selected for insertion. Because of that, no property that changes the presentation (label, sorting, filtering etc)
+ * or the (primary) insert behaviour ({@link CompletionItem.insertText insertText}) can be changed.
+ *
+ * This function may fill in {@link CompletionItem.additionalTextEdits additionalTextEdits}. However, that means an item might be
+ * inserted *before* resolving is done and in that case the editor will do a best effort to still apply those additional
+ * text edits.
+ *
+ * @param item A completion item currently active in the UI.
+ * @param token A cancellation token.
+ * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given
+ * `item`. When no result is returned, the given `item` will be used.
+ */
+ resolveCompletionItem?(item: T, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A document link is a range in a text document that links to an internal or external resource, like another
+ * text document or a web site.
+ */
+ export class DocumentLink {
+
+ /**
+ * The range this link applies to.
+ */
+ range: Range;
+
+ /**
+ * The uri this link points to.
+ */
+ target?: Uri;
+
+ /**
+ * The tooltip text when you hover over this link.
+ *
+ * If a tooltip is provided, is will be displayed in a string that includes instructions on how to
+ * trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS,
+ * user settings, and localization.
+ */
+ tooltip?: string;
+
+ /**
+ * Creates a new document link.
+ *
+ * @param range The range the document link applies to. Must not be empty.
+ * @param target The uri the document link points to.
+ */
+ constructor(range: Range, target?: Uri);
+ }
+
+ /**
+ * The document link provider defines the contract between extensions and feature of showing
+ * links in the editor.
+ */
+ export interface DocumentLinkProvider {
+
+ /**
+ * Provide links for the given document. Note that the editor ships with a default provider that detects
+ * `http(s)` and `file` links.
+ *
+ * @param document The document in which the command was invoked.
+ * @param token A cancellation token.
+ * @return An array of {@link DocumentLink document links} or a thenable that resolves to such. The lack of a result
+ * can be signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult;
+
+ /**
+ * Given a link fill in its {@link DocumentLink.target target}. This method is called when an incomplete
+ * link is selected in the UI. Providers can implement this method and return incomplete links
+ * (without target) from the {@linkcode DocumentLinkProvider.provideDocumentLinks provideDocumentLinks} method which
+ * often helps to improve performance.
+ *
+ * @param link The link that is to be resolved.
+ * @param token A cancellation token.
+ */
+ resolveDocumentLink?(link: T, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Represents a color in RGBA space.
+ */
+ export class Color {
+
+ /**
+ * The red component of this color in the range [0-1].
+ */
+ readonly red: number;
+
+ /**
+ * The green component of this color in the range [0-1].
+ */
+ readonly green: number;
+
+ /**
+ * The blue component of this color in the range [0-1].
+ */
+ readonly blue: number;
+
+ /**
+ * The alpha component of this color in the range [0-1].
+ */
+ readonly alpha: number;
+
+ /**
+ * Creates a new color instance.
+ *
+ * @param red The red component.
+ * @param green The green component.
+ * @param blue The blue component.
+ * @param alpha The alpha component.
+ */
+ constructor(red: number, green: number, blue: number, alpha: number);
+ }
+
+ /**
+ * Represents a color range from a document.
+ */
+ export class ColorInformation {
+
+ /**
+ * The range in the document where this color appears.
+ */
+ range: Range;
+
+ /**
+ * The actual color value for this color range.
+ */
+ color: Color;
+
+ /**
+ * Creates a new color range.
+ *
+ * @param range The range the color appears in. Must not be empty.
+ * @param color The value of the color.
+ * @param format The format in which this color is currently formatted.
+ */
+ constructor(range: Range, color: Color);
+ }
+
+ /**
+ * A color presentation object describes how a {@linkcode Color} should be represented as text and what
+ * edits are required to refer to it from source code.
+ *
+ * For some languages one color can have multiple presentations, e.g. css can represent the color red with
+ * the constant `Red`, the hex-value `#ff0000`, or in rgba and hsla forms. In csharp other representations
+ * apply, e.g. `System.Drawing.Color.Red`.
+ */
+ export class ColorPresentation {
+
+ /**
+ * The label of this color presentation. It will be shown on the color
+ * picker header. By default this is also the text that is inserted when selecting
+ * this color presentation.
+ */
+ label: string;
+
+ /**
+ * An {@link TextEdit edit} which is applied to a document when selecting
+ * this presentation for the color. When `falsy` the {@link ColorPresentation.label label}
+ * is used.
+ */
+ textEdit?: TextEdit;
+
+ /**
+ * An optional array of additional {@link TextEdit text edits} that are applied when
+ * selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves.
+ */
+ additionalTextEdits?: TextEdit[];
+
+ /**
+ * Creates a new color presentation.
+ *
+ * @param label The label of this color presentation.
+ */
+ constructor(label: string);
+ }
+
+ /**
+ * The document color provider defines the contract between extensions and feature of
+ * picking and modifying colors in the editor.
+ */
+ export interface DocumentColorProvider {
+
+ /**
+ * Provide colors for the given document.
+ *
+ * @param document The document in which the command was invoked.
+ * @param token A cancellation token.
+ * @return An array of {@link ColorInformation color information} or a thenable that resolves to such. The lack of a result
+ * can be signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideDocumentColors(document: TextDocument, token: CancellationToken): ProviderResult;
+
+ /**
+ * Provide {@link ColorPresentation representations} for a color.
+ *
+ * @param color The color to show and insert.
+ * @param context A context object with additional information
+ * @param token A cancellation token.
+ * @return An array of color presentations or a thenable that resolves to such. The lack of a result
+ * can be signaled by returning `undefined`, `null`, or an empty array.
+ */
+ provideColorPresentations(color: Color, context: { document: TextDocument, range: Range }, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A line based folding range. To be valid, start and end line must be bigger than zero and smaller than the number of lines in the document.
+ * Invalid ranges will be ignored.
+ */
+ export class FoldingRange {
+
+ /**
+ * The zero-based start line of the range to fold. The folded area starts after the line's last character.
+ * To be valid, the end must be zero or larger and smaller than the number of lines in the document.
+ */
+ start: number;
+
+ /**
+ * The zero-based end line of the range to fold. The folded area ends with the line's last character.
+ * To be valid, the end must be zero or larger and smaller than the number of lines in the document.
+ */
+ end: number;
+
+ /**
+ * Describes the {@link FoldingRangeKind Kind} of the folding range such as {@link FoldingRangeKind.Comment Comment} or
+ * {@link FoldingRangeKind.Region Region}. The kind is used to categorize folding ranges and used by commands
+ * like 'Fold all comments'. See
+ * {@link FoldingRangeKind} for an enumeration of all kinds.
+ * If not set, the range is originated from a syntax element.
+ */
+ kind?: FoldingRangeKind;
+
+ /**
+ * Creates a new folding range.
+ *
+ * @param start The start line of the folded range.
+ * @param end The end line of the folded range.
+ * @param kind The kind of the folding range.
+ */
+ constructor(start: number, end: number, kind?: FoldingRangeKind);
+ }
+
+ /**
+ * An enumeration of specific folding range kinds. The kind is an optional field of a {@link FoldingRange}
+ * and is used to distinguish specific folding ranges such as ranges originated from comments. The kind is used by commands like
+ * `Fold all comments` or `Fold all regions`.
+ * If the kind is not set on the range, the range originated from a syntax element other than comments, imports or region markers.
+ */
+ export enum FoldingRangeKind {
+ /**
+ * Kind for folding range representing a comment.
+ */
+ Comment = 1,
+ /**
+ * Kind for folding range representing a import.
+ */
+ Imports = 2,
+ /**
+ * Kind for folding range representing regions originating from folding markers like `#region` and `#endregion`.
+ */
+ Region = 3
+ }
+
+ /**
+ * Folding context (for future use)
+ */
+ export interface FoldingContext {
+ }
+
+ /**
+ * The folding range provider interface defines the contract between extensions and
+ * [Folding](https://code.visualstudio.com/docs/editor/codebasics#_folding) in the editor.
+ */
+ export interface FoldingRangeProvider {
+
+ /**
+ * An optional event to signal that the folding ranges from this provider have changed.
+ */
+ onDidChangeFoldingRanges?: Event;
+
+ /**
+ * Returns a list of folding ranges or null and undefined if the provider
+ * does not want to participate or was cancelled.
+ * @param document The document in which the command was invoked.
+ * @param context Additional context information (for future use)
+ * @param token A cancellation token.
+ */
+ provideFoldingRanges(document: TextDocument, context: FoldingContext, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A selection range represents a part of a selection hierarchy. A selection range
+ * may have a parent selection range that contains it.
+ */
+ export class SelectionRange {
+
+ /**
+ * The {@link Range} of this selection range.
+ */
+ range: Range;
+
+ /**
+ * The parent selection range containing this range.
+ */
+ parent?: SelectionRange;
+
+ /**
+ * Creates a new selection range.
+ *
+ * @param range The range of the selection range.
+ * @param parent The parent of the selection range.
+ */
+ constructor(range: Range, parent?: SelectionRange);
+ }
+
+ export interface SelectionRangeProvider {
+ /**
+ * Provide selection ranges for the given positions.
+ *
+ * Selection ranges should be computed individually and independent for each position. The editor will merge
+ * and deduplicate ranges but providers must return hierarchies of selection ranges so that a range
+ * is {@link Range.contains contained} by its parent.
+ *
+ * @param document The document in which the command was invoked.
+ * @param positions The positions at which the command was invoked.
+ * @param token A cancellation token.
+ * @return Selection ranges or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideSelectionRanges(document: TextDocument, positions: Position[], token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Represents programming constructs like functions or constructors in the context
+ * of call hierarchy.
+ */
+ export class CallHierarchyItem {
+ /**
+ * The name of this item.
+ */
+ name: string;
+
+ /**
+ * The kind of this item.
+ */
+ kind: SymbolKind;
+
+ /**
+ * Tags for this item.
+ */
+ tags?: readonly SymbolTag[];
+
+ /**
+ * 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 function.
+ * Must be contained by the {@linkcode CallHierarchyItem.range range}.
+ */
+ selectionRange: Range;
+
+ /**
+ * Creates a new call hierarchy item.
+ */
+ constructor(kind: SymbolKind, name: string, detail: string, uri: Uri, range: Range, selectionRange: Range);
+ }
+
+ /**
+ * Represents an incoming call, e.g. a caller of a method or constructor.
+ */
+ export class CallHierarchyIncomingCall {
+
+ /**
+ * The item that makes the call.
+ */
+ from: CallHierarchyItem;
+
+ /**
+ * The range at which at which the calls appears. This is relative to the caller
+ * denoted by {@linkcode CallHierarchyIncomingCall.from this.from}.
+ */
+ fromRanges: Range[];
+
+ /**
+ * Create a new call object.
+ *
+ * @param item The item making the call.
+ * @param fromRanges The ranges at which the calls appear.
+ */
+ constructor(item: CallHierarchyItem, fromRanges: Range[]);
+ }
+
+ /**
+ * Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc.
+ */
+ export class CallHierarchyOutgoingCall {
+
+ /**
+ * The item that is called.
+ */
+ to: CallHierarchyItem;
+
+ /**
+ * The range at which this item is called. This is the range relative to the caller, e.g the item
+ * passed to {@linkcode CallHierarchyProvider.provideCallHierarchyOutgoingCalls provideCallHierarchyOutgoingCalls}
+ * and not {@linkcode CallHierarchyOutgoingCall.to this.to}.
+ */
+ fromRanges: Range[];
+
+ /**
+ * Create a new call object.
+ *
+ * @param item The item being called
+ * @param fromRanges The ranges at which the calls appear.
+ */
+ constructor(item: CallHierarchyItem, fromRanges: Range[]);
+ }
+
+ /**
+ * The call hierarchy provider interface describes the contract between extensions
+ * and the call hierarchy feature which allows to browse calls and caller of function,
+ * methods, constructor etc.
+ */
+ export interface CallHierarchyProvider {
+
+ /**
+ * Bootstraps call hierarchy by returning the item that is denoted by the given document
+ * and position. This item will be used as entry into the call 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 A call hierarchy item or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+
+ /**
+ * Provide all incoming calls for an item, e.g all callers for a method. In graph terms this describes directed
+ * and annotated edges inside the call 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 incoming calls should be computed.
+ * @param token A cancellation token.
+ * @returns A set of incoming calls or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideCallHierarchyIncomingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult;
+
+ /**
+ * Provide all outgoing calls for an item, e.g call calls to functions, methods, or constructors from the given item. In
+ * graph terms this describes directed and annotated edges inside the call 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 outgoing calls should be computed.
+ * @param token A cancellation token.
+ * @returns A set of outgoing calls or a thenable that resolves to such. The lack of a result can be
+ * signaled by returning `undefined` or `null`.
+ */
+ provideCallHierarchyOutgoingCalls(item: CallHierarchyItem, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * Represents a list of ranges that can be edited together along with a word pattern to describe valid range contents.
+ */
+ export class LinkedEditingRanges {
+ /**
+ * Create a new linked editing ranges object.
+ *
+ * @param ranges A list of ranges that can be edited together
+ * @param wordPattern An optional word pattern that describes valid contents for the given ranges
+ */
+ constructor(ranges: Range[], wordPattern?: RegExp);
+
+ /**
+ * A list of ranges that can be edited together. The ranges must have
+ * identical length and text content. The ranges cannot overlap.
+ */
+ readonly ranges: Range[];
+
+ /**
+ * 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;
+ }
+
+ /**
+ * The linked editing range provider interface defines the contract between extensions and
+ * the linked editing feature.
+ */
+ export interface LinkedEditingRangeProvider {
+ /**
+ * For a given position in a document, returns the range of the symbol at the position and all ranges
+ * that have the same content. A change to one of the ranges can be applied to all other ranges if the new content
+ * is valid. An optional word pattern can be returned with the result to describe valid contents.
+ * If no result-specific word pattern is provided, the word pattern from the language configuration is used.
+ *
+ * @param document The document in which the provider was invoked.
+ * @param position The position at which the provider was invoked.
+ * @param token A cancellation token.
+ * @return A list of ranges that can be edited together
+ */
+ provideLinkedEditingRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A tuple of two characters, like a pair of
+ * opening and closing brackets.
+ */
+ export type CharacterPair = [string, string];
+
+ /**
+ * Describes how comments for a language work.
+ */
+ export interface CommentRule {
+
+ /**
+ * The line comment token, like `// this is a comment`
+ */
+ lineComment?: string;
+
+ /**
+ * The block comment character pair, like `/* block comment */`
+ */
+ blockComment?: CharacterPair;
+ }
+
+ /**
+ * Describes indentation rules for a language.
+ */
+ export interface IndentationRule {
+ /**
+ * If a line matches this pattern, then all the lines after it should be unindented once (until another rule matches).
+ */
+ decreaseIndentPattern: RegExp;
+ /**
+ * If a line matches this pattern, then all the lines after it should be indented once (until another rule matches).
+ */
+ increaseIndentPattern: RegExp;
+ /**
+ * If a line matches this pattern, then **only the next line** after it should be indented once.
+ */
+ indentNextLinePattern?: RegExp;
+ /**
+ * If a line matches this pattern, then its indentation should not be changed and it should not be evaluated against the other rules.
+ */
+ unIndentedLinePattern?: RegExp;
+ }
+
+ /**
+ * Describes what to do with the indentation when pressing Enter.
+ */
+ export enum IndentAction {
+ /**
+ * Insert new line and copy the previous line's indentation.
+ */
+ None = 0,
+ /**
+ * Insert new line and indent once (relative to the previous line's indentation).
+ */
+ Indent = 1,
+ /**
+ * Insert two new lines:
+ * - the first one indented which will hold the cursor
+ * - the second one at the same indentation level
+ */
+ IndentOutdent = 2,
+ /**
+ * Insert new line and outdent once (relative to the previous line's indentation).
+ */
+ Outdent = 3
+ }
+
+ /**
+ * Describes what to do when pressing Enter.
+ */
+ export interface EnterAction {
+ /**
+ * Describe what to do with the indentation.
+ */
+ indentAction: IndentAction;
+ /**
+ * Describes text to be appended after the new line and after the indentation.
+ */
+ appendText?: string;
+ /**
+ * Describes the number of characters to remove from the new line's indentation.
+ */
+ removeText?: number;
+ }
+
+ /**
+ * Describes a rule to be evaluated when pressing Enter.
+ */
+ export interface OnEnterRule {
+ /**
+ * This rule will only execute if the text before the cursor matches this regular expression.
+ */
+ beforeText: RegExp;
+ /**
+ * This rule will only execute if the text after the cursor matches this regular expression.
+ */
+ afterText?: RegExp;
+ /**
+ * This rule will only execute if the text above the current line matches this regular expression.
+ */
+ previousLineText?: RegExp;
+ /**
+ * The action to execute.
+ */
+ action: EnterAction;
+ }
+
+ /**
+ * The language configuration interfaces defines the contract between extensions
+ * and various editor features, like automatic bracket insertion, automatic indentation etc.
+ */
+ export interface LanguageConfiguration {
+ /**
+ * The language's comment settings.
+ */
+ comments?: CommentRule;
+ /**
+ * The language's brackets.
+ * This configuration implicitly affects pressing Enter around these brackets.
+ */
+ brackets?: CharacterPair[];
+ /**
+ * The language's word definition.
+ * If the language supports Unicode identifiers (e.g. JavaScript), it is preferable
+ * to provide a word definition that uses exclusion of known separators.
+ * e.g.: A regex that matches anything except known separators (and dot is allowed to occur in a floating point number):
+ * /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g
+ */
+ wordPattern?: RegExp;
+ /**
+ * The language's indentation settings.
+ */
+ indentationRules?: IndentationRule;
+ /**
+ * The language's rules to be evaluated when pressing Enter.
+ */
+ onEnterRules?: OnEnterRule[];
+
+ /**
+ * **Deprecated** Do not use.
+ *
+ * @deprecated Will be replaced by a better API soon.
+ */
+ __electricCharacterSupport?: {
+ /**
+ * This property is deprecated and will be **ignored** from
+ * the editor.
+ * @deprecated
+ */
+ brackets?: any;
+ /**
+ * This property is deprecated and not fully supported anymore by
+ * the editor (scope and lineStart are ignored).
+ * Use the autoClosingPairs property in the language configuration file instead.
+ * @deprecated
+ */
+ docComment?: {
+ scope: string;
+ open: string;
+ lineStart: string;
+ close?: string;
+ };
+ };
+
+ /**
+ * **Deprecated** Do not use.
+ *
+ * @deprecated * Use the autoClosingPairs property in the language configuration file instead.
+ */
+ __characterPairSupport?: {
+ autoClosingPairs: {
+ open: string;
+ close: string;
+ notIn?: string[];
+ }[];
+ };
+ }
+
+ /**
+ * The configuration target
+ */
+ export enum ConfigurationTarget {
+ /**
+ * Global configuration
+ */
+ Global = 1,
+
+ /**
+ * Workspace configuration
+ */
+ Workspace = 2,
+
+ /**
+ * Workspace folder configuration
+ */
+ WorkspaceFolder = 3
+ }
+
+ /**
+ * Represents the configuration. It is a merged view of
+ *
+ * - *Default Settings*
+ * - *Global (User) Settings*
+ * - *Workspace settings*
+ * - *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.
+ *
+ * ```
+ * `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
+ *
+ * ```ts
+ * defaultValue = 'on';
+ * globalValue = 'relative'
+ * workspaceFolderValue = 'off'
+ * value = 'off'
+ * ```
+ *
+ * Example 2: Language Values
+ *
+ * ```ts
+ * defaultValue = 'on';
+ * globalValue = 'relative'
+ * workspaceFolderValue = 'off'
+ * globalLanguageValue = 'on'
+ * value = 'on'
+ * ```
+ *
+ * Example 3: Object Values
+ *
+ * ```ts
+ * defaultValue = { "a": 1, "b": 2 };
+ * globalValue = { "b": 3, "c": 4 };
+ * value = { "a": 1, "b": 3, "c": 4 };
+ * ```
+ *
+ * *Note:* Workspace and Workspace Folder configurations contains `launch` and `tasks` settings. Their basename will be
+ * part of the section identifier. The following snippets shows how to retrieve all configurations
+ * from `launch.json`:
+ *
+ * ```ts
+ * // launch.json configuration
+ * const config = workspace.getConfiguration('launch', vscode.workspace.workspaceFolders[0].uri);
+ *
+ * // retrieve values
+ * const values = config.get('configurations');
+ * ```
+ *
+ * Refer to [Settings](https://code.visualstudio.com/docs/getstarted/settings) for more information.
+ */
+ export interface WorkspaceConfiguration {
+
+ /**
+ * Return a value from this configuration.
+ *
+ * @param section Configuration name, supports _dotted_ names.
+ * @return The value `section` denotes or `undefined`.
+ */
+ get(section: string): T | undefined;
+
+ /**
+ * Return a value from this configuration.
+ *
+ * @param section Configuration name, supports _dotted_ names.
+ * @param defaultValue A value should be returned when no value could be found, is `undefined`.
+ * @return The value `section` denotes or the default.
+ */
+ get(section: string, defaultValue: T): T;
+
+ /**
+ * Check if this configuration has a certain value.
+ *
+ * @param section Configuration name, supports _dotted_ names.
+ * @return `true` if the section doesn't resolve to `undefined`.
+ */
+ has(section: string): boolean;
+
+ /**
+ * Retrieve all information about a configuration setting. A configuration value
+ * often consists of a *default* value, a global or installation-wide value,
+ * a workspace-specific value, folder-specific value
+ * and language-specific values (if {@link WorkspaceConfiguration} is scoped to a language).
+ *
+ * Also provides all language ids under which the given configuration setting is defined.
+ *
+ * *Note:* The configuration name must denote a leaf in the configuration tree
+ * (`editor.fontSize` vs `editor`) otherwise no result is returned.
+ *
+ * @param section Configuration name, supports _dotted_ names.
+ * @return Information about a configuration setting or `undefined`.
+ */
+ inspect(section: string): {
+ key: string;
+
+ defaultValue?: T;
+ globalValue?: T;
+ workspaceValue?: T,
+ workspaceFolderValue?: T,
+
+ defaultLanguageValue?: T;
+ globalLanguageValue?: T;
+ workspaceLanguageValue?: T;
+ workspaceFolderLanguageValue?: T;
+
+ languageIds?: string[];
+
+ } | undefined;
+
+ /**
+ * Update a configuration value. The updated configuration values are persisted.
+ *
+ * A value can be changed in
+ *
+ * - {@link ConfigurationTarget.Global Global settings}: Changes the value for all instances of the editor.
+ * - {@link ConfigurationTarget.Workspace Workspace settings}: Changes the value for current workspace, if available.
+ * - {@link ConfigurationTarget.WorkspaceFolder Workspace folder settings}: Changes the value for settings from one of the {@link workspace.workspaceFolders Workspace Folders} under which the requested resource belongs to.
+ * - Language settings: Changes the value for the requested languageId.
+ *
+ * *Note:* To remove a configuration value use `undefined`, like so: `config.update('somekey', undefined)`
+ *
+ * @param section Configuration name, supports _dotted_ names.
+ * @param value The new value.
+ * @param configurationTarget The {@link ConfigurationTarget configuration target} or a boolean value.
+ * - If `true` updates {@link ConfigurationTarget.Global Global settings}.
+ * - If `false` updates {@link ConfigurationTarget.Workspace Workspace settings}.
+ * - If `undefined` or `null` updates to {@link ConfigurationTarget.WorkspaceFolder Workspace folder settings} if configuration is resource specific,
+ * otherwise to {@link ConfigurationTarget.Workspace Workspace settings}.
+ * @param overrideInLanguage Whether to update the value in the scope of requested languageId or not.
+ * - If `true` updates the value under the requested languageId.
+ * - If `undefined` updates the value under the requested languageId only if the configuration is defined for the language.
+ * @throws error while updating
+ * - configuration which is not registered.
+ * - window configuration to workspace folder
+ * - configuration to workspace or workspace folder when no workspace is opened.
+ * - configuration to workspace folder when there is no workspace folder settings.
+ * - configuration to workspace folder when {@link WorkspaceConfiguration} is not scoped to a resource.
+ */
+ update(section: string, value: any, configurationTarget?: ConfigurationTarget | boolean | null, overrideInLanguage?: boolean): Thenable;
+
+ /**
+ * Readable dictionary that backs this configuration.
+ */
+ readonly [key: string]: any;
+ }
+
+ /**
+ * Represents a location inside a resource, such as a line
+ * inside a text file.
+ */
+ export class Location {
+
+ /**
+ * The resource identifier of this location.
+ */
+ uri: Uri;
+
+ /**
+ * The document range of this location.
+ */
+ range: Range;
+
+ /**
+ * Creates a new location object.
+ *
+ * @param uri The resource identifier.
+ * @param rangeOrPosition The range or position. Positions will be converted to an empty range.
+ */
+ constructor(uri: Uri, rangeOrPosition: Range | Position);
+ }
+
+ /**
+ * Represents the connection of two locations. Provides additional metadata over normal {@link Location locations},
+ * including an origin range.
+ */
+ export interface LocationLink {
+ /**
+ * Span of the origin of this link.
+ *
+ * Used as the underlined span for mouse definition hover. Defaults to the word range at
+ * the definition position.
+ */
+ originSelectionRange?: Range;
+
+ /**
+ * The target resource identifier of this link.
+ */
+ targetUri: Uri;
+
+ /**
+ * The full target range of this link.
+ */
+ targetRange: Range;
+
+ /**
+ * The span of this link.
+ */
+ targetSelectionRange?: Range;
+ }
+
+ /**
+ * The event that is fired when diagnostics change.
+ */
+ export interface DiagnosticChangeEvent {
+
+ /**
+ * An array of resources for which diagnostics have changed.
+ */
+ readonly uris: readonly Uri[];
+ }
+
+ /**
+ * Represents the severity of diagnostics.
+ */
+ export enum DiagnosticSeverity {
+
+ /**
+ * Something not allowed by the rules of a language or other means.
+ */
+ Error = 0,
+
+ /**
+ * Something suspicious but allowed.
+ */
+ Warning = 1,
+
+ /**
+ * Something to inform about but not a problem.
+ */
+ Information = 2,
+
+ /**
+ * Something to hint to a better way of doing it, like proposing
+ * a refactoring.
+ */
+ Hint = 3
+ }
+
+ /**
+ * Represents a related message and source code location for a diagnostic. This should be
+ * used to point to code locations that cause or related to a diagnostics, e.g. when duplicating
+ * a symbol in a scope.
+ */
+ export class DiagnosticRelatedInformation {
+
+ /**
+ * The location of this related diagnostic information.
+ */
+ location: Location;
+
+ /**
+ * The message of this related diagnostic information.
+ */
+ message: string;
+
+ /**
+ * Creates a new related diagnostic information object.
+ *
+ * @param location The location.
+ * @param message The message.
+ */
+ constructor(location: Location, message: string);
+ }
+
+ /**
+ * Additional metadata about the type of a diagnostic.
+ */
+ export enum DiagnosticTag {
+ /**
+ * Unused or unnecessary code.
+ *
+ * Diagnostics with this tag are rendered faded out. The amount of fading
+ * is controlled by the `"editorUnnecessaryCode.opacity"` theme color. For
+ * example, `"editorUnnecessaryCode.opacity": "#000000c0"` will render the
+ * code with 75% opacity. For high contrast themes, use the
+ * `"editorUnnecessaryCode.border"` theme color to underline unnecessary code
+ * instead of fading it out.
+ */
+ Unnecessary = 1,
+
+ /**
+ * Deprecated or obsolete code.
+ *
+ * Diagnostics with this tag are rendered with a strike through.
+ */
+ Deprecated = 2,
+ }
+
+ /**
+ * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
+ * are only valid in the scope of a file.
+ */
+ export class Diagnostic {
+
+ /**
+ * The range to which this diagnostic applies.
+ */
+ range: Range;
+
+ /**
+ * The human-readable message.
+ */
+ message: string;
+
+ /**
+ * The severity, default is {@link DiagnosticSeverity.Error error}.
+ */
+ severity: DiagnosticSeverity;
+
+ /**
+ * A human-readable string describing the source of this
+ * diagnostic, e.g. 'typescript' or 'super lint'.
+ */
+ source?: string;
+
+ /**
+ * A code or identifier for this diagnostic.
+ * Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}.
+ */
+ code?: string | number | {
+ /**
+ * A code or identifier for this diagnostic.
+ * Should be used for later processing, e.g. when providing {@link CodeActionContext code actions}.
+ */
+ value: string | number;
+
+ /**
+ * A target URI to open with more information about the diagnostic error.
+ */
+ target: Uri;
+ };
+
+ /**
+ * An array of related diagnostic information, e.g. when symbol-names within
+ * a scope collide all definitions can be marked via this property.
+ */
+ relatedInformation?: DiagnosticRelatedInformation[];
+
+ /**
+ * Additional metadata about the diagnostic.
+ */
+ tags?: DiagnosticTag[];
+
+ /**
+ * Creates a new diagnostic object.
+ *
+ * @param range The range to which this diagnostic applies.
+ * @param message The human-readable message.
+ * @param severity The severity, default is {@link DiagnosticSeverity.Error error}.
+ */
+ constructor(range: Range, message: string, severity?: DiagnosticSeverity);
+ }
+
+ /**
+ * A diagnostics collection is a container that manages a set of
+ * {@link Diagnostic diagnostics}. Diagnostics are always scopes to a
+ * diagnostics collection and a resource.
+ *
+ * To get an instance of a `DiagnosticCollection` use
+ * {@link languages.createDiagnosticCollection createDiagnosticCollection}.
+ */
+ export interface DiagnosticCollection {
+
+ /**
+ * The name of this diagnostic collection, for instance `typescript`. Every diagnostic
+ * from this collection will be associated with this name. Also, the task framework uses this
+ * name when defining [problem matchers](https://code.visualstudio.com/docs/editor/tasks#_defining-a-problem-matcher).
+ */
+ readonly name: string;
+
+ /**
+ * Assign diagnostics for given resource. Will replace
+ * existing diagnostics for that resource.
+ *
+ * @param uri A resource identifier.
+ * @param diagnostics Array of diagnostics or `undefined`
+ */
+ set(uri: Uri, diagnostics: readonly Diagnostic[] | undefined): void;
+
+ /**
+ * Replace diagnostics for multiple resources in this collection.
+ *
+ * _Note_ that multiple tuples of the same uri will be merged, e.g
+ * `[[file1, [d1]], [file1, [d2]]]` is equivalent to `[[file1, [d1, d2]]]`.
+ * If a diagnostics item is `undefined` as in `[file1, undefined]`
+ * all previous but not subsequent diagnostics are removed.
+ *
+ * @param entries An array of tuples, like `[[file1, [d1, d2]], [file2, [d3, d4, d5]]]`, or `undefined`.
+ */
+ set(entries: ReadonlyArray<[Uri, readonly Diagnostic[] | undefined]>): void;
+
+ /**
+ * Remove all diagnostics from this collection that belong
+ * to the provided `uri`. The same as `#set(uri, undefined)`.
+ *
+ * @param uri A resource identifier.
+ */
+ delete(uri: Uri): void;
+
+ /**
+ * Remove all diagnostics from this collection. The same
+ * as calling `#set(undefined)`;
+ */
+ clear(): void;
+
+ /**
+ * Iterate over each entry in this collection.
+ *
+ * @param callback Function to execute for each entry.
+ * @param thisArg The `this` context used when invoking the handler function.
+ */
+ forEach(callback: (uri: Uri, diagnostics: readonly Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void;
+
+ /**
+ * Get the diagnostics for a given resource. *Note* that you cannot
+ * modify the diagnostics-array returned from this call.
+ *
+ * @param uri A resource identifier.
+ * @returns An immutable array of {@link Diagnostic diagnostics} or `undefined`.
+ */
+ get(uri: Uri): readonly Diagnostic[] | undefined;
+
+ /**
+ * Check if this collection contains diagnostics for a
+ * given resource.
+ *
+ * @param uri A resource identifier.
+ * @returns `true` if this collection has diagnostic for the given resource.
+ */
+ has(uri: Uri): boolean;
+
+ /**
+ * Dispose and free associated resources. Calls
+ * {@link DiagnosticCollection.clear clear}.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * Denotes a location of an editor in the window. Editors can be arranged in a grid
+ * and each column represents one editor location in that grid by counting the editors
+ * in order of their appearance.
+ */
+ export enum ViewColumn {
+ /**
+ * A *symbolic* editor column representing the currently active column. This value
+ * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value
+ * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Active`.
+ */
+ Active = -1,
+ /**
+ * A *symbolic* editor column representing the column to the side of the active one. This value
+ * can be used when opening editors, but the *resolved* {@link TextEditor.viewColumn viewColumn}-value
+ * of editors will always be `One`, `Two`, `Three`,... or `undefined` but never `Beside`.
+ */
+ Beside = -2,
+ /**
+ * The first editor column.
+ */
+ One = 1,
+ /**
+ * The second editor column.
+ */
+ Two = 2,
+ /**
+ * The third editor column.
+ */
+ Three = 3,
+ /**
+ * The fourth editor column.
+ */
+ Four = 4,
+ /**
+ * The fifth editor column.
+ */
+ Five = 5,
+ /**
+ * The sixth editor column.
+ */
+ Six = 6,
+ /**
+ * The seventh editor column.
+ */
+ Seven = 7,
+ /**
+ * The eighth editor column.
+ */
+ Eight = 8,
+ /**
+ * The ninth editor column.
+ */
+ Nine = 9
+ }
+
+ /**
+ * An output channel is a container for readonly textual information.
+ *
+ * To get an instance of an `OutputChannel` use
+ * {@link window.createOutputChannel createOutputChannel}.
+ */
+ export interface OutputChannel {
+
+ /**
+ * The human-readable name of this output channel.
+ */
+ readonly name: string;
+
+ /**
+ * Append the given value to the channel.
+ *
+ * @param value A string, falsy values will not be printed.
+ */
+ append(value: string): void;
+
+ /**
+ * Append the given value and a line feed character
+ * to the channel.
+ *
+ * @param value A string, falsy values will be printed.
+ */
+ appendLine(value: string): void;
+
+ /**
+ * Removes all output from the channel.
+ */
+ clear(): void;
+
+ /**
+ * Reveal this channel in the UI.
+ *
+ * @param preserveFocus When `true` the channel will not take focus.
+ */
+ show(preserveFocus?: boolean): void;
+
+ /**
+ * Reveal this channel in the UI.
+ *
+ * @deprecated Use the overload with just one parameter (`show(preserveFocus?: boolean): void`).
+ *
+ * @param column This argument is **deprecated** and will be ignored.
+ * @param preserveFocus When `true` the channel will not take focus.
+ */
+ show(column?: ViewColumn, preserveFocus?: boolean): void;
+
+ /**
+ * Hide this channel from the UI.
+ */
+ hide(): void;
+
+ /**
+ * Dispose and free associated resources.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * Accessibility information which controls screen reader behavior.
+ */
+ export interface AccessibilityInformation {
+ /**
+ * Label to be read out by a screen reader once the item has focus.
+ */
+ label: string;
+
+ /**
+ * Role of the widget which defines how a screen reader interacts with it.
+ * The role should be set in special cases when for example a tree-like element behaves like a checkbox.
+ * If role is not specified the editor will pick the appropriate role automatically.
+ * More about aria roles can be found here https://w3c.github.io/aria/#widget_roles
+ */
+ role?: string;
+ }
+
+ /**
+ * Represents the alignment of status bar items.
+ */
+ export enum StatusBarAlignment {
+
+ /**
+ * Aligned to the left side.
+ */
+ Left = 1,
+
+ /**
+ * Aligned to the right side.
+ */
+ Right = 2
+ }
+
+ /**
+ * A status bar item is a status bar contribution that can
+ * show text and icons and run a command on click.
+ */
+ export interface StatusBarItem {
+
+ /**
+ * The identifier of this item.
+ *
+ * *Note*: if no identifier was provided by the {@linkcode window.createStatusBarItem}
+ * method, the identifier will match the {@link Extension.id extension identifier}.
+ */
+ readonly id: string;
+
+ /**
+ * The alignment of this item.
+ */
+ readonly alignment: StatusBarAlignment;
+
+ /**
+ * The priority of this item. Higher value means the item should
+ * be shown more to the left.
+ */
+ readonly priority?: number;
+
+ /**
+ * The name of the entry, like 'Python Language Indicator', 'Git Status' etc.
+ * Try to keep the length of the name short, yet descriptive enough that
+ * users can understand what the status bar item is about.
+ */
+ name: string | undefined;
+
+ /**
+ * The text to show for the entry. You can embed icons in the text by leveraging the syntax:
+ *
+ * `My text $(icon-name) contains icons like $(icon-name) this one.`
+ *
+ * Where the icon-name is taken from the ThemeIcon [icon set](https://code.visualstudio.com/api/references/icons-in-labels#icon-listing), e.g.
+ * `light-bulb`, `thumbsup`, `zap` etc.
+ */
+ text: string;
+
+ /**
+ * The tooltip text when you hover over this entry.
+ */
+ tooltip: string | MarkdownString | undefined;
+
+ /**
+ * The foreground color for this entry.
+ */
+ color: string | ThemeColor | undefined;
+
+ /**
+ * The background color for this entry.
+ *
+ * *Note*: only the following colors are supported:
+ * * `new ThemeColor('statusBarItem.errorBackground')`
+ * * `new ThemeColor('statusBarItem.warningBackground')`
+ *
+ * More background colors may be supported in the future.
+ *
+ * *Note*: when a background color is set, the statusbar may override
+ * the `color` choice to ensure the entry is readable in all themes.
+ */
+ backgroundColor: ThemeColor | undefined;
+
+ /**
+ * {@linkcode Command} or identifier of a command to run on click.
+ *
+ * The command must be {@link commands.getCommands known}.
+ *
+ * Note that if this is a {@linkcode Command} object, only the {@linkcode Command.command command} and {@linkcode Command.arguments arguments}
+ * are used by the editor.
+ */
+ command: string | Command | undefined;
+
+ /**
+ * Accessibility information used when a screen reader interacts with this StatusBar item
+ */
+ accessibilityInformation?: AccessibilityInformation;
+
+ /**
+ * Shows the entry in the status bar.
+ */
+ show(): void;
+
+ /**
+ * Hide the entry in the status bar.
+ */
+ hide(): void;
+
+ /**
+ * Dispose and free associated resources. Call
+ * {@link StatusBarItem.hide hide}.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * Defines a generalized way of reporting progress updates.
+ */
+ export interface Progress {
+
+ /**
+ * Report a progress update.
+ * @param value A progress item, like a message and/or an
+ * report on how much work finished
+ */
+ report(value: T): void;
+ }
+
+ /**
+ * An individual terminal instance within the integrated terminal.
+ */
+ export interface Terminal {
+
+ /**
+ * The name of the terminal.
+ */
+ readonly name: string;
+
+ /**
+ * The process ID of the shell process.
+ */
+ readonly processId: Thenable;
+
+ /**
+ * The object used to initialize the terminal, this is useful for example to detecting the
+ * shell type of when the terminal was not launched by this extension or for detecting what
+ * folder the shell was launched in.
+ */
+ readonly creationOptions: Readonly;
+
+ /**
+ * The exit status of the terminal, this will be undefined while the terminal is active.
+ *
+ * **Example:** Show a notification with the exit code when the terminal exits with a
+ * non-zero exit code.
+ * ```typescript
+ * window.onDidCloseTerminal(t => {
+ * if (t.exitStatus && t.exitStatus.code) {
+ * vscode.window.showInformationMessage(`Exit code: ${t.exitStatus.code}`);
+ * }
+ * });
+ * ```
+ */
+ readonly exitStatus: TerminalExitStatus | undefined;
+
+ /**
+ * Send text to the terminal. The text is written to the stdin of the underlying pty process
+ * (shell) of the terminal.
+ *
+ * @param text The text to send.
+ * @param addNewLine Whether to add a new line to the text being sent, this is normally
+ * required to run a command in the terminal. The character(s) added are \n or \r\n
+ * depending on the platform. This defaults to `true`.
+ */
+ sendText(text: string, addNewLine?: boolean): void;
+
+ /**
+ * Show the terminal panel and reveal this terminal in the UI.
+ *
+ * @param preserveFocus When `true` the terminal will not take focus.
+ */
+ show(preserveFocus?: boolean): void;
+
+ /**
+ * Hide the terminal panel if this terminal is currently showing.
+ */
+ hide(): void;
+
+ /**
+ * Dispose and free associated resources.
+ */
+ dispose(): void;
+ }
+
+ /**
+ * Provides information on a line in a terminal in order to provide links for it.
+ */
+ export interface TerminalLinkContext {
+ /**
+ * This is the text from the unwrapped line in the terminal.
+ */
+ line: string;
+
+ /**
+ * The terminal the link belongs to.
+ */
+ terminal: Terminal;
+ }
+
+ /**
+ * A provider that enables detection and handling of links within terminals.
+ */
+ export interface TerminalLinkProvider {
+ /**
+ * Provide terminal links for the given context. Note that this can be called multiple times
+ * even before previous calls resolve, make sure to not share global objects (eg. `RegExp`)
+ * that could have problems when asynchronous usage may overlap.
+ * @param context Information about what links are being provided for.
+ * @param token A cancellation token.
+ * @return A list of terminal links for the given line.
+ */
+ provideTerminalLinks(context: TerminalLinkContext, token: CancellationToken): ProviderResult;
+
+ /**
+ * Handle an activated terminal link.
+ * @param link The link to handle.
+ */
+ handleTerminalLink(link: T): ProviderResult;
+ }
+
+ /**
+ * A link on a terminal line.
+ */
+ export class TerminalLink {
+ /**
+ * The start index of the link on {@link TerminalLinkContext.line}.
+ */
+ startIndex: number;
+
+ /**
+ * The length of the link on {@link TerminalLinkContext.line}.
+ */
+ length: number;
+
+ /**
+ * The tooltip text when you hover over this link.
+ *
+ * If a tooltip is provided, is will be displayed in a string that includes instructions on
+ * how to trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary
+ * depending on OS, user settings, and localization.
+ */
+ tooltip?: string;
+
+ /**
+ * Creates a new terminal link.
+ * @param startIndex The start index of the link on {@link TerminalLinkContext.line}.
+ * @param length The length of the link on {@link TerminalLinkContext.line}.
+ * @param tooltip The tooltip text when you hover over this link.
+ *
+ * If a tooltip is provided, is will be displayed in a string that includes instructions on
+ * how to trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary
+ * depending on OS, user settings, and localization.
+ */
+ constructor(startIndex: number, length: number, tooltip?: string);
+ }
+
+ /**
+ * Provides a terminal profile for the contributed terminal profile when launched via the UI or
+ * command.
+ */
+ export interface TerminalProfileProvider {
+ /**
+ * Provide the terminal profile.
+ * @param token A cancellation token that indicates the result is no longer needed.
+ * @returns The terminal profile.
+ */
+ provideTerminalProfile(token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * A terminal profile defines how a terminal will be launched.
+ */
+ export class TerminalProfile {
+ /**
+ * The options that the terminal will launch with.
+ */
+ options: TerminalOptions | ExtensionTerminalOptions;
+
+ /**
+ * Creates a new terminal profile.
+ * @param options The options that the terminal will launch with.
+ */
+ constructor(options: TerminalOptions | ExtensionTerminalOptions);
+ }
+
+ /**
+ * A file decoration represents metadata that can be rendered with a file.
+ */
+ export class FileDecoration {
+
+ /**
+ * A very short string that represents this decoration.
+ */
+ badge?: string;
+
+ /**
+ * A human-readable tooltip for this decoration.
+ */
+ tooltip?: string;
+
+ /**
+ * The color of this decoration.
+ */
+ color?: ThemeColor;
+
+ /**
+ * A flag expressing that this decoration should be
+ * propagated to its parents.
+ */
+ propagate?: boolean;
+
+ /**
+ * Creates a new decoration.
+ *
+ * @param badge A letter that represents the decoration.
+ * @param tooltip The tooltip of the decoration.
+ * @param color The color of the decoration.
+ */
+ constructor(badge?: string, tooltip?: string, color?: ThemeColor);
+ }
+
+ /**
+ * The decoration provider interfaces defines the contract between extensions and
+ * file decorations.
+ */
+ export interface FileDecorationProvider {
+
+ /**
+ * An optional event to signal that decorations for one or many files have changed.
+ *
+ * *Note* that this event should be used to propagate information about children.
+ *
+ * @see {@link EventEmitter}
+ */
+ onDidChangeFileDecorations?: Event;
+
+ /**
+ * Provide decorations for a given uri.
+ *
+ * *Note* that this function is only called when a file gets rendered in the UI.
+ * This means a decoration from a descendent that propagates upwards must be signaled
+ * to the editor via the {@link FileDecorationProvider.onDidChangeFileDecorations onDidChangeFileDecorations}-event.
+ *
+ * @param uri The uri of the file to provide a decoration for.
+ * @param token A cancellation token.
+ * @returns A decoration or a thenable that resolves to such.
+ */
+ provideFileDecoration(uri: Uri, token: CancellationToken): ProviderResult;
+ }
+
+
+ /**
+ * In a remote window the extension kind describes if an extension
+ * runs where the UI (window) runs or if an extension runs remotely.
+ */
+ export enum ExtensionKind {
+
+ /**
+ * Extension runs where the UI runs.
+ */
+ UI = 1,
+
+ /**
+ * Extension runs where the remote extension host runs.
+ */
+ Workspace = 2
+ }
+
+ /**
+ * Represents an extension.
+ *
+ * To get an instance of an `Extension` use {@link extensions.getExtension getExtension}.
+ */
+ export interface Extension {
+
+ /**
+ * The canonical extension identifier in the form of: `publisher.name`.
+ */
+ readonly id: string;
+
+ /**
+ * The uri of the directory containing the extension.
+ */
+ readonly extensionUri: Uri;
+
+ /**
+ * The absolute file path of the directory containing this extension. Shorthand
+ * notation for {@link Extension.extensionUri Extension.extensionUri.fsPath} (independent of the uri scheme).
+ */
+ readonly extensionPath: string;
+
+ /**
+ * `true` if the extension has been activated.
+ */
+ readonly isActive: boolean;
+
+ /**
+ * The parsed contents of the extension's package.json.
+ */
+ readonly packageJSON: any;
+
+ /**
+ * The extension kind describes if an extension runs where the UI runs
+ * or if an extension runs where the remote extension host runs. The extension kind
+ * is defined in the `package.json`-file of extensions but can also be refined
+ * via the `remote.extensionKind`-setting. When no remote extension host exists,
+ * the value is {@linkcode ExtensionKind.UI}.
+ */
+ extensionKind: ExtensionKind;
+
+ /**
+ * The public API exported by this extension. It is an invalid action
+ * to access this field before this extension has been activated.
+ */
+ readonly exports: T;
+
+ /**
+ * Activates this extension and returns its public API.
+ *
+ * @return A promise that will resolve when this extension has been activated.
+ */
+ activate(): Thenable;
+ }
+
+ /**
+ * The ExtensionMode is provided on the `ExtensionContext` and indicates the
+ * mode the specific extension is running in.
+ */
+ export enum ExtensionMode {
+ /**
+ * The extension is installed normally (for example, from the marketplace
+ * or VSIX) in the editor.
+ */
+ Production = 1,
+
+ /**
+ * The extension is running from an `--extensionDevelopmentPath` provided
+ * when launching the editor.
+ */
+ Development = 2,
+
+ /**
+ * The extension is running from an `--extensionTestsPath` and
+ * the extension host is running unit tests.
+ */
+ Test = 3,
+ }
+
+ /**
+ * An extension context is a collection of utilities private to an
+ * extension.
+ *
+ * An instance of an `ExtensionContext` is provided as the first
+ * parameter to the `activate`-call of an extension.
+ */
+ export interface ExtensionContext {
+
+ /**
+ * An array to which disposables can be added. When this
+ * extension is deactivated the disposables will be disposed.
+ */
+ readonly subscriptions: { dispose(): any }[];
+
+ /**
+ * A memento object that stores state in the context
+ * of the currently opened {@link workspace.workspaceFolders workspace}.
+ */
+ readonly workspaceState: Memento;
+
+ /**
+ * A memento object that stores state independent
+ * of the current opened {@link workspace.workspaceFolders workspace}.
+ */
+ readonly globalState: Memento & {
+ /**
+ * Set the keys whose values should be synchronized across devices when synchronizing user-data
+ * like configuration, extensions, and mementos.
+ *
+ * Note that this function defines the whole set of keys whose values are synchronized:
+ * - calling it with an empty array stops synchronization for this memento
+ * - calling it with a non-empty array replaces all keys whose values are synchronized
+ *
+ * For any given set of keys this function needs to be called only once but there is no harm in
+ * repeatedly calling it.
+ *
+ * @param keys The set of keys whose values are synced.
+ */
+ setKeysForSync(keys: readonly string[]): void;
+ };
+
+ /**
+ * A storage utility for secrets. Secrets are persisted across reloads and are independent of the
+ * current opened {@link workspace.workspaceFolders workspace}.
+ */
+ readonly secrets: SecretStorage;
+
+ /**
+ * The uri of the directory containing the extension.
+ */
+ readonly extensionUri: Uri;
+
+ /**
+ * The absolute file path of the directory containing the extension. Shorthand
+ * notation for {@link TextDocument.uri ExtensionContext.extensionUri.fsPath} (independent of the uri scheme).
+ */
+ readonly extensionPath: string;
+
+ /**
+ * Gets the extension's environment variable collection for this workspace, enabling changes
+ * to be applied to terminal environment variables.
+ */
+ readonly environmentVariableCollection: EnvironmentVariableCollection;
+
+ /**
+ * Get the absolute path of a resource contained in the extension.
+ *
+ * *Note* that an absolute uri can be constructed via {@linkcode Uri.joinPath} and
+ * {@linkcode ExtensionContext.extensionUri extensionUri}, e.g. `vscode.Uri.joinPath(context.extensionUri, relativePath);`
+ *
+ * @param relativePath A relative path to a resource contained in the extension.
+ * @return The absolute path of the resource.
+ */
+ asAbsolutePath(relativePath: string): string;
+
+ /**
+ * The uri of a workspace specific directory in which the extension
+ * can store private state. The directory might not exist and creation is
+ * up to the extension. However, the parent directory is guaranteed to be existent.
+ * The value is `undefined` when no workspace nor folder has been opened.
+ *
+ * Use {@linkcode ExtensionContext.workspaceState workspaceState} or
+ * {@linkcode ExtensionContext.globalState globalState} to store key value data.
+ *
+ * @see {@linkcode FileSystem workspace.fs} for how to read and write files and folders from
+ * an uri.
+ */
+ readonly storageUri: Uri | undefined;
+
+ /**
+ * An absolute file path of a workspace specific directory in which the extension
+ * can store private state. The directory might not exist on disk and creation is
+ * up to the extension. However, the parent directory is guaranteed to be existent.
+ *
+ * Use {@linkcode ExtensionContext.workspaceState workspaceState} or
+ * {@linkcode ExtensionContext.globalState globalState} to store key value data.
+ *
+ * @deprecated Use {@link ExtensionContext.storageUri storageUri} instead.
+ */
+ readonly storagePath: string | undefined;
+
+ /**
+ * The uri of a directory in which the extension can store global state.
+ * The directory might not exist on disk and creation is
+ * up to the extension. However, the parent directory is guaranteed to be existent.
+ *
+ * Use {@linkcode ExtensionContext.globalState globalState} to store key value data.
+ *
+ * @see {@linkcode FileSystem workspace.fs} for how to read and write files and folders from
+ * an uri.
+ */
+ readonly globalStorageUri: Uri;
+
+ /**
+ * An absolute file path in which the extension can store global state.
+ * The directory might not exist on disk and creation is
+ * up to the extension. However, the parent directory is guaranteed to be existent.
+ *
+ * Use {@linkcode ExtensionContext.globalState globalState} to store key value data.
+ *
+ * @deprecated Use {@link ExtensionContext.globalStorageUri globalStorageUri} instead.
+ */
+ readonly globalStoragePath: string;
+
+ /**
+ * The uri of a directory in which the extension can create log files.
+ * The directory might not exist on disk and creation is up to the extension. However,
+ * the parent directory is guaranteed to be existent.
+ *
+ * @see {@linkcode FileSystem workspace.fs} for how to read and write files and folders from
+ * an uri.
+ */
+ readonly logUri: Uri;
+
+ /**
+ * An absolute file path of a directory in which the extension can create log files.
+ * The directory might not exist on disk and creation is up to the extension. However,
+ * the parent directory is guaranteed to be existent.
+ *
+ * @deprecated Use {@link ExtensionContext.logUri logUri} instead.
+ */
+ readonly logPath: string;
+
+ /**
+ * The mode the extension is running in. This is specific to the current
+ * extension. One extension may be in `ExtensionMode.Development` while
+ * other extensions in the host run in `ExtensionMode.Release`.
+ */
+ readonly extensionMode: ExtensionMode;
+
+ /**
+ * The current `Extension` instance.
+ */
+ readonly extension: Extension;
+ }
+
+ /**
+ * A memento represents a storage utility. It can store and retrieve
+ * values.
+ */
+ export interface Memento {
+
+ /**
+ * Returns the stored keys.
+ *
+ * @return The stored keys.
+ */
+ keys(): readonly string[];
+
+ /**
+ * Return a value.
+ *
+ * @param key A string.
+ * @return The stored value or `undefined`.
+ */
+ get(key: string): T | undefined;
+
+ /**
+ * Return a value.
+ *
+ * @param key A string.
+ * @param defaultValue A value that should be returned when there is no
+ * value (`undefined`) with the given key.
+ * @return The stored value or the defaultValue.
+ */
+ get(key: string, defaultValue: T): T;
+
+ /**
+ * Store a value. The value must be JSON-stringifyable.
+ *
+ * @param key A string.
+ * @param value A value. MUST not contain cyclic references.
+ */
+ update(key: string, value: any): Thenable;
+ }
+
+ /**
+ * The event data that is fired when a secret is added or removed.
+ */
+ export interface SecretStorageChangeEvent {
+ /**
+ * The key of the secret that has changed.
+ */
+ readonly key: string;
+ }
+
+ /**
+ * Represents a storage utility for secrets, information that is
+ * sensitive.
+ */
+ export interface SecretStorage {
+ /**
+ * Retrieve a secret that was stored with key. Returns undefined if there
+ * is no password matching that key.
+ * @param key The key the secret was stored under.
+ * @returns The stored value or `undefined`.
+ */
+ get(key: string): Thenable;
+
+ /**
+ * Store a secret under a given key.
+ * @param key The key to store the secret under.
+ * @param value The secret.
+ */
+ store(key: string, value: string): Thenable;
+
+ /**
+ * Remove a secret from storage.
+ * @param key The key the secret was stored under.
+ */
+ delete(key: string): Thenable;
+
+ /**
+ * Fires when a secret is stored or deleted.
+ */
+ onDidChange: Event;
+ }
+
+ /**
+ * Represents a color theme kind.
+ */
+ export enum ColorThemeKind {
+ Light = 1,
+ Dark = 2,
+ HighContrast = 3
+ }
+
+ /**
+ * Represents a color theme.
+ */
+ export interface ColorTheme {
+
+ /**
+ * The kind of this color theme: light, dark or high contrast.
+ */
+ readonly kind: ColorThemeKind;
+ }
+
+ /**
+ * Controls the behaviour of the terminal's visibility.
+ */
+ export enum TaskRevealKind {
+ /**
+ * Always brings the terminal to front if the task is executed.
+ */
+ Always = 1,
+
+ /**
+ * Only brings the terminal to front if a problem is detected executing the task
+ * (e.g. the task couldn't be started because).
+ */
+ Silent = 2,
+
+ /**
+ * The terminal never comes to front when the task is executed.
+ */
+ Never = 3
+ }
+
+ /**
+ * Controls how the task channel is used between tasks
+ */
+ export enum TaskPanelKind {
+
+ /**
+ * Shares a panel with other tasks. This is the default.
+ */
+ Shared = 1,
+
+ /**
+ * Uses a dedicated panel for this tasks. The panel is not
+ * shared with other tasks.
+ */
+ Dedicated = 2,
+
+ /**
+ * Creates a new panel whenever this task is executed.
+ */
+ New = 3
+ }
+
+ /**
+ * Controls how the task is presented in the UI.
+ */
+ export interface TaskPresentationOptions {
+ /**
+ * Controls whether the task output is reveal in the user interface.
+ * Defaults to `RevealKind.Always`.
+ */
+ reveal?: TaskRevealKind;
+
+ /**
+ * Controls whether the command associated with the task is echoed
+ * in the user interface.
+ */
+ echo?: boolean;
+
+ /**
+ * Controls whether the panel showing the task output is taking focus.
+ */
+ focus?: boolean;
+
+ /**
+ * Controls if the task panel is used for this task only (dedicated),
+ * shared between tasks (shared) or if a new panel is created on
+ * every task execution (new). Defaults to `TaskInstanceKind.Shared`
+ */
+ panel?: TaskPanelKind;
+
+ /**
+ * Controls whether to show the "Terminal will be reused by tasks, press any key to close it" message.
+ */
+ showReuseMessage?: boolean;
+
+ /**
+ * Controls whether the terminal is cleared before executing the task.
+ */
+ clear?: boolean;
+ }
+
+ /**
+ * A grouping for tasks. The editor by default supports the
+ * 'Clean', 'Build', 'RebuildAll' and 'Test' group.
+ */
+ export class TaskGroup {
+
+ /**
+ * The clean task group;
+ */
+ static Clean: TaskGroup;
+
+ /**
+ * The build task group;
+ */
+ static Build: TaskGroup;
+
+ /**
+ * The rebuild all task group;
+ */
+ static Rebuild: TaskGroup;
+
+ /**
+ * The test all task group;
+ */
+ static Test: TaskGroup;
+
+ private constructor(id: string, label: string);
+ }
+
+ /**
+ * A structure that defines a task kind in the system.
+ * The value must be JSON-stringifyable.
+ */
+ export interface TaskDefinition {
+ /**
+ * The task definition describing the task provided by an extension.
+ * Usually a task provider defines more properties to identify
+ * a task. They need to be defined in the package.json of the
+ * extension under the 'taskDefinitions' extension point. The npm
+ * task definition for example looks like this
+ * ```typescript
+ * interface NpmTaskDefinition extends TaskDefinition {
+ * script: string;
+ * }
+ * ```
+ *
+ * Note that type identifier starting with a '$' are reserved for internal
+ * usages and shouldn't be used by extensions.
+ */
+ readonly type: string;
+
+ /**
+ * Additional attributes of a concrete task definition.
+ */
+ [name: string]: any;
+ }
+
+ /**
+ * Options for a process execution
+ */
+ export interface ProcessExecutionOptions {
+ /**
+ * The current working directory of the executed program or shell.
+ * If omitted the tools current workspace root is used.
+ */
+ cwd?: string;
+
+ /**
+ * The additional environment of the executed program or shell. If omitted
+ * the parent process' environment is used. If provided it is merged with
+ * the parent process' environment.
+ */
+ env?: { [key: string]: string };
+ }
+
+ /**
+ * The execution of a task happens as an external process
+ * without shell interaction.
+ */
+ export class ProcessExecution {
+
+ /**
+ * Creates a process execution.
+ *
+ * @param process The process to start.
+ * @param options Optional options for the started process.
+ */
+ constructor(process: string, options?: ProcessExecutionOptions);
+
+ /**
+ * Creates a process execution.
+ *
+ * @param process The process to start.
+ * @param args Arguments to be passed to the process.
+ * @param options Optional options for the started process.
+ */
+ constructor(process: string, args: string[], options?: ProcessExecutionOptions);
+
+ /**
+ * The process to be executed.
+ */
+ process: string;
+
+ /**
+ * The arguments passed to the process. Defaults to an empty array.
+ */
+ args: string[];
+
+ /**
+ * The process options used when the process is executed.
+ * Defaults to undefined.
+ */
+ options?: ProcessExecutionOptions;
+ }
+
+ /**
+ * The shell quoting options.
+ */
+ export interface ShellQuotingOptions {
+
+ /**
+ * The character used to do character escaping. If a string is provided only spaces
+ * are escaped. If a `{ escapeChar, charsToEscape }` literal is provide all characters
+ * in `charsToEscape` are escaped using the `escapeChar`.
+ */
+ escape?: string | {
+ /**
+ * The escape character.
+ */
+ escapeChar: string;
+ /**
+ * The characters to escape.
+ */
+ charsToEscape: string;
+ };
+
+ /**
+ * The character used for strong quoting. The string's length must be 1.
+ */
+ strong?: string;
+
+ /**
+ * The character used for weak quoting. The string's length must be 1.
+ */
+ weak?: string;
+ }
+
+ /**
+ * Options for a shell execution
+ */
+ export interface ShellExecutionOptions {
+ /**
+ * The shell executable.
+ */
+ executable?: string;
+
+ /**
+ * The arguments to be passed to the shell executable used to run the task. Most shells
+ * require special arguments to execute a command. For example `bash` requires the `-c`
+ * argument to execute a command, `PowerShell` requires `-Command` and `cmd` requires both
+ * `/d` and `/c`.
+ */
+ shellArgs?: string[];
+
+ /**
+ * The shell quotes supported by this shell.
+ */
+ shellQuoting?: ShellQuotingOptions;
+
+ /**
+ * The current working directory of the executed shell.
+ * If omitted the tools current workspace root is used.
+ */
+ cwd?: string;
+
+ /**
+ * The additional environment of the executed shell. If omitted
+ * the parent process' environment is used. If provided it is merged with
+ * the parent process' environment.
+ */
+ env?: { [key: string]: string };
+ }
+
+ /**
+ * Defines how an argument should be quoted if it contains
+ * spaces or unsupported characters.
+ */
+ export enum ShellQuoting {
+
+ /**
+ * Character escaping should be used. This for example
+ * uses \ on bash and ` on PowerShell.
+ */
+ Escape = 1,
+
+ /**
+ * Strong string quoting should be used. This for example
+ * uses " for Windows cmd and ' for bash and PowerShell.
+ * Strong quoting treats arguments as literal strings.
+ * Under PowerShell echo 'The value is $(2 * 3)' will
+ * print `The value is $(2 * 3)`
+ */
+ Strong = 2,
+
+ /**
+ * Weak string quoting should be used. This for example
+ * uses " for Windows cmd, bash and PowerShell. Weak quoting
+ * still performs some kind of evaluation inside the quoted
+ * string. Under PowerShell echo "The value is $(2 * 3)"
+ * will print `The value is 6`
+ */
+ Weak = 3
+ }
+
+ /**
+ * A string that will be quoted depending on the used shell.
+ */
+ export interface ShellQuotedString {
+ /**
+ * The actual string value.
+ */
+ value: string;
+
+ /**
+ * The quoting style to use.
+ */
+ quoting: ShellQuoting;
+ }
+
+ export class ShellExecution {
+ /**
+ * Creates a shell execution with a full command line.
+ *
+ * @param commandLine The command line to execute.
+ * @param options Optional options for the started the shell.
+ */
+ constructor(commandLine: string, options?: ShellExecutionOptions);
+
+ /**
+ * Creates a shell execution with a command and arguments. For the real execution the editor will
+ * construct a command line from the command and the arguments. This is subject to interpretation
+ * especially when it comes to quoting. If full control over the command line is needed please
+ * use the constructor that creates a `ShellExecution` with the full command line.
+ *
+ * @param command The command to execute.
+ * @param args The command arguments.
+ * @param options Optional options for the started the shell.
+ */
+ constructor(command: string | ShellQuotedString, args: (string | ShellQuotedString)[], options?: ShellExecutionOptions);
+
+ /**
+ * The shell command line. Is `undefined` if created with a command and arguments.
+ */
+ commandLine: string | undefined;
+
+ /**
+ * The shell command. Is `undefined` if created with a full command line.
+ */
+ command: string | ShellQuotedString;
+
+ /**
+ * The shell args. Is `undefined` if created with a full command line.
+ */
+ args: (string | ShellQuotedString)[];
+
+ /**
+ * The shell options used when the command line is executed in a shell.
+ * Defaults to undefined.
+ */
+ options?: ShellExecutionOptions;
+ }
+
+ /**
+ * Class used to execute an extension callback as a task.
+ */
+ export class CustomExecution {
+ /**
+ * Constructs a CustomExecution task object. The callback will be executed when the task is run, at which point the
+ * extension should return the Pseudoterminal it will "run in". The task should wait to do further execution until
+ * {@link Pseudoterminal.open} is called. Task cancellation should be handled using
+ * {@link Pseudoterminal.close}. When the task is complete fire
+ * {@link Pseudoterminal.onDidClose}.
+ * @param callback The callback that will be called when the task is started by a user. Any ${} style variables that
+ * were in the task definition will be resolved and passed into the callback as `resolvedDefinition`.
+ */
+ constructor(callback: (resolvedDefinition: TaskDefinition) => Thenable);
+ }
+
+ /**
+ * The scope of a task.
+ */
+ export enum TaskScope {
+ /**
+ * The task is a global task. Global tasks are currently not supported.
+ */
+ Global = 1,
+
+ /**
+ * The task is a workspace task
+ */
+ Workspace = 2
+ }
+
+ /**
+ * Run options for a task.
+ */
+ export interface RunOptions {
+ /**
+ * Controls whether task variables are re-evaluated on rerun.
+ */
+ reevaluateOnRerun?: boolean;
+ }
+
+ /**
+ * A task to execute
+ */
+ export class Task {
+
+ /**
+ * Creates a new task.
+ *
+ * @param definition The task definition as defined in the taskDefinitions extension point.
+ * @param scope Specifies the task's scope. It is either a global or a workspace task or a task for a specific workspace folder. Global tasks are currently not supported.
+ * @param name The task's name. Is presented in the user interface.
+ * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface.
+ * @param execution The process or shell execution.
+ * @param problemMatchers the names of problem matchers to use, like '$tsc'
+ * or '$eslint'. Problem matchers can be contributed by an extension using
+ * the `problemMatchers` extension point.
+ */
+ constructor(taskDefinition: TaskDefinition, scope: WorkspaceFolder | TaskScope.Global | TaskScope.Workspace, name: string, source: string, execution?: ProcessExecution | ShellExecution | CustomExecution, problemMatchers?: string | string[]);
+
+ /**
+ * Creates a new task.
+ *
+ * @deprecated Use the new constructors that allow specifying a scope for the task.
+ *
+ * @param definition The task definition as defined in the taskDefinitions extension point.
+ * @param name The task's name. Is presented in the user interface.
+ * @param source The task's source (e.g. 'gulp', 'npm', ...). Is presented in the user interface.
+ * @param execution The process or shell execution.
+ * @param problemMatchers the names of problem matchers to use, like '$tsc'
+ * or '$eslint'. Problem matchers can be contributed by an extension using
+ * the `problemMatchers` extension point.
+ */
+ constructor(taskDefinition: TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]);
+
+ /**
+ * The task's definition.
+ */
+ definition: TaskDefinition;
+
+ /**
+ * The task's scope.
+ */
+ readonly scope?: TaskScope.Global | TaskScope.Workspace | WorkspaceFolder;
+
+ /**
+ * The task's name
+ */
+ name: string;
+
+ /**
+ * A human-readable string which is rendered less prominently on a separate line in places
+ * where the task's name is displayed. Supports rendering of {@link ThemeIcon theme icons}
+ * via the `$()`-syntax.
+ */
+ detail?: string;
+
+ /**
+ * The task's execution engine
+ */
+ execution?: ProcessExecution | ShellExecution | CustomExecution;
+
+ /**
+ * Whether the task is a background task or not.
+ */
+ isBackground: boolean;
+
+ /**
+ * A human-readable string describing the source of this shell task, e.g. 'gulp'
+ * or 'npm'. Supports rendering of {@link ThemeIcon theme icons} via the `$()`-syntax.
+ */
+ source: string;
+
+ /**
+ * The task group this tasks belongs to. See TaskGroup
+ * for a predefined set of available groups.
+ * Defaults to undefined meaning that the task doesn't
+ * belong to any special group.
+ */
+ group?: TaskGroup;
+
+ /**
+ * The presentation options. Defaults to an empty literal.
+ */
+ presentationOptions: TaskPresentationOptions;
+
+ /**
+ * The problem matchers attached to the task. Defaults to an empty
+ * array.
+ */
+ problemMatchers: string[];
+
+ /**
+ * Run options for the task
+ */
+ runOptions: RunOptions;
+ }
+
+ /**
+ * A task provider allows to add tasks to the task service.
+ * A task provider is registered via {@link tasks.registerTaskProvider}.
+ */
+ export interface TaskProvider {
+ /**
+ * Provides tasks.
+ * @param token A cancellation token.
+ * @return an array of tasks
+ */
+ provideTasks(token: CancellationToken): ProviderResult;
+
+ /**
+ * Resolves a task that has no {@linkcode Task.execution execution} set. Tasks are
+ * often created from information found in the `tasks.json`-file. Such tasks miss
+ * the information on how to execute them and a task provider must fill in
+ * the missing information in the `resolveTask`-method. This method will not be
+ * called for tasks returned from the above `provideTasks` method since those
+ * tasks are always fully resolved. A valid default implementation for the
+ * `resolveTask` method is to return `undefined`.
+ *
+ * Note that when filling in the properties of `task`, you _must_ be sure to
+ * use the exact same `TaskDefinition` and not create a new one. Other properties
+ * may be changed.
+ *
+ * @param task The task to resolve.
+ * @param token A cancellation token.
+ * @return The resolved task
+ */
+ resolveTask(task: T, token: CancellationToken): ProviderResult;
+ }
+
+ /**
+ * An object representing an executed Task. It can be used
+ * to terminate a task.
+ *
+ * This interface is not intended to be implemented.
+ */
+ export interface TaskExecution {
+ /**
+ * The task that got started.
+ */
+ task: Task;
+
+ /**
+ * Terminates the task execution.
+ */
+ terminate(): void;
+ }
+
+ /**
+ * An event signaling the start of a task execution.
+ *
+ * This interface is not intended to be implemented.
+ */
+ interface TaskStartEvent {
+ /**
+ * The task item representing the task that got started.
+ */
+ readonly execution: TaskExecution;
+ }
+
+ /**
+ * An event signaling the end of an executed task.
+ *
+ * This interface is not intended to be implemented.
+ */
+ interface TaskEndEvent {
+ /**
+ * The task item representing the task that finished.
+ */
+ readonly execution: TaskExecution;
+ }
+
+ /**
+ * An event signaling the start of a process execution
+ * triggered through a task
+ */
+ export interface TaskProcessStartEvent {
+
+ /**
+ * The task execution for which the process got started.
+ */
+ readonly execution: TaskExecution;
+
+ /**
+ * The underlying process id.
+ */
+ readonly processId: number;
+ }
+
+ /**
+ * An event signaling the end of a process execution
+ * triggered through a task
+ */
+ export interface TaskProcessEndEvent {
+
+ /**
+ * The task execution for which the process got started.
+ */
+ readonly execution: TaskExecution;
+
+ /**
+ * The process's exit code. Will be `undefined` when the task is terminated.
+ */
+ readonly exitCode: number | undefined;
+ }
+
+ export interface TaskFilter {
+ /**
+ * The task version as used in the tasks.json file.
+ * The string support the package.json semver notation.
+ */
+ version?: string;
+
+ /**
+ * The task type to return;
+ */
+ type?: string;
+ }
+
+ /**
+ * Namespace for tasks functionality.
+ */
+ export namespace tasks {
+
+ /**
+ * Register a task provider.
+ *
+ * @param type The task kind type this provider is registered for.
+ * @param provider A task provider.
+ * @return A {@link Disposable} that unregisters this provider when being disposed.
+ */
+ export function registerTaskProvider(type: string, provider: TaskProvider): Disposable;
+
+ /**
+ * Fetches all tasks available in the systems. This includes tasks
+ * from `tasks.json` files as well as tasks from task providers
+ * contributed through extensions.
+ *
+ * @param filter Optional filter to select tasks of a certain type or version.
+ */
+ export function fetchTasks(filter?: TaskFilter): Thenable;
+
+ /**
+ * Executes a task that is managed by the editor. The returned
+ * task execution can be used to terminate the task.
+ *
+ * @throws When running a ShellExecution or a ProcessExecution
+ * task in an environment where a new process cannot be started.
+ * In such an environment, only CustomExecution tasks can be run.
+ *
+ * @param task the task to execute
+ */
+ export function executeTask(task: Task): Thenable;
+
+ /**
+ * The currently active task executions or an empty array.
+ */
+ export const taskExecutions: readonly TaskExecution[];
+
+ /**
+ * Fires when a task starts.
+ */
+ export const onDidStartTask: Event;
+
+ /**
+ * Fires when a task ends.
+ */
+ export const onDidEndTask: Event;
+
+ /**
+ * Fires when the underlying process has been started.
+ * This event will not fire for tasks that don't
+ * execute an underlying process.
+ */
+ export const onDidStartTaskProcess: Event;
+
+ /**
+ * Fires when the underlying process has ended.
+ * This event will not fire for tasks that don't
+ * execute an underlying process.
+ */
+ export const onDidEndTaskProcess: Event;
+ }
+
+ /**
+ * Enumeration of file types. The types `File` and `Directory` can also be
+ * a symbolic links, in that case use `FileType.File | FileType.SymbolicLink` and
+ * `FileType.Directory | FileType.SymbolicLink`.
+ */
+ export enum FileType {
+ /**
+ * The file type is unknown.
+ */
+ Unknown = 0,
+ /**
+ * A regular file.
+ */
+ File = 1,
+ /**
+ * A directory.
+ */
+ Directory = 2,
+ /**
+ * A symbolic link to a file.
+ */
+ SymbolicLink = 64
+ }
+
+ /**
+ * The `FileStat`-type represents metadata about a file
+ */
+ export interface FileStat {
+ /**
+ * The type of the file, e.g. is a regular file, a directory, or symbolic link
+ * to a file.
+ *
+ * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`.
+ */
+ type: FileType;
+ /**
+ * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
+ */
+ ctime: number;
+ /**
+ * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC.
+ *
+ * *Note:* If the file changed, it is important to provide an updated `mtime` that advanced
+ * from the previous value. Otherwise there may be optimizations in place that will not show
+ * the updated file contents in an editor for example.
+ */
+ mtime: number;
+ /**
+ * The size in bytes.
+ *
+ * *Note:* If the file changed, it is important to provide an updated `size`. Otherwise there
+ * may be optimizations in place that will not show the updated file contents in an editor for
+ * example.
+ */
+ size: number;
+ }
+
+ /**
+ * A type that filesystem providers should use to signal errors.
+ *
+ * This class has factory methods for common error-cases, like `FileNotFound` when
+ * a file or folder doesn't exist, use them like so: `throw vscode.FileSystemError.FileNotFound(someUri);`
+ */
+ export class FileSystemError extends Error {
+
+ /**
+ * Create an error to signal that a file or folder wasn't found.
+ * @param messageOrUri Message or uri.
+ */
+ static FileNotFound(messageOrUri?: string | Uri): FileSystemError;
+
+ /**
+ * Create an error to signal that a file or folder already exists, e.g. when
+ * creating but not overwriting a file.
+ * @param messageOrUri Message or uri.
+ */
+ static FileExists(messageOrUri?: string | Uri): FileSystemError;
+
+ /**
+ * Create an error to signal that a file is not a folder.
+ * @param messageOrUri Message or uri.
+ */
+ static FileNotADirectory(messageOrUri?: string | Uri): FileSystemError;
+
+ /**
+ * Create an error to signal that a file is a folder.
+ * @param messageOrUri Message or uri.
+ */
+ static FileIsADirectory(messageOrUri?: string | Uri): FileSystemError;
+
+ /**
+ * Create an error to signal that an operation lacks required permissions.
+ * @param messageOrUri Message or uri.
+ */
+ static NoPermissions(messageOrUri?: string | Uri): FileSystemError;
+
+ /**
+ * Create an error to signal that the file system is unavailable or too busy to
+ * complete a request.
+ * @param messageOrUri Message or uri.
+ */
+ static Unavailable(messageOrUri?: string | Uri): FileSystemError;
+
+ /**
+ * Creates a new filesystem error.
+ *
+ * @param messageOrUri Message or uri.
+ */
+ constructor(messageOrUri?: string | Uri);
+
+ /**
+ * A code that identifies this error.
+ *
+ * Possible values are names of errors, like {@linkcode FileSystemError.FileNotFound FileNotFound},
+ * or `Unknown` for unspecified errors.
+ */
+ readonly code: string;
+ }
+
+ /**
+ * Enumeration of file change types.
+ */
+ export enum FileChangeType {
+
+ /**
+ * The contents or metadata of a file have changed.
+ */
+ Changed = 1,
+
+ /**
+ * A file has been created.
+ */
+ Created = 2,
+
+ /**
+ * A file has been deleted.
+ */
+ Deleted = 3,
+ }
+
+ /**
+ * The event filesystem providers must use to signal a file change.
+ */
+ export interface FileChangeEvent {
+
+ /**
+ * The type of change.
+ */
+ readonly type: FileChangeType;
+
+ /**
+ * The uri of the file that has changed.
+ */
+ readonly uri: Uri;
+ }
+
+ /**
+ * The filesystem provider defines what the editor needs to read, write, discover,
+ * and to manage files and folders. It allows extensions to serve files from remote places,
+ * like ftp-servers, and to seamlessly integrate those into the editor.
+ *
+ * * *Note 1:* The filesystem provider API works with {@link Uri uris} and assumes hierarchical
+ * paths, e.g. `foo:/my/path` is a child of `foo:/my/` and a parent of `foo:/my/path/deeper`.
+ * * *Note 2:* There is an activation event `onFileSystem:` that fires when a file
+ * or folder is being accessed.
+ * * *Note 3:* The word 'file' is often used to denote all {@link FileType kinds} of files, e.g.
+ * folders, symbolic links, and regular files.
+ */
+ export interface FileSystemProvider {
+
+ /**
+ * An event to signal that a resource has been created, changed, or deleted. This
+ * event should fire for resources that are being {@link FileSystemProvider.watch watched}
+ * by clients of this provider.
+ *
+ * *Note:* It is important that the metadata of the file that changed provides an
+ * updated `mtime` that advanced from the previous value in the {@link FileStat stat} and a
+ * correct `size` value. Otherwise there may be optimizations in place that will not show
+ * the change in an editor for example.
+ */
+ readonly onDidChangeFile: Event;
+
+ /**
+ * Subscribe to events in the file or folder denoted by `uri`.
+ *
+ * The editor will call this function for files and folders. In the latter case, the
+ * options differ from defaults, e.g. what files/folders to exclude from watching
+ * and if subfolders, sub-subfolder, etc. should be watched (`recursive`).
+ *
+ * @param uri The uri of the file to be watched.
+ * @param options Configures the watch.
+ * @returns A disposable that tells the provider to stop watching the `uri`.
+ */
+ watch(uri: Uri, options: { recursive: boolean; excludes: string[] }): Disposable;
+
+ /**
+ * Retrieve metadata about a file.
+ *
+ * Note that the metadata for symbolic links should be the metadata of the file they refer to.
+ * Still, the {@link FileType.SymbolicLink SymbolicLink}-type must be used in addition to the actual type, e.g.
+ * `FileType.SymbolicLink | FileType.Directory`.
+ *
+ * @param uri The uri of the file to retrieve metadata about.
+ * @return The file metadata about the file.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist.
+ */
+ stat(uri: Uri): FileStat | Thenable;
+
+ /**
+ * Retrieve all entries of a {@link FileType.Directory directory}.
+ *
+ * @param uri The uri of the folder.
+ * @return An array of name/type-tuples or a thenable that resolves to such.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist.
+ */
+ readDirectory(uri: Uri): [string, FileType][] | Thenable<[string, FileType][]>;
+
+ /**
+ * Create a new directory (Note, that new files are created via `write`-calls).
+ *
+ * @param uri The uri of the new folder.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist, e.g. no mkdirp-logic required.
+ * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists.
+ * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient.
+ */
+ createDirectory(uri: Uri): void | Thenable;
+
+ /**
+ * Read the entire contents of a file.
+ *
+ * @param uri The uri of the file.
+ * @return An array of bytes or a thenable that resolves to such.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist.
+ */
+ readFile(uri: Uri): Uint8Array | Thenable;
+
+ /**
+ * Write data to a file, replacing its entire contents.
+ *
+ * @param uri The uri of the file.
+ * @param content The new content of the file.
+ * @param options Defines if missing files should or must be created.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist and `create` is not set.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when the parent of `uri` doesn't exist and `create` is set, e.g. no mkdirp-logic required.
+ * @throws {@linkcode FileSystemError.FileExists FileExists} when `uri` already exists, `create` is set but `overwrite` is not set.
+ * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient.
+ */
+ writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void | Thenable;
+
+ /**
+ * Delete a file.
+ *
+ * @param uri The resource that is to be deleted.
+ * @param options Defines if deletion of folders is recursive.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `uri` doesn't exist.
+ * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient.
+ */
+ delete(uri: Uri, options: { recursive: boolean }): void | Thenable;
+
+ /**
+ * Rename a file or folder.
+ *
+ * @param oldUri The existing file.
+ * @param newUri The new location.
+ * @param options Defines if existing files should be overwritten.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `oldUri` doesn't exist.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `newUri` doesn't exist, e.g. no mkdirp-logic required.
+ * @throws {@linkcode FileSystemError.FileExists FileExists} when `newUri` exists and when the `overwrite` option is not `true`.
+ * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient.
+ */
+ rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void | Thenable;
+
+ /**
+ * Copy files or folders. Implementing this function is optional but it will speedup
+ * the copy operation.
+ *
+ * @param source The existing file.
+ * @param destination The destination location.
+ * @param options Defines if existing files should be overwritten.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when `source` doesn't exist.
+ * @throws {@linkcode FileSystemError.FileNotFound FileNotFound} when parent of `destination` doesn't exist, e.g. no mkdirp-logic required.
+ * @throws {@linkcode FileSystemError.FileExists FileExists} when `destination` exists and when the `overwrite` option is not `true`.
+ * @throws {@linkcode FileSystemError.NoPermissions NoPermissions} when permissions aren't sufficient.
+ */
+ copy?(source: Uri, destination: Uri, options: { overwrite: boolean }): void | Thenable;
+ }
+
+ /**
+ * The file system interface exposes the editor's built-in and contributed
+ * {@link FileSystemProvider file system providers}. It allows extensions to work
+ * with files from the local disk as well as files from remote places, like the
+ * remote extension host or ftp-servers.
+ *
+ * *Note* that an instance of this interface is available as {@linkcode workspace.fs}.
+ */
+ export interface FileSystem {
+
+ /**
+ * Retrieve metadata about a file.
+ *
+ * @param uri The uri of the file to retrieve metadata about.
+ * @return The file metadata about the file.
+ */
+ stat(uri: Uri): Thenable;
+
+ /**
+ * Retrieve all entries of a {@link FileType.Directory directory}.
+ *
+ * @param uri The uri of the folder.
+ * @return An array of name/type-tuples or a thenable that resolves to such.
+ */
+ readDirectory(uri: Uri): Thenable<[string, FileType][]>;
+
+ /**
+ * Create a new directory (Note, that new files are created via `write`-calls).
+ *
+ * *Note* that missing directories are created automatically, e.g this call has
+ * `mkdirp` semantics.
+ *
+ * @param uri The uri of the new folder.
+ */
+ createDirectory(uri: Uri): Thenable;
+
+ /**
+ * Read the entire contents of a file.
+ *
+ * @param uri The uri of the file.
+ * @return An array of bytes or a thenable that resolves to such.
+ */
+ readFile(uri: Uri): Thenable;
+
+ /**
+ * Write data to a file, replacing its entire contents.
+ *
+ * @param uri The uri of the file.
+ * @param content The new content of the file.
+ */
+ writeFile(uri: Uri, content: Uint8Array): Thenable;
+
+ /**
+ * Delete a file.
+ *
+ * @param uri The resource that is to be deleted.
+ * @param options Defines if trash can should be used and if deletion of folders is recursive
+ */
+ delete(uri: Uri, options?: { recursive?: boolean, useTrash?: boolean }): Thenable;
+
+ /**
+ * Rename a file or folder.
+ *
+ * @param oldUri The existing file.
+ * @param newUri The new location.
+ * @param options Defines if existing files should be overwritten.
+ */
+ rename(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable;
+
+ /**
+ * Copy files or folders.
+ *
+ * @param source The existing file.
+ * @param destination The destination location.
+ * @param options Defines if existing files should be overwritten.
+ */
+ copy(source: Uri, target: Uri, options?: { overwrite?: boolean }): Thenable;
+
+ /**
+ * Check if a given file system supports writing files.
+ *
+ * Keep in mind that just because a file system supports writing, that does
+ * not mean that writes will always succeed. There may be permissions issues
+ * or other errors that prevent writing a file.
+ *
+ * @param scheme The scheme of the filesystem, for example `file` or `git`.
+ *
+ * @return `true` if the file system supports writing, `false` if it does not
+ * support writing (i.e. it is readonly), and `undefined` if the editor does not
+ * know about the filesystem.
+ */
+ isWritableFileSystem(scheme: string): boolean | undefined;
+ }
+
+ /**
+ * Defines a port mapping used for localhost inside the webview.
+ */
+ export interface WebviewPortMapping {
+ /**
+ * Localhost port to remap inside the webview.
+ */
+ readonly webviewPort: number;
+
+ /**
+ * Destination port. The `webviewPort` is resolved to this port.
+ */
+ readonly extensionHostPort: number;
+ }
+
+ /**
+ * Content settings for a webview.
+ */
+ export interface WebviewOptions {
+ /**
+ * Controls whether scripts are enabled in the webview content or not.
+ *
+ * Defaults to false (scripts-disabled).
+ */
+ readonly enableScripts?: boolean;
+
+ /**
+ * Controls whether command uris are enabled in webview content or not.
+ *
+ * Defaults to false.
+ */
+ readonly enableCommandUris?: boolean;
+
+ /**
+ * Root paths from which the webview can load local (filesystem) resources using uris from `asWebviewUri`
+ *
+ * Default to the root folders of the current workspace plus the extension's install directory.
+ *
+ * Pass in an empty array to disallow access to any local resources.
+ */
+ readonly localResourceRoots?: readonly Uri[];
+
+ /**
+ * Mappings of localhost ports used inside the webview.
+ *
+ * Port mapping allow webviews to transparently define how localhost ports are resolved. This can be used
+ * to allow using a static localhost port inside the webview that is resolved to random port that a service is
+ * running on.
+ *
+ * If a webview accesses localhost content, we recommend that you specify port mappings even if
+ * the `webviewPort` and `extensionHostPort` ports are the same.
+ *
+ * *Note* that port mappings only work for `http` or `https` urls. Websocket urls (e.g. `ws://localhost:3000`)
+ * cannot be mapped to another port.
+ */
+ readonly portMapping?: readonly WebviewPortMapping[];
+ }
+
+ /**
+ * Displays html content, similarly to an iframe.
+ */
+ export interface Webview {
+ /**
+ * Content settings for the webview.
+ */
+ options: WebviewOptions;
+
+ /**
+ * HTML contents of the webview.
+ *
+ * This should be a complete, valid html document. Changing this property causes the webview to be reloaded.
+ *
+ * Webviews are sandboxed from normal extension process, so all communication with the webview must use
+ * message passing. To send a message from the extension to the webview, use {@linkcode Webview.postMessage postMessage}.
+ * To send message from the webview back to an extension, use the `acquireVsCodeApi` function inside the webview
+ * to get a handle to the editor's api and then call `.postMessage()`:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * To load a resources from the workspace inside a webview, use the {@linkcode Webview.asWebviewUri asWebviewUri} method
+ * and ensure the resource's directory is listed in {@linkcode WebviewOptions.localResourceRoots}.
+ *
+ * Keep in mind that even though webviews are sandboxed, they still allow running scripts and loading arbitrary content,
+ * so extensions must follow all standard web security best practices when working with webviews. This includes
+ * properly sanitizing all untrusted input (including content from the workspace) and
+ * setting a [content security policy](https://aka.ms/vscode-api-webview-csp).
+ */
+ html: string;
+
+ /**
+ * Fired when the webview content posts a message.
+ *
+ * Webview content can post strings or json serializable objects back to an extension. They cannot
+ * post `Blob`, `File`, `ImageData` and other DOM specific objects since the extension that receives the
+ * message does not run in a browser environment.
+ */
+ readonly onDidReceiveMessage: Event;
+
+ /**
+ * Post a message to the webview content.
+ *
+ * Messages are only delivered if the webview is live (either visible or in the
+ * background with `retainContextWhenHidden`).
+ *
+ * @param message Body of the message. This must be a string or other json serializable object.
+ *
+ * For older versions of vscode, if an `ArrayBuffer` is included in `message`,
+ * it will not be serialized properly and will not be received by the webview.
+ * Similarly any TypedArrays, such as a `Uint8Array`, will be very inefficiently
+ * serialized and will also not be recreated as a typed array inside the webview.
+ *
+ * However if your extension targets vscode 1.57+ in the `engines` field of its
+ * `package.json`, any `ArrayBuffer` values that appear in `message` will be more
+ * efficiently transferred to the webview and will also be correctly recreated inside
+ * of the webview.
+ */
+ postMessage(message: any): Thenable;
+
+ /**
+ * Convert a uri for the local file system to one that can be used inside webviews.
+ *
+ * Webviews cannot directly load resources from the workspace or local file system using `file:` uris. The
+ * `asWebviewUri` function takes a local `file:` uri and converts it into a uri that can be used inside of
+ * a webview to load the same resource:
+ *
+ * ```ts
+ * webview.html = ``
+ * ```
+ */
+ asWebviewUri(localResource: Uri): Uri;
+
+ /**
+ * Content security policy source for webview resources.
+ *
+ * This is the origin that should be used in a content security policy rule:
+ *
+ * ```
+ * img-src https: ${webview.cspSource} ...;
+ * ```
+ */
+ readonly cspSource: string;
+ }
+
+ /**
+ * Content settings for a webview panel.
+ */
+ export interface WebviewPanelOptions {
+ /**
+ * Controls if the find widget is enabled in the panel.
+ *
+ * Defaults to false.
+ */
+ readonly enableFindWidget?: boolean;
+
+ /**
+ * Controls if the webview panel's content (iframe) is kept around even when the panel
+ * is no longer visible.
+ *
+ * Normally the webview panel's html context is created when the panel becomes visible
+ * and destroyed when it is hidden. Extensions that have complex state
+ * or UI can set the `retainContextWhenHidden` to make the editor keep the webview
+ * context around, even when the webview moves to a background tab. When a webview using
+ * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended.
+ * When the panel becomes visible again, the context is automatically restored
+ * in the exact same state it was in originally. You cannot send messages to a
+ * hidden webview, even with `retainContextWhenHidden` enabled.
+ *
+ * `retainContextWhenHidden` has a high memory overhead and should only be used if
+ * your panel's context cannot be quickly saved and restored.
+ */
+ readonly retainContextWhenHidden?: boolean;
+ }
+
+ /**
+ * A panel that contains a webview.
+ */
+ interface WebviewPanel {
+ /**
+ * Identifies the type of the webview panel, such as `'markdown.preview'`.
+ */
+ readonly viewType: string;
+
+ /**
+ * Title of the panel shown in UI.
+ */
+ title: string;
+
+ /**
+ * Icon for the panel shown in UI.
+ */
+ iconPath?: Uri | { light: Uri; dark: Uri };
+
+ /**
+ * {@linkcode Webview} belonging to the panel.
+ */
+ readonly webview: Webview;
+
+ /**
+ * Content settings for the webview panel.
+ */
+ readonly options: WebviewPanelOptions;
+
+ /**
+ * Editor position of the panel. This property is only set if the webview is in
+ * one of the editor view columns.
+ */
+ readonly viewColumn?: ViewColumn;
+
+ /**
+ * Whether the panel is active (focused by the user).
+ */
+ readonly active: boolean;
+
+ /**
+ * Whether the panel is visible.
+ */
+ readonly visible: boolean;
+
+ /**
+ * Fired when the panel's view state changes.
+ */
+ readonly onDidChangeViewState: Event;
+
+ /**
+ * Fired when the panel is disposed.
+ *
+ * This may be because the user closed the panel or because `.dispose()` was
+ * called on it.
+ *
+ * Trying to use the panel after it has been disposed throws an exception.
+ */
+ readonly onDidDispose: Event;
+
+ /**
+ * Show the webview panel in a given column.
+ *
+ * A webview panel may only show in a single column at a time. If it is already showing, this
+ * method moves it to a new column.
+ *
+ * @param viewColumn View column to show the panel in. Shows in the current `viewColumn` if undefined.
+ * @param preserveFocus When `true`, the webview will not take focus.
+ */
+ reveal(viewColumn?: ViewColumn, preserveFocus?: boolean): void;
+
+ /**
+ * Dispose of the webview panel.
+ *
+ * This closes the panel if it showing and disposes of the resources owned by the webview.
+ * Webview panels are also disposed when the user closes the webview panel. Both cases
+ * fire the `onDispose` event.
+ */
+ dispose(): any;
+ }
+
+ /**
+ * Event fired when a webview panel's view state changes.
+ */
+ export interface WebviewPanelOnDidChangeViewStateEvent {
+ /**
+ * Webview panel whose view state changed.
+ */
+ readonly webviewPanel: WebviewPanel;
+ }
+
+ /**
+ * Restore webview panels that have been persisted when vscode shuts down.
+ *
+ * There are two types of webview persistence:
+ *
+ * - Persistence within a session.
+ * - Persistence across sessions (across restarts of the editor).
+ *
+ * A `WebviewPanelSerializer` is only required for the second case: persisting a webview across sessions.
+ *
+ * Persistence within a session allows a webview to save its state when it becomes hidden
+ * and restore its content from this state when it becomes visible again. It is powered entirely
+ * by the webview content itself. To save off a persisted state, call `acquireVsCodeApi().setState()` with
+ * any json serializable object. To restore the state again, call `getState()`
+ *
+ * ```js
+ * // Within the webview
+ * const vscode = acquireVsCodeApi();
+ *
+ * // Get existing state
+ * const oldState = vscode.getState() || { value: 0 };
+ *
+ * // Update state
+ * setState({ value: oldState.value + 1 })
+ * ```
+ *
+ * A `WebviewPanelSerializer` extends this persistence across restarts of the editor. When the editor is shutdown,
+ * it will save off the state from `setState` of all webviews that have a serializer. When the
+ * webview first becomes visible after the restart, this state is passed to `deserializeWebviewPanel`.
+ * The extension can then restore the old `WebviewPanel` from this state.
+ *
+ * @param T Type of the webview's state.
+ */
+ interface WebviewPanelSerializer {
+ /**
+ * Restore a webview panel from its serialized `state`.
+ *
+ * Called when a serialized webview first becomes visible.
+ *
+ * @param webviewPanel Webview panel to restore. The serializer should take ownership of this panel. The
+ * serializer must restore the webview's `.html` and hook up all webview events.
+ * @param state Persisted state from the webview content.
+ *
+ * @return Thenable indicating that the webview has been fully restored.
+ */
+ deserializeWebviewPanel(webviewPanel: WebviewPanel, state: T): Thenable;
+ }
+
+ /**
+ * A webview based view.
+ */
+ export interface WebviewView {
+ /**
+ * Identifies the type of the webview view, such as `'hexEditor.dataView'`.
+ */
+ readonly viewType: string;
+
+ /**
+ * The underlying webview for the view.
+ */
+ readonly webview: Webview;
+
+ /**
+ * View title displayed in the UI.
+ *
+ * The view title is initially taken from the extension `package.json` contribution.
+ */
+ title?: string;
+
+ /**
+ * Human-readable string which is rendered less prominently in the title.
+ */
+ description?: string;
+
+ /**
+ * Event fired when the view is disposed.
+ *
+ * Views are disposed when they are explicitly hidden by a user (this happens when a user
+ * right clicks in a view and unchecks the webview view).
+ *
+ * Trying to use the view after it has been disposed throws an exception.
+ */
+ readonly onDidDispose: Event;
+
+ /**
+ * Tracks if the webview is currently visible.
+ *
+ * Views are visible when they are on the screen and expanded.
+ */
+ readonly visible: boolean;
+
+ /**
+ * Event fired when the visibility of the view changes.
+ *
+ * Actions that trigger a visibility change:
+ *
+ * - The view is collapsed or expanded.
+ * - The user switches to a different view group in the sidebar or panel.
+ *
+ * Note that hiding a view using the context menu instead disposes of the view and fires `onDidDispose`.
+ */
+ readonly onDidChangeVisibility: Event;
+
+ /**
+ * Reveal the view in the UI.
+ *
+ * If the view is collapsed, this will expand it.
+ *
+ * @param preserveFocus When `true` the view will not take focus.
+ */
+ show(preserveFocus?: boolean): void;
+ }
+
+ /**
+ * Additional information the webview view being resolved.
+ *
+ * @param T Type of the webview's state.
+ */
+ interface WebviewViewResolveContext {
+ /**
+ * Persisted state from the webview content.
+ *
+ * To save resources, the editor normally deallocates webview documents (the iframe content) that are not visible.
+ * For example, when the user collapse a view or switches to another top level activity in the sidebar, the
+ * `WebviewView` itself is kept alive but the webview's underlying document is deallocated. It is recreated when
+ * the view becomes visible again.
+ *
+ * You can prevent this behavior by setting `retainContextWhenHidden` in the `WebviewOptions`. However this
+ * increases resource usage and should be avoided wherever possible. Instead, you can use persisted state to
+ * save off a webview's state so that it can be quickly recreated as needed.
+ *
+ * To save off a persisted state, inside the webview call `acquireVsCodeApi().setState()` with
+ * any json serializable object. To restore the state again, call `getState()`. For example:
+ *
+ * ```js
+ * // Within the webview
+ * const vscode = acquireVsCodeApi();
+ *
+ * // Get existing state
+ * const oldState = vscode.getState() || { value: 0 };
+ *
+ * // Update state
+ * setState({ value: oldState.value + 1 })
+ * ```
+ *
+ * The editor ensures that the persisted state is saved correctly when a webview is hidden and across
+ * editor restarts.
+ */
+ readonly state: T | undefined;
+ }
+
+ /**
+ * Provider for creating `WebviewView` elements.
+ */
+ export interface WebviewViewProvider {
+ /**
+ * Revolves a webview view.
+ *
+ * `resolveWebviewView` is called when a view first becomes visible. This may happen when the view is
+ * first loaded or when the user hides and then shows a view again.
+ *
+ * @param webviewView Webview view to restore. The provider should take ownership of this view. The
+ * provider must set the webview's `.html` and hook up all webview events it is interested in.
+ * @param context Additional metadata about the view being resolved.
+ * @param token Cancellation token indicating that the view being provided is no longer needed.
+ *
+ * @return Optional thenable indicating that the view has been fully resolved.
+ */
+ resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Thenable | void;
+ }
+
+ /**
+ * Provider for text based custom editors.
+ *
+ * Text based custom editors use a {@linkcode TextDocument} as their data model. This considerably simplifies
+ * implementing a custom editor as it allows the editor to handle many common operations such as
+ * undo and backup. The provider is responsible for synchronizing text changes between the webview and the `TextDocument`.
+ */
+ export interface CustomTextEditorProvider {
+
+ /**
+ * Resolve a custom editor for a given text resource.
+ *
+ * This is called when a user first opens a resource for a `CustomTextEditorProvider`, or if they reopen an
+ * existing editor using this `CustomTextEditorProvider`.
+ *
+ *
+ * @param document Document for the resource to resolve.
+ *
+ * @param webviewPanel The webview panel used to display the editor UI for this resource.
+ *
+ * During resolve, the provider must fill in the initial html for the content webview panel and hook up all
+ * the event listeners on it that it is interested in. The provider can also hold onto the `WebviewPanel` to
+ * use later for example in a command. See {@linkcode WebviewPanel} for additional details.
+ *
+ * @param token A cancellation token that indicates the result is no longer needed.
+ *
+ * @return Thenable indicating that the custom editor has been resolved.
+ */
+ resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void;
+ }
+
+ /**
+ * Represents a custom document used by a {@linkcode CustomEditorProvider}.
+ *
+ * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is
+ * managed by the editor. When no more references remain to a `CustomDocument`, it is disposed of.
+ */
+ interface CustomDocument {
+ /**
+ * The associated uri for this document.
+ */
+ readonly uri: Uri;
+
+ /**
+ * Dispose of the custom document.
+ *
+ * This is invoked by the editor when there are no more references to a given `CustomDocument` (for example when
+ * all editors associated with the document have been closed.)
+ */
+ dispose(): void;
+ }
+
+ /**
+ * Event triggered by extensions to signal to the editor that an edit has occurred on an {@linkcode CustomDocument}.
+ *
+ * @see {@linkcode CustomEditorProvider.onDidChangeCustomDocument}.
+ */
+ interface CustomDocumentEditEvent {
+
+ /**
+ * The document that the edit is for.
+ */
+ readonly document: T;
+
+ /**
+ * Undo the edit operation.
+ *
+ * This is invoked by the editor when the user undoes this edit. To implement `undo`, your
+ * extension should restore the document and editor to the state they were in just before this
+ * edit was added to the editor's internal edit stack by `onDidChangeCustomDocument`.
+ */
+ undo(): Thenable | void;
+
+ /**
+ * Redo the edit operation.
+ *
+ * This is invoked by the editor when the user redoes this edit. To implement `redo`, your
+ * extension should restore the document and editor to the state they were in just after this
+ * edit was added to the editor's internal edit stack by `onDidChangeCustomDocument`.
+ */
+ redo(): Thenable | void;
+
+ /**
+ * Display name describing the edit.
+ *
+ * This will be shown to users in the UI for undo/redo operations.
+ */
+ readonly label?: string;
+ }
+
+ /**
+ * Event triggered by extensions to signal to the editor that the content of a {@linkcode CustomDocument}
+ * has changed.
+ *
+ * @see {@linkcode CustomEditorProvider.onDidChangeCustomDocument}.
+ */
+ interface CustomDocumentContentChangeEvent {
+ /**
+ * The document that the change is for.
+ */
+ readonly document: T;
+ }
+
+ /**
+ * A backup for an {@linkcode CustomDocument}.
+ */
+ interface CustomDocumentBackup {
+ /**
+ * Unique identifier for the backup.
+ *
+ * This id is passed back to your extension in `openCustomDocument` when opening a custom editor from a backup.
+ */
+ readonly id: string;
+
+ /**
+ * Delete the current backup.
+ *
+ * This is called by the editor when it is clear the current backup is no longer needed, such as when a new backup
+ * is made or when the file is saved.
+ */
+ delete(): void;
+ }
+
+ /**
+ * Additional information used to implement {@linkcode CustomEditableDocument.backup}.
+ */
+ interface CustomDocumentBackupContext {
+ /**
+ * Suggested file location to write the new backup.
+ *
+ * Note that your extension is free to ignore this and use its own strategy for backup.
+ *
+ * If the editor is for a resource from the current workspace, `destination` will point to a file inside
+ * `ExtensionContext.storagePath`. The parent folder of `destination` may not exist, so make sure to created it
+ * before writing the backup to this location.
+ */
+ readonly destination: Uri;
+ }
+
+ /**
+ * Additional information about the opening custom document.
+ */
+ interface CustomDocumentOpenContext {
+ /**
+ * The id of the backup to restore the document from or `undefined` if there is no backup.
+ *
+ * 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;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Provider for readonly custom editors that use a custom document model.
+ *
+ * Custom editors use {@linkcode CustomDocument} as their document model instead of a {@linkcode TextDocument}.
+ *
+ * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple
+ * text based documents, use {@linkcode CustomTextEditorProvider} instead.
+ *
+ * @param T Type of the custom document returned by this provider.
+ */
+ export interface CustomReadonlyEditorProvider {
+
+ /**
+ * Create a new document for a given resource.
+ *
+ * `openCustomDocument` is called when the first time an editor for a given resource is opened. The opened
+ * document is then passed to `resolveCustomEditor` so that the editor can be shown to the user.
+ *
+ * Already opened `CustomDocument` are re-used if the user opened additional editors. When all editors for a
+ * given resource are closed, the `CustomDocument` is disposed of. Opening an editor at this point will
+ * trigger another call to `openCustomDocument`.
+ *
+ * @param uri Uri of the document to open.
+ * @param openContext Additional information about the opening custom document.
+ * @param token A cancellation token that indicates the result is no longer needed.
+ *
+ * @return The custom document.
+ */
+ openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): Thenable | T;
+
+ /**
+ * Resolve a custom editor for a given resource.
+ *
+ * This is called whenever the user opens a new editor for this `CustomEditorProvider`.
+ *
+ * @param document Document for the resource being resolved.
+ *
+ * @param webviewPanel The webview panel used to display the editor UI for this resource.
+ *
+ * During resolve, the provider must fill in the initial html for the content webview panel and hook up all
+ * the event listeners on it that it is interested in. The provider can also hold onto the `WebviewPanel` to
+ * use later for example in a command. See {@linkcode WebviewPanel} for additional details.
+ *
+ * @param token A cancellation token that indicates the result is no longer needed.
+ *
+ * @return Optional thenable indicating that the custom editor has been resolved.
+ */
+ resolveCustomEditor(document: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void;
+ }
+
+ /**
+ * Provider for editable custom editors that use a custom document model.
+ *
+ * Custom editors use {@linkcode CustomDocument} as their document model instead of a {@linkcode TextDocument}.
+ * This gives extensions full control over actions such as edit, save, and backup.
+ *
+ * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple
+ * text based documents, use {@linkcode CustomTextEditorProvider} instead.
+ *
+ * @param T Type of the custom document returned by this provider.
+ */
+ export interface CustomEditorProvider extends CustomReadonlyEditorProvider {
+ /**
+ * Signal that an edit has occurred inside a custom editor.
+ *
+ * This event must be fired by your extension whenever an edit happens in a custom editor. An edit can be
+ * anything from changing some text, to cropping an image, to reordering a list. Your extension is free to
+ * define what an edit is and what data is stored on each edit.
+ *
+ * Firing `onDidChange` causes the editors to be marked as being dirty. This is cleared when the user either
+ * saves or reverts the file.
+ *
+ * Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows
+ * users to undo and redo the edit using the editor's standard keyboard shortcuts. The editor will also mark
+ * the editor as no longer being dirty if the user undoes all edits to the last saved state.
+ *
+ * Editors that support editing but cannot use the editor's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`.
+ * The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either
+ * `save` or `revert` the file.
+ *
+ * An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events.
+ */
+ readonly onDidChangeCustomDocument: Event> | Event>;
+
+ /**
+ * Save a custom document.
+ *
+ * This method is invoked by the editor when the user saves a custom editor. This can happen when the user
+ * triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled.
+ *
+ * To implement `save`, the implementer must persist the custom editor. This usually means writing the
+ * file data for the custom document to disk. After `save` completes, any associated editor instances will
+ * no longer be marked as dirty.
+ *
+ * @param document Document to save.
+ * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered).
+ *
+ * @return Thenable signaling that saving has completed.
+ */
+ saveCustomDocument(document: T, cancellation: CancellationToken): Thenable;
+
+ /**
+ * Save a custom document to a different location.
+ *
+ * This method is invoked by the editor when the user triggers 'save as' on a custom editor. The implementer must
+ * persist the custom editor to `destination`.
+ *
+ * When the user accepts save as, the current editor is be replaced by an non-dirty editor for the newly saved file.
+ *
+ * @param document Document to save.
+ * @param destination Location to save to.
+ * @param cancellation Token that signals the save is no longer required.
+ *
+ * @return Thenable signaling that saving has completed.
+ */
+ saveCustomDocumentAs(document: T, destination: Uri, cancellation: CancellationToken): Thenable;
+
+ /**
+ * Revert a custom document to its last saved state.
+ *
+ * This method is invoked by the editor when the user triggers `File: Revert File` in a custom editor. (Note that
+ * this is only used using the editor's `File: Revert File` command and not on a `git revert` of the file).
+ *
+ * To implement `revert`, the implementer must make sure all editor instances (webviews) for `document`
+ * are displaying the document in the same state is saved in. This usually means reloading the file from the
+ * workspace.
+ *
+ * @param document Document to revert.
+ * @param cancellation Token that signals the revert is no longer required.
+ *
+ * @return Thenable signaling that the change has completed.
+ */
+ revertCustomDocument(document: T, cancellation: CancellationToken): Thenable;
+
+ /**
+ * Back up a dirty custom document.
+ *
+ * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in
+ * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in
+ * the `ExtensionContext.storagePath`. When the editor reloads and your custom editor is opened for a resource,
+ * your extension should first check to see if any backups exist for the resource. If there is a backup, your
+ * extension should load the file contents from there instead of from the resource in the workspace.
+ *
+ * `backup` is triggered approximately one second after the user stops editing the document. If the user
+ * rapidly edits the document, `backup` will not be invoked until the editing stops.
+ *
+ * `backup` is not invoked when `auto save` is enabled (since auto save already persists the resource).
+ *
+ * @param document Document to backup.
+ * @param context Information that can be used to backup the document.
+ * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your
+ * extension to decided how to respond to cancellation. If for example your extension is backing up a large file
+ * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather
+ * than cancelling it to ensure that the editor has some valid backup.
+ */
+ backupCustomDocument(document: T, context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable;
+ }
+
+ /**
+ * The clipboard provides read and write access to the system's clipboard.
+ */
+ export interface Clipboard {
+
+ /**
+ * Read the current clipboard contents as text.
+ * @returns A thenable that resolves to a string.
+ */
+ readText(): Thenable;
+
+ /**
+ * Writes text into the clipboard.
+ * @returns A thenable that resolves when writing happened.
+ */
+ writeText(value: string): Thenable;
+ }
+
+ /**
+ * Possible kinds of UI that can use extensions.
+ */
+ export enum UIKind {
+
+ /**
+ * Extensions are accessed from a desktop application.
+ */
+ Desktop = 1,
+
+ /**
+ * Extensions are accessed from a web browser.
+ */
+ Web = 2
+ }
+
+ /**
+ * Namespace describing the environment the editor runs in.
+ */
+ export namespace env {
+
+ /**
+ * The application name of the editor, like 'VS Code'.
+ */
+ export const appName: string;
+
+ /**
+ * The application root folder from which the editor is running.
+ *
+ * *Note* that the value is the empty string when running in an
+ * environment that has no representation of an application root folder.
+ */
+ export const appRoot: string;
+
+ /**
+ * The custom uri scheme the editor registers to in the operating system.
+ */
+ export const uriScheme: string;
+
+ /**
+ * Represents the preferred user-language, like `de-CH`, `fr`, or `en-US`.
+ */
+ export const language: string;
+
+ /**
+ * The system clipboard.
+ */
+ export const clipboard: Clipboard;
+
+ /**
+ * A unique identifier for the computer.
+ */
+ export const machineId: string;
+
+ /**
+ * A unique identifier for the current session.
+ * Changes each time the editor is started.
+ */
+ export const sessionId: string;
+
+ /**
+ * Indicates that this is a fresh install of the application.
+ * `true` if within the first day of installation otherwise `false`.
+ */
+ export const isNewAppInstall: boolean;
+
+ /**
+ * Indicates whether the users has telemetry enabled.
+ * Can be observed to determine if the extension should send telemetry.
+ */
+ export const isTelemetryEnabled: boolean;
+
+ /**
+ * An {@link Event} which fires when the user enabled or disables telemetry.
+ * `true` if the user has enabled telemetry or `false` if the user has disabled telemetry.
+ */
+ export const onDidChangeTelemetryEnabled: Event