├── .yarnrc ├── sample ├── package.nls.json ├── test-workspace │ ├── folder │ │ ├── x.txt │ │ └── .bar │ │ │ └── .foo │ ├── hello.txt │ ├── world.txt │ └── folder_with_utf_8_🧿 │ │ └── !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~ ├── .vscode │ ├── extensions.json │ ├── settings.json │ ├── tasks.json │ └── launch.json ├── tsconfig.runTest.json ├── src │ └── web │ │ ├── test │ │ ├── suite │ │ │ ├── extension.test.ts │ │ │ ├── index.ts │ │ │ ├── search.test.ts │ │ │ └── fs.test.ts │ │ └── runTest.ts │ │ └── extension.ts ├── README.md ├── tsconfig.json ├── package.json └── webpack.config.js ├── fs-provider ├── package.nls.json ├── .vscode │ ├── extensions.json │ ├── settings.json │ └── tasks.json ├── tsconfig.json ├── package.json ├── webpack.config.js ├── vscode.proposed.fileSearchProvider.d.ts ├── src │ ├── fsExtensionMain.ts │ └── fsProvider.ts └── vscode.proposed.textSearchProvider.d.ts ├── .prettierrc ├── tsconfig.tsbuildinfo ├── .gitignore ├── .npmignore ├── .vscode ├── tasks.json ├── settings.json └── launch.json ├── tsconfig.json ├── .editorconfig ├── src ├── server │ ├── tsconfig.json │ ├── main.ts │ ├── mounts.ts │ ├── extensions.ts │ ├── app.ts │ ├── download.ts │ ├── workbench.ts │ └── index.ts └── browser │ ├── tsconfig-amd.json │ ├── tsconfig-esm.json │ ├── main.ts │ └── workbench.api.d.ts ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── views ├── workbench-esm.html └── workbench.html ├── CHANGELOG.md ├── eslint.config.mjs ├── package.json ├── SECURITY.md └── README.md /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true -------------------------------------------------------------------------------- /sample/package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /fs-provider/package.nls.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /sample/test-workspace/folder/x.txt: -------------------------------------------------------------------------------- 1 | // x -------------------------------------------------------------------------------- /sample/test-workspace/folder/.bar/.foo: -------------------------------------------------------------------------------- 1 | foo -------------------------------------------------------------------------------- /sample/test-workspace/hello.txt: -------------------------------------------------------------------------------- 1 | // hello -------------------------------------------------------------------------------- /sample/test-workspace/world.txt: -------------------------------------------------------------------------------- 1 | // world -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 120, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"fileNames":[],"fileInfos":[],"root":[],"options":{"composite":true},"version":"5.9.3"} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode-test 2 | .vscode-test-web/ 3 | node_modules 4 | out 5 | sample/dist 6 | fs-provider/dist 7 | tsconfig.tsbuildinfo 8 | -------------------------------------------------------------------------------- /sample/test-workspace/folder_with_utf_8_🧿/!#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~: -------------------------------------------------------------------------------- 1 | test_utf_8_🧿 -------------------------------------------------------------------------------- /sample/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /fs-provider/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint", 6 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /sample/tsconfig.runTest.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2022", 5 | "outDir": "dist", 6 | "lib": [ 7 | "ES2022" 8 | ], 9 | "rootDir": "src", 10 | "strict": true 11 | }, 12 | "include": [ 13 | "./src/web/test/runTest.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .npmignore 3 | .editorconfig 4 | .eslint.config.js 5 | .prettierrc 6 | .github/ 7 | build/ 8 | src/ 9 | sample/ 10 | fs-provider/src/ 11 | fs-provider/node_modules/ 12 | 13 | .vscode-test-web/ 14 | tsconfig.json 15 | tslint.json 16 | webpack.config.js 17 | 18 | **/*.js.map 19 | **/*.d.ts 20 | !out/server/index.d.ts 21 | *.tgz -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "npm", 6 | "type": "shell", 7 | "command": "npm", 8 | "args": [ 9 | "run", 10 | "watch" 11 | ], 12 | "isBackground": true, 13 | "problemMatcher": "$tsc-watch", 14 | "group": "build" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "composite": true 5 | }, 6 | "files": [], 7 | "references": [ 8 | { 9 | "path": "./src/server/tsconfig.json" 10 | }, 11 | { 12 | "path": "./src/browser/tsconfig-esm.json" 13 | }, 14 | { 15 | "path": "./src/browser/tsconfig-amd.json" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Tab indentation 7 | [*] 8 | indent_style = tab 9 | trim_trailing_whitespace = true 10 | 11 | # The indent size used in the `package.json` file cannot be changed 12 | # https://github.com/npm/npm/pull/3180#issuecomment-16336516 13 | [{*.yml,*.yaml,package.json}] 14 | indent_style = space 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /sample/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /fs-provider/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /src/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "commonjs", 5 | "lib": [ 6 | "ES2022" 7 | ], 8 | "outDir": "../../out/server", 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": false, 12 | "noImplicitThis": true, 13 | "noUnusedLocals": true, 14 | "alwaysStrict": true, 15 | "skipLibCheck": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "sourceMap": false, 18 | "newLine": "lf" 19 | } 20 | } -------------------------------------------------------------------------------- /src/browser/tsconfig-amd.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "AMD", 5 | "lib": [ 6 | "ES2022", 7 | "DOM", 8 | ], 9 | "outDir": "../../out/browser/amd", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "alwaysStrict": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "sourceMap": false, 19 | "newLine": "lf", 20 | "removeComments": true 21 | } 22 | } -------------------------------------------------------------------------------- /src/browser/tsconfig-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ES2022", 5 | "lib": [ 6 | "ES2022", 7 | "DOM", 8 | ], 9 | "outDir": "../../out/browser/esm", 10 | "declaration": true, 11 | "strict": true, 12 | "noImplicitAny": false, 13 | "noImplicitThis": true, 14 | "noUnusedLocals": true, 15 | "alwaysStrict": true, 16 | "skipLibCheck": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "sourceMap": false, 19 | "newLine": "lf", 20 | "removeComments": true 21 | } 22 | } -------------------------------------------------------------------------------- /sample/src/web/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Web Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 |

2 |

vscode-test-web-sample

3 |

4 | 5 | Sample for using https://github.com/microsoft/vscode-test-web. 6 | 7 | Continuously tested with latest changes: 8 | 9 | - [Azure DevOps](https://dev.azure.com/vscode/vscode-test-web/_build?definitionId=15) 10 | - [Travis](https://travis-ci.org/github/microsoft/vscode-test-web) 11 | 12 | When making changes to `vscode-test-web` library, you should compile and run the tests in this sample project locally to make sure the tests can still run successfully. 13 | 14 | ```bash 15 | npm run install 16 | npm run compile 17 | npm run test 18 | ``` -------------------------------------------------------------------------------- /sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2022", 5 | "lib": [ 6 | "ES2022", "WebWorker" 7 | ], 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */ 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | }, 15 | "exclude": [ 16 | "node_modules", 17 | ".vscode-test" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /sample/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile-web", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": [ 14 | "$ts-webpack", 15 | "$tslint-webpack" 16 | ] 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch-web", 21 | "group": "build", 22 | "isBackground": true, 23 | "problemMatcher": [ 24 | "$ts-webpack-watch", 25 | "$tslint-webpack-watch" 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /fs-provider/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "compile-web", 9 | "group": { 10 | "kind": "build", 11 | "isDefault": true 12 | }, 13 | "problemMatcher": [ 14 | "$ts-webpack", 15 | "$tslint-webpack" 16 | ] 17 | }, 18 | { 19 | "type": "npm", 20 | "script": "watch-web", 21 | "group": "build", 22 | "isBackground": true, 23 | "problemMatcher": [ 24 | "$ts-webpack-watch", 25 | "$tslint-webpack-watch" 26 | ] 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "editor.insertSpaces": true, 4 | "files.eol": "\n", 5 | "files.trimTrailingWhitespace": true, 6 | "files.exclude": { 7 | "**/.git": true, 8 | "**/.DS_Store": true, 9 | "**/*.js": { 10 | "when": "$(basename).ts" 11 | } 12 | }, 13 | "prettier.semi": true, 14 | "git.branchProtection": [ 15 | "main" 16 | ], 17 | "git.branchProtectionPrompt": "alwaysCommitToNewBranch", 18 | "git.branchRandomName.enable": true, 19 | "githubPullRequests.assignCreated": "${user}", 20 | "githubPullRequests.defaultMergeMethod": "squash" 21 | } -------------------------------------------------------------------------------- /fs-provider/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2022", 5 | "outDir": "dist", 6 | "lib": [ 7 | "ES2022", "WebWorker" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "src", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /sample/src/web/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | // imports mocha for the browser, defining the `mocha` global. 2 | require('mocha/mocha'); 3 | 4 | export function run(): Promise { 5 | 6 | return new Promise((c, e) => { 7 | mocha.setup({ 8 | ui: 'tdd', 9 | reporter: undefined 10 | }); 11 | 12 | // bundles all files in the current directory matching `*.test` 13 | const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r); 14 | importAll(require.context('.', true, /\.test$/)); 15 | 16 | try { 17 | // Run the mocha test 18 | mocha.run(failures => { 19 | if (failures > 0) { 20 | e(new Error(`${failures} tests failed.`)); 21 | } else { 22 | c(); 23 | } 24 | }); 25 | } catch (err) { 26 | console.error(err); 27 | e(err); 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | name: Tests 4 | 5 | permissions: {} 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | os: [macos-latest, ubuntu-latest, windows-latest] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | - name: Install Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 22.x 22 | - name: Install root project dependencies 23 | run: npm ci 24 | - name: Install extensions dependencies 25 | run: npm run install-extensions 26 | - name: Run tests 27 | uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 28 | with: 29 | run: npm run sample-tests 30 | -------------------------------------------------------------------------------- /sample/src/web/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '../../../..'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 9 | 10 | // The path to module with the test runner and tests 11 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 12 | 13 | const folderPath = path.resolve(__dirname, '../../../test-workspace'); 14 | 15 | const attachArgName = '--waitForDebugger='; 16 | const waitForDebugger = process.argv.find(arg => arg.startsWith(attachArgName)); 17 | 18 | // Start a web server that serves VSCode in a browser, run the tests 19 | await runTests({ 20 | browserType: 'chromium', 21 | extensionDevelopmentPath, 22 | extensionTestsPath, 23 | folderPath, 24 | waitForDebugger: waitForDebugger ? Number(waitForDebugger.slice(attachArgName.length)) : undefined, 25 | }); 26 | } catch (err) { 27 | console.error('Failed to run tests'); 28 | process.exit(1); 29 | } 30 | } 31 | 32 | main(); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /sample/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Web Extension in VS Code", 6 | "type": "pwa-extensionHost", 7 | "debugWebWorkerHost": true, 8 | "request": "launch", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceFolder}", 11 | "--extensionDevelopmentKind=web ", 12 | "${workspaceFolder}/test-workspace" 13 | ], 14 | "outFiles": [ 15 | "${workspaceFolder}/dist/web/**/*.js" 16 | ], 17 | "preLaunchTask": "npm: watch-web" 18 | }, 19 | { 20 | "name": "Web Extension Tests in VS Code", 21 | "type": "extensionHost", 22 | "debugWebWorkerHost": true, 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionDevelopmentKind=web", 27 | "--extensionTestsPath=${workspaceFolder}/dist/web/test/suite/index", 28 | "${workspaceFolder}/test-workspace" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/dist/web/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch-web" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-test-web-sample", 3 | "displayName": "vscode-test-web-sample", 4 | "description": "", 5 | "version": "0.0.1", 6 | "license": "MIT", 7 | "engines": { 8 | "vscode": "^1.72.0" 9 | }, 10 | "categories": [ 11 | "Other" 12 | ], 13 | "activationEvents": [ 14 | "*" 15 | ], 16 | "browser": "./dist/web/extension.js", 17 | "contributes": { 18 | "commands": [ 19 | { 20 | "command": "vscode-test-web-sample.helloWorld", 21 | "title": "Hello World" 22 | }, 23 | { 24 | "command": "vscode-test-web-sample.findFiles", 25 | "title": "Find files" 26 | } 27 | ] 28 | }, 29 | "scripts": { 30 | "test": "node ./dist/web/test/runTest.js", 31 | "pretest": "npm run compile-web && tsc -p tsconfig.runTest.json", 32 | "vscode:prepublish": "npm run package-web", 33 | "compile-web": "webpack", 34 | "watch-web": "webpack --watch", 35 | "package-web": "webpack --mode production --devtool hidden-source-map" 36 | }, 37 | "devDependencies": { 38 | "@types/mocha": "10.0.10", 39 | "@types/vscode": "^1.94.0", 40 | "@types/webpack-env": "^1.18.8", 41 | "assert": "^2.1.0", 42 | "mocha": "^11.7.5", 43 | "process": "^0.11.10", 44 | "ts-loader": "^9.5.4", 45 | "typescript": "^5.9.3", 46 | "webpack": "^5.103.0", 47 | "webpack-cli": "^6.0.1" 48 | }, 49 | "dependencies": { 50 | "@vscode/test-web": "file:.." 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /fs-provider/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-test-web-fs", 3 | "private": true, 4 | "displayName": "vscode-test-web file system provider", 5 | "description": "Provides a file provider for web tests to access local files and folders.", 6 | "publisher": "vscode", 7 | "version": "0.0.1", 8 | "license": "MIT", 9 | "engines": { 10 | "vscode": "^1.72.0" 11 | }, 12 | "categories": [ 13 | "Other" 14 | ], 15 | "activationEvents": [ 16 | "onFileSystem:vscode-test-web", 17 | "onSearch:vscode-test-web" 18 | ], 19 | "enabledApiProposals": [ 20 | "fileSearchProvider", 21 | "textSearchProvider" 22 | ], 23 | "contributes": { 24 | "resourceLabelFormatters": [ 25 | { 26 | "authority": "mount", 27 | "scheme": "vscode-test-web", 28 | "formatting": { 29 | "workspaceSuffix": "Test Files", 30 | "label": "${path}" 31 | } 32 | } 33 | ] 34 | }, 35 | "browser": "./dist/fsExtensionMain.js", 36 | "scripts": { 37 | "vscode:prepublish": "npm run package-web", 38 | "compile-web": "webpack", 39 | "watch-web": "webpack --watch", 40 | "package-web": "webpack --mode production --devtool hidden-source-map" 41 | }, 42 | "devDependencies": { 43 | "@types/vscode": "^1.94.0", 44 | "minimatch": "10.1.1", 45 | "path-browserify": "^1.0.1", 46 | "process": "^0.11.10", 47 | "request-light": "^0.8.0", 48 | "ts-loader": "^9.5.4", 49 | "vscode-uri": "^3.1.0", 50 | "webpack": "^5.103.0", 51 | "webpack-cli": "^6.0.1" 52 | }, 53 | "dependencies": { 54 | "@vscode/test-web": "file:.." 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/server/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import createApp from './app'; 7 | 8 | export interface IConfig { 9 | readonly extensionPaths: string[] | undefined; 10 | readonly extensionIds: GalleryExtensionInfo[] | undefined; 11 | readonly extensionDevelopmentPath: string | undefined; 12 | readonly extensionTestsPath: string | undefined; 13 | readonly build: Sources | Static | CDN; 14 | readonly folderUri: string | undefined; 15 | readonly folderMountPath: string | undefined; 16 | readonly printServerLog: boolean; 17 | readonly coi: boolean; 18 | readonly esm: boolean; 19 | } 20 | 21 | export interface GalleryExtensionInfo { 22 | readonly id: string; 23 | readonly preRelease?: boolean; 24 | } 25 | 26 | export interface Sources { 27 | readonly type: 'sources'; 28 | readonly location: string; 29 | } 30 | 31 | export interface Static { 32 | readonly type: 'static'; 33 | readonly location: string; 34 | readonly quality: 'stable' | 'insider'; 35 | readonly version: string; 36 | } 37 | 38 | export interface CDN { 39 | readonly type: 'cdn'; 40 | readonly uri: string; 41 | } 42 | 43 | export interface IServer { 44 | close(): void; 45 | } 46 | 47 | export async function runServer(host: string, port: number | undefined, config: IConfig): Promise { 48 | const app = await createApp(config); 49 | try { 50 | const server = app.listen(port, host); 51 | console.log(`Listening on http://${host}:${port}`); 52 | return server; 53 | } catch (e) { 54 | console.error(`Failed to listen to port ${port} on host ${host}`, e); 55 | throw e; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /views/workbench-esm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 45 | {{WORKBENCH_MAIN}} 46 | 47 | -------------------------------------------------------------------------------- /sample/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 'use strict'; 8 | 9 | //@ts-check 10 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 11 | 12 | const path = require('path'); 13 | const webpack = require('webpack'); 14 | 15 | /** @type WebpackConfig */ 16 | const webExtensionConfig = { 17 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 18 | target: 'webworker', // extensions run in a webworker context 19 | entry: { 20 | 'extension': './src/web/extension.ts', 21 | 'test/suite/index': './src/web/test/suite/index.ts' 22 | }, 23 | output: { 24 | filename: '[name].js', 25 | path: path.join(__dirname, './dist/web'), 26 | libraryTarget: 'commonjs', 27 | devtoolModuleFilenameTemplate: "../../[resource-path]", 28 | }, 29 | resolve: { 30 | mainFields: ['module', 'main'], 31 | extensions: ['.ts', '.js'], // support ts-files and js-files 32 | alias: { 33 | }, 34 | fallback: { 35 | 'assert': require.resolve('assert') 36 | } 37 | }, 38 | module: { 39 | rules: [{ 40 | test: /\.ts$/, 41 | exclude: /node_modules/, 42 | use: [ 43 | { 44 | loader: 'ts-loader' 45 | } 46 | ] 47 | }] 48 | }, 49 | plugins: [ 50 | new webpack.ProvidePlugin({ 51 | process: 'process/browser', 52 | }), 53 | ], 54 | externals: { 55 | 'vscode': 'commonjs vscode', // ignored because it doesn't exist 56 | }, 57 | performance: { 58 | hints: false 59 | }, 60 | devtool: 'nosources-source-map' 61 | }; 62 | 63 | module.exports = [ webExtensionConfig ]; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## 0.0.58 3 | * new option `--commit` to specify the build of VS Code to use. By default the latest build is used. 4 | 5 | ## 0.0.37 6 | * new option `--testRunnerDataDir` to set the temporary folder for storing the VS Code builds used for running the tests 7 | 8 | ## 0.0.28 9 | * new option `--coi` to enable cross origin isolation. 10 | 11 | ## 0.0.22 12 | * new option `--printServerLog` replacing `--hideServerLog`. 13 | * new option `--browser` replacing `--browserType`. 14 | 15 | ## 0.0.20 16 | * new option `--extensionId publisher.name[@prerelease]` to include one or more extensions. 17 | 18 | ## 0.0.18 19 | * new option `--browserType none` to start the server without opening a browser. 20 | 21 | ## 0.0.17 22 | * new options `--host` and `--port`: If provided runs the server from the given host and port. 23 | * new option `--verbose` to print out the browser console log. 24 | 25 | ## 0.0.16 26 | * new option `--sourcesPath`: If provided, runs the server from VS Code sources at the given location. 27 | * option `--version` is deprecated and replaced with `quality`. Supported values: `stable`, `insiders`. Instead of `sources` use `--insiders`. 28 | 29 | ## 0.0.14 30 | * new option `--extensionPath` : A path pointing to a folder containing additional extensions to include. Argument can be provided multiple times. 31 | * new option `--permission`: Permission granted to the opened browser: e.g. clipboard-read, clipboard-write. See full list of options [here](https://playwright.dev/docs/1.14/emulation#permissions). Argument can be provided multiple times. 32 | * new option `--hideServerLog`: If set, hides the server log. Defaults to true when an extensionTestsPath is provided, otherwise false. 33 | * close server when browser is closed 34 | 35 | ## 0.0.9 36 | 37 | * new option `folderPath`: A local folder to open VS Code on. The folder content will be available as a virtual file system and opened as workspace. 38 | 39 | 40 | ### 0.0.1 | 41 | 42 | - Initial version 43 | 44 | 45 | -------------------------------------------------------------------------------- /sample/src/web/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import * as vscode from 'vscode'; 4 | 5 | // this method is called when your extension is activated 6 | // your extension is activated the very first time the command is executed 7 | export function activate(context: vscode.ExtensionContext) { 8 | 9 | // Use the console to output diagnostic information (console.log) and errors (console.error) 10 | // This line of code will only be executed once when your extension is activated 11 | console.log('Congratulations, your extension "vscode-test-web-sample" is now active in the web extension host!'); 12 | 13 | // The command has been defined in the package.json file 14 | // Now provide the implementation of the command with registerCommand 15 | // The commandId parameter must match the command field in package.json 16 | let disposable = vscode.commands.registerCommand('vscode-test-web-sample.helloWorld', () => { 17 | // The code you place here will be executed every time your command is executed 18 | 19 | // Display a message box to the user 20 | vscode.window.showInformationMessage('Hello World from vscode-test-web-sample in a web extension host!'); 21 | }); 22 | 23 | context.subscriptions.push(disposable); 24 | 25 | let findFilesDisposable = vscode.commands.registerCommand('vscode-test-web-sample.findFiles', () => { 26 | vscode.window.showInputBox({ title: 'Enter a pattern', placeHolder: '**/*.md' }) 27 | .then((pattern) => { 28 | return pattern ? vscode.workspace.findFiles(pattern) : undefined; 29 | }) 30 | .then((results) => { 31 | if (!results) { 32 | return vscode.window.showErrorMessage('Find files returned undefined'); 33 | } 34 | let summary = `Found:\n${results.map(uri => ` - ${uri.path}`).join('\n')}`; 35 | return vscode.window.showInformationMessage(summary); 36 | }); 37 | }); 38 | 39 | context.subscriptions.push(findFilesDisposable); 40 | 41 | } 42 | 43 | // this method is called when your extension is deactivated 44 | export function deactivate() { } 45 | -------------------------------------------------------------------------------- /sample/src/web/test/suite/search.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | suite('Workspace search', () => { 5 | // tests findFiles operation against the current workspace folder 6 | // when running with `@vscode/test-web`, this will be a virtual file system, powered 7 | // by the vscoe-web-test file system provider 8 | 9 | const workspaceFolder = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]; 10 | assert.ok(workspaceFolder, 'Expecting an open folder'); 11 | 12 | const workspaceFolderUri = workspaceFolder.uri; 13 | 14 | function getUri(path: string): vscode.Uri { 15 | return vscode.Uri.joinPath(workspaceFolderUri, path); 16 | } 17 | 18 | async function assertEntries(path: string, expectedFiles: string[], expectedFolders: string[]) { 19 | const entrySorter = (e1: [string, vscode.FileType], e2: [string, vscode.FileType]) => { 20 | const d = e1[1] - e2[1]; 21 | if (d === 0) { 22 | return e1[0].localeCompare(e2[0]); 23 | } 24 | return d; 25 | }; 26 | 27 | let entries = await vscode.workspace.fs.readDirectory(getUri(path)); 28 | entries = entries.sort(entrySorter); 29 | 30 | let expected = expectedFolders 31 | .map<[string, vscode.FileType]>((name) => [name, vscode.FileType.Directory]) 32 | .concat(expectedFiles.map((name) => [name, vscode.FileType.File])) 33 | .sort(entrySorter); 34 | 35 | assert.deepStrictEqual(entries, expected); 36 | } 37 | 38 | async function assertFindsFiles(pattern: string, expectedFiles: string[]) { 39 | let entries = await vscode.workspace.findFiles(pattern); 40 | let foundFiles = entries.map((uri) => uri.path.substring(uri.path.lastIndexOf('/') + 1)); 41 | 42 | assert.deepStrictEqual(foundFiles, expectedFiles); 43 | } 44 | 45 | // commented out because of https://github.com/microsoft/vscode/issues/227248 46 | test('Find files', async () => { 47 | debugger; 48 | await assertEntries('/folder', ['x.txt'], ['.bar']); 49 | await assertEntries('/folder/', ['x.txt'], ['.bar']); 50 | await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); 51 | 52 | await assertFindsFiles('**/*.txt', ['x.txt', 'hello.txt', 'world.txt']); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import header from "@tony.ganchev/eslint-plugin-header"; 7 | import tsParser from "@typescript-eslint/parser"; 8 | import path from "node:path"; 9 | import { fileURLToPath } from "node:url"; 10 | import js from "@eslint/js"; 11 | import { FlatCompat } from "@eslint/eslintrc"; 12 | 13 | const __filename = fileURLToPath(import.meta.url); 14 | const __dirname = path.dirname(__filename); 15 | const compat = new FlatCompat({ 16 | baseDirectory: __dirname, 17 | recommendedConfig: js.configs.recommended, 18 | allConfig: js.configs.all 19 | }); 20 | 21 | export default [{ 22 | ignores: ["**/*.d.ts", "**/*.test.ts", "**/*.js", "sample/**/*.*"], 23 | }, ...compat.extends("plugin:@typescript-eslint/recommended"), { 24 | plugins: { 25 | header, 26 | }, 27 | 28 | languageOptions: { 29 | parser: tsParser, 30 | ecmaVersion: 2018, 31 | sourceType: "module", 32 | }, 33 | 34 | rules: { 35 | "@typescript-eslint/no-use-before-define": "off", 36 | "@typescript-eslint/explicit-function-return-type": "off", 37 | "@typescript-eslint/no-explicit-any": "off", 38 | "@typescript-eslint/no-non-null-assertion": "off", 39 | "@typescript-eslint/explicit-module-boundary-types": "off", 40 | "@typescript-eslint/no-unused-vars": "off", 41 | "@typescript-eslint/no-var-requires": "error", 42 | 43 | "header/header": [ 44 | 2, 45 | "block", 46 | [ 47 | "---------------------------------------------------------------------------------------------", 48 | " * Copyright (c) Microsoft Corporation. All rights reserved.", 49 | " * Licensed under the MIT License. See License.txt in the project root for license information.", 50 | " *--------------------------------------------------------------------------------------------"], 51 | ], 52 | }, 53 | }]; -------------------------------------------------------------------------------- /fs-provider/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 7 | 8 | const path = require('path'); 9 | const webpack = require('webpack'); 10 | 11 | /** @type WebpackConfig */ 12 | const webConfig = { 13 | context: __dirname, 14 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 15 | target: 'webworker', // web extensions run in a webworker context 16 | entry: { 17 | 'fsExtensionMain': './src/fsExtensionMain.ts', // source of the web extension main file 18 | }, 19 | output: { 20 | filename: '[name].js', 21 | path: path.join(__dirname, './dist'), 22 | libraryTarget: 'commonjs', 23 | devtoolModuleFilenameTemplate: '../[resource-path]' 24 | }, 25 | resolve: { 26 | mainFields: ['browser', 'module', 'main'], // look for `browser` entry point in imported node modules 27 | extensions: ['.ts', '.js'], // support ts-files and js-files 28 | alias: { 29 | // provides alternate implementation for node module and source files 30 | }, 31 | fallback: { 32 | // Webpack 5 no longer polyfills Node.js core modules automatically. 33 | // see https://webpack.js.org/configuration/resolve/#resolvefallback 34 | // for the list of Node.js core module polyfills. 35 | }, 36 | }, 37 | module: { 38 | rules: [ 39 | { 40 | test: /\.ts$/, 41 | exclude: /node_modules/, 42 | use: [ 43 | { 44 | loader: 'ts-loader', 45 | }, 46 | ], 47 | }, 48 | ], 49 | }, 50 | plugins: [ 51 | new webpack.ProvidePlugin({ 52 | process: 'process/browser', // provide a shim for the global `process` variable 53 | }), 54 | ], 55 | externals: { 56 | vscode: 'commonjs vscode', // ignored because it doesn't exist 57 | }, 58 | performance: { 59 | hints: false, 60 | }, 61 | devtool: 'nosources-source-map', // create a source map that points to the original source file 62 | }; 63 | 64 | module.exports = [webConfig]; 65 | -------------------------------------------------------------------------------- /views/workbench.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 60 | 63 | {{WORKBENCH_MAIN}} 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vscode/test-web", 3 | "version": "0.0.77", 4 | "scripts": { 5 | "install-extensions": "npm i --prefix=fs-provider && npm i --prefix=sample", 6 | "compile": "tsc -b ./ && npm run compile-fs-provider", 7 | "watch": "tsc -b -w ./", 8 | "prepack": "npm run compile", 9 | "test": "eslint src && tsc --noEmit", 10 | "preversion": "npm test", 11 | "postversion": "git push && git push --tags", 12 | "compile-fs-provider": "npm run --prefix=fs-provider compile-web", 13 | "compile-sample": "npm run --prefix=sample compile-web", 14 | "sample": "npm run compile && npm run compile-sample && node . --extensionDevelopmentPath=sample sample/test-workspace", 15 | "sample-tests": "npm run compile && npm run compile-sample && node . --extensionDevelopmentPath=sample --extensionTestsPath=sample/dist/web/test/suite/index.js --headless=true sample/test-workspace", 16 | "empty": "npm run compile && node ." 17 | }, 18 | "main": "./out/server/index.js", 19 | "bin": { 20 | "vscode-test-web": "./out/server/index.js" 21 | }, 22 | "engines": { 23 | "node": ">=20" 24 | }, 25 | "dependencies": { 26 | "@koa/cors": "^5.0.0", 27 | "@koa/router": "^15.0.0", 28 | "@playwright/browser-chromium": "^1.57.0", 29 | "tinyglobby": "^0.2.15", 30 | "gunzip-maybe": "^1.4.2", 31 | "http-proxy-agent": "^7.0.2", 32 | "https-proxy-agent": "^7.0.6", 33 | "koa": "^3.1.1", 34 | "koa-morgan": "^1.0.1", 35 | "koa-mount": "^4.2.0", 36 | "koa-static": "^5.0.0", 37 | "minimist": "^1.2.8", 38 | "playwright": "^1.57.0", 39 | "tar-fs": "^3.1.1", 40 | "vscode-uri": "^3.1.0" 41 | }, 42 | "devDependencies": { 43 | "@eslint/eslintrc": "^3.3.3", 44 | "@eslint/js": "^9.39.1", 45 | "@types/gunzip-maybe": "^1.4.3", 46 | "@types/koa": "^3.0.1", 47 | "@types/koa__router": "^12.0.5", 48 | "@types/koa-morgan": "^1.0.9", 49 | "@types/koa-mount": "^4.0.5", 50 | "@types/koa-static": "^4.0.4", 51 | "@types/minimist": "^1.2.5", 52 | "@types/node": "^20.16.13", 53 | "@types/tar-fs": "^2.0.4", 54 | "@typescript-eslint/eslint-plugin": "^8.48.1", 55 | "@typescript-eslint/parser": "^8.48.1", 56 | "eslint": "^9.39.1", 57 | "@tony.ganchev/eslint-plugin-header": "^3.1.11", 58 | "typescript": "^5.9.3" 59 | }, 60 | "license": "MIT", 61 | "author": "Visual Studio Code Team", 62 | "repository": { 63 | "type": "git", 64 | "url": "https://github.com/microsoft/vscode-test-web.git" 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/microsoft/vscode-test-web/issues" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch sample test", 11 | "outputCapture": "std", 12 | "program": "${workspaceFolder}/sample/dist/web/test/runTest.js", 13 | "args": ["--waitForDebugger=9229"], 14 | "cascadeTerminateToConfigurations": ["Launch sample test"], 15 | "presentation": { 16 | "hidden": true, 17 | } 18 | }, 19 | { 20 | "type": "pwa-chrome", 21 | "request": "attach", 22 | "name": "Attach sample test", 23 | "skipFiles": [ 24 | "/**" 25 | ], 26 | "port": 9229, 27 | "timeout": 30000, // give it time to download vscode if needed 28 | "resolveSourceMapLocations": [ 29 | "!**/vs/**", // exclude core vscode sources 30 | "!**/static/build/extensions/**", // exclude built-in extensions 31 | ], 32 | "webRoot": "${workspaceFolder}/sample", // only needed since sample is in a subdir 33 | "presentation": { 34 | "hidden": true, 35 | } 36 | }, 37 | { 38 | "type": "pwa-node", 39 | "request": "launch", 40 | "name": "Run in Chromium", 41 | "skipFiles": [ 42 | "/**" 43 | ], 44 | "program": "${workspaceFolder}/out/server/index.js", 45 | "args": [ 46 | "--browserType=chromium", 47 | "--extensionDevelopmentPath=${workspaceFolder}/sample", 48 | "sample/test-workspace" 49 | ], 50 | "outFiles": [ 51 | "${workspaceFolder}/out/**/*.js" 52 | ] 53 | }, 54 | { 55 | "type": "pwa-node", 56 | "request": "launch", 57 | "name": "Run Test in Chromium", 58 | "skipFiles": [ 59 | "/**" 60 | ], 61 | "program": "${workspaceFolder}/out/server/index.js", 62 | "args": [ 63 | "--browserType=chromium", 64 | "--extensionDevelopmentPath=${workspaceFolder}/sample", 65 | "--extensionTestsPath=${workspaceFolder}/sample/dist/web/test/suite/index.js" 66 | ], 67 | "outFiles": [ 68 | "${workspaceFolder}/out/**/*.js" 69 | ] 70 | } 71 | ], 72 | "compounds": [ 73 | { 74 | "name": "Debug Sample Test", 75 | "configurations": [ 76 | "Launch sample test", 77 | "Attach sample test" 78 | ] 79 | } 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/server/mounts.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { IConfig } from './main'; 7 | 8 | import * as Koa from 'koa'; 9 | import * as kstatic from 'koa-static'; 10 | import * as kmount from 'koa-mount'; 11 | import Router, { RouterMiddleware } from '@koa/router'; 12 | 13 | import { Dirent, promises as fs, Stats } from 'fs'; 14 | import * as path from 'path'; 15 | 16 | const mountPrefix = '/static/mount'; 17 | export const fsProviderExtensionPrefix = '/static/extensions/fs'; 18 | export const fsProviderFolderUri = 'vscode-test-web://mount/'; 19 | 20 | export function configureMounts(config: IConfig, app: Koa): void { 21 | const folderMountPath = config.folderMountPath; 22 | if (folderMountPath) { 23 | console.log(`Serving local content ${folderMountPath} at ${mountPrefix}`); 24 | app.use(fileOps(mountPrefix, folderMountPath)); 25 | app.use(kmount(mountPrefix, kstatic(folderMountPath, { hidden: true }))); 26 | 27 | app.use(kmount(fsProviderExtensionPrefix, kstatic(path.join(__dirname, '../../fs-provider'), { hidden: true }))); 28 | } 29 | } 30 | 31 | function fileOps(mountPrefix: string, folderMountPath: string): RouterMiddleware { 32 | const router = new Router(); 33 | router.get(`${mountPrefix}{/*path}`, async (ctx, next) => { 34 | if (ctx.query.stat !== undefined) { 35 | const p = path.join(folderMountPath, decodeURIComponent(ctx.path.substring(mountPrefix.length))); 36 | try { 37 | const stats = await fs.stat(p); 38 | ctx.body = { 39 | type: getFileType(stats), 40 | ctime: stats.ctime.getTime(), 41 | mtime: stats.mtime.getTime(), 42 | size: stats.size, 43 | }; 44 | } catch (e) { 45 | ctx.body = { error: (e as NodeJS.ErrnoException).code }; 46 | } 47 | } else if (ctx.query.readdir !== undefined) { 48 | const p = path.join(folderMountPath, decodeURIComponent(ctx.path.substring(mountPrefix.length))); 49 | try { 50 | const entries = await fs.readdir(p, { withFileTypes: true }); 51 | ctx.body = entries.map((d) => ({ name: d.name, type: getFileType(d) })); 52 | } catch (e) { 53 | ctx.body = { error: (e as NodeJS.ErrnoException).code }; 54 | } 55 | } else { 56 | return next(); 57 | } 58 | }); 59 | return router.routes(); 60 | } 61 | 62 | enum FileType { 63 | Unknown = 0, 64 | File = 1, 65 | Directory = 2, 66 | SymbolicLink = 64, 67 | } 68 | 69 | function getFileType(stats: Stats | Dirent) { 70 | if (stats.isFile()) { 71 | return FileType.File; 72 | } else if (stats.isDirectory()) { 73 | return FileType.Directory; 74 | } else if (stats.isSymbolicLink()) { 75 | return FileType.SymbolicLink; 76 | } 77 | return FileType.Unknown; 78 | } 79 | -------------------------------------------------------------------------------- /fs-provider/vscode.proposed.fileSearchProvider.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module 'vscode' { 7 | 8 | // https://github.com/microsoft/vscode/issues/73524 9 | 10 | /** 11 | * The parameters of a query for file search. 12 | */ 13 | export interface FileSearchQuery { 14 | /** 15 | * The search pattern to match against file paths. 16 | * To be correctly interpreted by Quick Open, this is interpreted in a relaxed way. The picker will apply its own highlighting and scoring on the results. 17 | * 18 | * Tips for matching in Quick Open: 19 | * With the pattern, the picker will use the file name and file paths to score each entry. The score will determine the ordering and filtering. 20 | * The scoring prioritizes prefix and substring matching. Then, it checks and it checks whether the pattern's letters appear in the same order as in the target (file name and path). 21 | * If a file does not match at all using our criteria, it will be omitted from Quick Open. 22 | */ 23 | pattern: string; 24 | } 25 | 26 | /** 27 | * Options that apply to file search. 28 | */ 29 | export interface FileSearchOptions extends SearchOptions { 30 | /** 31 | * The maximum number of results to be returned. 32 | */ 33 | maxResults?: number; 34 | 35 | /** 36 | * A CancellationToken that represents the session for this search query. If the provider chooses to, this object can be used as the key for a cache, 37 | * and searches with the same session object can search the same cache. When the token is cancelled, the session is complete and the cache can be cleared. 38 | */ 39 | session?: CancellationToken; 40 | } 41 | 42 | /** 43 | * A FileSearchProvider provides search results for files in the given folder that match a query string. It can be invoked by quickopen or other extensions. 44 | * 45 | * A FileSearchProvider is the more powerful of two ways to implement file search in the editor. Use a FileSearchProvider if you wish to search within a folder for 46 | * all files that match the user's query. 47 | * 48 | * The FileSearchProvider will be invoked on every keypress in quickopen. When `workspace.findFiles` is called, it will be invoked with an empty query string, 49 | * and in that case, every file in the folder should be returned. 50 | */ 51 | export interface FileSearchProvider { 52 | /** 53 | * Provide the set of files that match a certain file path pattern. 54 | * @param query The parameters for this query. 55 | * @param options A set of options to consider while searching files. 56 | * @param token A cancellation token. 57 | */ 58 | provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): ProviderResult; 59 | } 60 | 61 | export namespace workspace { 62 | /** 63 | * Register a search provider. 64 | * 65 | * Only one provider can be registered per scheme. 66 | * 67 | * @param scheme The provider will be invoked for workspace folders that have this file scheme. 68 | * @param provider The provider. 69 | * @return A {@link Disposable} that unregisters this provider when being disposed. 70 | */ 71 | export function registerFileSearchProvider(scheme: string, provider: FileSearchProvider): Disposable; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/server/extensions.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { promises as fs } from 'fs'; 7 | import * as path from 'path'; 8 | import { fileExists } from './download'; 9 | 10 | export interface URIComponents { 11 | scheme: string; 12 | authority: string; 13 | path: string; 14 | } 15 | 16 | export async function scanForExtensions( 17 | rootPath: string, 18 | serverURI: URIComponents 19 | ): Promise { 20 | const result: URIComponents[] = []; 21 | async function getExtension(relativePosixFolderPath: string): Promise { 22 | try { 23 | const packageJSONPath = path.join(rootPath, relativePosixFolderPath, 'package.json'); 24 | if ((await fs.stat(packageJSONPath)).isFile()) { 25 | return { 26 | scheme: serverURI.scheme, 27 | authority: serverURI.authority, 28 | path: path.posix.join(serverURI.path, relativePosixFolderPath), 29 | }; 30 | } 31 | } catch { 32 | return undefined; 33 | } 34 | } 35 | 36 | async function processFolder(relativePosixFolderPath: string) { 37 | const extension = await getExtension(relativePosixFolderPath); 38 | if (extension) { 39 | result.push(extension); 40 | } else { 41 | const folderPath = path.join(rootPath, relativePosixFolderPath); 42 | const entries = await fs.readdir(folderPath, { withFileTypes: true }); 43 | for (const entry of entries) { 44 | if (entry.isDirectory() && entry.name.charAt(0) !== '.') { 45 | await processFolder(path.posix.join(relativePosixFolderPath, entry.name)); 46 | } 47 | } 48 | } 49 | } 50 | 51 | await processFolder(''); 52 | return result; 53 | } 54 | 55 | /** running from VS Code sources */ 56 | 57 | export interface IScannedBuiltinExtension { 58 | extensionPath: string; // name of the folder 59 | packageJSON: any; 60 | packageNLS?: any; 61 | readmePath?: string; 62 | changelogPath?: string; 63 | } 64 | 65 | export const prebuiltExtensionsLocation = '.build/builtInExtensions'; 66 | 67 | export async function getScannedBuiltinExtensions(vsCodeDevLocation: string): Promise { 68 | // use the build utility as to not duplicate the code 69 | const extensionsUtil = await getExtensionsUtil(vsCodeDevLocation); 70 | 71 | const localExtensions : IScannedBuiltinExtension[] = extensionsUtil.scanBuiltinExtensions(path.join(vsCodeDevLocation, 'extensions')); 72 | const prebuiltExtensions : IScannedBuiltinExtension[] = extensionsUtil.scanBuiltinExtensions(path.join(vsCodeDevLocation, prebuiltExtensionsLocation)); 73 | for (const ext of localExtensions) { 74 | let browserMain: string | undefined = ext.packageJSON.browser; 75 | if (browserMain) { 76 | if (!browserMain.endsWith('.js')) { 77 | browserMain = browserMain + '.js'; 78 | } 79 | const browserMainLocation = path.join(vsCodeDevLocation, 'extensions', ext.extensionPath, browserMain); 80 | if (!(await fileExists(browserMainLocation))) { 81 | console.log(`${browserMainLocation} not found. Make sure all extensions are compiled (use 'npm run watch-web').`); 82 | } 83 | } 84 | } 85 | return localExtensions.concat(prebuiltExtensions); 86 | } 87 | 88 | async function getExtensionsUtil(vsCodeDevLocation: string) { 89 | const base = path.join(vsCodeDevLocation, 'build', 'lib'); 90 | try { 91 | return await import(path.join(base, 'extensions.ts')); 92 | } catch { 93 | return await import(path.join(base, 'extensions.js')); 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/server/app.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { ReadStream } from 'fs'; 7 | import * as Koa from 'koa'; 8 | import * as morgan from 'koa-morgan'; 9 | import * as kstatic from 'koa-static'; 10 | import * as kmount from 'koa-mount'; 11 | import * as cors from '@koa/cors'; 12 | import { basename, join } from 'path'; 13 | import { IConfig } from './main'; 14 | import workbench from './workbench'; 15 | import { configureMounts } from './mounts'; 16 | import { prebuiltExtensionsLocation } from './extensions'; 17 | 18 | export default async function createApp(config: IConfig): Promise { 19 | const app = new Koa(); 20 | 21 | app.use(morgan('dev', { skip: (req, res) => !config.printServerLog && (res.statusCode >= 200 && res.statusCode < 300) })); 22 | 23 | // CORS 24 | app.use( 25 | cors({ 26 | allowMethods: ['GET'], 27 | credentials: true, 28 | origin: (ctx: Koa.Context) => { 29 | const origin = ctx.get('Origin'); 30 | if ( 31 | /^https:\/\/[^.]+\.vscode-cdn\.net$/.test(origin) || // needed for the webviewContent 32 | /^https:\/\/[^.]+\.vscode-webview\.net$/.test(origin) || 33 | new RegExp(`^${ctx.protocol}://[^.]+\\.${ctx.host}$`).test(origin) // match subdomains of localhost 34 | ) { 35 | return origin; 36 | } 37 | 38 | return undefined as any; 39 | }, 40 | }) 41 | ); 42 | 43 | if (config.build.type !== 'sources' && config.build.type !== 'static') { 44 | // CSP: frame-ancestors 45 | app.use((ctx, next) => { 46 | ctx.set('Content-Security-Policy', `frame-ancestors 'none'`); 47 | return next(); 48 | }); 49 | } 50 | 51 | // COI 52 | app.use((ctx, next) => { 53 | // set COOP/COEP depending on vscode-coi-flags 54 | const value = ctx.query['vscode-coi']; 55 | if (value === '1') { 56 | ctx.set('Cross-Origin-Opener-Policy', 'same-origin'); 57 | } else if (value === '2') { 58 | ctx.set('Cross-Origin-Embedder-Policy', 'require-corp'); 59 | } else if (value === '3' || value === '') { 60 | ctx.set('Cross-Origin-Opener-Policy', 'same-origin'); 61 | ctx.set('Cross-Origin-Embedder-Policy', 'require-corp'); 62 | } 63 | 64 | // set CORP on all resources 65 | ctx.set('Cross-Origin-Resource-Policy', 'cross-origin') 66 | return next() 67 | }) 68 | 69 | // shift the line numbers of source maps in extensions by 2 as the content is wrapped by an anonymous function 70 | app.use(async (ctx, next) => { 71 | await next(); 72 | if (ctx.status === 200 && ctx.path.match(/\/(dev)?extensions\/.*\.js\.map$/) && ctx.body instanceof ReadStream) { 73 | // we know it's a ReadStream as that's what kstatic uses 74 | const chunks: Buffer[] = []; 75 | for await (const chunk of ctx.body) { 76 | chunks.push(Buffer.from(chunk)); 77 | } 78 | const bodyContent = Buffer.concat(chunks).toString("utf-8"); 79 | ctx.response.body = `{"version":3,"file":"${basename(ctx.path)}","sections":[{"offset":{"line":2,"column":0},"map":${bodyContent} }]}`; 80 | } 81 | }); 82 | 83 | const serveOptions: kstatic.Options = { hidden: true }; 84 | 85 | if (config.extensionDevelopmentPath) { 86 | console.log('Serving dev extensions from ' + config.extensionDevelopmentPath); 87 | app.use(kmount('/static/devextensions', kstatic(config.extensionDevelopmentPath, serveOptions))); 88 | } 89 | 90 | if (config.build.type === 'static') { 91 | app.use(kmount('/static/build', kstatic(config.build.location, serveOptions))); 92 | } else if (config.build.type === 'sources') { 93 | console.log('Serving VS Code sources from ' + config.build.location); 94 | app.use(kmount('/static/sources', kstatic(config.build.location, serveOptions))); 95 | app.use(kmount('/static/sources', kstatic(join(config.build.location, 'resources', 'server'), serveOptions))); // for manifest.json, favicon and code icons. 96 | 97 | // built-in extension are at 'extensions` as well as prebuilt extensions downloaded from the marketplace 98 | app.use(kmount(`/static/sources/extensions`, kstatic(join(config.build.location, prebuiltExtensionsLocation), serveOptions))); 99 | } 100 | 101 | configureMounts(config, app); 102 | 103 | if (config.extensionPaths) { 104 | config.extensionPaths.forEach((extensionPath, index) => { 105 | console.log('Serving additional built-in extensions from ' + extensionPath); 106 | app.use(kmount(`/static/extensions/${index}`, kstatic(extensionPath, serveOptions))); 107 | }); 108 | } 109 | 110 | app.use(workbench(config)); 111 | 112 | return app; 113 | } 114 | -------------------------------------------------------------------------------- /fs-provider/src/fsExtensionMain.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { xhr } from 'request-light'; 7 | import { Uri, FileStat, FileType, workspace, ExtensionContext, FileSystemError } from 'vscode'; 8 | import { Entry, MemFileSystemProvider, File, Directory } from './fsProvider'; 9 | 10 | const SCHEME = 'vscode-test-web'; 11 | 12 | export function activate(context: ExtensionContext) { 13 | const serverUri = context.extensionUri.with({ path: '/static/mount', query: undefined }); 14 | const serverBackedRootDirectory = new ServerBackedDirectory(serverUri, [], ''); 15 | 16 | const memFsProvider = new MemFileSystemProvider(SCHEME, serverBackedRootDirectory, context.extensionUri); 17 | const disposable = workspace.registerFileSystemProvider(SCHEME, memFsProvider); 18 | context.subscriptions.push(disposable); 19 | 20 | const searchDisposable = workspace.registerFileSearchProvider(SCHEME, memFsProvider); 21 | context.subscriptions.push(searchDisposable); 22 | 23 | console.log(`vscode-test-web-support fs provider registers for ${SCHEME}, initial content from ${serverUri.toString(/*skipEncoding*/ true)}`); 24 | } 25 | 26 | class ServerBackedFile implements File { 27 | readonly type = FileType.File; 28 | private _stats: Promise | undefined; 29 | private _content: Promise | undefined; 30 | constructor(private readonly _serverRoot: Uri, public pathSegments: readonly string[], public name: string) { 31 | } 32 | get stats(): Promise { 33 | if (this._stats === undefined) { 34 | this._stats = getStats(this._serverRoot, this.pathSegments); 35 | } 36 | return this._stats; 37 | } 38 | set stats(stats: Promise) { 39 | this._stats = stats; 40 | } 41 | get content(): Promise { 42 | if (this._content === undefined) { 43 | this._content = getContent(this._serverRoot, this.pathSegments); 44 | } 45 | return this._content; 46 | } 47 | set content(content: Promise) { 48 | this._content = content; 49 | } 50 | } 51 | 52 | class ServerBackedDirectory implements Directory { 53 | readonly type = FileType.Directory; 54 | private _stats: Promise | undefined; 55 | private _entries: Promise> | undefined; 56 | constructor(private readonly _serverRoot: Uri, public pathSegments: readonly string[], public name: string) { 57 | } 58 | get stats(): Promise { 59 | if (this._stats === undefined) { 60 | this._stats = getStats(this._serverRoot, this.pathSegments); 61 | } 62 | return this._stats; 63 | } 64 | set stats(stats: Promise) { 65 | this._stats = stats; 66 | } 67 | get entries(): Promise> { 68 | if (this._entries === undefined) { 69 | this._entries = getEntries(this._serverRoot, this.pathSegments); 70 | } 71 | return this._entries; 72 | } 73 | set entries(entries: Promise>) { 74 | this._entries = entries; 75 | } 76 | } 77 | 78 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 79 | function isEntry(e: any): e is Entry { 80 | return e && (e.type === FileType.Directory || e.type === FileType.File) && typeof e.name === 'string' && e.name.length > 0; 81 | } 82 | 83 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 84 | function isStat(e: any): e is FileStat { 85 | return e && (e.type === FileType.Directory || e.type === FileType.File) && typeof e.ctime === 'number' && typeof e.mtime === 'number' && typeof e.size === 'number'; 86 | } 87 | 88 | function getServerUri(serverRoot: Uri, pathSegments: readonly string[]): Uri { 89 | return Uri.joinPath(serverRoot, ...pathSegments); 90 | } 91 | 92 | async function getEntries(serverRoot: Uri, pathSegments: readonly string[]): Promise> { 93 | const url = getServerUri(serverRoot, pathSegments).with({ query: 'readdir' }).toString(/*skipEncoding*/ true); 94 | const response = await xhr({ url }); 95 | if (response.status === 200 && response.status <= 204) { 96 | try { 97 | const res = JSON.parse(response.responseText); 98 | if (Array.isArray(res)) { 99 | const entries = new Map(); 100 | for (const r of res) { 101 | if (isEntry(r)) { 102 | const newPathSegments = [...pathSegments, encodeURIComponent(r.name)]; 103 | const newEntry: Entry = r.type === FileType.Directory ? new ServerBackedDirectory(serverRoot, newPathSegments, r.name) : new ServerBackedFile(serverRoot, newPathSegments, r.name); 104 | entries.set(newEntry.name, newEntry); 105 | } 106 | } 107 | return entries; 108 | } 109 | } catch { 110 | // ignore 111 | } 112 | console.log(`Invalid server response format for ${url}.`); 113 | } else { 114 | console.log(`Invalid server response for ${url}. Status ${response.status}`); 115 | } 116 | return new Map(); 117 | } 118 | 119 | async function getStats(serverRoot: Uri, pathSegments: readonly string[]): Promise { 120 | const serverUri = getServerUri(serverRoot, pathSegments); 121 | const url = serverUri.with({ query: 'stat' }).toString(/*skipEncoding*/ true); 122 | const response = await xhr({ url }); 123 | if (response.status === 200 && response.status <= 204) { 124 | const res = JSON.parse(response.responseText); 125 | if (isStat(res)) { 126 | return res; 127 | } 128 | throw FileSystemError.FileNotFound(`Invalid server response for ${serverUri.toString(/*skipEncoding*/ true)}.`); 129 | } 130 | throw FileSystemError.FileNotFound(`Invalid server response for ${serverUri.toString(/*skipEncoding*/ true)}. Status ${response.status}.`); 131 | } 132 | 133 | async function getContent(serverRoot: Uri, pathSegments: readonly string[]): Promise { 134 | const serverUri = getServerUri(serverRoot, pathSegments); 135 | const response = await xhr({ url: serverUri.toString(/*skipEncoding*/ true) }); 136 | if (response.status >= 200 && response.status <= 204) { 137 | return response.body; 138 | } 139 | throw FileSystemError.FileNotFound(`Invalid server response for ${serverUri.toString(/*skipEncoding*/ true)}. Status ${response.status}.`); 140 | } 141 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @vscode/test-web 2 | 3 | This module helps testing VS Code web extensions locally. 4 | 5 | [![Test Status Badge](https://github.com/microsoft/vscode-test-web/workflows/Tests/badge.svg)](https://github.com/microsoft/vscode-test-web/actions/workflows/tests.yml) 6 | [![npm Package](https://img.shields.io/npm/v/@vscode/test-web.svg?style=flat-square)](https://www.npmjs.org/package/@vscode/test-web) 7 | [![NPM Downloads](https://img.shields.io/npm/dm/@vscode/test-web.svg)](https://npmjs.org/package/@vscode/test-web) 8 | 9 | 10 | See the [web extensions guide](https://code.visualstudio.com/api/extension-guides/web-extensions) to learn about web extensions. 11 | 12 | The node module runs a local web server that serves VS Code in the browser including the extension under development. Additionally the extension tests are automatically run. 13 | 14 | The node module provides a command line as well as an API. 15 | 16 | ## Usage 17 | 18 | Via command line: 19 | 20 | Test a web extension in a browser: 21 | 22 | ```sh 23 | vscode-test-web --browserType=chromium --extensionDevelopmentPath=$extensionLocation 24 | ``` 25 | 26 | Run web extension tests: 27 | 28 | ```sh 29 | vscode-test-web --browserType=chromium --extensionDevelopmentPath=$extensionLocation --extensionTestsPath=$extensionLocation/dist/web/test/suite/index.js 30 | ``` 31 | 32 | Open VS Code in the Browser on a folder with test data from the local disk: 33 | 34 | ```sh 35 | vscode-test-web --browserType=chromium --extensionDevelopmentPath=$extensionLocation $testDataLocation 36 | ``` 37 | 38 | VS Code for the Web will open on a virtual workspace (scheme `vscode-test-web`), backed by a file system provider that gets the file/folder data from the local disk. Changes to the file system are kept in memory and are not written back to disk. 39 | 40 | Open VS Code in the Browser with external network access: 41 | 42 | ```sh 43 | vscode-test-web --browserType=chromium --browserOption=--disable-web-security extensionDevelopmentPath=$extensionLocation 44 | ``` 45 | 46 | This allows the extension being tested to make network requests to external hosts. 47 | 48 | Via API: 49 | 50 | ```ts 51 | async function go() { 52 | try { 53 | // The folder containing the Extension Manifest package.json 54 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 55 | 56 | // The path to module with the test runner and tests 57 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 58 | 59 | // Start a web server that serves VSCode in a browser, run the tests 60 | await runTests({ 61 | browserType: 'chromium', 62 | extensionDevelopmentPath 63 | extensionTestsPath 64 | }); 65 | } catch (err) { 66 | console.error('Failed to run tests'); 67 | process.exit(1); 68 | } 69 | } 70 | 71 | go() 72 | ``` 73 | 74 | CLI options: 75 | 76 | |Option|Argument Description| 77 | |-----|-----| 78 | | --browser | The browser to launch: `chromium` (default), `firefox`, `webkit` or `none`. | 79 | | --browserOption | Command line argument to use when launching the browser instance. Argument can be provided multiple times. | 80 | | --extensionDevelopmentPath | A path pointing to an extension under development to include. | 81 | | --extensionTestsPath | A path to a test module to run. | 82 | | --quality | `insiders` (default), or `stable`. Ignored when sourcesPath is provided. | 83 | | --commit | commitHash The servion of the server to use. Defaults to latest build version of the given quality. Ignored when sourcesPath is provided. | 84 | | --sourcesPath | If set, runs the server from VS Code sources located at the given path. Make sure the sources and extensions are compiled (`npm run compile` and `npm run compile-web`). | 85 | | --headless | If set, hides the browser. Defaults to true when an extensionTestsPath is provided, otherwise false. | 86 | | --permission | Permission granted to the opened browser: e.g. `clipboard-read`, `clipboard-write`. See [full list of options](https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions). Argument can be provided multiple times. | 87 | | --coi | If set, enables cross origin isolation. Defaults to false. | 88 | | --folder-uri | URI of the workspace to open VS Code on. Ignored when `folderPath` is provided. | 89 | | --extensionPath | A path pointing to a folder containing additional extensions to include. Argument can be provided multiple times. | 90 | | --extensionId | The id of an extension include. The format is `${publisher}.${name}`. Append `@prerelease` to use the prerelease version. | 91 | | --host | The host name the server is opened on. Defaults to `localhost`. | 92 | | --port | The port the server is opened on. Defaults to `3000`. | 93 | | --open-devtools | If set, opens the dev tools in the browser. | 94 | | --verbose | If set, prints out more information when running the server. | 95 | | --printServerLog | If set, prints the server access log. | 96 | | --testRunnerDataDir | If set, the temporary folder for storing the VS Code builds used for running the tests | 97 | | folderPath | A local folder to open VS Code on. The folder content will be available as a virtual file system and opened as workspace. | 98 | 99 | Corresponding options are available in the API. 100 | 101 | ## Development 102 | 103 | - `npm i && npm run install-extensions` 104 | - Make necessary changes in [`src`](./src) 105 | - `npm run compile` (or `npm run watch`) 106 | 107 | - run `npm run sample` to launch VS Code Browser with the `sample` extension bundled in this repo. 108 | 109 | - run `npm run sample-tests` to launch VS Code Browser running the extension tests of the `sample` extension bundled in this repo. 110 | 111 | 112 | ## License 113 | 114 | [MIT](LICENSE) 115 | 116 | ## Contributing 117 | 118 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 119 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 120 | the rights to use your contribution. For details, visit https://cla.microsoft.com. 121 | 122 | When you submit a pull request, a CLA-bot will automatically determine whether you need to provide 123 | a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions 124 | provided by the bot. You will only need to do this once across all repos using our CLA. 125 | 126 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 127 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 128 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 129 | -------------------------------------------------------------------------------- /sample/src/web/test/suite/fs.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | suite('Workspace folder access', () => { 5 | 6 | // tests various file system operation against the current workspace folder 7 | // when running with `@vscode/test-web`, this will be a virtual file system, powered 8 | // by the vscoe-web-test file system provider 9 | 10 | const workspaceFolder = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]; 11 | assert.ok(workspaceFolder, 'Expecting an open folder'); 12 | 13 | const workspaceFolderUri = workspaceFolder.uri; 14 | 15 | function getUri(path: string): vscode.Uri { 16 | return vscode.Uri.joinPath(workspaceFolderUri, path); 17 | } 18 | 19 | async function createFile(path: string, content: string) { 20 | const arr = new TextEncoder().encode(content); 21 | await vscode.workspace.fs.writeFile(getUri(path), arr); 22 | await assertStats(path, true, arr.length); 23 | } 24 | 25 | async function createFolder(path: string) { 26 | await vscode.workspace.fs.createDirectory(getUri(path)); 27 | await assertStats(path, false); 28 | } 29 | 30 | async function deleteEntry(path: string, isFile: boolean) { 31 | await assertStats(path, isFile); 32 | await vscode.workspace.fs.delete(getUri(path), { recursive: true }); 33 | await assertNotExisting(path, isFile); 34 | } 35 | 36 | async function assertEntries(path: string, expectedFiles: string[], expectedFolders: string[]) { 37 | const entrySorter = (e1: [string, vscode.FileType], e2: [string, vscode.FileType]) => { 38 | const d = e1[1] - e2[1]; 39 | if (d === 0) { 40 | return e1[0].localeCompare(e2[0]); 41 | } 42 | return d; 43 | }; 44 | 45 | let entries = await vscode.workspace.fs.readDirectory(getUri(path)); 46 | entries = entries.sort(entrySorter); 47 | 48 | let expected = expectedFolders.map<[string, vscode.FileType]>(name => [name, vscode.FileType.Directory]) 49 | .concat(expectedFiles.map(name => [name, vscode.FileType.File])) 50 | .sort(entrySorter); 51 | 52 | assert.deepStrictEqual(entries, expected); 53 | } 54 | 55 | async function assertContent(path: string, expected: string) { 56 | let array = await vscode.workspace.fs.readFile(getUri(path)); 57 | const content = new TextDecoder().decode(array); 58 | assert.deepStrictEqual(content, expected); 59 | await assertStats(path, true, array.length); 60 | } 61 | 62 | async function assertStats(path: string, isFile: boolean, expectedSize?: number) { 63 | let stats = await vscode.workspace.fs.stat(getUri(path)); 64 | assert.deepStrictEqual(stats.type, isFile ? vscode.FileType.File : vscode.FileType.Directory); 65 | assert.deepStrictEqual(typeof stats.mtime, 'number'); 66 | assert.deepStrictEqual(typeof stats.ctime, 'number'); 67 | if (expectedSize !== undefined) { 68 | assert.deepStrictEqual(stats.size, expectedSize); 69 | } else { 70 | assert.deepStrictEqual(typeof stats.size, 'number'); 71 | } 72 | } 73 | 74 | async function assertNotExisting(path: string, isFile: boolean) { 75 | await assert.rejects(async () => { 76 | await assertStats(path, isFile); 77 | }); 78 | } 79 | 80 | test('Folder contents', async () => { 81 | await assertEntries('/folder', ['x.txt'], ['.bar']); 82 | await assertEntries('/folder/', ['x.txt'], ['.bar']); 83 | await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); 84 | await assertEntries('/folder/.bar', ['.foo'], []); 85 | await assertEntries('/folder_with_utf_8_🧿', ['!#$%&\'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~'], []); 86 | }); 87 | 88 | test('File contents', async () => { 89 | await assertContent('/hello.txt', '// hello'); 90 | await assertContent('/world.txt', '// world'); 91 | await assertContent('/folder_with_utf_8_🧿/!#$%&\'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~', 'test_utf_8_🧿'); 92 | }); 93 | 94 | test('File stats', async () => { 95 | await assertStats('/hello.txt', true, 8); 96 | await assertStats('/world.txt', true, 8); 97 | await assertStats('/folder/x.txt', true, 4); 98 | await assertStats('/folder/', false); 99 | await assertStats('/folder/.bar', false); 100 | await assertStats('/folder/.bar/.foo', true, 3); 101 | await assertStats('/folder_with_utf_8_🧿/!#$%&\'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~', true, 15); 102 | await assertStats('/', false); 103 | }); 104 | 105 | test('Create and delete directory', async () => { 106 | await createFolder('/more'); 107 | await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿', 'more' ]); 108 | await deleteEntry('/more', false); 109 | await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); 110 | }); 111 | 112 | test('Create and delete file', async () => { 113 | await createFile('/more.txt', 'content'); 114 | await assertEntries('/', ['hello.txt', 'world.txt', 'more.txt'], ['folder', 'folder_with_utf_8_🧿']); 115 | await assertContent('/more.txt', 'content'); 116 | 117 | await deleteEntry('/more.txt', true); 118 | await assertEntries('/', ['hello.txt', 'world.txt'], ['folder', 'folder_with_utf_8_🧿']); 119 | 120 | await createFile('/folder/more.txt', 'moreContent'); 121 | await assertEntries('/folder', ['x.txt', 'more.txt'], ['.bar']); 122 | await assertContent('/folder/more.txt', 'moreContent'); 123 | await deleteEntry('/folder/more.txt', true); 124 | await assertEntries('/folder', ['x.txt'], ['.bar']); 125 | 126 | }); 127 | 128 | test('Rename', async () => { 129 | await createFolder('/folder/testing'); 130 | await createFile('/folder/testing/doc.txt', 'more'); 131 | await createFolder('/folder/testing/inner'); 132 | await assertEntries('/folder', ['x.txt'], ['testing', '.bar']); 133 | await assertEntries('/folder/testing', ['doc.txt'], ['inner']); 134 | 135 | await vscode.workspace.fs.rename(getUri('/folder/testing'), getUri('/folder/newTesting')); 136 | await assertEntries('/folder', ['x.txt'], ['newTesting', '.bar']); 137 | await assertEntries('/folder/newTesting', ['doc.txt'], ['inner']); 138 | await assertEntries('/folder/newTesting/inner', [], []); 139 | await assertNotExisting('/folder/testing', false); 140 | 141 | await deleteEntry('/folder/newTesting', false); 142 | }); 143 | 144 | test('Copy', async () => { 145 | 146 | await vscode.workspace.fs.copy(getUri('/folder'), getUri('/copyOf/archive/')); 147 | await assertEntries('/folder', ['x.txt'], ['.bar']); 148 | await assertEntries('/copyOf', [], ['archive']); 149 | await assertEntries('/copyOf/archive', ['x.txt'], ['.bar']); 150 | 151 | await deleteEntry('/copyOf', false); 152 | }); 153 | 154 | }); 155 | -------------------------------------------------------------------------------- /src/server/download.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { promises as fs, existsSync } from 'fs'; 7 | import * as path from 'path'; 8 | 9 | import * as https from 'https'; 10 | import * as http from 'http'; 11 | import { HttpsProxyAgent } from 'https-proxy-agent'; 12 | import { HttpProxyAgent } from 'http-proxy-agent'; 13 | import { URL } from 'url'; 14 | 15 | import { Static } from './main'; 16 | 17 | interface DownloadInfo { 18 | url: string; 19 | version: string; 20 | } 21 | 22 | async function getLatestBuild(quality: 'stable' | 'insider'): Promise { 23 | return await fetchJSON(`https://update.code.visualstudio.com/api/update/web-standalone/${quality}/latest`); 24 | } 25 | 26 | export async function getDownloadURL(quality: 'stable' | 'insider', commit: string): Promise { 27 | return new Promise((resolve, reject) => { 28 | const url = `https://update.code.visualstudio.com/commit:${commit}/web-standalone/${quality}`; 29 | https.get(url, { method: 'HEAD', ...getAgent(url) }, res => { 30 | if ((res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) && res.headers.location) { 31 | resolve(res.headers.location); 32 | } else { 33 | resolve(undefined); 34 | } 35 | res.resume(); // Discard response body 36 | }); 37 | }); 38 | } 39 | 40 | const reset = '\x1b[G\x1b[0K'; 41 | 42 | async function downloadAndUntar(downloadUrl: string, destination: string, message: string): Promise { 43 | process.stdout.write(message); 44 | 45 | if (!existsSync(destination)) { 46 | await fs.mkdir(destination, { recursive: true }); 47 | } 48 | 49 | const tar = await import('tar-fs'); 50 | const gunzip = await import('gunzip-maybe'); 51 | 52 | return new Promise((resolve, reject) => { 53 | const httpLibrary = downloadUrl.startsWith('https') ? https : http; 54 | 55 | httpLibrary.get(downloadUrl, getAgent(downloadUrl), res => { 56 | const total = Number(res.headers['content-length']); 57 | let received = 0; 58 | let timeout: NodeJS.Timeout | undefined; 59 | 60 | res.on('data', chunk => { 61 | if (!timeout) { 62 | timeout = setTimeout(() => { 63 | process.stdout.write(`${reset}${message}: ${received}/${total} (${(received / total * 100).toFixed()}%)`); 64 | timeout = undefined; 65 | }, 100); 66 | } 67 | 68 | received += chunk.length; 69 | }); 70 | res.on('error', reject); 71 | res.on('end', () => { 72 | if (timeout) { 73 | clearTimeout(timeout); 74 | } 75 | 76 | process.stdout.write(`${reset}${message}: complete\n`); 77 | }); 78 | 79 | const extract = res.pipe(gunzip()).pipe(tar.extract(destination, { strip: 1 })); 80 | extract.on('finish', () => { 81 | process.stdout.write(`Extracted to ${destination}\n`); 82 | resolve(); 83 | }); 84 | extract.on('error', reject); 85 | }); 86 | }); 87 | } 88 | 89 | 90 | export async function downloadAndUnzipVSCode(vscodeTestDir: string, quality: 'stable' | 'insider', commit: string | undefined): Promise { 91 | let retries = 3; 92 | do { 93 | try { 94 | let downloadURL: string | undefined; 95 | if (!commit) { 96 | const info = await getLatestBuild(quality); 97 | commit = info.version; 98 | downloadURL = info.url; 99 | } 100 | 101 | const folderName = `vscode-web-${quality}-${commit}`; 102 | const downloadedPath = path.resolve(vscodeTestDir, folderName); 103 | if (existsSync(downloadedPath) && existsSync(path.join(downloadedPath, 'version'))) { 104 | return { type: 'static', location: downloadedPath, quality, version: commit }; 105 | } 106 | 107 | if (!downloadURL) { 108 | downloadURL = await getDownloadURL(quality, commit); 109 | if (!downloadURL) { 110 | throw Error(`Failed to find a download for ${quality} and ${commit}`); 111 | } 112 | } 113 | 114 | if (existsSync(vscodeTestDir)) { 115 | await fs.rm(vscodeTestDir, { recursive: true, maxRetries: 5 }); 116 | } 117 | 118 | await fs.mkdir(vscodeTestDir, { recursive: true }); 119 | 120 | const productName = `VS Code ${quality === 'stable' ? 'Stable' : 'Insiders'}`; 121 | 122 | try { 123 | await downloadAndUntar(downloadURL, downloadedPath, `Downloading ${productName}`); 124 | await fs.writeFile(path.join(downloadedPath, 'version'), folderName); 125 | } catch (err) { 126 | console.error(err); 127 | throw Error(`Failed to download and unpack ${productName}.${commit ? ' Did you specify a valid commit?' : ''}`); 128 | } 129 | return { type: 'static', location: downloadedPath, quality, version: commit }; 130 | } catch (e) { 131 | retries--; 132 | if (retries === 0) { 133 | throw e; 134 | } 135 | console.log(`Download and install failed with '${e}'. Retrying... (${retries} attempts left)`); 136 | await new Promise(resolve => setTimeout(resolve, 3000)); 137 | } 138 | } while (true); 139 | } 140 | 141 | export async function fetch(api: string): Promise { 142 | return new Promise((resolve, reject) => { 143 | const httpLibrary = api.startsWith('https') ? https : http; 144 | httpLibrary.get(api, getAgent(api), res => { 145 | if (res.statusCode !== 200) { 146 | reject('Failed to get content from ' + api + '. Status code: ' + res.statusCode ); 147 | } 148 | 149 | let data = ''; 150 | 151 | res.on('data', chunk => { 152 | data += chunk; 153 | }); 154 | 155 | res.on('end', () => { 156 | resolve(data); 157 | }); 158 | 159 | res.on('error', err => { 160 | reject(err); 161 | }); 162 | }); 163 | }); 164 | } 165 | 166 | 167 | export async function fetchJSON(api: string): Promise { 168 | const data = await fetch(api); 169 | try { 170 | return JSON.parse(data); 171 | } catch (err) { 172 | throw new Error(`Failed to parse response from ${api}`); 173 | } 174 | } 175 | 176 | let PROXY_AGENT: HttpProxyAgent | undefined = undefined; 177 | let HTTPS_PROXY_AGENT: HttpsProxyAgent | undefined = undefined; 178 | 179 | if (process.env.npm_config_proxy) { 180 | PROXY_AGENT = new HttpProxyAgent(process.env.npm_config_proxy); 181 | HTTPS_PROXY_AGENT = new HttpsProxyAgent(process.env.npm_config_proxy); 182 | } 183 | if (process.env.npm_config_https_proxy) { 184 | HTTPS_PROXY_AGENT = new HttpsProxyAgent(process.env.npm_config_https_proxy); 185 | } 186 | 187 | function getAgent(url: string): https.RequestOptions { 188 | const parsed = new URL(url); 189 | const options: https.RequestOptions = {}; 190 | if (PROXY_AGENT && parsed.protocol.startsWith('http:')) { 191 | options.agent = PROXY_AGENT; 192 | } 193 | 194 | if (HTTPS_PROXY_AGENT && parsed.protocol.startsWith('https:')) { 195 | options.agent = HTTPS_PROXY_AGENT; 196 | } 197 | 198 | return options; 199 | } 200 | 201 | export async function directoryExists(path: string): Promise { 202 | try { 203 | const stats = await fs.stat(path); 204 | return stats.isDirectory(); 205 | } catch { 206 | return false; 207 | } 208 | } 209 | 210 | export async function fileExists(path: string): Promise { 211 | try { 212 | const stats = await fs.stat(path); 213 | return stats.isFile(); 214 | } catch { 215 | return false; 216 | } 217 | } 218 | 219 | export async function readFileInRepo(pathInRepo: string): Promise { 220 | return (await fs.readFile(path.resolve(__dirname, '../..', pathInRepo))).toString() 221 | } 222 | 223 | -------------------------------------------------------------------------------- /fs-provider/vscode.proposed.textSearchProvider.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | declare module 'vscode' { 7 | 8 | // https://github.com/microsoft/vscode/issues/59921 9 | 10 | /** 11 | * The parameters of a query for text search. 12 | */ 13 | export interface TextSearchQuery { 14 | /** 15 | * The text pattern to search for. 16 | */ 17 | pattern: string; 18 | 19 | /** 20 | * Whether or not `pattern` should match multiple lines of text. 21 | */ 22 | isMultiline?: boolean; 23 | 24 | /** 25 | * Whether or not `pattern` should be interpreted as a regular expression. 26 | */ 27 | isRegExp?: boolean; 28 | 29 | /** 30 | * Whether or not the search should be case-sensitive. 31 | */ 32 | isCaseSensitive?: boolean; 33 | 34 | /** 35 | * Whether or not to search for whole word matches only. 36 | */ 37 | isWordMatch?: boolean; 38 | } 39 | 40 | /** 41 | * A file glob pattern to match file paths against. 42 | * TODO@roblourens merge this with the GlobPattern docs/definition in vscode.d.ts. 43 | * @see {@link GlobPattern} 44 | */ 45 | export type GlobString = string; 46 | 47 | /** 48 | * Options common to file and text search 49 | */ 50 | export interface SearchOptions { 51 | /** 52 | * The root folder to search within. 53 | */ 54 | folder: Uri; 55 | 56 | /** 57 | * Files that match an `includes` glob pattern should be included in the search. 58 | */ 59 | includes: GlobString[]; 60 | 61 | /** 62 | * Files that match an `excludes` glob pattern should be excluded from the search. 63 | */ 64 | excludes: GlobString[]; 65 | 66 | /** 67 | * Whether external files that exclude files, like .gitignore, should be respected. 68 | * See the vscode setting `"search.useIgnoreFiles"`. 69 | */ 70 | useIgnoreFiles: boolean; 71 | 72 | /** 73 | * Whether symlinks should be followed while searching. 74 | * See the vscode setting `"search.followSymlinks"`. 75 | */ 76 | followSymlinks: boolean; 77 | 78 | /** 79 | * Whether global files that exclude files, like .gitignore, should be respected. 80 | * See the vscode setting `"search.useGlobalIgnoreFiles"`. 81 | */ 82 | useGlobalIgnoreFiles: boolean; 83 | 84 | /** 85 | * Whether files in parent directories that exclude files, like .gitignore, should be respected. 86 | * See the vscode setting `"search.useParentIgnoreFiles"`. 87 | */ 88 | useParentIgnoreFiles: boolean; 89 | } 90 | 91 | /** 92 | * Options to specify the size of the result text preview. 93 | * These options don't affect the size of the match itself, just the amount of preview text. 94 | */ 95 | export interface TextSearchPreviewOptions { 96 | /** 97 | * The maximum number of lines in the preview. 98 | * Only search providers that support multiline search will ever return more than one line in the match. 99 | */ 100 | matchLines: number; 101 | 102 | /** 103 | * The maximum number of characters included per line. 104 | */ 105 | charsPerLine: number; 106 | } 107 | 108 | /** 109 | * Options that apply to text search. 110 | */ 111 | export interface TextSearchOptions extends SearchOptions { 112 | /** 113 | * The maximum number of results to be returned. 114 | */ 115 | maxResults: number; 116 | 117 | /** 118 | * Options to specify the size of the result text preview. 119 | */ 120 | previewOptions?: TextSearchPreviewOptions; 121 | 122 | /** 123 | * Exclude files larger than `maxFileSize` in bytes. 124 | */ 125 | maxFileSize?: number; 126 | 127 | /** 128 | * Interpret files using this encoding. 129 | * See the vscode setting `"files.encoding"` 130 | */ 131 | encoding?: string; 132 | 133 | /** 134 | * Number of lines of context to include before each match. 135 | */ 136 | beforeContext?: number; 137 | 138 | /** 139 | * Number of lines of context to include after each match. 140 | */ 141 | afterContext?: number; 142 | } 143 | 144 | /** 145 | * Represents the severity of a TextSearchComplete message. 146 | */ 147 | export enum TextSearchCompleteMessageType { 148 | Information = 1, 149 | Warning = 2, 150 | } 151 | 152 | /** 153 | * A message regarding a completed search. 154 | */ 155 | export interface TextSearchCompleteMessage { 156 | /** 157 | * Markdown text of the message. 158 | */ 159 | text: string; 160 | /** 161 | * Whether the source of the message is trusted, command links are disabled for untrusted message sources. 162 | * Messaged are untrusted by default. 163 | */ 164 | trusted?: boolean; 165 | /** 166 | * The message type, this affects how the message will be rendered. 167 | */ 168 | type: TextSearchCompleteMessageType; 169 | } 170 | 171 | /** 172 | * Information collected when text search is complete. 173 | */ 174 | export interface TextSearchComplete { 175 | /** 176 | * Whether the search hit the limit on the maximum number of search results. 177 | * `maxResults` on {@linkcode TextSearchOptions} specifies the max number of results. 178 | * - If exactly that number of matches exist, this should be false. 179 | * - If `maxResults` matches are returned and more exist, this should be true. 180 | * - If search hits an internal limit which is less than `maxResults`, this should be true. 181 | */ 182 | limitHit?: boolean; 183 | 184 | /** 185 | * Additional information regarding the state of the completed search. 186 | * 187 | * Messages with "Information" style support links in markdown syntax: 188 | * - Click to [run a command](command:workbench.action.OpenQuickPick) 189 | * - Click to [open a website](https://aka.ms) 190 | * 191 | * Commands may optionally return { triggerSearch: true } to signal to the editor that the original search should run be again. 192 | */ 193 | message?: TextSearchCompleteMessage | TextSearchCompleteMessage[]; 194 | } 195 | 196 | /** 197 | * A preview of the text result. 198 | */ 199 | export interface TextSearchMatchPreview { 200 | /** 201 | * The matching lines of text, or a portion of the matching line that contains the match. 202 | */ 203 | text: string; 204 | 205 | /** 206 | * The Range within `text` corresponding to the text of the match. 207 | * The number of matches must match the TextSearchMatch's range property. 208 | */ 209 | matches: Range | Range[]; 210 | } 211 | 212 | /** 213 | * A match from a text search 214 | */ 215 | export interface TextSearchMatch { 216 | /** 217 | * The uri for the matching document. 218 | */ 219 | uri: Uri; 220 | 221 | /** 222 | * The range of the match within the document, or multiple ranges for multiple matches. 223 | */ 224 | ranges: Range | Range[]; 225 | 226 | /** 227 | * A preview of the text match. 228 | */ 229 | preview: TextSearchMatchPreview; 230 | } 231 | 232 | /** 233 | * A line of context surrounding a TextSearchMatch. 234 | */ 235 | export interface TextSearchContext { 236 | /** 237 | * The uri for the matching document. 238 | */ 239 | uri: Uri; 240 | 241 | /** 242 | * One line of text. 243 | * previewOptions.charsPerLine applies to this 244 | */ 245 | text: string; 246 | 247 | /** 248 | * The line number of this line of context. 249 | */ 250 | lineNumber: number; 251 | } 252 | 253 | export type TextSearchResult = TextSearchMatch | TextSearchContext; 254 | 255 | /** 256 | * A TextSearchProvider provides search results for text results inside files in the workspace. 257 | */ 258 | export interface TextSearchProvider { 259 | /** 260 | * Provide results that match the given text pattern. 261 | * @param query The parameters for this query. 262 | * @param options A set of options to consider while searching. 263 | * @param progress A progress callback that must be invoked for all results. 264 | * @param token A cancellation token. 265 | */ 266 | provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, token: CancellationToken): ProviderResult; 267 | } 268 | 269 | export namespace workspace { 270 | /** 271 | * Register a text search provider. 272 | * 273 | * Only one provider can be registered per scheme. 274 | * 275 | * @param scheme The provider will be invoked for workspace folders that have this file scheme. 276 | * @param provider The provider. 277 | * @return A {@link Disposable} that unregisters this provider when being disposed. 278 | */ 279 | export function registerTextSearchProvider(scheme: string, provider: TextSearchProvider): Disposable; 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /fs-provider/src/fsProvider.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { EventEmitter, Event, Uri, FileSystemProvider, Disposable, FileType, FileStat, FileSystemError, FileChangeType, FileChangeEvent, FileSearchQuery, FileSearchOptions, CancellationToken, ProviderResult, FileSearchProvider } from 'vscode'; 7 | import { Utils } from 'vscode-uri'; 8 | import { Minimatch } from 'minimatch'; 9 | 10 | export interface File { 11 | readonly type: FileType.File; 12 | name: string; 13 | stats: Promise; 14 | content: Promise; 15 | } 16 | 17 | export interface Directory { 18 | readonly type: FileType.Directory; 19 | name: string; 20 | stats: Promise; 21 | entries: Promise>; 22 | } 23 | 24 | export type Entry = File | Directory; 25 | 26 | 27 | function newFileStat(type: FileType, size: number): Promise { 28 | return Promise.resolve({ type, ctime: Date.now(), mtime: Date.now(), size }); 29 | } 30 | 31 | function modifiedFileStat(stats: FileStat, size?: number): Promise { 32 | return Promise.resolve({ type: stats.type, ctime: stats.ctime, mtime: Date.now(), size: size ?? stats.size }); 33 | } 34 | 35 | export class MemFileSystemProvider implements FileSystemProvider, FileSearchProvider { 36 | constructor(private readonly scheme: string, private readonly root: Directory, private readonly extensionUri: Uri) { 37 | } 38 | 39 | // --- manage file metadata 40 | 41 | async stat(resource: Uri): Promise { 42 | const entry = await this._lookup(resource, false); 43 | return entry.stats; 44 | } 45 | 46 | async readDirectory(resource: Uri): Promise<[string, FileType][]> { 47 | const entry = await this._lookupAsDirectory(resource, false); 48 | const entries = await entry.entries; 49 | const result: [string, FileType][] = []; 50 | entries.forEach((child, name) => result.push([name, child.type])); 51 | return result; 52 | } 53 | 54 | // --- manage file contents 55 | 56 | async readFile(resource: Uri): Promise { 57 | const entry = await this._lookupAsFile(resource, false); 58 | return entry.content; 59 | } 60 | 61 | async writeFile(uri: Uri, content: Uint8Array, opts: { create: boolean; overwrite: boolean; }): Promise { 62 | const basename = Utils.basename(uri); 63 | const parent = await this._lookupParentDirectory(uri); 64 | const entries = await parent.entries; 65 | let entry = entries.get(basename); 66 | if (entry && entry.type === FileType.Directory) { 67 | throw FileSystemError.FileIsADirectory(uri); 68 | } 69 | if (!entry && !opts.create) { 70 | throw FileSystemError.FileNotFound(uri); 71 | } 72 | if (entry && opts.create && !opts.overwrite) { 73 | throw FileSystemError.FileExists(uri); 74 | } 75 | const stats = newFileStat(FileType.File, content.byteLength); 76 | if (!entry) { 77 | entry = { type: FileType.File, name: basename, stats, content: Promise.resolve(content) }; 78 | entries.set(basename, entry); 79 | this._fireSoon({ type: FileChangeType.Created, uri }); 80 | } else { 81 | entry.stats = stats; 82 | entry.content = Promise.resolve(content); 83 | } 84 | this._fireSoon({ type: FileChangeType.Changed, uri }); 85 | } 86 | 87 | // --- manage files/folders 88 | 89 | async rename(from: Uri, to: Uri, opts: { overwrite: boolean; }): Promise { 90 | if (!opts.overwrite && await this._lookup(to, true)) { 91 | throw FileSystemError.FileExists(to); 92 | } 93 | 94 | const entry = await this._lookup(from, false); 95 | const oldParent = await this._lookupParentDirectory(from); 96 | 97 | const newParent = await this._lookupParentDirectory(to); 98 | const newName = Utils.basename(to); 99 | 100 | const oldParentEntries = await oldParent.entries; 101 | 102 | oldParentEntries.delete(entry.name); 103 | 104 | entry.name = newName; 105 | 106 | const newParentEntries = await newParent.entries; 107 | newParentEntries.set(newName, entry); 108 | 109 | this._fireSoon( 110 | { type: FileChangeType.Deleted, uri: from }, 111 | { type: FileChangeType.Created, uri: to } 112 | ); 113 | } 114 | 115 | async delete(uri: Uri, opts: { recursive: boolean; }): Promise { 116 | const dirname = Utils.dirname(uri); 117 | const basename = Utils.basename(uri); 118 | const parent = await this._lookupAsDirectory(dirname, false); 119 | const parentEntries = await parent.entries; 120 | if (parentEntries.has(basename)) { 121 | parentEntries.delete(basename); 122 | parent.stats = newFileStat(parent.type, -1); 123 | this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { uri, type: FileChangeType.Deleted }); 124 | } 125 | } 126 | 127 | async createDirectory(uri: Uri): Promise { 128 | const basename = Utils.basename(uri); 129 | const dirname = Utils.dirname(uri); 130 | const parent = await this._lookupAsDirectory(dirname, false); 131 | const parentEntries = await parent.entries; 132 | 133 | const entry: Directory = { type: FileType.Directory, name: basename, stats: newFileStat(FileType.Directory, 0), entries: Promise.resolve(new Map()) }; 134 | parentEntries.set(entry.name, entry); 135 | const stats = await parent.stats; 136 | parent.stats = modifiedFileStat(stats, stats.size + 1); 137 | this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { type: FileChangeType.Created, uri }); 138 | } 139 | 140 | // --- search 141 | 142 | async provideFileSearchResults(query: FileSearchQuery, options: FileSearchOptions, token: CancellationToken): Promise { 143 | const pattern = query.pattern; 144 | 145 | // Pattern is always blank: https://github.com/microsoft/vscode/issues/200892 146 | const glob = pattern ? new Minimatch(pattern) : undefined; 147 | 148 | const result: Uri[] = []; 149 | const dive = async (folderUri: Uri) => { 150 | const directory = await this._lookupAsDirectory(folderUri, false); 151 | for (const [name, entry] of await directory.entries) { 152 | /* support options.includes && options.excludes */ 153 | 154 | if (typeof options.maxResults !== 'undefined' && result.length >= options.maxResults) { 155 | break; 156 | } 157 | 158 | const uri = Uri.joinPath(folderUri, entry.name); 159 | if (entry.type === FileType.File) { 160 | const toMatch = uri.toString(); 161 | // Pattern is always blank: https://github.com/microsoft/vscode/issues/200892 162 | if (!glob || glob.match(toMatch)) { 163 | result.push(uri); 164 | } 165 | } else if (entry.type === FileType.Directory) { 166 | await dive(uri); 167 | } 168 | } 169 | }; 170 | 171 | await dive(options.folder); 172 | return result; 173 | } 174 | 175 | // --- lookup 176 | 177 | private async _lookup(uri: Uri, silent: false): Promise; 178 | private async _lookup(uri: Uri, silent: boolean): Promise; 179 | private async _lookup(uri: Uri, silent: boolean): Promise { 180 | if (uri.scheme !== this.scheme) { 181 | if (!silent) { 182 | throw FileSystemError.FileNotFound(uri); 183 | } else { 184 | return undefined; 185 | } 186 | } 187 | let entry: Entry | undefined = this.root; 188 | const parts = uri.path.split('/'); 189 | for (const part of parts) { 190 | if (!part) { 191 | continue; 192 | } 193 | let child: Entry | undefined; 194 | if (entry.type === FileType.Directory) { 195 | child = (await entry.entries).get(part); 196 | } 197 | if (!child) { 198 | if (!silent) { 199 | throw FileSystemError.FileNotFound(uri); 200 | } else { 201 | return undefined; 202 | } 203 | } 204 | entry = child; 205 | } 206 | return entry; 207 | } 208 | 209 | private async _lookupAsDirectory(uri: Uri, silent: boolean): Promise { 210 | const entry = await this._lookup(uri, silent); 211 | if (entry?.type === FileType.Directory) { 212 | return entry; 213 | } 214 | throw FileSystemError.FileNotADirectory(uri); 215 | } 216 | 217 | private async _lookupAsFile(uri: Uri, silent: boolean): Promise { 218 | const entry = await this._lookup(uri, silent); 219 | if (!entry) { 220 | throw FileSystemError.FileNotFound(uri); 221 | } 222 | if (entry.type === FileType.File) { 223 | return entry; 224 | } 225 | throw FileSystemError.FileIsADirectory(uri); 226 | } 227 | 228 | private _lookupParentDirectory(uri: Uri): Promise { 229 | const dirname = Utils.dirname(uri); 230 | return this._lookupAsDirectory(dirname, false); 231 | } 232 | 233 | // --- manage file events 234 | 235 | private readonly _onDidChangeFile = new EventEmitter(); 236 | readonly onDidChangeFile: Event = this._onDidChangeFile.event; 237 | 238 | private _bufferedChanges: FileChangeEvent[] = []; 239 | private _fireSoonHandle?: NodeJS.Timeout; 240 | 241 | watch(resource: Uri, opts: { recursive: boolean; excludes: string[]; }): Disposable { 242 | // ignore, fires for all changes... 243 | return Disposable.from(); 244 | } 245 | 246 | private _fireSoon(...changes: FileChangeEvent[]): void { 247 | this._bufferedChanges.push(...changes); 248 | 249 | if (this._fireSoonHandle) { 250 | clearTimeout(this._fireSoonHandle); 251 | } 252 | 253 | this._fireSoonHandle = setTimeout(() => { 254 | this._onDidChangeFile.fire(this._bufferedChanges); 255 | this._bufferedChanges.length = 0; 256 | }, 5); 257 | } 258 | 259 | dispose() { 260 | this._onDidChangeFile.dispose(); 261 | } 262 | } -------------------------------------------------------------------------------- /src/browser/main.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | /// 6 | 7 | import { create, IWorkspaceProvider, IWorkbenchConstructionOptions, UriComponents, IWorkspace, URI, IURLCallbackProvider, Emitter, IDisposable} from './workbench.api'; 8 | 9 | class WorkspaceProvider implements IWorkspaceProvider { 10 | 11 | private static QUERY_PARAM_EMPTY_WINDOW = 'ew'; 12 | private static QUERY_PARAM_FOLDER = 'folder'; 13 | private static QUERY_PARAM_WORKSPACE = 'workspace'; 14 | 15 | private static QUERY_PARAM_PAYLOAD = 'payload'; 16 | 17 | static create(config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents }) { 18 | let foundWorkspace = false; 19 | let workspace: IWorkspace; 20 | let payload = Object.create(null); 21 | 22 | const query = new URL(document.location.href).searchParams; 23 | query.forEach((value, key) => { 24 | switch (key) { 25 | 26 | // Folder 27 | case WorkspaceProvider.QUERY_PARAM_FOLDER: 28 | workspace = { folderUri: URI.parse(value) }; 29 | foundWorkspace = true; 30 | break; 31 | 32 | // Workspace 33 | case WorkspaceProvider.QUERY_PARAM_WORKSPACE: 34 | workspace = { workspaceUri: URI.parse(value) }; 35 | foundWorkspace = true; 36 | break; 37 | 38 | // Empty 39 | case WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW: 40 | workspace = undefined; 41 | foundWorkspace = true; 42 | break; 43 | 44 | // Payload 45 | case WorkspaceProvider.QUERY_PARAM_PAYLOAD: 46 | try { 47 | payload = JSON.parse(value); 48 | } catch (error) { 49 | console.error(error); // possible invalid JSON 50 | } 51 | break; 52 | } 53 | }); 54 | 55 | // If no workspace is provided through the URL, check for config 56 | // attribute from server 57 | if (!foundWorkspace) { 58 | if (config.folderUri) { 59 | workspace = { folderUri: URI.revive(config.folderUri) }; 60 | } else if (config.workspaceUri) { 61 | workspace = { workspaceUri: URI.revive(config.workspaceUri) }; 62 | } 63 | } 64 | 65 | return new WorkspaceProvider(workspace, payload); 66 | } 67 | 68 | readonly trusted = true; 69 | 70 | private constructor( 71 | readonly workspace: IWorkspace, 72 | readonly payload: object, 73 | ) { 74 | } 75 | 76 | async open(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise { 77 | if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) { 78 | return true; // return early if workspace and environment is not changing and we are reusing window 79 | } 80 | 81 | const targetHref = this.createTargetUrl(workspace, options); 82 | if (targetHref) { 83 | if (options?.reuse) { 84 | window.location.href = targetHref; 85 | return true; 86 | } else { 87 | return !!window.open(targetHref); 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): string | undefined { 94 | 95 | // Empty 96 | let targetHref: string | undefined = undefined; 97 | if (!workspace) { 98 | targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_EMPTY_WINDOW}=true`; 99 | } 100 | 101 | // Folder 102 | else if ('folderUri' in workspace) { 103 | const queryParamFolder = encodeURIComponent(workspace.folderUri.toString(true)); 104 | targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_FOLDER}=${queryParamFolder}`; 105 | } 106 | 107 | // Workspace 108 | else if ('workspaceUri' in workspace) { 109 | const queryParamWorkspace = encodeURIComponent(workspace.workspaceUri.toString(true)); 110 | targetHref = `${document.location.origin}${document.location.pathname}?${WorkspaceProvider.QUERY_PARAM_WORKSPACE}=${queryParamWorkspace}`; 111 | } 112 | 113 | // Append payload if any 114 | if (options?.payload) { 115 | targetHref += `&${WorkspaceProvider.QUERY_PARAM_PAYLOAD}=${encodeURIComponent(JSON.stringify(options.payload))}`; 116 | } 117 | 118 | return targetHref; 119 | } 120 | 121 | private isSame(workspaceA: IWorkspace, workspaceB: IWorkspace): boolean { 122 | if (!workspaceA || !workspaceB) { 123 | return workspaceA === workspaceB; // both empty 124 | } 125 | 126 | if ('folderUri' in workspaceA && 'folderUri' in workspaceB) { 127 | return this.isEqualURI(workspaceA.folderUri, workspaceB.folderUri); // same workspace 128 | } 129 | 130 | if ('workspaceUri' in workspaceA && 'workspaceUri' in workspaceB) { 131 | return this.isEqualURI(workspaceA.workspaceUri, workspaceB.workspaceUri); // same workspace 132 | } 133 | 134 | return false; 135 | } 136 | 137 | private isEqualURI(a: UriComponents, b: UriComponents): boolean { 138 | return a.scheme === b.scheme && a.authority === b.authority && a.path === b.path; 139 | } 140 | 141 | } 142 | 143 | class LocalStorageURLCallbackProvider implements IURLCallbackProvider, IDisposable { 144 | 145 | private static REQUEST_ID = 0; 146 | 147 | private static QUERY_KEYS: ('scheme' | 'authority' | 'path' | 'query' | 'fragment')[] = [ 148 | 'scheme', 149 | 'authority', 150 | 'path', 151 | 'query', 152 | 'fragment' 153 | ]; 154 | 155 | private readonly _onCallback = new Emitter(); 156 | readonly onCallback = this._onCallback.event; 157 | 158 | private pendingCallbacks = new Set(); 159 | private lastTimeChecked = Date.now(); 160 | private checkCallbacksTimeout: unknown | undefined = undefined; 161 | private onDidChangeLocalStorageDisposable: IDisposable | undefined; 162 | 163 | constructor(private readonly _callbackRoute: string) { 164 | } 165 | 166 | create(options: Partial = {}): URI { 167 | const id = ++LocalStorageURLCallbackProvider.REQUEST_ID; 168 | const queryParams: string[] = [`vscode-reqid=${id}`]; 169 | 170 | for (const key of LocalStorageURLCallbackProvider.QUERY_KEYS) { 171 | const value = options[key]; 172 | 173 | if (value) { 174 | queryParams.push(`vscode-${key}=${encodeURIComponent(value)}`); 175 | } 176 | } 177 | 178 | // TODO@joao remove eventually 179 | // https://github.com/microsoft/vscode-dev/issues/62 180 | // https://github.com/microsoft/vscode/blob/159479eb5ae451a66b5dac3c12d564f32f454796/extensions/github-authentication/src/githubServer.ts#L50-L50 181 | if (!(options.authority === 'vscode.github-authentication' && options.path === '/dummy')) { 182 | const key = `vscode-web.url-callbacks[${id}]`; 183 | localStorage.removeItem(key); 184 | 185 | this.pendingCallbacks.add(id); 186 | this.startListening(); 187 | } 188 | 189 | return URI.parse(window.location.href).with({ path: this._callbackRoute, query: queryParams.join('&') }); 190 | } 191 | 192 | private startListening(): void { 193 | if (this.onDidChangeLocalStorageDisposable) { 194 | return; 195 | } 196 | 197 | const fn = () => this.onDidChangeLocalStorage(); 198 | window.addEventListener('storage', fn); 199 | this.onDidChangeLocalStorageDisposable = { dispose: () => window.removeEventListener('storage', fn) }; 200 | } 201 | 202 | private stopListening(): void { 203 | this.onDidChangeLocalStorageDisposable?.dispose(); 204 | this.onDidChangeLocalStorageDisposable = undefined; 205 | } 206 | 207 | // this fires every time local storage changes, but we 208 | // don't want to check more often than once a second 209 | private async onDidChangeLocalStorage(): Promise { 210 | const ellapsed = Date.now() - this.lastTimeChecked; 211 | 212 | if (ellapsed > 1000) { 213 | this.checkCallbacks(); 214 | } else if (this.checkCallbacksTimeout === undefined) { 215 | this.checkCallbacksTimeout = setTimeout(() => { 216 | this.checkCallbacksTimeout = undefined; 217 | this.checkCallbacks(); 218 | }, 1000 - ellapsed); 219 | } 220 | } 221 | 222 | private checkCallbacks(): void { 223 | let pendingCallbacks: Set | undefined; 224 | 225 | for (const id of this.pendingCallbacks) { 226 | const key = `vscode-web.url-callbacks[${id}]`; 227 | const result = localStorage.getItem(key); 228 | 229 | if (result !== null) { 230 | try { 231 | this._onCallback.fire(URI.revive(JSON.parse(result))); 232 | } catch (error) { 233 | console.error(error); 234 | } 235 | 236 | pendingCallbacks = pendingCallbacks ?? new Set(this.pendingCallbacks); 237 | pendingCallbacks.delete(id); 238 | localStorage.removeItem(key); 239 | } 240 | } 241 | 242 | if (pendingCallbacks) { 243 | this.pendingCallbacks = pendingCallbacks; 244 | 245 | if (this.pendingCallbacks.size === 0) { 246 | this.stopListening(); 247 | } 248 | } 249 | 250 | this.lastTimeChecked = Date.now(); 251 | } 252 | 253 | dispose(): void { 254 | this._onCallback.dispose(); 255 | } 256 | } 257 | 258 | (function () { 259 | const configElement = window.document.getElementById('vscode-workbench-web-configuration'); 260 | const configElementAttribute = configElement ? configElement.getAttribute('data-settings') : undefined; 261 | if (!configElement || !configElementAttribute) { 262 | throw new Error('Missing web configuration element'); 263 | } 264 | const config: IWorkbenchConstructionOptions & { folderUri?: UriComponents; workspaceUri?: UriComponents; callbackRoute: string } = JSON.parse(configElementAttribute); 265 | 266 | create(window.document.body, { 267 | ...config, 268 | workspaceProvider: WorkspaceProvider.create(config), 269 | urlCallbackProvider: new LocalStorageURLCallbackProvider(config.callbackRoute) 270 | }); 271 | 272 | })(); -------------------------------------------------------------------------------- /src/server/workbench.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from 'path'; 7 | import { promises as fs } from 'fs'; 8 | import { URI } from 'vscode-uri'; 9 | import Router, { RouterMiddleware } from '@koa/router'; 10 | 11 | import { GalleryExtensionInfo, IConfig } from './main'; 12 | import { getScannedBuiltinExtensions, IScannedBuiltinExtension, scanForExtensions, URIComponents } from './extensions'; 13 | import { fsProviderExtensionPrefix, fsProviderFolderUri } from './mounts'; 14 | import { readFileInRepo } from './download'; 15 | 16 | interface IDevelopmentOptions { 17 | extensionTestsPath?: URIComponents; 18 | extensions?: URIComponents[]; 19 | } 20 | 21 | interface IWorkbenchOptions { 22 | additionalBuiltinExtensions?: (string | URIComponents | GalleryExtensionInfo)[]; 23 | developmentOptions?: IDevelopmentOptions; 24 | productConfiguration?: { [key: string]: any }; 25 | 26 | // options of the builtin workbench (vs/code/browser/workbench/workbench) 27 | folderUri?: URIComponents; 28 | workspaceUri?: URIComponents; 29 | } 30 | 31 | function asJSON(value: unknown): string { 32 | return JSON.stringify(value).replace(/"/g, '"'); 33 | } 34 | 35 | class Workbench { 36 | constructor(readonly baseUrl: string, readonly dev: boolean, readonly esm: boolean, private devCSSModules: string[], private readonly builtInExtensions: IScannedBuiltinExtension[] = [], private readonly productOverrides?: Record) { } 37 | 38 | async render(workbenchWebConfiguration: IWorkbenchOptions): Promise { 39 | if (this.productOverrides) { 40 | workbenchWebConfiguration.productConfiguration = { ...workbenchWebConfiguration.productConfiguration, ...this.productOverrides }; 41 | } 42 | const values: { [key: string]: string } = { 43 | WORKBENCH_WEB_CONFIGURATION: asJSON(workbenchWebConfiguration), 44 | WORKBENCH_WEB_BASE_URL: this.baseUrl, 45 | WORKBENCH_BUILTIN_EXTENSIONS: asJSON(this.builtInExtensions), 46 | WORKBENCH_MAIN: await this.getMain() 47 | }; 48 | 49 | try { 50 | const workbenchTemplate = await readFileInRepo(`views/workbench${this.esm ? '-esm' : ''}.html`); 51 | return workbenchTemplate.replace(/\{\{([^}]+)\}\}/g, (_, key) => values[key] ?? 'undefined'); 52 | } catch (e) { 53 | return String(e); 54 | } 55 | } 56 | 57 | async getMain() { 58 | const lines: string[] = []; 59 | if (this.esm) { 60 | let workbenchMain = await readFileInRepo(`out/browser/esm/main.js`); 61 | if (this.dev) { 62 | lines.push( 63 | "", 66 | ""); 83 | workbenchMain = workbenchMain.replace('./workbench.api', `${this.baseUrl}/out/vs/workbench/workbench.web.main.internal.js`); 84 | lines.push(``); 85 | } else { 86 | workbenchMain = workbenchMain.replace('./workbench.api', `${this.baseUrl}/out/vs/workbench/workbench.web.main.internal.js`); 87 | lines.push(``); 88 | lines.push(``); 89 | } 90 | return lines.join('\n'); 91 | } else { 92 | let workbenchMain = await readFileInRepo(`out/browser/amd/main.js`); // defines a AMD module `vscode-web-browser-main` 93 | workbenchMain = workbenchMain.replace('./workbench.api', `vs/workbench/workbench.web.main`); 94 | workbenchMain = workbenchMain + '\nrequire(["vscode-web-browser-main"], function() { });'; 95 | if (this.dev) { 96 | 97 | } else { 98 | lines.push(``); 99 | lines.push(``); 100 | lines.push(``); 101 | } 102 | lines.push(``); 103 | } 104 | return lines.join('\n'); 105 | } 106 | 107 | async renderCallback(): Promise { 108 | return await readFileInRepo(`views/callback.html`); 109 | } 110 | } 111 | 112 | async function getWorkbenchOptions( 113 | ctx: { protocol: string; host: string }, 114 | config: IConfig 115 | ): Promise { 116 | const options: IWorkbenchOptions = {}; 117 | options.productConfiguration = { enableTelemetry: false }; 118 | if (config.extensionPaths) { 119 | const extensionPromises = config.extensionPaths.map((extensionPath, index) => { 120 | return scanForExtensions(extensionPath, { 121 | scheme: ctx.protocol, 122 | authority: ctx.host, 123 | path: `/static/extensions/${index}`, 124 | }); 125 | }); 126 | options.additionalBuiltinExtensions = (await Promise.all(extensionPromises)).flat(); 127 | } 128 | if (config.extensionIds) { 129 | if (!options.additionalBuiltinExtensions) { 130 | options.additionalBuiltinExtensions = []; 131 | } 132 | 133 | options.additionalBuiltinExtensions.push(...config.extensionIds); 134 | } 135 | if (config.extensionDevelopmentPath) { 136 | const developmentOptions: IDevelopmentOptions = (options.developmentOptions = {}); 137 | 138 | developmentOptions.extensions = await scanForExtensions( 139 | config.extensionDevelopmentPath, 140 | { scheme: ctx.protocol, authority: ctx.host, path: '/static/devextensions' }, 141 | ); 142 | if (config.extensionTestsPath) { 143 | let relativePath = path.relative(config.extensionDevelopmentPath, config.extensionTestsPath); 144 | if (process.platform === 'win32') { 145 | relativePath = relativePath.replace(/\\/g, '/'); 146 | } 147 | developmentOptions.extensionTestsPath = { 148 | scheme: ctx.protocol, 149 | authority: ctx.host, 150 | path: path.posix.join('/static/devextensions', relativePath), 151 | }; 152 | } 153 | } 154 | if (config.folderMountPath) { 155 | if (!options.additionalBuiltinExtensions) { 156 | options.additionalBuiltinExtensions = []; 157 | } 158 | options.additionalBuiltinExtensions.push({ scheme: ctx.protocol, authority: ctx.host, path: fsProviderExtensionPrefix }); 159 | options.folderUri = URI.parse(fsProviderFolderUri); 160 | 161 | options.productConfiguration.extensionEnabledApiProposals = { 162 | "vscode.vscode-test-web-fs": [ 163 | "fileSearchProvider", 164 | "textSearchProvider" 165 | ] 166 | }; 167 | } else if (config.folderUri) { 168 | options.folderUri = URI.parse(config.folderUri); 169 | } else { 170 | options.workspaceUri = URI.from({ scheme: 'tmp', path: `/default.code-workspace` }); 171 | } 172 | return options; 173 | } 174 | 175 | export default function (config: IConfig): RouterMiddleware { 176 | const router = new Router<{ workbench: Workbench }>(); 177 | 178 | router.use(async (ctx, next) => { 179 | if (config.build.type === 'sources') { 180 | const builtInExtensions = await getScannedBuiltinExtensions(config.build.location); 181 | const productOverrides = await getProductOverrides(config.build.location); 182 | const esm = config.esm || await isESM(config.build.location); 183 | console.log('Using ESM loader:', esm); 184 | const devCSSModules = esm ? await getDevCssModules(config.build.location) : []; 185 | ctx.state.workbench = new Workbench(`${ctx.protocol}://${ctx.host}/static/sources`, true, esm, devCSSModules, builtInExtensions, { 186 | ...productOverrides, 187 | webEndpointUrlTemplate: `${ctx.protocol}://{{uuid}}.${ctx.host}/static/sources`, 188 | webviewContentExternalBaseUrlTemplate: `${ctx.protocol}://{{uuid}}.${ctx.host}/static/sources/out/vs/workbench/contrib/webview/browser/pre/` 189 | }); 190 | } else if (config.build.type === 'static') { 191 | const baseUrl = `${ctx.protocol}://${ctx.host}/static/build`; 192 | ctx.state.workbench = new Workbench(baseUrl, false, config.esm, [], [], { 193 | webEndpointUrlTemplate: `${ctx.protocol}://{{uuid}}.${ctx.host}/static/build`, 194 | webviewContentExternalBaseUrlTemplate: `${ctx.protocol}://{{uuid}}.${ctx.host}/static/build/out/vs/workbench/contrib/webview/browser/pre/` 195 | }); 196 | } else if (config.build.type === 'cdn') { 197 | ctx.state.workbench = new Workbench(config.build.uri, false, config.esm, []); 198 | } 199 | await next(); 200 | }); 201 | 202 | router.get('/callback', async ctx => { 203 | ctx.body = await ctx.state.workbench.renderCallback(); 204 | }); 205 | 206 | router.get('/', async ctx => { 207 | const options = await getWorkbenchOptions(ctx, config); 208 | ctx.body = await ctx.state.workbench.render(options); 209 | if (config.coi) { 210 | ctx.set('Cross-Origin-Opener-Policy', 'same-origin'); 211 | ctx.set('Cross-Origin-Embedder-Policy', 'require-corp'); 212 | } 213 | }); 214 | 215 | return router.routes(); 216 | } 217 | 218 | async function getProductOverrides(vsCodeDevLocation: string): Promise | undefined> { 219 | try { 220 | return JSON.parse((await fs.readFile(path.join(vsCodeDevLocation, 'product.overrides.json'))).toString()); 221 | } catch (e) { 222 | return undefined; 223 | } 224 | } 225 | 226 | async function getDevCssModules(vsCodeDevLocation: string): Promise { 227 | const glob = await import('tinyglobby') 228 | return glob.glob('**/*.css', { cwd: path.join(vsCodeDevLocation, 'out') }); 229 | } 230 | 231 | async function isESM(vsCodeDevLocation: string): Promise { 232 | try { 233 | const packageJSON = await fs.readFile(path.join(vsCodeDevLocation, 'out', 'package.json')); 234 | return JSON.parse(packageJSON.toString()).type === 'module'; 235 | } catch (e) { 236 | // ignore 237 | } 238 | try { 239 | const packageJSON = await fs.readFile(path.join(vsCodeDevLocation, 'package.json')); 240 | return JSON.parse(packageJSON.toString()).type === 'module'; 241 | } catch (e) { 242 | return false; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/server/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable header/header */ 3 | 4 | /*--------------------------------------------------------------------------------------------- 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. See License.txt in the project root for license information. 7 | *--------------------------------------------------------------------------------------------*/ 8 | 9 | import { IConfig, runServer, Static, Sources } from './main'; 10 | import { downloadAndUnzipVSCode, directoryExists, fileExists, readFileInRepo } from './download'; 11 | 12 | import * as playwright from 'playwright'; 13 | import * as minimist from 'minimist'; 14 | import * as path from 'path'; 15 | 16 | export type BrowserType = 'chromium' | 'firefox' | 'webkit' | 'none'; 17 | export type VSCodeQuality = 'insiders' | 'stable'; 18 | 19 | export type GalleryExtension = { readonly id: string; readonly preRelease?: boolean }; 20 | export interface Options { 21 | 22 | /** 23 | * Browser to open: 'chromium' | 'firefox' | 'webkit' | 'none'. 24 | */ 25 | browserType: BrowserType; 26 | 27 | /** 28 | * Browser command line options. 29 | */ 30 | browserOptions?: string[]; 31 | 32 | /** 33 | * Absolute path to folder that contains one or more extensions (in subfolders). 34 | * Extension folders include a `package.json` extension manifest. 35 | */ 36 | extensionDevelopmentPath?: string; 37 | 38 | /** 39 | * Absolute path to the extension tests runner module. 40 | * Can be either a file path or a directory path that contains an `index.js`. 41 | * The module is expected to have a `run` function of the following signature: 42 | * 43 | * ```ts 44 | * function run(): Promise; 45 | * ``` 46 | * 47 | * When running the extension test, the Extension Development Host will call this function 48 | * that runs the test suite. This function should throws an error if any test fails. 49 | */ 50 | extensionTestsPath?: string; 51 | 52 | /** 53 | * The quality of the VS Code to use. Supported qualities are: 54 | * - `'stable'` : The latest stable build will be used 55 | * - `'insiders'` : The latest insiders build will be used 56 | * 57 | * Currently defaults to `insiders`, which is latest stable insiders. 58 | * 59 | * The setting is ignored when a vsCodeDevPath is provided. 60 | */ 61 | quality?: VSCodeQuality; 62 | 63 | /** 64 | * The commit of the VS Code build to use. If not set, the latest build is used. 65 | * 66 | * The setting is ignored when a vsCodeDevPath is provided. 67 | */ 68 | commit?: string; 69 | 70 | /** 71 | * @deprecated. Use `quality` or `vsCodeDevPath` instead. 72 | */ 73 | version?: string; 74 | 75 | /** 76 | * Open the dev tools. 77 | */ 78 | devTools?: boolean; 79 | 80 | /** 81 | * Do not show the browser. Defaults to `true` if a `extensionTestsPath` is provided, `false` otherwise. 82 | */ 83 | headless?: boolean; 84 | 85 | /** 86 | * If set, opens the page with cross origin isolation enabled. 87 | */ 88 | coi?: boolean; 89 | 90 | /** 91 | * If set, serves the page with ESM usage. 92 | */ 93 | esm?: boolean; 94 | 95 | /** 96 | * @deprecated. Use `printServerLog` instead. 97 | */ 98 | hideServerLog?: boolean; 99 | 100 | /** 101 | * If set, the server access log is printed to the console. Defaults to `false`. 102 | */ 103 | printServerLog?: boolean; 104 | 105 | /** 106 | * Expose browser debugging on this port number, and wait for the debugger to attach before running tests. 107 | */ 108 | waitForDebugger?: number; 109 | 110 | /** 111 | * A local path to open VSCode on. VS Code for the browser will open an a virtual 112 | * file system ('vscode-test-web://mount') where the files of the local folder will served. 113 | * The file system is read/write, but modifications are stored in memory and not written back to disk. 114 | */ 115 | folderPath?: string; 116 | 117 | /** 118 | * The folder URI to open VSCode on. If 'folderPath' is set this will be ignored and 'vscode-test-web://mount' 119 | * is used as folder URI instead. 120 | */ 121 | folderUri?: string; 122 | 123 | /** 124 | * Permissions granted to the opened browser. An list of permissions can be found at 125 | * https://playwright.dev/docs/api/class-browsercontext#browser-context-grant-permissions 126 | * Example: [ 'clipboard-read', 'clipboard-write' ] 127 | */ 128 | permissions?: string[]; 129 | 130 | /** 131 | * Absolute paths pointing to built-in extensions to include. 132 | */ 133 | extensionPaths?: string[]; 134 | 135 | /** 136 | * List of extensions to include. The id format is ${publisher}.${name}. 137 | */ 138 | extensionIds?: GalleryExtension[]; 139 | 140 | /** 141 | * Absolute path pointing to VS Code sources to use. 142 | */ 143 | vsCodeDevPath?: string; 144 | 145 | /** 146 | * Print out more information while the server is running, e.g. the console output in the browser 147 | */ 148 | verbose?: boolean; 149 | 150 | /** 151 | * The port to start the server on. Defaults to `3000`. 152 | */ 153 | port?: number; 154 | 155 | /** 156 | * The host name to start the server on. Defaults to `localhost` 157 | */ 158 | host?: string; 159 | 160 | /** 161 | * The temporary folder for storing the VS Code builds used for running the tests. Defaults to `$CURRENT_WORKING_DIR/.vscode-test-web`. 162 | */ 163 | testRunnerDataDir?: string; 164 | } 165 | 166 | export interface Disposable { 167 | dispose(): void; 168 | } 169 | 170 | /** 171 | * Runs the tests in a browser. 172 | * 173 | * @param options The options defining browser type, extension and test location. 174 | */ 175 | export async function runTests(options: Options & { extensionTestsPath: string }): Promise { 176 | const config: IConfig = { 177 | extensionDevelopmentPath: options.extensionDevelopmentPath, 178 | extensionTestsPath: options.extensionTestsPath, 179 | build: await getBuild(options), 180 | folderUri: options.folderUri, 181 | folderMountPath: options.folderPath, 182 | printServerLog: options.printServerLog ?? options.hideServerLog === false, 183 | extensionPaths: options.extensionPaths, 184 | extensionIds: options.extensionIds, 185 | coi: !!options.coi, 186 | esm: !!options.esm, 187 | }; 188 | 189 | const host = options.host ?? 'localhost'; 190 | const port = options.port ?? 3000; 191 | const server = await runServer(host, port, config); 192 | 193 | return new Promise(async (s, e) => { 194 | const endpoint = `http://${host}:${port}`; 195 | 196 | const configPage = async (page: playwright.Page, browser: playwright.Browser) => { 197 | type Severity = 'error' | 'warning' | 'info'; 198 | const unreportedOutput: { type: Severity; args: unknown[] }[] = []; 199 | await page.exposeFunction('codeAutomationLog', (type: Severity, args: unknown[]) => { 200 | console[type](...args); 201 | }); 202 | 203 | await page.exposeFunction('codeAutomationExit', async (code: number) => { 204 | try { 205 | await browser.close(); 206 | } catch (error) { 207 | console.error(`Error when closing browser: ${error}`); 208 | } 209 | if (unreportedOutput.length) { 210 | console.error(`There were ${unreportedOutput.length} messages that could not be reported to the console:`); 211 | unreportedOutput.forEach(({ type, args }) => console[type](...args)); 212 | } 213 | server.close(); 214 | if (code === 0) { 215 | s(); 216 | } else { 217 | e(new Error('Test failed')); 218 | } 219 | }); 220 | }; 221 | console.log(`Opening browser on ${endpoint}...`); 222 | const context = await openBrowser(endpoint, options, configPage); 223 | if (context) { 224 | context.once('close', () => server.close()); 225 | } else { 226 | server.close(); 227 | e(new Error('Can not run test as opening of browser failed.')); 228 | } 229 | }); 230 | } 231 | 232 | async function getBuild(options: Options): Promise { 233 | if (options.vsCodeDevPath) { 234 | return { 235 | type: 'sources', 236 | location: options.vsCodeDevPath, 237 | }; 238 | } 239 | const quality = options.quality || options.version; 240 | const commit = options.commit; 241 | const testRunnerDataDir = options.testRunnerDataDir ?? path.resolve(process.cwd(), '.vscode-test-web'); 242 | return await downloadAndUnzipVSCode(testRunnerDataDir, quality === 'stable' ? 'stable' : 'insider', commit); 243 | } 244 | 245 | export async function open(options: Options): Promise { 246 | const config: IConfig = { 247 | extensionDevelopmentPath: options.extensionDevelopmentPath, 248 | extensionTestsPath: options.extensionTestsPath, 249 | build: await getBuild(options), 250 | folderUri: options.folderUri, 251 | folderMountPath: options.folderPath, 252 | printServerLog: options.printServerLog ?? options.hideServerLog === false, 253 | extensionPaths: options.extensionPaths, 254 | extensionIds: options.extensionIds, 255 | coi: !!options.coi, 256 | esm: !!options.esm, 257 | }; 258 | 259 | const host = options.host ?? 'localhost'; 260 | const port = options.port ?? 3000; 261 | const server = await runServer(host, port, config); 262 | 263 | const endpoint = `http://${host}:${port}`; 264 | 265 | const context = await openBrowser(endpoint, options); 266 | context?.once('close', () => server.close()); 267 | return { 268 | dispose: () => { 269 | server.close(); 270 | context?.browser()?.close(); 271 | }, 272 | }; 273 | } 274 | 275 | async function openBrowser(endpoint: string, options: Options, configPage?: (page: playwright.Page, browser: playwright.Browser) => Promise): Promise { 276 | if (options.browserType === 'none') { 277 | return undefined; 278 | } 279 | 280 | const browserType = await playwright[options.browserType]; 281 | if (!browserType) { 282 | console.error(`Can not open browser type: ${options.browserType}`); 283 | return undefined; 284 | } 285 | 286 | const args: string[] = []; 287 | 288 | if (options.browserOptions) { 289 | args.push(...options.browserOptions); 290 | } 291 | 292 | if (process.platform === 'linux' && options.browserType === 'chromium') { 293 | args.push('--no-sandbox'); 294 | } 295 | 296 | if (options.waitForDebugger) { 297 | args.push(`--remote-debugging-port=${options.waitForDebugger}`); 298 | } 299 | 300 | const headless = options.headless ?? options.extensionTestsPath !== undefined; 301 | 302 | const browser = await browserType.launch({ headless, args, devtools: options.devTools }); 303 | const context = await browser.newContext({ viewport: null }); 304 | if (options.permissions) { 305 | context.grantPermissions(options.permissions); 306 | } 307 | 308 | // forcefully close browser if last page is closed. workaround for https://github.com/microsoft/playwright/issues/2946 309 | let openPages = 0; 310 | context.on('page', page => { 311 | openPages++; 312 | page.once('close', () => { 313 | openPages--; 314 | if (openPages === 0) { 315 | browser.close(); 316 | } 317 | }); 318 | }); 319 | 320 | const page = context.pages()[0] ?? (await context.newPage()); 321 | if (configPage) { 322 | await configPage(page, browser); 323 | } 324 | if (options.waitForDebugger) { 325 | await page.waitForFunction(() => '__jsDebugIsReady' in globalThis); 326 | } 327 | if (options.verbose) { 328 | page.on('console', (message) => { 329 | console.log(message.text()); 330 | }); 331 | } 332 | 333 | await page.goto(endpoint); 334 | 335 | return context; 336 | } 337 | 338 | function validateStringOrUndefined(options: CommandLineOptions, name: keyof CommandLineOptions): string | undefined { 339 | const value = options[name]; 340 | if (value === undefined || typeof value === 'string') { 341 | return value; 342 | } 343 | console.log(`'${name}' needs to be a string value.`); 344 | showHelp(); 345 | process.exit(-1); 346 | } 347 | 348 | async function validatePathOrUndefined(options: CommandLineOptions, name: keyof CommandLineOptions, isFile?: boolean): Promise { 349 | const loc = validateStringOrUndefined(options, name); 350 | return loc && validatePath(loc, isFile); 351 | } 352 | 353 | function validateBooleanOrUndefined(options: CommandLineOptions, name: keyof CommandLineOptions): boolean | undefined { 354 | const value = options[name]; 355 | if (value === undefined || typeof value === 'boolean') { 356 | return value; 357 | } 358 | console.log(`'${name}' needs to be a boolean value.`); 359 | showHelp(); 360 | process.exit(-1); 361 | } 362 | 363 | function validatePrintServerLog(options: CommandLineOptions): boolean { 364 | const printServerLog = validateBooleanOrUndefined(options, 'printServerLog'); 365 | if (printServerLog !== undefined) { 366 | return printServerLog; 367 | } 368 | const hideServerLog = validateBooleanOrUndefined(options, 'hideServerLog'); 369 | if (hideServerLog !== undefined) { 370 | return !hideServerLog; 371 | } 372 | return false; 373 | } 374 | 375 | function validateBrowserType(options: CommandLineOptions): BrowserType { 376 | const browserType = options.browser || options.browserType; 377 | if (browserType === undefined) { 378 | return 'chromium'; 379 | } 380 | if (options.browserType && options.browser) { 381 | console.log(`Ignoring browserType option '${options.browserType}' as browser option '${options.browser}' is set.`); 382 | } 383 | 384 | if (typeof browserType === 'string' && ['chromium', 'firefox', 'webkit', 'none'].includes(browserType)) { 385 | return browserType as BrowserType; 386 | } 387 | console.log(`Invalid browser option ${browserType}.`); 388 | showHelp(); 389 | process.exit(-1); 390 | } 391 | 392 | function validatePermissions(permissions: unknown): string[] | undefined { 393 | if (permissions === undefined) { 394 | return undefined; 395 | } 396 | function isValidPermission(p: unknown): p is string { 397 | return typeof p === 'string'; 398 | } 399 | if (isValidPermission(permissions)) { 400 | return [permissions]; 401 | } 402 | if (Array.isArray(permissions) && permissions.every(isValidPermission)) { 403 | return permissions; 404 | } 405 | 406 | console.log(`Invalid permission: ${permissions}`); 407 | showHelp(); 408 | process.exit(-1); 409 | } 410 | 411 | function validateBrowserOptions(browserOptions: unknown): string[] | undefined { 412 | if (browserOptions === undefined) { 413 | return undefined; 414 | } 415 | function isValidOption(p: unknown): p is string { 416 | return typeof p === 'string'; 417 | } 418 | if (isValidOption(browserOptions)) { 419 | return [browserOptions]; 420 | } 421 | if (Array.isArray(browserOptions) && browserOptions.every(isValidOption)) { 422 | return browserOptions; 423 | } 424 | 425 | console.log(`Invalid browser option: ${browserOptions}`); 426 | showHelp(); 427 | process.exit(-1); 428 | } 429 | 430 | async function validateExtensionPaths(extensionPaths: unknown): Promise { 431 | if (extensionPaths === undefined) { 432 | return undefined; 433 | } 434 | if (!Array.isArray(extensionPaths)) { 435 | extensionPaths = [extensionPaths]; 436 | } 437 | if (Array.isArray(extensionPaths)) { 438 | const res: string[] = []; 439 | for (const extensionPath of extensionPaths) { 440 | if (typeof extensionPath === 'string') { 441 | res.push(await validatePath(extensionPath)); 442 | } else { 443 | break; 444 | } 445 | } 446 | return res; 447 | } 448 | 449 | console.log(`Invalid extensionPath`); 450 | showHelp(); 451 | process.exit(-1); 452 | } 453 | 454 | const EXTENSION_IDENTIFIER_PATTERN = /^([a-z0-9A-Z][a-z0-9-A-Z]*\.[a-z0-9A-Z][a-z0-9-A-Z]*)(@prerelease)?$/; 455 | 456 | async function validateExtensionIds(extensionIds: unknown): Promise { 457 | if (extensionIds === undefined) { 458 | return undefined; 459 | } 460 | if (!Array.isArray(extensionIds)) { 461 | extensionIds = [extensionIds]; 462 | } 463 | if (Array.isArray(extensionIds)) { 464 | const res: GalleryExtension[] = []; 465 | for (const extensionId of extensionIds) { 466 | const m = typeof extensionId === 'string' && extensionId.match(EXTENSION_IDENTIFIER_PATTERN); 467 | if (m) { 468 | if (m[2]) { 469 | res.push({ id: m[1], preRelease: true }); 470 | } else { 471 | res.push({ id: m[1] }); 472 | } 473 | } else { 474 | console.log(`Invalid extension id: ${extensionId}. Format is publisher.name[@prerelease].`); 475 | break; 476 | } 477 | } 478 | return res; 479 | } else { 480 | console.log(`Invalid extensionId`); 481 | } 482 | 483 | showHelp(); 484 | process.exit(-1); 485 | } 486 | 487 | async function validatePath(loc: string, isFile?: boolean): Promise { 488 | loc = path.resolve(loc); 489 | if (isFile) { 490 | if (!(await fileExists(loc))) { 491 | console.log(`'${loc}' must be an existing file.`); 492 | process.exit(-1); 493 | } 494 | } else { 495 | if (!(await directoryExists(loc))) { 496 | console.log(`'${loc}' must be an existing folder.`); 497 | process.exit(-1); 498 | } 499 | } 500 | return loc; 501 | } 502 | 503 | function validateQuality(quality: unknown, version: unknown, vsCodeDevPath: string | undefined): VSCodeQuality | undefined { 504 | if (version) { 505 | console.log(`--version has been replaced by --quality`); 506 | quality = quality || version; 507 | } 508 | 509 | if (vsCodeDevPath && quality) { 510 | console.log(`Sources folder is provided as input, quality is ignored.`); 511 | return undefined; 512 | } 513 | if (quality === undefined || (typeof quality === 'string' && ['insiders', 'stable'].includes(quality))) { 514 | return quality as VSCodeQuality; 515 | } 516 | if (version === 'sources') { 517 | console.log(`Instead of version=sources use 'sourcesPath' with the location of the VS Code repository.`); 518 | } else { 519 | console.log(`Invalid quality.`); 520 | } 521 | showHelp(); 522 | process.exit(-1); 523 | } 524 | 525 | function validateCommit(commit: unknown, vsCodeDevPath: string | undefined): string | undefined { 526 | 527 | if (vsCodeDevPath && commit) { 528 | console.log(`Sources folder is provided as input, commit is ignored.`); 529 | return undefined; 530 | } 531 | if (commit === undefined || (typeof commit === 'string' && commit.match(/^[0-9a-f]{40}$/))) { 532 | return commit; 533 | } else { 534 | console.log(`Invalid format for commit. Expected a 40 character long SHA1 hash.`); 535 | } 536 | showHelp(); 537 | process.exit(-1); 538 | } 539 | 540 | function validatePortNumber(port: unknown): number | undefined { 541 | if (typeof port === 'string') { 542 | const number = Number.parseInt(port); 543 | if (!Number.isNaN(number) && number >= 0) { 544 | return number; 545 | } 546 | } 547 | return undefined; 548 | } 549 | 550 | interface CommandLineOptions { 551 | browser?: string; 552 | browserOptions?: string; 553 | browserType?: string; 554 | extensionDevelopmentPath?: string; 555 | extensionTestsPath?: string; 556 | quality?: string; 557 | commit?: string; 558 | sourcesPath?: string; 559 | 'open-devtools'?: boolean; 560 | headless?: boolean; 561 | hideServerLog?: boolean; 562 | printServerLog?: boolean; 563 | permission?: string | string[]; 564 | 'folder-uri'?: string; 565 | extensionPath?: string | string[]; 566 | extensionId?: string | string[]; 567 | host?: string; 568 | port?: string; 569 | verbose?: boolean; 570 | coi?: boolean; 571 | esm?: boolean; 572 | help?: boolean; 573 | testRunnerDataDir?: string; 574 | } 575 | 576 | function showHelp() { 577 | console.log('Usage:'); 578 | console.log(` --browser 'chromium' | 'firefox' | 'webkit' | 'none': The browser to launch. [Optional, defaults to 'chromium']`); 579 | console.log(` --browserOption option: Command line argument to use when launching the browser instance. [Optional, Multiple]`) 580 | console.log(` --extensionDevelopmentPath path: A path pointing to an extension under development to include. [Optional]`); 581 | console.log(` --extensionTestsPath path: A path to a test module to run. [Optional]`); 582 | console.log(` --quality 'insiders' | 'stable' [Optional, default 'insiders', ignored when running from sources]`); 583 | console.log(` --commit commitHash [Optional, defaults to latest build version of the given quality, ignored when running from sources]`); 584 | console.log(` --sourcesPath path: If provided, running from VS Code sources at the given location. [Optional]`); 585 | console.log(` --open-devtools: If set, opens the dev tools. [Optional]`); 586 | console.log(` --headless: Whether to hide the browser. Defaults to true when an extensionTestsPath is provided, otherwise false. [Optional]`); 587 | console.log(` --permission: Permission granted in the opened browser: e.g. 'clipboard-read', 'clipboard-write'. [Optional, Multiple]`); 588 | console.log(` --coi: Enables cross origin isolation [Optional]`); 589 | console.log(` --esm: Serve the ESM variant of VS Code [Optional]`); 590 | console.log(` --folder-uri: workspace to open VS Code on. Ignored when folderPath is provided. [Optional]`); 591 | console.log(` --extensionPath: A path pointing to a folder containing additional extensions to include [Optional, Multiple]`); 592 | console.log(` --extensionId: The id of an extension include. The format is '\${publisher}.\${name}'. Append '@prerelease' to use a prerelease version [Optional, Multiple]`); 593 | console.log(` --host: The host name the server is opened on. [Optional, defaults to localhost]`); 594 | console.log(` --port: The port the server is opened on. [Optional, defaults to 3000]`); 595 | console.log(` --open-devtools: If set, opens the dev tools. [Optional]`); 596 | console.log(` --verbose: If set, prints out more information when running the server. [Optional]`); 597 | console.log(` --printServerLog: If set, prints the server access log. [Optional]`); 598 | console.log(` --testRunnerDataDir: If set, the temporary folder for storing the VS Code builds used for running the tests. [Optional, defaults to '$CURRENT_WORKING_DIR/.vscode-test-web']`); 599 | console.log(` folderPath. A local folder to open VS Code on. The folder content will be available as a virtual file system. [Optional]`); 600 | } 601 | 602 | async function cliMain(): Promise { 603 | process.on('unhandledRejection', (e: any) => { 604 | console.error('unhandledRejection', e); 605 | }); 606 | process.on('uncaughtException', (e: any) => { 607 | console.error('uncaughtException', e); 608 | }); 609 | 610 | const manifest = JSON.parse(await readFileInRepo('package.json')); 611 | console.log(`${manifest.name}: ${manifest.version}`); 612 | 613 | const options: minimist.Opts = { 614 | string: ['extensionDevelopmentPath', 'extensionTestsPath', 'browser', 'browserOption', 'browserType', 'quality', 'version', 'commit', 'waitForDebugger', 'folder-uri', 'permission', 'extensionPath', 'extensionId', 'sourcesPath', 'host', 'port', 'testRunnerDataDir'], 615 | boolean: ['open-devtools', 'headless', 'hideServerLog', 'printServerLog', 'help', 'verbose', 'coi', 'esm'], 616 | unknown: arg => { 617 | if (arg.startsWith('-')) { 618 | console.log(`Unknown argument ${arg}`); 619 | showHelp(); 620 | process.exit(); 621 | } 622 | return true; 623 | }, 624 | }; 625 | const args = minimist(process.argv.slice(2), options); 626 | if (args.help) { 627 | showHelp(); 628 | process.exit(); 629 | } 630 | 631 | const browserOptions = validateBrowserOptions(args.browserOption); 632 | const browserType = validateBrowserType(args); 633 | const extensionTestsPath = await validatePathOrUndefined(args, 'extensionTestsPath', true); 634 | const extensionDevelopmentPath = await validatePathOrUndefined(args, 'extensionDevelopmentPath'); 635 | const extensionPaths = await validateExtensionPaths(args.extensionPath); 636 | const extensionIds = await validateExtensionIds(args.extensionId); 637 | const vsCodeDevPath = await validatePathOrUndefined(args, 'sourcesPath'); 638 | const quality = validateQuality(args.quality, args.version, vsCodeDevPath); 639 | const commit = validateCommit(args.commit, vsCodeDevPath); 640 | const devTools = validateBooleanOrUndefined(args, 'open-devtools'); 641 | const headless = validateBooleanOrUndefined(args, 'headless'); 642 | const permissions = validatePermissions(args.permission); 643 | const printServerLog = validatePrintServerLog(args); 644 | const verbose = validateBooleanOrUndefined(args, 'verbose'); 645 | const port = validatePortNumber(args.port); 646 | const host = validateStringOrUndefined(args, 'host'); 647 | const coi = validateBooleanOrUndefined(args, 'coi'); 648 | const esm = validateBooleanOrUndefined(args, 'esm'); 649 | const testRunnerDataDir = validateStringOrUndefined(args, 'testRunnerDataDir'); 650 | 651 | const waitForDebugger = validatePortNumber(args.waitForDebugger); 652 | 653 | let folderUri = validateStringOrUndefined(args, 'folder-uri'); 654 | let folderPath: string | undefined; 655 | 656 | const inputs = args._; 657 | if (inputs.length) { 658 | const input = await validatePath(inputs[0]); 659 | if (input) { 660 | folderPath = input; 661 | if (folderUri) { 662 | console.log(`Local folder provided as input, ignoring 'folder-uri'`); 663 | folderUri = undefined; 664 | } 665 | } 666 | } 667 | 668 | if (extensionTestsPath) { 669 | try { 670 | await runTests({ 671 | extensionTestsPath, 672 | extensionDevelopmentPath, 673 | browserOptions, 674 | browserType, 675 | quality, 676 | commit, 677 | devTools, 678 | waitForDebugger, 679 | folderUri, 680 | folderPath, 681 | headless, 682 | printServerLog, 683 | permissions, 684 | extensionPaths, 685 | extensionIds, 686 | vsCodeDevPath, 687 | verbose, 688 | esm, 689 | coi, 690 | host, 691 | port, 692 | testRunnerDataDir, 693 | }); 694 | } catch (e) { 695 | console.log('Error running tests:', e); 696 | process.exit(1); 697 | } 698 | } else { 699 | try { 700 | await open({ 701 | extensionDevelopmentPath, 702 | browserOptions, 703 | browserType, 704 | quality, 705 | commit, 706 | devTools, 707 | waitForDebugger, 708 | folderUri, 709 | folderPath, 710 | headless, 711 | printServerLog, 712 | permissions, 713 | extensionPaths, 714 | extensionIds, 715 | vsCodeDevPath, 716 | verbose, 717 | esm, 718 | coi, 719 | host, 720 | port, 721 | testRunnerDataDir, 722 | }); 723 | } catch (e) { 724 | console.log('Error opening browser:', e); 725 | process.exit(1); 726 | } 727 | } 728 | } 729 | 730 | if (require.main === module) { 731 | cliMain(); 732 | } 733 | -------------------------------------------------------------------------------- /src/browser/workbench.api.d.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | interface UriComponents { 6 | scheme?: string; 7 | authority?: string; 8 | path?: string; 9 | query?: string; 10 | fragment?: string; 11 | } 12 | 13 | declare class URI implements UriComponents { 14 | /** 15 | * Create an URI from a string, e.g. `http://www.example.com/some/path`, 16 | * `file:///usr/home`, or `scheme:with/path`. 17 | * 18 | * *Note* that for a while uris without a `scheme` were accepted. That is not correct 19 | * as all uris should have a scheme. To avoid breakage of existing code the optional 20 | * `strict`-argument has been added. We *strongly* advise to use it, e.g. `Uri.parse('my:uri', true)` 21 | * 22 | * @see {@link Uri.toString} 23 | * @param value The string value of an Uri. 24 | * @param strict Throw an error when `value` is empty or when no `scheme` can be parsed. 25 | * @return A new Uri instance. 26 | */ 27 | static parse(value: string, strict?: boolean): URI; 28 | 29 | /** 30 | * Create an URI from a file system path. The {@link URI.scheme scheme} 31 | * will be `file`. 32 | * 33 | * The *difference* between {@link URI.parse} and {@link URI.file} is that the latter treats the argument 34 | * as path, not as stringified-uri. E.g. `Uri.file(path)` is *not* the same as 35 | * `Uri.parse('file://' + path)` because the path might contain characters that are 36 | * interpreted (# and ?). See the following sample: 37 | * ```ts 38 | * const good = URI.file('/coding/c#/project1'); 39 | * good.scheme === 'file'; 40 | * good.path === '/coding/c#/project1'; 41 | * good.fragment === ''; 42 | * 43 | * const bad = URI.parse('file://' + '/coding/c#/project1'); 44 | * bad.scheme === 'file'; 45 | * bad.path === '/coding/c'; // path is now broken 46 | * bad.fragment === '/project1'; 47 | * ``` 48 | * 49 | * @param path A file system or UNC path. 50 | * @return A new Uri instance. 51 | */ 52 | static file(path: string): URI; 53 | 54 | /** 55 | * Create a new uri which path is the result of joining 56 | * the path of the base uri with the provided path segments. 57 | * 58 | * - Note 1: `joinPath` only affects the path component 59 | * and all other components (scheme, authority, query, and fragment) are 60 | * left as they are. 61 | * - Note 2: The base uri must have a path; an error is thrown otherwise. 62 | * 63 | * The path segments are normalized in the following ways: 64 | * - sequences of path separators (`/` or `\`) are replaced with a single separator 65 | * - for `file`-uris on windows, the backslash-character (`\`) is considered a path-separator 66 | * - the `..`-segment denotes the parent segment, the `.` denotes the current segment 67 | * - paths have a root which always remains, for instance on windows drive-letters are roots 68 | * so that is true: `joinPath(Uri.file('file:///c:/root'), '../../other').fsPath === 'c:/other'` 69 | * 70 | * @param base An uri. Must have a path. 71 | * @param pathSegments One more more path fragments 72 | * @returns A new uri which path is joined with the given fragments 73 | */ 74 | static joinPath(base: URI, ...pathSegments: string[]): URI; 75 | 76 | /** 77 | * Create an URI from its component parts 78 | * 79 | * @see {@link Uri.toString} 80 | * @param components The component parts of an Uri. 81 | * @return A new Uri instance. 82 | */ 83 | static from(components: { 84 | readonly scheme: string; 85 | readonly authority?: string; 86 | readonly path?: string; 87 | readonly query?: string; 88 | readonly fragment?: string; 89 | }): URI; 90 | 91 | /** 92 | * Use the `file` and `parse` factory functions to create new `Uri` objects. 93 | */ 94 | private constructor(scheme: string, authority: string, path: string, query: string, fragment: string); 95 | 96 | /** 97 | * Scheme is the `http` part of `http://www.example.com/some/path?query#fragment`. 98 | * The part before the first colon. 99 | */ 100 | readonly scheme: string; 101 | 102 | /** 103 | * Authority is the `www.example.com` part of `http://www.example.com/some/path?query#fragment`. 104 | * The part between the first double slashes and the next slash. 105 | */ 106 | readonly authority: string; 107 | 108 | /** 109 | * Path is the `/some/path` part of `http://www.example.com/some/path?query#fragment`. 110 | */ 111 | readonly path: string; 112 | 113 | /** 114 | * Query is the `query` part of `http://www.example.com/some/path?query#fragment`. 115 | */ 116 | readonly query: string; 117 | 118 | /** 119 | * Fragment is the `fragment` part of `http://www.example.com/some/path?query#fragment`. 120 | */ 121 | readonly fragment: string; 122 | 123 | /** 124 | * The string representing the corresponding file system path of this Uri. 125 | * 126 | * Will handle UNC paths and normalize windows drive letters to lower-case. Also 127 | * uses the platform specific path separator. 128 | * 129 | * * Will *not* validate the path for invalid characters and semantics. 130 | * * Will *not* look at the scheme of this Uri. 131 | * * The resulting string shall *not* be used for display purposes but 132 | * for disk operations, like `readFile` et al. 133 | * 134 | * The *difference* to the {@linkcode Uri.path path}-property is the use of the platform specific 135 | * path separator and the handling of UNC paths. The sample below outlines the difference: 136 | * ```ts 137 | * const u = URI.parse('file://server/c$/folder/file.txt') 138 | * u.authority === 'server' 139 | * u.path === '/shares/c$/file.txt' 140 | * u.fsPath === '\\server\c$\folder\file.txt' 141 | * ``` 142 | */ 143 | readonly fsPath: string; 144 | 145 | /** 146 | * Derive a new Uri from this Uri. 147 | * 148 | * ```ts 149 | * let file = Uri.parse('before:some/file/path'); 150 | * let other = file.with({ scheme: 'after' }); 151 | * assert.ok(other.toString() === 'after:some/file/path'); 152 | * ``` 153 | * 154 | * @param change An object that describes a change to this Uri. To unset components use `null` or 155 | * the empty string. 156 | * @return A new Uri that reflects the given change. Will return `this` Uri if the change 157 | * is not changing anything. 158 | */ 159 | with(change: { scheme?: string; authority?: string; path?: string; query?: string; fragment?: string }): URI; 160 | 161 | /** 162 | * Returns a string representation of this Uri. The representation and normalization 163 | * of a URI depends on the scheme. 164 | * 165 | * * The resulting string can be safely used with {@link Uri.parse}. 166 | * * The resulting string shall *not* be used for display purposes. 167 | * 168 | * *Note* that the implementation will encode _aggressive_ which often leads to unexpected, 169 | * but not incorrect, results. For instance, colons are encoded to `%3A` which might be unexpected 170 | * in file-uri. Also `&` and `=` will be encoded which might be unexpected for http-uris. For stability 171 | * reasons this cannot be changed anymore. If you suffer from too aggressive encoding you should use 172 | * the `skipEncoding`-argument: `uri.toString(true)`. 173 | * 174 | * @param skipEncoding Do not percentage-encode the result, defaults to `false`. Note that 175 | * the `#` and `?` characters occurring in the path will always be encoded. 176 | * @returns A string representation of this Uri. 177 | */ 178 | toString(skipEncoding?: boolean): string; 179 | 180 | /** 181 | * Returns a JSON representation of this Uri. 182 | * 183 | * @return An object. 184 | */ 185 | toJSON(): any; 186 | 187 | static revive(data: UriComponents | URI): URI; 188 | static revive(data: UriComponents | URI | undefined): URI | undefined; 189 | static revive(data: UriComponents | URI | null): URI | null; 190 | static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; 191 | static revive(data: UriComponents | URI | undefined | null): URI | undefined | null; 192 | } 193 | 194 | interface IAction extends IDisposable { 195 | readonly id: string; 196 | label: string; 197 | tooltip: string; 198 | class: string | undefined; 199 | enabled: boolean; 200 | checked?: boolean; 201 | run(event?: unknown): unknown; 202 | } 203 | 204 | interface TunnelPrivacy { 205 | themeIcon: string; 206 | id: string; 207 | label: string; 208 | } 209 | 210 | interface TunnelProviderFeatures { 211 | elevation: boolean; 212 | /** 213 | * @deprecated 214 | */ 215 | public?: boolean; 216 | privacyOptions: TunnelPrivacy[]; 217 | } 218 | 219 | interface IDisposable { 220 | dispose(): void; 221 | } 222 | 223 | declare abstract class Disposable implements IDisposable { 224 | static readonly None: IDisposable; 225 | constructor(); 226 | dispose(): void; 227 | } 228 | 229 | /** 230 | * To an event a function with one or zero parameters 231 | * can be subscribed. The event is the subscriber function itself. 232 | */ 233 | interface Event { 234 | (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable; 235 | } 236 | 237 | interface EmitterOptions { 238 | onFirstListenerAdd?: Function; 239 | onFirstListenerDidAdd?: Function; 240 | onListenerDidAdd?: Function; 241 | onLastListenerRemove?: Function; 242 | } 243 | 244 | declare class Emitter { 245 | constructor(options?: EmitterOptions); 246 | readonly event: Event; 247 | fire(event: T): void; 248 | dispose(): void; 249 | } 250 | 251 | interface IWebSocket { 252 | readonly onData: Event; 253 | readonly onOpen: Event; 254 | readonly onClose: Event; 255 | readonly onError: Event; 256 | 257 | send(data: ArrayBuffer | ArrayBufferView): void; 258 | close(): void; 259 | } 260 | 261 | interface IWebSocketFactory { 262 | create(url: string): IWebSocket; 263 | } 264 | 265 | /** 266 | * A workspace to open in the workbench can either be: 267 | * - a workspace file with 0-N folders (via `workspaceUri`) 268 | * - a single folder (via `folderUri`) 269 | * - empty (via `undefined`) 270 | */ 271 | type IWorkspace = { workspaceUri: URI } | { folderUri: URI } | undefined; 272 | 273 | interface IWorkspaceProvider { 274 | /** 275 | * The initial workspace to open. 276 | */ 277 | readonly workspace: IWorkspace; 278 | 279 | /** 280 | * Arbitrary payload from the `IWorkspaceProvider.open` call. 281 | */ 282 | readonly payload?: object; 283 | 284 | /** 285 | * Return `true` if the provided [workspace](#IWorkspaceProvider.workspace) is trusted, `false` if not trusted, `undefined` if unknown. 286 | */ 287 | readonly trusted: boolean | undefined; 288 | 289 | /** 290 | * Asks to open a workspace in the current or a new window. 291 | * 292 | * @param workspace the workspace to open. 293 | * @param options optional options for the workspace to open. 294 | * - `reuse`: whether to open inside the current window or a new window 295 | * - `payload`: arbitrary payload that should be made available 296 | * to the opening window via the `IWorkspaceProvider.payload` property. 297 | * @param payload optional payload to send to the workspace to open. 298 | */ 299 | open(workspace: IWorkspace, options?: { reuse?: boolean; payload?: object }): Promise; 300 | } 301 | 302 | interface ISecretStorageProvider { 303 | type: 'in-memory' | 'persisted' | 'unknown'; 304 | get(key: string): Promise; 305 | set(key: string, value: string): Promise; 306 | delete(key: string): Promise; 307 | } 308 | 309 | interface IURLCallbackProvider { 310 | /** 311 | * Indicates that a Uri has been opened outside of VSCode. The Uri 312 | * will be forwarded to all installed Uri handlers in the system. 313 | */ 314 | readonly onCallback: Event; 315 | 316 | /** 317 | * Creates a Uri that - if opened in a browser - must result in 318 | * the `onCallback` to fire. 319 | * 320 | * The optional `Partial` must be properly restored for 321 | * the Uri passed to the `onCallback` handler. 322 | * 323 | * For example: if a Uri is to be created with `scheme:"vscode"`, 324 | * `authority:"foo"` and `path:"bar"` the `onCallback` should fire 325 | * with a Uri `vscode://foo/bar`. 326 | * 327 | * If there are additional `query` values in the Uri, they should 328 | * be added to the list of provided `query` arguments from the 329 | * `Partial`. 330 | */ 331 | create(options?: Partial): URI; 332 | } 333 | 334 | interface IUpdate { 335 | version: string; 336 | } 337 | 338 | interface IUpdateProvider { 339 | /** 340 | * Should return with the `IUpdate` object if an update is 341 | * available or `null` otherwise to signal that there are 342 | * no updates. 343 | */ 344 | checkForUpdate(): Promise; 345 | } 346 | 347 | declare const enum LogLevel { 348 | Trace, 349 | Debug, 350 | Info, 351 | Warning, 352 | Error, 353 | Critical, 354 | Off, 355 | } 356 | 357 | interface IResourceUriProvider { 358 | (uri: URI): URI; 359 | } 360 | 361 | /** 362 | * The identifier of an extension in the format: `PUBLISHER.NAME`. 363 | * For example: `vscode.csharp` 364 | */ 365 | type ExtensionId = string; 366 | 367 | export type MarketplaceExtension = ExtensionId | { readonly id: ExtensionId; preRelease?: boolean }; 368 | 369 | interface ICommonTelemetryPropertiesResolver { 370 | (): { [key: string]: any }; 371 | } 372 | interface IExternalUriResolver { 373 | (uri: URI): Promise; 374 | } 375 | 376 | /** 377 | * External URL opener 378 | */ 379 | interface IExternalURLOpener { 380 | /** 381 | * Overrides the behavior when an external URL is about to be opened. 382 | * Returning false means that the URL wasn't handled, and the default 383 | * handling behavior should be used: `window.open(href, '_blank', 'noopener');` 384 | * 385 | * @returns true if URL was handled, false otherwise. 386 | */ 387 | openExternal(href: string): boolean | Promise; 388 | } 389 | 390 | interface ITunnelProvider { 391 | /** 392 | * Support for creating tunnels. 393 | */ 394 | tunnelFactory?: ITunnelFactory; 395 | 396 | /** 397 | * Support for filtering candidate ports. 398 | */ 399 | showPortCandidate?: IShowPortCandidate; 400 | 401 | /** 402 | * The features that the tunnel provider supports. 403 | */ 404 | features?: TunnelProviderFeatures; 405 | } 406 | 407 | interface ITunnelFactory { 408 | (tunnelOptions: ITunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise | undefined; 409 | } 410 | 411 | interface ITunnelOptions { 412 | remoteAddress: { port: number; host: string }; 413 | 414 | /** 415 | * The desired local port. If this port can't be used, then another will be chosen. 416 | */ 417 | localAddressPort?: number; 418 | 419 | label?: string; 420 | 421 | /** 422 | * @deprecated Use privacy instead 423 | */ 424 | public?: boolean; 425 | 426 | privacy?: string; 427 | 428 | protocol?: string; 429 | } 430 | 431 | interface TunnelCreationOptions { 432 | /** 433 | * True when the local operating system will require elevation to use the requested local port. 434 | */ 435 | elevationRequired?: boolean; 436 | } 437 | 438 | interface ITunnel { 439 | remoteAddress: { port: number; host: string }; 440 | 441 | /** 442 | * The complete local address(ex. localhost:1234) 443 | */ 444 | localAddress: string; 445 | 446 | /** 447 | * @deprecated Use privacy instead 448 | */ 449 | public?: boolean; 450 | 451 | privacy?: string; 452 | 453 | /** 454 | * If protocol is not provided, it is assumed to be http, regardless of the localAddress 455 | */ 456 | protocol?: string; 457 | 458 | /** 459 | * Implementers of Tunnel should fire onDidDispose when dispose is called. 460 | */ 461 | onDidDispose: Event; 462 | 463 | dispose(): Promise | void; 464 | } 465 | 466 | interface IShowPortCandidate { 467 | (host: string, port: number, detail: string): Promise; 468 | } 469 | 470 | declare const enum Menu { 471 | CommandPalette, 472 | StatusBarWindowIndicatorMenu, 473 | } 474 | 475 | interface ICommand { 476 | /** 477 | * An identifier for the command. Commands can be executed from extensions 478 | * using the `vscode.commands.executeCommand` API using that command ID. 479 | */ 480 | id: string; 481 | 482 | /** 483 | * The optional label of the command. If provided, the command will appear 484 | * in the command palette. 485 | */ 486 | label?: string; 487 | 488 | /** 489 | * The optional menus to append this command to. Only valid if `label` is 490 | * provided as well. 491 | * @default Menu.CommandPalette 492 | */ 493 | menu?: Menu | Menu[]; 494 | 495 | /** 496 | * A function that is being executed with any arguments passed over. The 497 | * return type will be send back to the caller. 498 | * 499 | * Note: arguments and return type should be serializable so that they can 500 | * be exchanged across processes boundaries. 501 | */ 502 | handler: (...args: any[]) => unknown; 503 | } 504 | 505 | interface IHomeIndicator { 506 | /** 507 | * The link to open when clicking the home indicator. 508 | */ 509 | href: string; 510 | 511 | /** 512 | * The icon name for the home indicator. This needs to be one of the existing 513 | * icons from our Codicon icon set. For example `sync`. 514 | */ 515 | icon: string; 516 | 517 | /** 518 | * A tooltip that will appear while hovering over the home indicator. 519 | */ 520 | title: string; 521 | } 522 | 523 | interface IWelcomeBanner { 524 | /** 525 | * Welcome banner message to appear as text. 526 | */ 527 | message: string; 528 | 529 | /** 530 | * Optional icon for the banner. This needs to be one of the existing 531 | * icons from our Codicon icon set. For example `code`. If not provided, 532 | * a default icon will be used. 533 | */ 534 | icon?: string; 535 | 536 | /** 537 | * Optional actions to appear as links after the welcome banner message. 538 | */ 539 | actions?: IWelcomeLinkAction[]; 540 | } 541 | 542 | interface IWelcomeLinkAction { 543 | /** 544 | * The link to open when clicking. Supports command invocation when 545 | * using the `command:` value. 546 | */ 547 | href: string; 548 | 549 | /** 550 | * The label to show for the action link. 551 | */ 552 | label: string; 553 | 554 | /** 555 | * A tooltip that will appear while hovering over the action link. 556 | */ 557 | title?: string; 558 | } 559 | 560 | interface IWindowIndicator { 561 | /** 562 | * Triggering this event will cause the window indicator to update. 563 | */ 564 | onDidChange: Event; 565 | 566 | /** 567 | * Label of the window indicator may include octicons 568 | * e.g. `$(remote) label` 569 | */ 570 | label: string; 571 | 572 | /** 573 | * Tooltip of the window indicator should not include 574 | * octicons and be descriptive. 575 | */ 576 | tooltip: string; 577 | 578 | /** 579 | * If provided, overrides the default command that 580 | * is executed when clicking on the window indicator. 581 | */ 582 | command?: string; 583 | } 584 | 585 | declare enum ColorScheme { 586 | DARK = 'dark', 587 | LIGHT = 'light', 588 | HIGH_CONTRAST_LIGHT = 'hcLight', 589 | HIGH_CONTRAST_DARK = 'hcDark', 590 | } 591 | 592 | interface IInitialColorTheme { 593 | /** 594 | * Initial color theme type. 595 | */ 596 | themeType: ColorScheme; 597 | 598 | /** 599 | * A list of workbench colors to apply initially. 600 | */ 601 | colors?: { [colorId: string]: string }; 602 | } 603 | 604 | interface IWelcomeDialog { 605 | 606 | /** 607 | * Unique identifier of the welcome dialog. The identifier will be used to determine 608 | * if the dialog has been previously displayed. 609 | */ 610 | id: string; 611 | 612 | /** 613 | * Title of the welcome dialog. 614 | */ 615 | title: string; 616 | 617 | /** 618 | * Button text of the welcome dialog. 619 | */ 620 | buttonText: string; 621 | 622 | /** 623 | * Button command to execute from the welcome dialog. 624 | */ 625 | buttonCommand: string; 626 | 627 | /** 628 | * Message text for the welcome dialog. 629 | */ 630 | message: string; 631 | 632 | /** 633 | * Media to include in the welcome dialog. 634 | */ 635 | media: { altText: string; path: string }; 636 | } 637 | 638 | interface IDevelopmentOptions { 639 | /** 640 | * Current logging level. Default is `LogLevel.Info`. 641 | */ 642 | readonly logLevel?: LogLevel; 643 | 644 | /** 645 | * Location of a module containing extension tests to run once the workbench is open. 646 | */ 647 | readonly extensionTestsPath?: UriComponents; 648 | 649 | /** 650 | * Add extensions under development. 651 | */ 652 | readonly extensions?: readonly UriComponents[]; 653 | 654 | /** 655 | * Whether to enable the smoke test driver. 656 | */ 657 | readonly enableSmokeTestDriver?: boolean; 658 | } 659 | 660 | interface IDefaultView { 661 | /** 662 | * The identifier of the view to show by default. 663 | */ 664 | readonly id: string; 665 | } 666 | 667 | declare enum EditorActivation { 668 | /** 669 | * Activate the editor after it opened. This will automatically restore 670 | * the editor if it is minimized. 671 | */ 672 | ACTIVATE = 1, 673 | 674 | /** 675 | * Only restore the editor if it is minimized but do not activate it. 676 | * 677 | * Note: will only work in combination with the `preserveFocus: true` option. 678 | * Otherwise, if focus moves into the editor, it will activate and restore 679 | * automatically. 680 | */ 681 | RESTORE, 682 | 683 | /** 684 | * Preserve the current active editor. 685 | * 686 | * Note: will only work in combination with the `preserveFocus: true` option. 687 | * Otherwise, if focus moves into the editor, it will activate and restore 688 | * automatically. 689 | */ 690 | PRESERVE, 691 | } 692 | 693 | declare enum EditorResolution { 694 | /** 695 | * Displays a picker and allows the user to decide which editor to use. 696 | */ 697 | PICK, 698 | 699 | /** 700 | * Disables editor resolving. 701 | */ 702 | DISABLED, 703 | 704 | /** 705 | * Only exclusive editors are considered. 706 | */ 707 | EXCLUSIVE_ONLY, 708 | } 709 | 710 | declare enum EditorOpenSource { 711 | /** 712 | * Default: the editor is opening via a programmatic call 713 | * to the editor service API. 714 | */ 715 | API, 716 | 717 | /** 718 | * Indicates that a user action triggered the opening, e.g. 719 | * via mouse or keyboard use. 720 | */ 721 | USER, 722 | } 723 | 724 | interface IEditorOptions { 725 | /** 726 | * Tells the editor to not receive keyboard focus when the editor is being opened. 727 | * 728 | * Will also not activate the group the editor opens in unless the group is already 729 | * the active one. This behaviour can be overridden via the `activation` option. 730 | */ 731 | preserveFocus?: boolean; 732 | 733 | /** 734 | * This option is only relevant if an editor is opened into a group that is not active 735 | * already and allows to control if the inactive group should become active, restored 736 | * or preserved. 737 | * 738 | * By default, the editor group will become active unless `preserveFocus` or `inactive` 739 | * is specified. 740 | */ 741 | activation?: EditorActivation; 742 | 743 | /** 744 | * Tells the editor to reload the editor input in the editor even if it is identical to the one 745 | * already showing. By default, the editor will not reload the input if it is identical to the 746 | * one showing. 747 | */ 748 | forceReload?: boolean; 749 | 750 | /** 751 | * Will reveal the editor if it is already opened and visible in any of the opened editor groups. 752 | * 753 | * Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly 754 | * to the side of another one or into a specific editor group. 755 | */ 756 | revealIfVisible?: boolean; 757 | 758 | /** 759 | * Will reveal the editor if it is already opened (even when not visible) in any of the opened editor groups. 760 | * 761 | * Note that this option is just a hint that might be ignored if the user wants to open an editor explicitly 762 | * to the side of another one or into a specific editor group. 763 | */ 764 | revealIfOpened?: boolean; 765 | 766 | /** 767 | * An editor that is pinned remains in the editor stack even when another editor is being opened. 768 | * An editor that is not pinned will always get replaced by another editor that is not pinned. 769 | */ 770 | pinned?: boolean; 771 | 772 | /** 773 | * An editor that is sticky moves to the beginning of the editors list within the group and will remain 774 | * there unless explicitly closed. Operations such as "Close All" will not close sticky editors. 775 | */ 776 | sticky?: boolean; 777 | 778 | /** 779 | * The index in the document stack where to insert the editor into when opening. 780 | */ 781 | index?: number; 782 | 783 | /** 784 | * An active editor that is opened will show its contents directly. Set to true to open an editor 785 | * in the background without loading its contents. 786 | * 787 | * Will also not activate the group the editor opens in unless the group is already 788 | * the active one. This behaviour can be overridden via the `activation` option. 789 | */ 790 | inactive?: boolean; 791 | 792 | /** 793 | * Will not show an error in case opening the editor fails and thus allows to show a custom error 794 | * message as needed. By default, an error will be presented as notification if opening was not possible. 795 | */ 796 | 797 | /** 798 | * In case of an error opening the editor, will not present this error to the user (e.g. by showing 799 | * a generic placeholder in the editor area). So it is up to the caller to provide error information 800 | * in that case. 801 | * 802 | * By default, an error when opening an editor will result in a placeholder editor that shows the error. 803 | * In certain cases a modal dialog may be presented to ask the user for further action. 804 | */ 805 | ignoreError?: boolean; 806 | 807 | /** 808 | * Allows to override the editor that should be used to display the input: 809 | * - `undefined`: let the editor decide for itself 810 | * - `string`: specific override by id 811 | * - `EditorResolution`: specific override handling 812 | */ 813 | override?: string | EditorResolution; 814 | 815 | /** 816 | * A optional hint to signal in which context the editor opens. 817 | * 818 | * If configured to be `EditorOpenSource.USER`, this hint can be 819 | * used in various places to control the experience. For example, 820 | * if the editor to open fails with an error, a notification could 821 | * inform about this in a modal dialog. If the editor opened through 822 | * some background task, the notification would show in the background, 823 | * not as a modal dialog. 824 | */ 825 | source?: EditorOpenSource; 826 | 827 | /** 828 | * An optional property to signal that certain view state should be 829 | * applied when opening the editor. 830 | */ 831 | viewState?: object; 832 | } 833 | 834 | interface ITextEditorSelection { 835 | readonly startLineNumber: number; 836 | readonly startColumn: number; 837 | readonly endLineNumber?: number; 838 | readonly endColumn?: number; 839 | } 840 | 841 | declare const enum TextEditorSelectionRevealType { 842 | /** 843 | * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically. 844 | */ 845 | Center = 0, 846 | 847 | /** 848 | * Option to scroll vertically or horizontally as necessary and reveal a range centered vertically only if it lies outside the viewport. 849 | */ 850 | CenterIfOutsideViewport = 1, 851 | 852 | /** 853 | * Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top. 854 | */ 855 | NearTop = 2, 856 | 857 | /** 858 | * Option to scroll vertically or horizontally as necessary and reveal a range close to the top of the viewport, but not quite at the top. 859 | * Only if it lies outside the viewport 860 | */ 861 | NearTopIfOutsideViewport = 3, 862 | } 863 | 864 | declare const enum TextEditorSelectionSource { 865 | /** 866 | * Programmatic source indicates a selection change that 867 | * was not triggered by the user via keyboard or mouse 868 | * but through text editor APIs. 869 | */ 870 | PROGRAMMATIC = 'api', 871 | 872 | /** 873 | * Navigation source indicates a selection change that 874 | * was caused via some command or UI component such as 875 | * an outline tree. 876 | */ 877 | NAVIGATION = 'code.navigation', 878 | 879 | /** 880 | * Jump source indicates a selection change that 881 | * was caused from within the text editor to another 882 | * location in the same or different text editor such 883 | * as "Go to definition". 884 | */ 885 | JUMP = 'code.jump', 886 | } 887 | 888 | interface ITextEditorOptions extends IEditorOptions { 889 | /** 890 | * Text editor selection. 891 | */ 892 | selection?: ITextEditorSelection; 893 | 894 | /** 895 | * Option to control the text editor selection reveal type. 896 | * Defaults to TextEditorSelectionRevealType.Center 897 | */ 898 | selectionRevealType?: TextEditorSelectionRevealType; 899 | 900 | /** 901 | * Source of the call that caused the selection. 902 | */ 903 | selectionSource?: TextEditorSelectionSource | string; 904 | } 905 | 906 | interface IDefaultEditor { 907 | /** 908 | * The location of the editor in the editor grid layout. 909 | * Editors are layed out in editor groups and the view 910 | * column is counted from top left to bottom right in 911 | * the order of appearance beginning with `1`. 912 | * 913 | * If not provided, the editor will open in the active 914 | * group. 915 | */ 916 | readonly viewColumn?: number; 917 | 918 | /** 919 | * The resource of the editor to open. 920 | */ 921 | readonly uri: UriComponents; 922 | 923 | /** 924 | * Optional extra options like which editor 925 | * to use or which text to select. 926 | */ 927 | readonly options?: ITextEditorOptions; 928 | 929 | /** 930 | * Will not open an untitled editor in case 931 | * the resource does not exist. 932 | */ 933 | readonly openOnlyIfExists?: boolean; 934 | } 935 | 936 | declare const enum GroupOrientation { 937 | HORIZONTAL, 938 | VERTICAL, 939 | } 940 | 941 | interface GroupLayoutArgument { 942 | /** 943 | * Only applies when there are multiple groups 944 | * arranged next to each other in a row or column. 945 | * If provided, their sum must be 1 to be applied 946 | * per row or column. 947 | */ 948 | size?: number; 949 | 950 | /** 951 | * Editor groups will be laid out orthogonal to the 952 | * parent orientation. 953 | */ 954 | groups?: GroupLayoutArgument[]; 955 | } 956 | 957 | interface EditorGroupLayout { 958 | /** 959 | * The initial orientation of the editor groups at the root. 960 | */ 961 | orientation: GroupOrientation; 962 | 963 | /** 964 | * The editor groups at the root of the layout. 965 | */ 966 | groups: GroupLayoutArgument[]; 967 | } 968 | 969 | interface IDefaultLayout { 970 | /** 971 | * A list of views to show by default. 972 | */ 973 | readonly views?: IDefaultView[]; 974 | 975 | /** 976 | * A list of editors to show by default. 977 | */ 978 | readonly editors?: IDefaultEditor[]; 979 | 980 | /** 981 | * The layout to use for the workbench. 982 | */ 983 | readonly layout?: { 984 | /** 985 | * The layout of the editor area. 986 | */ 987 | readonly editors?: EditorGroupLayout; 988 | }; 989 | 990 | /** 991 | * Forces this layout to be applied even if this isn't 992 | * the first time the workspace has been opened 993 | */ 994 | readonly force?: boolean; 995 | } 996 | 997 | interface IProductQualityChangeHandler { 998 | /** 999 | * Handler is being called when the user wants to switch between 1000 | * `insider` or `stable` product qualities. 1001 | */ 1002 | (newQuality: 'insider' | 'stable'): void; 1003 | } 1004 | 1005 | /** 1006 | * Settings sync options 1007 | */ 1008 | interface ISettingsSyncOptions { 1009 | /** 1010 | * Is settings sync enabled 1011 | */ 1012 | readonly enabled: boolean; 1013 | 1014 | /** 1015 | * Version of extensions sync state. 1016 | * Extensions sync state will be reset if version is provided and different from previous version. 1017 | */ 1018 | readonly extensionsSyncStateVersion?: string; 1019 | 1020 | /** 1021 | * Handler is being called when the user changes Settings Sync enablement. 1022 | */ 1023 | enablementHandler?(enablement: boolean): void; 1024 | } 1025 | 1026 | interface IWorkbenchConstructionOptions { 1027 | //#region Connection related configuration 1028 | 1029 | /** 1030 | * The remote authority is the IP:PORT from where the workbench is served 1031 | * from. It is for example being used for the websocket connections as address. 1032 | */ 1033 | readonly remoteAuthority?: string; 1034 | 1035 | /** 1036 | * The connection token to send to the server. 1037 | */ 1038 | readonly connectionToken?: string | Promise; 1039 | 1040 | /** 1041 | * An endpoint to serve iframe content ("webview") from. This is required 1042 | * to provide full security isolation from the workbench host. 1043 | */ 1044 | readonly webviewEndpoint?: string; 1045 | 1046 | /** 1047 | * A factory for web sockets. 1048 | */ 1049 | readonly webSocketFactory?: IWebSocketFactory; 1050 | 1051 | /** 1052 | * A provider for resource URIs. 1053 | */ 1054 | readonly resourceUriProvider?: IResourceUriProvider; 1055 | 1056 | /** 1057 | * Resolves an external uri before it is opened. 1058 | */ 1059 | readonly resolveExternalUri?: IExternalUriResolver; 1060 | 1061 | /** 1062 | * A provider for supplying tunneling functionality, 1063 | * such as creating tunnels and showing candidate ports to forward. 1064 | */ 1065 | readonly tunnelProvider?: ITunnelProvider; 1066 | 1067 | /** 1068 | * Endpoints to be used for proxying authentication code exchange calls in the browser. 1069 | */ 1070 | readonly codeExchangeProxyEndpoints?: { [providerId: string]: string }; 1071 | 1072 | /** 1073 | * The identifier of an edit session associated with the current workspace. 1074 | */ 1075 | readonly editSessionId?: string; 1076 | 1077 | /** 1078 | * Resource delegation handler that allows for loading of resources when 1079 | * using remote resolvers. 1080 | * 1081 | * This is exclusive with {@link resourceUriProvider}. `resourceUriProvider` 1082 | * should be used if a {@link webSocketFactory} is used, and will be preferred. 1083 | */ 1084 | readonly remoteResourceProvider?: IRemoteResourceProvider; 1085 | 1086 | /** 1087 | * [TEMPORARY]: This will be removed soon. 1088 | * Endpoints to be used for proxying repository tarball download calls in the browser. 1089 | */ 1090 | readonly _tarballProxyEndpoints?: { [providerId: string]: string }; 1091 | 1092 | //#endregion 1093 | 1094 | //#region Workbench configuration 1095 | 1096 | /** 1097 | * A handler for opening workspaces and providing the initial workspace. 1098 | */ 1099 | readonly workspaceProvider?: IWorkspaceProvider; 1100 | 1101 | /** 1102 | * Settings sync options 1103 | */ 1104 | readonly settingsSyncOptions?: ISettingsSyncOptions; 1105 | 1106 | /** 1107 | * The secret storage provider to store and retrieve secrets. 1108 | */ 1109 | readonly secretStorageProvider?: ISecretStorageProvider; 1110 | 1111 | /** 1112 | * Additional builtin extensions those cannot be uninstalled but only be disabled. 1113 | * It can be one of the following: 1114 | * - an extension in the Marketplace 1115 | * - location of the extension where it is hosted. 1116 | */ 1117 | readonly additionalBuiltinExtensions?: readonly (MarketplaceExtension | UriComponents)[]; 1118 | 1119 | /** 1120 | * List of extensions to be enabled if they are installed. 1121 | * Note: This will not install extensions if not installed. 1122 | */ 1123 | readonly enabledExtensions?: readonly ExtensionId[]; 1124 | 1125 | /** 1126 | * Additional domains allowed to open from the workbench without the 1127 | * link protection popup. 1128 | */ 1129 | readonly additionalTrustedDomains?: string[]; 1130 | 1131 | /** 1132 | * Enable workspace trust feature for the current window 1133 | */ 1134 | readonly enableWorkspaceTrust?: boolean; 1135 | 1136 | /** 1137 | * Urls that will be opened externally that are allowed access 1138 | * to the opener window. This is primarily used to allow 1139 | * `window.close()` to be called from the newly opened window. 1140 | */ 1141 | readonly openerAllowedExternalUrlPrefixes?: string[]; 1142 | 1143 | /** 1144 | * Support for URL callbacks. 1145 | */ 1146 | readonly urlCallbackProvider?: IURLCallbackProvider; 1147 | 1148 | /** 1149 | * Support adding additional properties to telemetry. 1150 | */ 1151 | readonly resolveCommonTelemetryProperties?: ICommonTelemetryPropertiesResolver; 1152 | 1153 | /** 1154 | * A set of optional commands that should be registered with the commands 1155 | * registry. 1156 | * 1157 | * Note: commands can be called from extensions if the identifier is known! 1158 | */ 1159 | readonly commands?: readonly ICommand[]; 1160 | 1161 | /** 1162 | * Optional default layout to apply on first time the workspace is opened (unless `force` is specified). 1163 | */ 1164 | readonly defaultLayout?: IDefaultLayout; 1165 | 1166 | /** 1167 | * Optional configuration default overrides contributed to the workbench. 1168 | */ 1169 | readonly configurationDefaults?: Record; 1170 | 1171 | //#endregion 1172 | 1173 | //#region Profile options 1174 | 1175 | /** 1176 | * Profile to use for the workbench. 1177 | */ 1178 | readonly profile?: { readonly name: string; readonly contents?: string | UriComponents }; 1179 | 1180 | /** 1181 | * URI of the profile to preview. 1182 | */ 1183 | readonly profileToPreview?: UriComponents; 1184 | 1185 | //#endregion 1186 | 1187 | //#region Update/Quality related 1188 | 1189 | /** 1190 | * Support for update reporting 1191 | */ 1192 | readonly updateProvider?: IUpdateProvider; 1193 | 1194 | /** 1195 | * Support for product quality switching 1196 | */ 1197 | readonly productQualityChangeHandler?: IProductQualityChangeHandler; 1198 | 1199 | //#endregion 1200 | 1201 | //#region Branding 1202 | 1203 | /** 1204 | * Optional home indicator to appear above the hamburger menu in the activity bar. 1205 | */ 1206 | readonly homeIndicator?: IHomeIndicator; 1207 | 1208 | /** 1209 | * Optional welcome banner to appear above the workbench. Can be dismissed by the 1210 | * user. 1211 | */ 1212 | readonly welcomeBanner?: IWelcomeBanner; 1213 | 1214 | /** 1215 | * Optional override for the product configuration properties. 1216 | */ 1217 | readonly productConfiguration?: any; 1218 | 1219 | /** 1220 | * Optional override for properties of the window indicator in the status bar. 1221 | */ 1222 | readonly windowIndicator?: IWindowIndicator; 1223 | 1224 | /** 1225 | * Specifies the default theme type (LIGHT, DARK..) and allows to provide initial colors that are shown 1226 | * until the color theme that is specified in the settings (`editor.colorTheme`) is loaded and applied. 1227 | * Once there are persisted colors from a last run these will be used. 1228 | * 1229 | * The idea is that the colors match the main colors from the theme defined in the `configurationDefaults`. 1230 | */ 1231 | readonly initialColorTheme?: IInitialColorTheme; 1232 | 1233 | /** 1234 | * Welcome view dialog on first launch. Can be dismissed by the user. 1235 | */ 1236 | readonly welcomeDialog?: IWelcomeDialog; 1237 | 1238 | //#endregion 1239 | 1240 | //#region IPC 1241 | 1242 | readonly messagePorts?: ReadonlyMap; 1243 | 1244 | //#endregion 1245 | 1246 | //#region Authentication Providers 1247 | 1248 | /** 1249 | * Optional authentication provider contributions. These will be used over 1250 | * any authentication providers contributed via extensions. 1251 | */ 1252 | readonly authenticationProviders?: readonly IAuthenticationProvider[]; 1253 | 1254 | //#endregion 1255 | 1256 | //#region Development options 1257 | 1258 | readonly developmentOptions?: IDevelopmentOptions; 1259 | 1260 | //#endregion 1261 | } 1262 | 1263 | //#region Authentication Providers 1264 | 1265 | // Copied from https://github.com/microsoft/vscode/blob/83f9d6b3a2d425b4b1617dc6142538151caa7866/src/vs/workbench/services/authentication/common/authentication.ts#L13 1266 | 1267 | export interface IAuthenticationSessionAccount { 1268 | label: string; 1269 | id: string; 1270 | } 1271 | 1272 | export interface IAuthenticationSession { 1273 | id: string; 1274 | accessToken: string; 1275 | account: IAuthenticationSessionAccount; 1276 | scopes: ReadonlyArray; 1277 | idToken?: string; 1278 | } 1279 | 1280 | export interface IAuthenticationSessionsChangeEvent { 1281 | added?: ReadonlyArray; 1282 | removed?: ReadonlyArray; 1283 | changed?: ReadonlyArray; 1284 | } 1285 | 1286 | export interface IAuthenticationProviderCreateSessionOptions { 1287 | sessionToRecreate?: IAuthenticationSession; 1288 | } 1289 | 1290 | export interface IAuthenticationProviderSessionOptions { 1291 | /** 1292 | * The account that is being asked about. If this is passed in, the provider should 1293 | * attempt to return the sessions that are only related to this account. 1294 | */ 1295 | account?: IAuthenticationSessionAccount; 1296 | } 1297 | 1298 | /** 1299 | * Represents an authentication provider. 1300 | */ 1301 | export interface IAuthenticationProvider { 1302 | /** 1303 | * The unique identifier of the authentication provider. 1304 | */ 1305 | readonly id: string; 1306 | 1307 | /** 1308 | * The display label of the authentication provider. 1309 | */ 1310 | readonly label: string; 1311 | 1312 | /** 1313 | * Indicates whether the authentication provider supports multiple accounts. 1314 | */ 1315 | readonly supportsMultipleAccounts: boolean; 1316 | 1317 | /** 1318 | * An {@link Event} which fires when the array of sessions has changed, or data 1319 | * within a session has changed. 1320 | */ 1321 | readonly onDidChangeSessions: Event; 1322 | 1323 | /** 1324 | * Retrieves a list of authentication sessions. 1325 | * @param scopes - An optional list of scopes. If provided, the sessions returned should match these permissions, otherwise all sessions should be returned. 1326 | * @returns A promise that resolves to an array of authentication sessions. 1327 | */ 1328 | getSessions(scopes: string[] | undefined, options: IAuthenticationProviderSessionOptions): Promise; 1329 | 1330 | /** 1331 | * Prompts the user to log in. 1332 | * If login is successful, the `onDidChangeSessions` event should be fired. 1333 | * If login fails, a rejected promise should be returned. 1334 | * If the provider does not support multiple accounts, this method should not be called if there is already an existing session matching the provided scopes. 1335 | * @param scopes - A list of scopes that the new session should be created with. 1336 | * @param options - Additional options for creating the session. 1337 | * @returns A promise that resolves to an authentication session. 1338 | */ 1339 | createSession(scopes: string[], options: IAuthenticationProviderSessionOptions): Promise; 1340 | 1341 | /** 1342 | * Removes the session corresponding to the specified session ID. 1343 | * If the removal is successful, the `onDidChangeSessions` event should be fired. 1344 | * If a session cannot be removed, the provider should reject with an error message. 1345 | * @param sessionId - The ID of the session to remove. 1346 | */ 1347 | removeSession(sessionId: string): Promise; 1348 | } 1349 | 1350 | //#endregion 1351 | 1352 | /** 1353 | * Utility provided in the {@link WorkbenchOptions} which allows loading resources 1354 | * when remote resolvers are used in the web. 1355 | */ 1356 | export interface IRemoteResourceProvider { 1357 | /** 1358 | * Path the workbench should delegate requests to. The embedder should 1359 | * install a service worker on this path and emit {@link onDidReceiveRequest} 1360 | * events when requests come in for that path. 1361 | */ 1362 | readonly path: string; 1363 | 1364 | /** 1365 | * Event that should fire when requests are made on the {@link pathPrefix}. 1366 | */ 1367 | readonly onDidReceiveRequest: Event; 1368 | } 1369 | 1370 | /** 1371 | * todo@connor4312: this may eventually gain more properties like method and 1372 | * headers, but for now we only deal with GET requests. 1373 | */ 1374 | export interface IRemoteResourceRequest { 1375 | /** 1376 | * Request URI. Generally will begin with the current 1377 | * origin and {@link IRemoteResourceProvider.pathPrefix}. 1378 | */ 1379 | uri: URI; 1380 | 1381 | /** 1382 | * A method called by the editor to issue a response to the request. 1383 | */ 1384 | respondWith(statusCode: number, body: Uint8Array, headers: Record): void; 1385 | } 1386 | 1387 | interface IPerformanceMark { 1388 | /** 1389 | * The name of a performace marker. 1390 | */ 1391 | readonly name: string; 1392 | 1393 | /** 1394 | * The UNIX timestamp at which the marker has been set. 1395 | */ 1396 | readonly startTime: number; 1397 | } 1398 | 1399 | interface IObservableValue { 1400 | onDidChange: Event; 1401 | readonly value: T; 1402 | } 1403 | 1404 | declare const enum TelemetryLevel { 1405 | NONE = 0, 1406 | CRASH = 1, 1407 | ERROR = 2, 1408 | USAGE = 3, 1409 | } 1410 | 1411 | declare const enum ProgressLocation { 1412 | Explorer = 1, 1413 | Scm = 3, 1414 | Extensions = 5, 1415 | Window = 10, 1416 | Notification = 15, 1417 | Dialog = 20, 1418 | } 1419 | 1420 | interface IProgressOptions { 1421 | readonly location: ProgressLocation | string; 1422 | readonly title?: string; 1423 | readonly source?: string | { label: string; id: string }; 1424 | readonly total?: number; 1425 | readonly cancellable?: boolean; 1426 | readonly buttons?: string[]; 1427 | } 1428 | 1429 | interface IProgressNotificationOptions extends IProgressOptions { 1430 | readonly location: ProgressLocation.Notification; 1431 | readonly primaryActions?: readonly IAction[]; 1432 | readonly secondaryActions?: readonly IAction[]; 1433 | readonly delay?: number; 1434 | readonly silent?: boolean; 1435 | readonly type?: 'syncing' | 'loading'; 1436 | } 1437 | 1438 | interface IProgressDialogOptions extends IProgressOptions { 1439 | readonly delay?: number; 1440 | readonly detail?: string; 1441 | readonly sticky?: boolean; 1442 | } 1443 | 1444 | interface IProgressWindowOptions extends IProgressOptions { 1445 | readonly location: ProgressLocation.Window; 1446 | readonly command?: string; 1447 | readonly type?: 'syncing' | 'loading'; 1448 | } 1449 | 1450 | interface IProgressCompositeOptions extends IProgressOptions { 1451 | readonly location: ProgressLocation.Explorer | ProgressLocation.Extensions | ProgressLocation.Scm | string; 1452 | readonly delay?: number; 1453 | } 1454 | 1455 | interface IProgressStep { 1456 | message?: string; 1457 | increment?: number; 1458 | total?: number; 1459 | } 1460 | 1461 | interface IProgress { 1462 | report(item: T): void; 1463 | } 1464 | 1465 | interface IWorkbench { 1466 | commands: { 1467 | /** 1468 | * Allows to execute any command if known with the provided arguments. 1469 | * 1470 | * @param command Identifier of the command to execute. 1471 | * @param rest Parameters passed to the command function. 1472 | * @return A promise that resolves to the returned value of the given command. 1473 | */ 1474 | executeCommand(command: string, ...args: any[]): Promise; 1475 | }; 1476 | 1477 | logger: { 1478 | /** 1479 | * Logging for embedder. 1480 | * 1481 | * @param level The log level of the message to be printed. 1482 | * @param message Message to be printed. 1483 | */ 1484 | log(level: LogLevel, message: string): void; 1485 | }; 1486 | 1487 | env: { 1488 | /** 1489 | * @returns the scheme to use for opening the associated desktop 1490 | * experience via protocol handler. 1491 | */ 1492 | getUriScheme(): Promise; 1493 | 1494 | /** 1495 | * Retrieve performance marks that have been collected during startup. This function 1496 | * returns tuples of source and marks. A source is a dedicated context, like 1497 | * the renderer or an extension host. 1498 | * 1499 | * *Note* that marks can be collected on different machines and in different processes 1500 | * and that therefore "different clocks" are used. So, comparing `startTime`-properties 1501 | * across contexts should be taken with a grain of salt. 1502 | * 1503 | * @returns A promise that resolves to tuples of source and marks. 1504 | */ 1505 | retrievePerformanceMarks(): Promise<[string, readonly PerformanceMark[]][]>; 1506 | 1507 | /** 1508 | * Allows to open a `URI` with the standard opener service of the 1509 | * workbench. 1510 | */ 1511 | openUri(target: URI): Promise; 1512 | 1513 | /** 1514 | * Current workbench telemetry level. 1515 | */ 1516 | readonly telemetryLevel: IObservableValue; 1517 | }; 1518 | 1519 | window: { 1520 | /** 1521 | * Show progress in the editor. Progress is shown while running the given callback 1522 | * and while the promise it returned isn't resolved nor rejected. 1523 | * 1524 | * @param task A callback returning a promise. 1525 | * @return A promise that resolves to the returned value of the given task result. 1526 | */ 1527 | withProgress( 1528 | options: 1529 | | IProgressOptions 1530 | | IProgressDialogOptions 1531 | | IProgressNotificationOptions 1532 | | IProgressWindowOptions 1533 | | IProgressCompositeOptions, 1534 | task: (progress: IProgress) => Promise 1535 | ): Promise; 1536 | 1537 | /** 1538 | * Show an information message to users. Optionally provide an array of items which will be presented as 1539 | * clickable buttons. 1540 | * 1541 | * @param message The message to show. 1542 | * @param items A set of items that will be rendered as actions in the message. 1543 | * @returns A thenable that resolves to the selected item or `undefined` when being dismissed. 1544 | */ 1545 | showInformationMessage(message: string, ...items: T[]): Promise; 1546 | }; 1547 | 1548 | workspace: { 1549 | /** 1550 | * Resolves once the remote authority has been resolved. 1551 | */ 1552 | didResolveRemoteAuthority(): Promise; 1553 | 1554 | /** 1555 | * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. 1556 | * By default, openTunnel only support localhost; however, a tunnelFactory can be used to support other ips. 1557 | * 1558 | * @throws When run in an environment without a remote. 1559 | * 1560 | * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. 1561 | */ 1562 | openTunnel(tunnelOptions: ITunnelOptions): Promise; 1563 | }; 1564 | 1565 | /** 1566 | * Triggers shutdown of the workbench programmatically. After this method is 1567 | * called, the workbench is not usable anymore and the page needs to reload 1568 | * or closed. 1569 | * 1570 | * This will also remove any `beforeUnload` handlers that would bring up a 1571 | * confirmation dialog. 1572 | * 1573 | * The returned promise should be awaited on to ensure any data to persist 1574 | * has been persisted. 1575 | */ 1576 | shutdown: () => Promise; 1577 | } 1578 | 1579 | /** 1580 | * Creates the workbench with the provided options in the provided container. 1581 | * 1582 | * @param domElement the container to create the workbench in 1583 | * @param options for setting up the workbench 1584 | */ 1585 | declare function create(domElement: HTMLElement, options: IWorkbenchConstructionOptions): IDisposable; 1586 | 1587 | //#region API Facade 1588 | 1589 | declare namespace commands { 1590 | /** 1591 | * Allows to execute any command if known with the provided arguments. 1592 | * 1593 | * @param command Identifier of the command to execute. 1594 | * @param rest Parameters passed to the command function. 1595 | * @return A promise that resolves to the returned value of the given command. 1596 | */ 1597 | function executeCommand(command: string, ...args: any[]): Promise; 1598 | } 1599 | 1600 | declare namespace logger { 1601 | /** 1602 | * Record log messages to be displayed in `Log (vscode.dev)` 1603 | * 1604 | * @param level The log level of the message to be printed. 1605 | * @param message The log to be printed. 1606 | */ 1607 | function log(level: LogLevel, message: string): void; 1608 | } 1609 | 1610 | declare namespace env { 1611 | /** 1612 | * @returns the scheme to use for opening the associated desktop 1613 | * experience via protocol handler. 1614 | */ 1615 | function getUriScheme(): Promise; 1616 | 1617 | /** 1618 | * Retrieve performance marks that have been collected during startup. This function 1619 | * returns tuples of source and marks. A source is a dedicated context, like 1620 | * the renderer or an extension host. 1621 | * 1622 | * *Note* that marks can be collected on different machines and in different processes 1623 | * and that therefore "different clocks" are used. So, comparing `startTime`-properties 1624 | * across contexts should be taken with a grain of salt. 1625 | * 1626 | * @returns A promise that resolves to tuples of source and marks. 1627 | */ 1628 | function retrievePerformanceMarks(): Promise<[string, readonly IPerformanceMark[]][]>; 1629 | 1630 | /** 1631 | * Allows to open a `URI` with the standard opener service of the 1632 | * workbench. 1633 | */ 1634 | function openUri(target: URI): Promise; 1635 | 1636 | /** 1637 | * Current workbench telemetry level. 1638 | */ 1639 | const telemetryLevel: Promise>; 1640 | } 1641 | 1642 | declare namespace window { 1643 | /** 1644 | * Show progress in the editor. Progress is shown while running the given callback 1645 | * and while the promise it returned isn't resolved nor rejected. 1646 | * 1647 | * @param task A callback returning a promise. 1648 | * @return A promise that resolves to the returned value of the given task result. 1649 | */ 1650 | function withProgress( 1651 | options: 1652 | | IProgressOptions 1653 | | IProgressDialogOptions 1654 | | IProgressNotificationOptions 1655 | | IProgressWindowOptions 1656 | | IProgressCompositeOptions, 1657 | task: (progress: IProgress) => Promise 1658 | ): Promise; 1659 | 1660 | /** 1661 | * Show an information message to users. Optionally provide an array of items which will be presented as 1662 | * clickable buttons. 1663 | * 1664 | * @param message The message to show. 1665 | * @param items A set of items that will be rendered as actions in the message. 1666 | * @returns A thenable that resolves to the selected item or `undefined` when being dismissed. 1667 | */ 1668 | function showInformationMessage(message: string, ...items: T[]): Promise; 1669 | } 1670 | 1671 | declare namespace workspace { 1672 | /** 1673 | * Resolves once the remote authority has been resolved. 1674 | */ 1675 | function didResolveRemoteAuthority(): Promise; 1676 | 1677 | /** 1678 | * Forwards a port. If the current embedder implements a tunnelFactory then that will be used to make the tunnel. 1679 | * By default, openTunnel only support localhost; however, a tunnelFactory can be used to support other ips. 1680 | * 1681 | * @throws When run in an environment without a remote. 1682 | * 1683 | * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. 1684 | */ 1685 | function openTunnel(tunnelOptions: ITunnelOptions): Promise; 1686 | } 1687 | 1688 | declare const enum RemoteAuthorityResolverErrorCode { 1689 | Unknown = 'Unknown', 1690 | NotAvailable = 'NotAvailable', 1691 | TemporarilyNotAvailable = 'TemporarilyNotAvailable', 1692 | NoResolverFound = 'NoResolverFound', 1693 | } 1694 | 1695 | declare class RemoteAuthorityResolverError extends Error { 1696 | static isNotAvailable(err: any): boolean; 1697 | static isTemporarilyNotAvailable(err: any): boolean; 1698 | static isNoResolverFound(err: any): err is RemoteAuthorityResolverError; 1699 | static isHandled(err: any): boolean; 1700 | constructor(message?: string, code?: RemoteAuthorityResolverErrorCode, detail?: any); 1701 | } 1702 | 1703 | export { 1704 | // Factory 1705 | create, 1706 | IWorkbenchConstructionOptions, 1707 | IWorkbench, 1708 | // Basic Types 1709 | URI, 1710 | UriComponents, 1711 | Event, 1712 | Emitter, 1713 | IDisposable, 1714 | Disposable, 1715 | IObservableValue, 1716 | // Workspace 1717 | IWorkspace, 1718 | IWorkspaceProvider, 1719 | // WebSockets 1720 | IWebSocketFactory, 1721 | IWebSocket, 1722 | // Resources 1723 | IResourceUriProvider, 1724 | // Secret Storage 1725 | ISecretStorageProvider, 1726 | // Callbacks 1727 | IURLCallbackProvider, 1728 | // SettingsSync 1729 | ISettingsSyncOptions, 1730 | // Updates/Quality 1731 | IUpdateProvider, 1732 | IUpdate, 1733 | IProductQualityChangeHandler, 1734 | // Telemetry 1735 | ICommonTelemetryPropertiesResolver, 1736 | // External Uris 1737 | IExternalUriResolver, 1738 | // External URL Opener 1739 | IExternalURLOpener, 1740 | // Tunnel 1741 | ITunnelProvider, 1742 | TunnelProviderFeatures, 1743 | TunnelPrivacy, 1744 | ITunnelFactory, 1745 | ITunnel, 1746 | ITunnelOptions, 1747 | // Ports 1748 | IShowPortCandidate, 1749 | // Commands 1750 | ICommand, 1751 | commands, 1752 | Menu, 1753 | // Logger 1754 | logger, 1755 | LogLevel, 1756 | // Window 1757 | window, 1758 | // Workspace 1759 | workspace, 1760 | // Progress 1761 | IProgress, 1762 | ProgressLocation, 1763 | IProgressStep, 1764 | IProgressOptions, 1765 | IProgressNotificationOptions, 1766 | IProgressDialogOptions, 1767 | IProgressWindowOptions, 1768 | IProgressCompositeOptions, 1769 | // Branding 1770 | IHomeIndicator, 1771 | IWindowIndicator, 1772 | IInitialColorTheme, 1773 | // Default layout 1774 | IDefaultView, 1775 | IDefaultEditor, 1776 | IEditorOptions, 1777 | ITextEditorOptions, 1778 | ITextEditorSelection, 1779 | IDefaultLayout, 1780 | EditorGroupLayout, 1781 | GroupOrientation, 1782 | GroupLayoutArgument, 1783 | // Env 1784 | IPerformanceMark, 1785 | env, 1786 | // Nav Bar 1787 | IWelcomeBanner, 1788 | // Telemetry 1789 | TelemetryLevel, 1790 | // Remote authority resolver error, 1791 | RemoteAuthorityResolverError, 1792 | RemoteAuthorityResolverErrorCode, 1793 | // Welcome dialog 1794 | IWelcomeDialog 1795 | }; 1796 | --------------------------------------------------------------------------------