diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1324b59bc0..d08dac3c03b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,8 +109,8 @@ jobs: uses: actions/cache@v2 with: path: "**/node_modules" - key: ${{ runner.os }}-cacheNodeModules11-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules11- + key: ${{ runner.os }}-cacheNodeModules13-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules13- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} @@ -165,8 +165,8 @@ jobs: uses: actions/cache@v2 with: path: "**/node_modules" - key: ${{ runner.os }}-cacheNodeModules11-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules11- + key: ${{ runner.os }}-cacheNodeModules13-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules13- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} @@ -218,8 +218,8 @@ jobs: uses: actions/cache@v2 with: path: "**/node_modules" - key: ${{ runner.os }}-cacheNodeModules11-${{ steps.nodeModulesCacheKey.outputs.value }} - restore-keys: ${{ runner.os }}-cacheNodeModules11- + key: ${{ runner.os }}-cacheNodeModules13-${{ steps.nodeModulesCacheKey.outputs.value }} + restore-keys: ${{ runner.os }}-cacheNodeModules13- - name: Get yarn cache directory path id: yarnCacheDirPath if: ${{ steps.cacheNodeModules.outputs.cache-hit != 'true' }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 7eede8c30a9..a97841683c0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -86,5 +86,5 @@ }, "typescript.tsc.autoDetect": "off", "notebook.experimental.useMarkdownRenderer": true, - "testing.autoRun.mode": "onlyPreviouslyRun", + "testing.autoRun.mode": "rerun", } diff --git a/.yarnrc b/.yarnrc index c142fdee01c..1965e671993 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://electronjs.org/headers" -target "11.4.2" +target "12.0.4" runtime "electron" diff --git a/build/.cachesalt b/build/.cachesalt index 057dd5fd39f..013244143e8 100644 --- a/build/.cachesalt +++ b/build/.cachesalt @@ -1 +1 @@ -2021-01-28T11:52:11.376Z +2021-04-07T03:52:18.011Z diff --git a/build/azure-pipelines/darwin/product-build-darwin-sign.yml b/build/azure-pipelines/darwin/product-build-darwin-sign.yml index b978cf57c64..4ad8349c51a 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-sign.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-sign.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index d5a72ca7ce9..186920fe96d 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -83,6 +83,7 @@ steps: set -e export npm_config_arch=$(VSCODE_ARCH) export npm_config_node_gyp=$(which node-gyp) + export npm_config_build_from_source=true export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk for i in {1..3}; do # try 3 times, for Terrapin @@ -107,17 +108,6 @@ steps: condition: and(succeeded(), ne(variables.NODE_MODULES_RESTORED, 'true')) displayName: Create node_modules archive - - script: | - set -e - export npm_config_arch=$(VSCODE_ARCH) - export npm_config_node_gyp=$(which node-gyp) - export npm_config_build_from_source=true - export SDKROOT=/Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.0.sdk - ls /Applications/Xcode_12.2.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/ - yarn electron-rebuild - displayName: Rebuild native modules for ARM64 - condition: and(succeeded(), eq(variables['VSCODE_ARCH'], 'arm64')) - # This script brings in the right resources (images, icons, etc) based on the quality (insiders, stable, exploration) - script: | set -e diff --git a/build/azure-pipelines/distro-build.yml b/build/azure-pipelines/distro-build.yml index 22d6983e7f8..dc5d2803476 100644 --- a/build/azure-pipelines/distro-build.yml +++ b/build/azure-pipelines/distro-build.yml @@ -8,7 +8,7 @@ pr: steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" diff --git a/build/azure-pipelines/exploration-build.yml b/build/azure-pipelines/exploration-build.yml index 719e6e469cb..ff3ce383c2f 100644 --- a/build/azure-pipelines/exploration-build.yml +++ b/build/azure-pipelines/exploration-build.yml @@ -7,7 +7,7 @@ pr: none steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: AzureKeyVault@1 displayName: "Azure Key Vault: Get Secrets" @@ -27,10 +27,10 @@ steps: git config user.email "vscode@microsoft.com" git config user.name "VSCode" - git checkout origin/electron-11.x.y + git checkout origin/electron-12.x.y git merge origin/main # Push main branch into exploration branch - git push origin HEAD:electron-11.x.y + git push origin HEAD:electron-12.x.y displayName: Sync & Merge Exploration diff --git a/build/azure-pipelines/linux/product-build-alpine.yml b/build/azure-pipelines/linux/product-build-alpine.yml index 4a1b8a2c64a..8376c079ce8 100644 --- a/build/azure-pipelines/linux/product-build-alpine.yml +++ b/build/azure-pipelines/linux/product-build-alpine.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 54c7af090c3..cb06bf6a724 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index c2f82083f38..f5e0288f0b9 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 5c386c94fc7..52c7758cfde 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.x" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/publish-types/publish-types.yml b/build/azure-pipelines/publish-types/publish-types.yml index 09964dc6ad0..df8b665fbcd 100644 --- a/build/azure-pipelines/publish-types/publish-types.yml +++ b/build/azure-pipelines/publish-types/publish-types.yml @@ -9,7 +9,7 @@ pr: none steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/release.yml b/build/azure-pipelines/release.yml index edd293c04f6..1c5ec73c856 100644 --- a/build/azure-pipelines/release.yml +++ b/build/azure-pipelines/release.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "10.x" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/sync-mooncake.yml b/build/azure-pipelines/sync-mooncake.yml index 280c9e6372d..6e379754f2f 100644 --- a/build/azure-pipelines/sync-mooncake.yml +++ b/build/azure-pipelines/sync-mooncake.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/web/product-build-web.yml b/build/azure-pipelines/web/product-build-web.yml index 0a8e1c36a88..772fe1c05ab 100644 --- a/build/azure-pipelines/web/product-build-web.yml +++ b/build/azure-pipelines/web/product-build-web.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index e5d961e3974..2dcaf8b2e01 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -1,7 +1,7 @@ steps: - task: NodeTool@0 inputs: - versionSpec: "12.18.3" + versionSpec: "14.x" - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 inputs: @@ -78,6 +78,7 @@ steps: . build/azure-pipelines/win32/retry.ps1 $ErrorActionPreference = "Stop" $env:npm_config_arch="$(VSCODE_ARCH)" + $env:npm_config_build_from_source="true" $env:CHILD_CONCURRENCY="1" retry { exec { yarn --frozen-lockfile } } env: diff --git a/build/builtin/main.js b/build/builtin/main.js index 42865157c36..3a11d363f96 100644 --- a/build/builtin/main.js +++ b/build/builtin/main.js @@ -23,7 +23,17 @@ ipcMain.handle('pickdir', async () => { }); app.once('ready', () => { - window = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, webviewTag: true, enableWebSQL: false, nativeWindowOpen: true } }); + window = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: true, + contextIsolation: false, + webviewTag: true, + enableWebSQL: false, + nativeWindowOpen: true + } + }); window.setMenuBarVisibility(false); window.loadURL(url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', slashes: true })); // window.webContents.openDevTools(); diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index a06f3f63d89..3eeba69d258 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -223,7 +223,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const dependenciesSrc = _.flatten(productionDependencies.map(d => path.relative(root, d.path)).map(d => [`${d}/**`, `!${d}/**/{test,tests}/**`])); const deps = gulp.src(dependenciesSrc, { base: '.', dot: true }) - .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-85/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.js.map'])) + .pipe(filter(['**', `!**/${config.version}/**`, '!**/bin/darwin-arm64-87/**', '!**/package-lock.json', '!**/yarn.lock', '!**/*.js.map'])) .pipe(util.cleanNodeModules(path.join(__dirname, '.moduleignore'))) .pipe(jsFilter) .pipe(util.rewriteSourceMappingURL(sourceMappingURLBase)) diff --git a/build/package.json b/build/package.json index a0a36a05c74..45fd664a501 100644 --- a/build/package.json +++ b/build/package.json @@ -24,7 +24,7 @@ "@types/minimist": "^1.2.1", "@types/mkdirp": "^1.0.1", "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9", + "@types/node": "^14.14.37", "@types/p-limit": "^2.2.0", "@types/plist": "^3.0.2", "@types/pump": "^1.0.1", diff --git a/build/yarn.lock b/build/yarn.lock index 62e3cd9d455..09e51a274c3 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -371,16 +371,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.51.tgz#b31d716fb8d58eeb95c068a039b9b6292817d5fb" integrity sha512-El3+WJk2D/ppWNd2X05aiP5l2k4EwF7KwheknQZls+I26eSICoWRhRIJ56jGgw2dqNGQ5LtNajmBU2ajS28EvQ== -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== - "@types/node@^14.14.21": version "14.14.22" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== +"@types/node@^14.14.37": + version "14.14.37" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" + integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== + "@types/p-limit@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@types/p-limit/-/p-limit-2.2.0.tgz#94a608e9b258a6c6156a13d1a14fd720dba70b97" diff --git a/cgmanifest.json b/cgmanifest.json index b7906232b32..aa54e3243ab 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "c0dfcf99c0bbc9c4763c70e5034eb1a970a9ff3b" + "commitHash": "5342041f85833c038dcbc5632d62fc10f7592323" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "87.0.4280.141" + "version": "89.0.4389.114" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "e3e0927bb93ed92bcdfe81e7ad9af3d78ccc74fb" + "commitHash": "bd60e93357a118204ea238d94e7a9e4209d93062" } }, "isOnlyProductionDependency": true, - "version": "12.18.3" + "version": "14.16.0" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "d6fe9727449ea51f8dc4a32260990577dcc9e5fb" + "commitHash": "9ce7c512475aa6aa91417a3b08e19f85a8587a30" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "11.4.2" + "version": "12.0.4" }, { "component": { diff --git a/extensions/css-language-features/server/test/index.js b/extensions/css-language-features/server/test/index.js index 4ab853bd503..1699883a574 100644 --- a/extensions/css-language-features/server/test/index.js +++ b/extensions/css-language-features/server/test/index.js @@ -11,7 +11,7 @@ const suite = 'Integration CSS Extension Tests'; const options = { ui: 'tdd', - useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index c472d1e23e7..4b44dea9339 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -8,6 +8,9 @@ "engines": { "vscode": "^1.5.0" }, + "workspaceTrust": { + "request": "never" + }, "activationEvents": [ "*" ], diff --git a/extensions/emmet/src/test/index.ts b/extensions/emmet/src/test/index.ts index cf92c7a6c3f..4f09c798c31 100644 --- a/extensions/emmet/src/test/index.ts +++ b/extensions/emmet/src/test/index.ts @@ -8,7 +8,7 @@ const testRunner = require('../../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/git/package.json b/extensions/git/package.json index ab7053169b0..e34386e9abe 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -26,6 +26,9 @@ "update-grammar": "node ./build/update-grammars.js", "test": "node ../../node_modules/mocha/bin/mocha" }, + "workspaceTrust": { + "request": "never" + }, "contributes": { "commands": [ { @@ -2369,7 +2372,7 @@ "byline": "^5.0.0", "file-type": "^7.2.0", "iconv-lite-umd": "0.6.8", - "jschardet": "2.2.1", + "jschardet": "2.3.0", "vscode-extension-telemetry": "0.1.7", "vscode-nls": "^4.0.0", "vscode-uri": "^2.0.0", diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index cefe23848fa..37c0eb94db4 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -1835,7 +1835,10 @@ export class Repository implements Disposable { // noop } - const sort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder') || 'alphabetically'; + let sort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder') || 'alphabetically'; + if (sort !== 'alphabetically' && sort !== 'committerdate') { + sort = 'alphabetically'; + } const [refs, remotes, submodules, rebaseCommit] = await Promise.all([this.repository.getRefs({ sort }), this.repository.getRemotes(), this.repository.getSubmodules(), this.getRebaseCommit()]); this._HEAD = HEAD; diff --git a/extensions/git/src/test/index.ts b/extensions/git/src/test/index.ts index 3fe682fc27f..bed72035cde 100644 --- a/extensions/git/src/test/index.ts +++ b/extensions/git/src/test/index.ts @@ -8,7 +8,7 @@ const testRunner = require('../../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index c60310b06b7..c964f284a3a 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -117,10 +117,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -jschardet@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.2.1.tgz#03b0264669a90c7a5c436a68c5a7d4e4cb0c9823" - integrity sha512-Ks2JNuUJoc7PGaZ7bVFtSEvOcr0rBq6Q1J5/7+zKWLT+g+4zziL63O0jg7y2jxhzIa1LVsHUbPXrbaWmz9iwDw== +jschardet@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" + integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== semver@^5.3.0: version "5.5.0" diff --git a/extensions/github/package.json b/extensions/github/package.json index f6e7cd6bdb5..603a13d90ae 100644 --- a/extensions/github/package.json +++ b/extensions/github/package.json @@ -19,6 +19,9 @@ "vscode.git" ], "main": "./out/extension.js", + "workspaceTrust": { + "request": "never" + }, "contributes": { "commands": [ { diff --git a/extensions/html-language-features/server/test/index.js b/extensions/html-language-features/server/test/index.js index 5f7aa21e58a..50e250b78b8 100644 --- a/extensions/html-language-features/server/test/index.js +++ b/extensions/html-language-features/server/test/index.js @@ -11,7 +11,7 @@ const suite = 'Integration HTML Extension Tests'; const options = { ui: 'tdd', - useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 55dccba4b7a..42d6f08fc07 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -434,14 +434,14 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider { private async onDidClickPreviewLink(href: string) { let [hrefPath, fragment] = decodeURIComponent(href).split('#'); - // We perviously already resolve absolute paths. - // Now make sure we handle relative file paths if (hrefPath[0] !== '/') { - // Fix #93691, use this.resource.fsPath instead of this.resource.path - hrefPath = path.join(path.dirname(this.resource.fsPath), hrefPath); + // We perviously already resolve absolute paths. + // Now make sure we handle relative file paths + const dirnameUri = vscode.Uri.parse(path.dirname(this.resource.path)); + hrefPath = vscode.Uri.joinPath(dirnameUri, hrefPath).path; } else { // Handle any normalized file paths - hrefPath = vscode.Uri.parse(hrefPath.replace('/file', '')).fsPath; + hrefPath = vscode.Uri.parse(hrefPath.replace('/file', '')).path; } const config = vscode.workspace.getConfiguration('markdown', this.resource); diff --git a/extensions/markdown-language-features/src/test/index.ts b/extensions/markdown-language-features/src/test/index.ts index 4589f10f8d9..2f00d0d5338 100644 --- a/extensions/markdown-language-features/src/test/index.ts +++ b/extensions/markdown-language-features/src/test/index.ts @@ -8,7 +8,7 @@ const testRunner = require('../../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/testing-editor-contributions/package.json b/extensions/testing-editor-contributions/package.json index 53a39faf7e2..383ed5fe09e 100644 --- a/extensions/testing-editor-contributions/package.json +++ b/extensions/testing-editor-contributions/package.json @@ -20,6 +20,9 @@ "dependencies": { "vscode-nls": "^5.0.0" }, + "workspaceTrust": { + "request": "never" + }, "scripts": { "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:testing-editor-contributions ./tsconfig.json" }, diff --git a/extensions/typescript-language-features/src/test-all.ts b/extensions/typescript-language-features/src/test-all.ts index ef6cc04ce70..71e88e4a60d 100644 --- a/extensions/typescript-language-features/src/test-all.ts +++ b/extensions/typescript-language-features/src/test-all.ts @@ -21,7 +21,7 @@ const testRunner = require('../../../test/integration/electron/testrunner'); // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle) + color: true, timeout: 60000, }); diff --git a/extensions/typescript-language-features/src/test/index.ts b/extensions/typescript-language-features/src/test/index.ts index 11204e863b4..1d2d4f89358 100644 --- a/extensions/typescript-language-features/src/test/index.ts +++ b/extensions/typescript-language-features/src/test/index.ts @@ -21,7 +21,7 @@ const testRunner = require('../../../../test/integration/electron/testrunner'); // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle) + color: true, timeout: 60000, }); diff --git a/extensions/typescript-language-features/src/test/smoke/index.ts b/extensions/typescript-language-features/src/test/smoke/index.ts index ab3d87566e5..16d163fa241 100644 --- a/extensions/typescript-language-features/src/test/smoke/index.ts +++ b/extensions/typescript-language-features/src/test/smoke/index.ts @@ -21,7 +21,7 @@ const testRunner = require('../../../../../test/integration/electron/testrunner' // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle) + color: true, timeout: 60000, }); diff --git a/extensions/typescript-language-features/src/test/unit/index.ts b/extensions/typescript-language-features/src/test/unit/index.ts index ab3d87566e5..16d163fa241 100644 --- a/extensions/typescript-language-features/src/test/unit/index.ts +++ b/extensions/typescript-language-features/src/test/unit/index.ts @@ -21,7 +21,7 @@ const testRunner = require('../../../../../test/integration/electron/testrunner' // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle) + color: true, timeout: 60000, }); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts index 2b283c396be..de9ebf36165 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts @@ -8,7 +8,7 @@ const testRunner = require('../../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts index e5b7a74623f..a23d3e55c73 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/terminal.test.ts @@ -468,7 +468,7 @@ import { assertNoRpc } from '../utils'; // const terminal = window.createTerminal({ name: 'foo', pty }); // }); - test('should respect dimension overrides', (done) => { + test.skip('should respect dimension overrides', (done) => { disposables.push(window.onDidOpenTerminal(term => { try { equal(terminal, term); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index ace2479b343..e60998c24be 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -619,7 +619,7 @@ suite('vscode API - workspace', () => { assert.equal(results.length, 1); const match = results[0]; assert(match.preview.text.indexOf('foo') >= 0); - assert.equal(vscode.workspace.asRelativePath(match.uri), '10linefile.ts'); + assert.equal(basename(vscode.workspace.asRelativePath(match.uri)), '10linefile.ts'); }); test('findTextInFiles, cancellation', async () => { diff --git a/extensions/vscode-api-tests/src/workspace-tests/index.ts b/extensions/vscode-api-tests/src/workspace-tests/index.ts index ba41f64f480..9c23f586b14 100644 --- a/extensions/vscode-api-tests/src/workspace-tests/index.ts +++ b/extensions/vscode-api-tests/src/workspace-tests/index.ts @@ -8,7 +8,7 @@ const testRunner = require('../../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/vscode-colorize-tests/src/index.ts b/extensions/vscode-colorize-tests/src/index.ts index f8066005703..4dcda1af3f9 100644 --- a/extensions/vscode-colorize-tests/src/index.ts +++ b/extensions/vscode-colorize-tests/src/index.ts @@ -10,7 +10,7 @@ const suite = 'Integration Colorize Tests'; const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/vscode-custom-editor-tests/src/test/index.ts b/extensions/vscode-custom-editor-tests/src/test/index.ts index dbaab6b4c74..4a5238f776c 100644 --- a/extensions/vscode-custom-editor-tests/src/test/index.ts +++ b/extensions/vscode-custom-editor-tests/src/test/index.ts @@ -10,7 +10,7 @@ const suite = 'Custom Editor Tests'; const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/vscode-notebook-tests/src/index.ts b/extensions/vscode-notebook-tests/src/index.ts index c6aec5845fc..2f8fb129206 100644 --- a/extensions/vscode-notebook-tests/src/index.ts +++ b/extensions/vscode-notebook-tests/src/index.ts @@ -8,7 +8,7 @@ const testRunner = require('../../../test/integration/electron/testrunner'); const options: any = { ui: 'tdd', - color: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + color: true, timeout: 60000 }; diff --git a/extensions/vscode-test-resolver/src/extension.ts b/extensions/vscode-test-resolver/src/extension.ts index 99d3619e316..c338957146b 100644 --- a/extensions/vscode-test-resolver/src/extension.ts +++ b/extensions/vscode-test-resolver/src/extension.ts @@ -203,7 +203,8 @@ export function activate(context: vscode.ExtensionContext) { proxyServer.listen(0, () => { const port = (proxyServer.address()).port; outputChannel.appendLine(`Going through proxy at port ${port}`); - res(new vscode.ResolvedAuthority('127.0.0.1', port)); + const r: vscode.ResolverResult = new vscode.ResolvedAuthority('127.0.0.1', port); + res(r); }); context.subscriptions.push({ dispose: () => { diff --git a/package.json b/package.json index ac2093b6a51..73ee03d2a78 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.56.0", - "distro": "1ba9507447dd90200177e4dd4d13e56cf29d8a85", + "distro": "0f67985367e21c8d8fce62aa56ebd77c29606588", "author": { "name": "Microsoft Corporation" }, @@ -48,7 +48,6 @@ "compile-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-web", "watch-web": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js watch-web", "eslint": "node build/eslint", - "electron-rebuild": "electron-rebuild --arch=arm64 --force --version=11.4.2", "playwright-install": "node build/azure-pipelines/common/installPlaywright.js", "compile-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-build", "compile-extensions-build": "node --max_old_space_size=4095 ./node_modules/gulp/bin/gulp.js compile-extensions-build", @@ -82,7 +81,7 @@ "vscode-proxy-agent": "^0.8.2", "vscode-regexpp": "^3.1.0", "vscode-ripgrep": "^1.11.3", - "vscode-sqlite3": "4.0.10", + "vscode-sqlite3": "4.0.11", "vscode-textmate": "5.2.0", "xterm": "4.12.0-beta.20", "xterm-addon-search": "0.9.0-beta.1", @@ -105,11 +104,12 @@ "@types/keytar": "^4.4.0", "@types/minimist": "^1.2.1", "@types/mocha": "^8.2.0", - "@types/node": "^12.19.9", + "@types/node": "^14.14.37", "@types/sinon": "^1.16.36", "@types/trusted-types": "^1.0.6", "@types/vscode-windows-registry": "^1.0.0", "@types/webpack": "^4.41.25", + "@types/wicg-file-system-access": "^2020.9.1", "@types/windows-foreground-love": "^0.3.0", "@types/windows-mutex": "^0.4.0", "@types/windows-process-tree": "^0.2.0", @@ -127,8 +127,7 @@ "cssnano": "^4.1.11", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "11.4.2", - "electron-rebuild": "2.0.3", + "electron": "12.0.4", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", diff --git a/product.json b/product.json index 08e03a2d3ee..f3a142c0ac7 100644 --- a/product.json +++ b/product.json @@ -33,7 +33,7 @@ "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.25", + "version": "1.44.26", "repo": "https://github.com/microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -48,7 +48,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.42.5", + "version": "1.42.6", "repo": "https://github.com/microsoft/vscode-node-debug2", "metadata": { "id": "36d19e17-7569-4841-a001-947eb18602b2", @@ -78,7 +78,7 @@ }, { "name": "ms-vscode.js-debug-companion", - "version": "1.0.9", + "version": "1.0.11", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -108,7 +108,7 @@ }, { "name": "ms-vscode.vscode-js-profile-table", - "version": "0.0.11", + "version": "0.0.16", "repo": "https://github.com/microsoft/vscode-js-profile-visualizer", "metadata": { "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", diff --git a/remote/.yarnrc b/remote/.yarnrc index cd436416b56..bce4202aea7 100644 --- a/remote/.yarnrc +++ b/remote/.yarnrc @@ -1,3 +1,3 @@ disturl "http://nodejs.org/dist" -target "12.18.3" +target "14.16.0" runtime "node" diff --git a/src/bootstrap-window.js b/src/bootstrap-window.js index d525e34b22d..ec669360321 100644 --- a/src/bootstrap-window.js +++ b/src/bootstrap-window.js @@ -32,7 +32,12 @@ * @param {string[]} modulePaths * @param {(result: unknown, configuration: ISandboxConfiguration) => Promise | undefined} resultCallback * @param {{ - * configureDeveloperKeybindings?: (config: ISandboxConfiguration) => {forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean}, + * configureDeveloperSettings?: (config: ISandboxConfiguration) => { + * forceDisableShowDevtoolsOnError?: boolean, + * forceEnableDeveloperKeybindings?: boolean, + * disallowReloadKeybinding?: boolean, + * removeDeveloperKeybindingsAfterLoad?: boolean + * }, * canModifyDOM?: (config: ISandboxConfiguration) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void @@ -41,8 +46,9 @@ async function load(modulePaths, resultCallback, options) { // Error handler (TODO@sandbox non-sandboxed only) + let showDevtoolsOnError = !!safeProcess.env['VSCODE_DEV']; safeProcess.on('uncaughtException', function (/** @type {string | Error} */ error) { - onUnexpectedError(error, enableDeveloperKeybindings); + onUnexpectedError(error, showDevtoolsOnError); }); // Await window configuration from preload @@ -51,8 +57,19 @@ const configuration = await preloadGlobals.context.resolveConfiguration(); performance.mark('code/didWaitForWindowConfig'); - // Developer keybindings - const { forceEnableDeveloperKeybindings, disallowReloadKeybinding, removeDeveloperKeybindingsAfterLoad } = typeof options?.configureDeveloperKeybindings === 'function' ? options.configureDeveloperKeybindings(configuration) : { forceEnableDeveloperKeybindings: false, disallowReloadKeybinding: false, removeDeveloperKeybindingsAfterLoad: false }; + // Developer settings + const { + forceDisableShowDevtoolsOnError, + forceEnableDeveloperKeybindings, + disallowReloadKeybinding, + removeDeveloperKeybindingsAfterLoad + } = typeof options?.configureDeveloperSettings === 'function' ? options.configureDeveloperSettings(configuration) : { + forceDisableShowDevtoolsOnError: false, + forceEnableDeveloperKeybindings: false, + disallowReloadKeybinding: false, + removeDeveloperKeybindingsAfterLoad: false + }; + showDevtoolsOnError = safeProcess.env['VSCODE_DEV'] && !forceDisableShowDevtoolsOnError; const enableDeveloperKeybindings = safeProcess.env['VSCODE_DEV'] || forceEnableDeveloperKeybindings; let developerDeveloperKeybindingsDisposable; if (enableDeveloperKeybindings) { @@ -232,10 +249,10 @@ /** * @param {string | Error} error - * @param {boolean} [enableDeveloperTools] + * @param {boolean} [showDevtoolsOnError] */ - function onUnexpectedError(error, enableDeveloperTools) { - if (enableDeveloperTools) { + function onUnexpectedError(error, showDevtoolsOnError) { + if (showDevtoolsOnError) { const ipcRenderer = preloadGlobals.ipcRenderer; ipcRenderer.send('vscode:openDevTools'); } diff --git a/src/tsconfig.json b/src/tsconfig.json index 2f1c74627b8..e6e78721e86 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -12,7 +12,8 @@ "semver", "sinon", "winreg", - "trusted-types" + "trusted-types", + "wicg-file-system-access" ], "plugins": [ { diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index a36bbe6f1f2..601aca6f9f0 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -2,9 +2,7 @@ "extends": "./tsconfig.base.json", "compilerOptions": { "noEmit": true, - "types": [ - "trusted-types" - ], + "types": ["trusted-types"], "paths": {}, "module": "amd", "moduleResolution": "classic", @@ -27,6 +25,7 @@ "vs/platform/*/browser/*" ], "exclude": [ - "node_modules/*" + "node_modules/*", + "vs/platform/files/browser/htmlFileSystemProvider.ts" ] } diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 90f56d92e05..58ebdb5b2c0 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1409,37 +1409,8 @@ export function multibyteAwareBtoa(str: string): string { */ export namespace WebFileSystemAccess { - // https://wicg.github.io/file-system-access/#dom-window-showdirectorypicker - export interface FileSystemAccess { - showDirectoryPicker: () => Promise; - } - - // https://wicg.github.io/file-system-access/#api-filesystemdirectoryhandle - export interface FileSystemDirectoryHandle { - readonly kind: 'directory', - readonly name: string, - - getFileHandle: (name: string, options?: { create?: boolean }) => Promise; - getDirectoryHandle: (name: string, options?: { create?: boolean }) => Promise; - } - - // https://wicg.github.io/file-system-access/#api-filesystemfilehandle - export interface FileSystemFileHandle { - readonly kind: 'file', - readonly name: string, - - createWritable: (options?: { keepExistingData?: boolean }) => Promise; - } - - // https://wicg.github.io/file-system-access/#api-filesystemwritablefilestream - export interface FileSystemWritableFileStream { - write: (buffer: Uint8Array) => Promise; - close: () => Promise; - } - - export function supported(obj: any & Window): obj is FileSystemAccess { - const candidate = obj as FileSystemAccess | undefined; - if (typeof candidate?.showDirectoryPicker === 'function') { + export function supported(obj: any & Window): boolean { + if (typeof obj?.showDirectoryPicker === 'function') { return true; } diff --git a/src/vs/base/browser/ui/actionbar/actionViewItems.ts b/src/vs/base/browser/ui/actionbar/actionViewItems.ts index 98c0c667054..67419acaaa0 100644 --- a/src/vs/base/browser/ui/actionbar/actionViewItems.ts +++ b/src/vs/base/browser/ui/actionbar/actionViewItems.ts @@ -174,6 +174,10 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { } } + isFocused(): boolean { + return !!this.element?.classList.contains('focused'); + } + blur(): void { if (this.element) { this.element.blur(); @@ -283,6 +287,10 @@ export class ActionViewItem extends BaseActionViewItem { } } + override isFocused(): boolean { + return !!this.label && this.label?.tabIndex === 0; + } + override blur(): void { if (this.label) { this.label.tabIndex = -1; diff --git a/src/vs/base/browser/ui/actionbar/actionbar.css b/src/vs/base/browser/ui/actionbar/actionbar.css index 539d960bc6f..7020bdc6700 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.css +++ b/src/vs/base/browser/ui/actionbar/actionbar.css @@ -29,7 +29,7 @@ position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */ } -.monaco-action-bar .eaction-item.disabled { +.monaco-action-bar .action-item.disabled { cursor: default; } diff --git a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts index c7f9e2b890a..2286815ee38 100644 --- a/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts +++ b/src/vs/base/browser/ui/dropdown/dropdownActionViewItem.ts @@ -7,14 +7,15 @@ import 'vs/css!./dropdown'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { IDisposable } from 'vs/base/common/lifecycle'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; -import { append, $ } from 'vs/base/browser/dom'; +import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { append, $, addDisposableListener, EventType } from 'vs/base/browser/dom'; import { Emitter } from 'vs/base/common/event'; import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown'; import { IContextMenuProvider } from 'vs/base/browser/contextmenu'; import { Codicon } from 'vs/base/common/codicons'; import { IActionViewItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; export interface IKeybindingProvider { (action: IAction): ResolvedKeybinding | undefined; @@ -184,6 +185,34 @@ export class ActionWithDropdownActionViewItem extends ActionViewItem { }; this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(this._register(new Action('dropdownAction', undefined)), menuActionsProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(this.options).menuActionClassNames || []] }); this.dropdownMenuActionViewItem.render(this.element); + + this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + let handled: boolean = false; + if (this.dropdownMenuActionViewItem?.isFocused() && event.equals(KeyCode.LeftArrow)) { + handled = true; + this.dropdownMenuActionViewItem?.blur(); + this.focus(); + } else if (this.isFocused() && event.equals(KeyCode.RightArrow)) { + handled = true; + this.blur(); + this.dropdownMenuActionViewItem?.focus(); + } + if (handled) { + event.preventDefault(); + event.stopPropagation(); + } + })); } } + + override blur(): void { + super.blur(); + this.dropdownMenuActionViewItem?.blur(); + } + + override setFocusable(focusable: boolean): void { + super.setFocusable(focusable); + this.dropdownMenuActionViewItem?.setFocusable(focusable); + } } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index c9be6ae0d68..4740d2c7c7d 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -1131,7 +1131,7 @@ export interface ICompressibleAsyncDataTreeOptionsUpdate extends IAsyncDataTreeO export class CompressibleAsyncDataTree extends AsyncDataTree { - protected readonly tree!: CompressibleObjectTree, TFilterData>; + protected override readonly tree!: CompressibleObjectTree, TFilterData>; protected readonly compressibleNodeMapper: CompressibleAsyncDataTreeNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node)); private filter?: ITreeFilter; diff --git a/src/vs/base/browser/ui/tree/dataTree.ts b/src/vs/base/browser/ui/tree/dataTree.ts index b548e2e3dae..3adca02b685 100644 --- a/src/vs/base/browser/ui/tree/dataTree.ts +++ b/src/vs/base/browser/ui/tree/dataTree.ts @@ -23,7 +23,7 @@ export interface IDataTreeViewState { export class DataTree extends AbstractTree { - protected model!: ObjectTreeModel; + protected override model!: ObjectTreeModel; private input: TInput | undefined; private identityProvider: IIdentityProvider | undefined; diff --git a/src/vs/base/browser/ui/tree/indexTree.ts b/src/vs/base/browser/ui/tree/indexTree.ts index 5272fa1971a..07b91eec917 100644 --- a/src/vs/base/browser/ui/tree/indexTree.ts +++ b/src/vs/base/browser/ui/tree/indexTree.ts @@ -14,7 +14,7 @@ export interface IIndexTreeOptions extends IAbstractTreeO export class IndexTree extends AbstractTree { - protected model!: IndexTreeModel; + protected override model!: IndexTreeModel; constructor( user: string, diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 0e417939857..7649792a90f 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -38,7 +38,7 @@ export interface IObjectTreeSetChildrenOptions { export class ObjectTree, TFilterData = void> extends AbstractTree { - protected model!: IObjectTreeModel; + protected override model!: IObjectTreeModel; override get onDidChangeCollapseState(): Event> { return this.model.onDidChangeCollapseState; } @@ -194,7 +194,7 @@ export interface ICompressibleObjectTreeOptionsUpdate extends IAbstractTreeOptio export class CompressibleObjectTree, TFilterData = void> extends ObjectTree implements ICompressedTreeNodeProvider { - protected model!: CompressibleObjectTreeModel; + protected override model!: CompressibleObjectTreeModel; constructor( user: string, diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 9df28d9b4d2..c25d74a7599 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -235,3 +235,11 @@ export function streamToBufferReadableStream(stream: streams.ReadableStreamEvent export function newWriteableBufferStream(options?: streams.WriteableStreamOptions): streams.WriteableStream { return streams.newWriteableStream(chunks => VSBuffer.concat(chunks), options); } + +export function prefixedBufferReadable(prefix: VSBuffer, readable: VSBufferReadable): VSBufferReadable { + return streams.prefixedReadable(prefix, readable, chunks => VSBuffer.concat(chunks)); +} + +export function prefixedBufferStream(prefix: VSBuffer, stream: VSBufferReadableStream): VSBufferReadableStream { + return streams.prefixedStream(prefix, stream, chunks => VSBuffer.concat(chunks)); +} diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index a639f631604..e858fa42e36 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -49,6 +49,16 @@ export function registerCodicon(id: string, def: Codicon): Codicon { return new Codicon(id, def); } +// Selects all codicon names encapsulated in the `$()` syntax and wraps the +// results with spaces so that screen readers can read the text better. +export function getCodiconAriaLabel(text: string | undefined) { + if (!text) { + return ''; + } + + return text.replace(/\$\((.*?)\)/g, (_match, codiconName) => ` ${codiconName} `).trim(); +} + export class Codicon implements CSSIcon { constructor(public readonly id: string, public readonly definition: Codicon | IconDefinition, public description?: string) { _registry.add(this); diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index 2ca5fe50f9a..36d543ee934 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -887,7 +887,7 @@ export class LinkedMap implements Map { this._tail = undefined; } else if (item === this._head) { - // This can only happend if size === 1 which is handle + // This can only happen if size === 1 which is handled // by the case above. if (!item.next) { throw new Error('Invalid list'); @@ -896,7 +896,7 @@ export class LinkedMap implements Map { this._head = item.next; } else if (item === this._tail) { - // This can only happend if size === 1 which is handle + // This can only happen if size === 1 which is handled // by the case above. if (!item.previous) { throw new Error('Invalid list'); @@ -1048,3 +1048,47 @@ export class LRUCache extends LinkedMap { } } } + +/** + * Wraps the map in type that only implements readonly properties. Useful + * in the extension host to prevent the consumer from making any mutations. + */ +export class ReadonlyMapView implements ReadonlyMap{ + readonly #source: ReadonlyMap; + + public get size() { + return this.#source.size; + } + + constructor(source: ReadonlyMap) { + this.#source = source; + } + + forEach(callbackfn: (value: V, key: K, map: ReadonlyMap) => void, thisArg?: any): void { + this.#source.forEach(callbackfn, thisArg); + } + + get(key: K): V | undefined { + return this.#source.get(key); + } + + has(key: K): boolean { + return this.#source.has(key); + } + + entries(): IterableIterator<[K, V]> { + return this.#source.entries(); + } + + keys(): IterableIterator { + return this.#source.keys(); + } + + values(): IterableIterator { + return this.#source.values(); + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.#source.entries(); + } +} diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index d49cdb611e9..a1b386590bc 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -122,6 +122,7 @@ export interface IProductConfiguration { readonly extensionSyncedKeys?: { readonly [extensionId: string]: string[]; }; readonly extensionAllowedProposedApi?: readonly string[]; readonly extensionWorkspaceTrustRequest?: { readonly [extensionId: string]: ExtensionWorkspaceTrustRequest }; + readonly extensionSupportsVirtualWorkspace?: { readonly [extensionId: string]: { default?: boolean, override?: boolean } }; readonly msftInternalDomains?: string[]; readonly linkProtectionTrustedDomains?: readonly string[]; diff --git a/src/vs/base/common/stream.ts b/src/vs/base/common/stream.ts index 97873ade780..67df6415d8f 100644 --- a/src/vs/base/common/stream.ts +++ b/src/vs/base/common/stream.ts @@ -142,15 +142,21 @@ export interface ReadableBufferedStream { } export function isReadableStream(obj: unknown): obj is ReadableStream { - const candidate = obj as ReadableStream; + const candidate = obj as ReadableStream | undefined; + if (!candidate) { + return false; + } - return candidate && [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every(fn => typeof fn === 'function'); + return [candidate.on, candidate.pause, candidate.resume, candidate.destroy].every(fn => typeof fn === 'function'); } export function isReadableBufferedStream(obj: unknown): obj is ReadableBufferedStream { - const candidate = obj as ReadableBufferedStream; + const candidate = obj as ReadableBufferedStream | undefined; + if (!candidate) { + return false; + } - return candidate && isReadableStream(candidate.stream) && Array.isArray(candidate.buffer) && typeof candidate.ended === 'boolean'; + return isReadableStream(candidate.stream) && Array.isArray(candidate.buffer) && typeof candidate.ended === 'boolean'; } export interface IReducer { @@ -626,11 +632,12 @@ export function toStream(t: T, reducer: IReducer): ReadableStream { } /** - * Helper + * Helper to create an empty stream */ export function emptyStream(): ReadableStream { const stream = newWriteableStream(() => { throw new Error('not supported'); }); stream.end(); + return stream; } @@ -667,3 +674,71 @@ export function transform(stream: ReadableStreamEvents(prefix: T, readable: Readable, reducer: IReducer): Readable { + let prefixHandled = false; + + return { + read: () => { + const chunk = readable.read(); + + // Handle prefix only once + if (!prefixHandled) { + prefixHandled = true; + + // If we have also a read-result, make + // sure to reduce it to a single result + if (chunk !== null) { + return reducer([prefix, chunk]); + } + + // Otherwise, just return prefix directly + return prefix; + } + + return chunk; + } + }; +} + +/** + * Helper to take an existing stream that will + * have a prefix injected to the beginning. + */ +export function prefixedStream(prefix: T, stream: ReadableStream, reducer: IReducer): ReadableStream { + let prefixHandled = false; + + const target = newWriteableStream(reducer); + + listenStream(stream, { + onData: data => { + + // Handle prefix only once + if (!prefixHandled) { + prefixHandled = true; + + return target.write(reducer([prefix, data])); + } + + return target.write(data); + }, + onError: error => target.error(error), + onEnd: () => { + + // Handle prefix only once + if (!prefixHandled) { + prefixHandled = true; + + target.write(prefix); + } + + target.end(); + } + }); + + return target; +} diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index 71966e69b8d..3e431196b99 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -286,3 +286,7 @@ export function NotImplementedProxy(name: string): { new(): T } { } }; } + +export function assertNever(value: never) { + throw new Error('Unreachable'); +} diff --git a/src/vs/base/node/decoder.ts b/src/vs/base/node/decoder.ts index 767cf6d89c1..36a3de5175c 100644 --- a/src/vs/base/node/decoder.ts +++ b/src/vs/base/node/decoder.ts @@ -18,7 +18,7 @@ export class LineDecoder { private stringDecoder: sd.StringDecoder; private remaining: string | null; - constructor(encoding: string = 'utf8') { + constructor(encoding: BufferEncoding = 'utf8') { this.stringDecoder = new sd.StringDecoder(encoding); this.remaining = null; } diff --git a/src/vs/base/node/id.ts b/src/vs/base/node/id.ts index 2799ffc718d..3ff83c8de59 100644 --- a/src/vs/base/node/id.ts +++ b/src/vs/base/node/id.ts @@ -56,8 +56,9 @@ export const virtualMachineHint: { value(): number } = new class { const interfaces = networkInterfaces(); for (let name in interfaces) { - if (Object.prototype.hasOwnProperty.call(interfaces, name)) { - for (const { mac, internal } of interfaces[name]) { + const networkInterface = interfaces[name]; + if (networkInterface) { + for (const { mac, internal } of networkInterface) { if (!internal) { interfaceCount += 1; if (this._isVirtualMachineMacAdress(mac.toUpperCase())) { diff --git a/src/vs/base/node/macAddress.ts b/src/vs/base/node/macAddress.ts index 35fec9fc86b..acfdf239380 100644 --- a/src/vs/base/node/macAddress.ts +++ b/src/vs/base/node/macAddress.ts @@ -34,10 +34,13 @@ function doGetMac(): Promise { return new Promise((resolve, reject) => { try { const ifaces = networkInterfaces(); - for (const [, infos] of Object.entries(ifaces)) { - for (const info of infos) { - if (validateMacAddress(info.mac)) { - return resolve(info.mac); + for (let name in ifaces) { + const networkInterface = ifaces[name]; + if (networkInterface) { + for (const { mac } of networkInterface) { + if (validateMacAddress(mac)) { + return resolve(mac); + } } } } diff --git a/src/vs/base/node/shell.ts b/src/vs/base/node/shell.ts index ae28f291d8f..3df529ecafa 100644 --- a/src/vs/base/node/shell.ts +++ b/src/vs/base/node/shell.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; +import { release, userInfo } from 'os'; import * as platform from 'vs/base/common/platform'; import { getFirstAvailablePowerShellInstallation } from 'vs/base/node/powershell'; import * as processes from 'vs/base/node/processes'; @@ -11,10 +11,10 @@ import * as processes from 'vs/base/node/processes'; /** * Gets the detected default shell for the _system_, not to be confused with VS Code's _default_ * shell that the terminal uses by default. - * @param p The platform to detect the shell of. + * @param os The platform to detect the shell of. */ -export async function getSystemShell(p: platform.Platform, env: platform.IProcessEnvironment): Promise { - if (p === platform.Platform.Windows) { +export async function getSystemShell(os: platform.OperatingSystem, env: platform.IProcessEnvironment): Promise { + if (os === platform.OperatingSystem.Windows) { if (platform.isWindows) { return getSystemShellWindows(); } @@ -22,11 +22,11 @@ export async function getSystemShell(p: platform.Platform, env: platform.IProces return processes.getWindowsShell(env); } - return getSystemShellUnixLike(p, env); + return getSystemShellUnixLike(os, env); } -export function getSystemShellSync(p: platform.Platform, env: platform.IProcessEnvironment): string { - if (p === platform.Platform.Windows) { +export function getSystemShellSync(os: platform.OperatingSystem, env: platform.IProcessEnvironment): string { + if (os === platform.OperatingSystem.Windows) { if (platform.isWindows) { return getSystemShellWindowsSync(env); } @@ -34,13 +34,13 @@ export function getSystemShellSync(p: platform.Platform, env: platform.IProcessE return processes.getWindowsShell(env); } - return getSystemShellUnixLike(p, env); + return getSystemShellUnixLike(os, env); } let _TERMINAL_DEFAULT_SHELL_UNIX_LIKE: string | null = null; -function getSystemShellUnixLike(p: platform.Platform, env: platform.IProcessEnvironment): string { +function getSystemShellUnixLike(os: platform.OperatingSystem, env: platform.IProcessEnvironment): string { // Only use $SHELL for the current OS - if (platform.isLinux && p === platform.Platform.Mac || platform.isMacintosh && p === platform.Platform.Linux) { + if (platform.isLinux && os === platform.OperatingSystem.Macintosh || platform.isMacintosh && os === platform.OperatingSystem.Linux) { return '/bin/bash'; } @@ -55,7 +55,7 @@ function getSystemShellUnixLike(p: platform.Platform, env: platform.IProcessEnvi try { // It's possible for $SHELL to be unset, this API reads /etc/passwd. See https://github.com/github/codespaces/issues/1639 // Node docs: "Throws a SystemError if a user has no username or homedir." - unixLikeTerminal = os.userInfo().shell; + unixLikeTerminal = userInfo().shell; } catch (err) { } } @@ -86,7 +86,7 @@ function getSystemShellWindowsSync(env: platform.IProcessEnvironment): string { return _TERMINAL_DEFAULT_SHELL_WINDOWS; } - const isAtLeastWindows10 = platform.isWindows && parseFloat(os.release()) >= 10; + const isAtLeastWindows10 = platform.isWindows && parseFloat(release()) >= 10; const is32ProcessOn64Windows = env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); const powerShellPath = `${env['windir']}\\${is32ProcessOn64Windows ? 'Sysnative' : 'System32'}\\WindowsPowerShell\\v1.0\\powershell.exe`; return isAtLeastWindows10 ? powerShellPath : processes.getWindowsShell(env); diff --git a/src/vs/base/parts/quickinput/browser/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts index 7126af1297a..706d46f19ab 100644 --- a/src/vs/base/parts/quickinput/browser/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -27,6 +27,7 @@ import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput' import { IListOptions, List, IListStyles, IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { localize } from 'vs/nls'; +import { getCodiconAriaLabel } from 'vs/base/common/codicons'; const $ = dom.$; @@ -427,7 +428,7 @@ export class QuickInputList { const saneDescription = item.description && item.description.replace(/\r?\n/g, ' '); const saneDetail = item.detail && item.detail.replace(/\r?\n/g, ' '); const saneAriaLabel = item.ariaLabel || [saneLabel, saneDescription, saneDetail] - .map(s => s && parseLabelWithIcons(s).text) + .map(s => getCodiconAriaLabel(s)) .filter(s => !!s) .join(', '); @@ -603,6 +604,7 @@ export class QuickInputList { // Filter by value (since we support icons in labels, use $(..) aware fuzzy matching) else { + let currentSeparator: IQuickPickSeparator | undefined; this.elements.forEach(element => { const labelHighlights = this.matchOnLabel ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneLabel))) : undefined; const descriptionHighlights = this.matchOnDescription ? withNullAsUndefined(matchesFuzzyIconAware(query, parseLabelWithIcons(element.saneDescription || ''))) : undefined; @@ -621,6 +623,16 @@ export class QuickInputList { element.hidden = !element.item.alwaysShow; } element.separator = undefined; + + // we can show the separator unless the list gets sorted by match + if (!this.sortByLabel) { + const previous = element.index && this.inputElements[element.index - 1]; + currentSeparator = previous && previous.type === 'separator' ? previous : currentSeparator; + if (currentSeparator && !element.hidden) { + element.separator = currentSeparator; + currentSeparator = undefined; + } + } }); } diff --git a/src/vs/base/test/common/codicons.test.ts b/src/vs/base/test/common/codicons.test.ts new file mode 100644 index 00000000000..02a30dbe509 --- /dev/null +++ b/src/vs/base/test/common/codicons.test.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { getCodiconAriaLabel } from 'vs/base/common/codicons'; + +suite('Codicon', () => { + test('Can get proper aria labels', () => { + // note, the spaces in the results are important + const testCases = new Map([ + ['', ''], + ['asdf', 'asdf'], + ['asdf$(squirrel)asdf', 'asdf squirrel asdf'], + ['asdf $(squirrel) asdf', 'asdf squirrel asdf'], + ['$(rocket)asdf', 'rocket asdf'], + ['$(rocket) asdf', 'rocket asdf'], + ['$(rocket)$(rocket)$(rocket)asdf', 'rocket rocket rocket asdf'], + ['$(rocket) asdf $(rocket)', 'rocket asdf rocket'], + ['$(rocket)asdf$(rocket)', 'rocket asdf rocket'], + ]); + + for (const [input, expected] of testCases) { + assert.strictEqual(getCodiconAriaLabel(input), expected); + } + }); +}); diff --git a/src/vs/base/test/common/stream.test.ts b/src/vs/base/test/common/stream.test.ts index 9b4d73f7a37..4cb309a84ea 100644 --- a/src/vs/base/test/common/stream.test.ts +++ b/src/vs/base/test/common/stream.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, listenStream } from 'vs/base/common/stream'; +import { isReadableStream, newWriteableStream, Readable, consumeReadable, peekReadable, consumeStream, ReadableStream, toStream, toReadable, transform, peekStream, isReadableBufferedStream, listenStream, prefixedReadable, prefixedStream } from 'vs/base/common/stream'; import { timeout } from 'vs/base/common/async'; suite('Stream', () => { @@ -386,7 +386,7 @@ suite('Stream', () => { test('toReadable', async () => { const readable = toReadable('1,2,3,4,5'); - const consumed = await consumeReadable(readable, strings => strings.join()); + const consumed = consumeReadable(readable, strings => strings.join()); assert.strictEqual(consumed, '1,2,3,4,5'); }); @@ -424,4 +424,49 @@ suite('Stream', () => { assert.strictEqual(listener1Called, true); assert.strictEqual(listener2Called, true); }); + + test('prefixedReadable', () => { + + // Basic + let readable = prefixedReadable('1,2', arrayToReadable(['3', '4', '5']), val => val.join(',')); + assert.strictEqual(consumeReadable(readable, val => val.join(',')), '1,2,3,4,5'); + + // Empty + readable = prefixedReadable('empty', arrayToReadable([]), val => val.join(',')); + assert.strictEqual(consumeReadable(readable, val => val.join(',')), 'empty'); + }); + + test('prefixedStream', async () => { + + // Basic + let stream = newWriteableStream(strings => strings.join()); + stream.write('3'); + stream.write('4'); + stream.write('5'); + stream.end(); + + let prefixStream = prefixedStream('1,2', stream, val => val.join(',')); + assert.strictEqual(await consumeStream(prefixStream, val => val.join(',')), '1,2,3,4,5'); + + // Empty + stream = newWriteableStream(strings => strings.join()); + stream.end(); + + prefixStream = prefixedStream('1,2', stream, val => val.join(',')); + assert.strictEqual(await consumeStream(prefixStream, val => val.join(',')), '1,2'); + + // Error + stream = newWriteableStream(strings => strings.join()); + stream.error(new Error('fail')); + + prefixStream = prefixedStream('error', stream, val => val.join(',')); + + let error; + try { + await consumeStream(prefixStream, val => val.join(',')); + } catch (e) { + error = e; + } + assert.ok(error); + }); }); diff --git a/src/vs/code/browser/workbench/workbench.ts b/src/vs/code/browser/workbench/workbench.ts index 337e04c1fc3..096b7d6d35f 100644 --- a/src/vs/code/browser/workbench/workbench.ts +++ b/src/vs/code/browser/workbench/workbench.ts @@ -282,23 +282,28 @@ class WorkspaceProvider implements IWorkspaceProvider { public readonly payload: object ) { } - async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { + async open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { if (options?.reuse && !options.payload && this.isSame(this.workspace, workspace)) { - return; // return early if workspace and environment is not changing and we are reusing window + return true; // return early if workspace and environment is not changing and we are reusing window } const targetHref = this.createTargetUrl(workspace, options); if (targetHref) { if (options?.reuse) { window.location.href = targetHref; + return true; } else { + let result; if (isStandalone) { - window.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window! + result = window.open(targetHref, '_blank', 'toolbar=no'); // ensures to open another 'standalone' window! } else { - window.open(targetHref); + result = window.open(targetHref); } + + return !!result; } } + return false; } private createTargetUrl(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): string | undefined { diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js index 0af18ab9fde..6c6e3f8cf5e 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcess.js +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcess.js @@ -16,7 +16,15 @@ // Load shared process into window bootstrapWindow.load(['vs/code/electron-browser/sharedProcess/sharedProcessMain'], function (sharedProcess, configuration) { return sharedProcess.main(configuration); - }); + }, + { + configureDeveloperSettings: function () { + return { + disallowReloadKeybinding: true + }; + } + } + ); /** * @returns {{ avoidMonkeyPatchFromAppInsights: () => void; }} @@ -34,7 +42,11 @@ * modules: string[], * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, * options?: { - * configureDeveloperKeybindings?: (config: ISandboxConfiguration) => {forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean}, + * configureDeveloperSettings?: (config: ISandboxConfiguration) => { + * forceEnableDeveloperKeybindings?: boolean, + * disallowReloadKeybinding?: boolean, + * removeDeveloperKeybindingsAfterLoad?: boolean + * }, * canModifyDOM?: (config: ISandboxConfiguration) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index ad96ee4aef7..0a1e2b55661 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import { release } from 'os'; +import { release, hostname } from 'os'; import { gracefulify } from 'graceful-fs'; import { ipcRenderer } from 'electron'; import product from 'vs/platform/product/common/product'; @@ -225,7 +225,7 @@ class SharedProcessMain extends Disposable { telemetryService = new TelemetryService({ appender: telemetryAppender, - commonProperties: resolveCommonProperties(fileService, release(), process.arch, productService.commit, productService.version, this.configuration.machineId, productService.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, this.configuration.machineId, productService.msftInternalDomains, installSourcePath), sendErrorTelemetry: true, piiPaths: [appRoot, extensionsPath] }, configurationService); diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 11fd33efc2d..6a92a3c203e 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -32,9 +32,13 @@ return require('vs/workbench/electron-browser/desktop.main').main(configuration); }, { - configureDeveloperKeybindings: function (windowConfig) { + configureDeveloperSettings: function (windowConfig) { return { - forceEnableDeveloperKeybindings: Array.isArray(windowConfig.extensionDevelopmentPath), + // disable automated devtools opening on error when running extension tests + // as this can lead to undeterministic test exectuion (devtools steals focus) + forceDisableShowDevtoolsOnError: typeof windowConfig.extensionTestsPath === 'string', + // enable devtools keybindings in extension development window + forceEnableDeveloperKeybindings: Array.isArray(windowConfig.extensionDevelopmentPath) && windowConfig.extensionDevelopmentPath.length > 0, removeDeveloperKeybindingsAfterLoad: true }; }, @@ -91,7 +95,12 @@ * modules: string[], * resultCallback: (result, configuration: INativeWindowConfiguration) => unknown, * options?: { - * configureDeveloperKeybindings?: (config: INativeWindowConfiguration & object) => {forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean}, + * configureDeveloperSettings?: (config: INativeWindowConfiguration & object) => { + * forceDisableShowDevtoolsOnError?: boolean, + * forceEnableDeveloperKeybindings?: boolean, + * disallowReloadKeybinding?: boolean, + * removeDeveloperKeybindingsAfterLoad?: boolean + * }, * canModifyDOM?: (config: INativeWindowConfiguration & object) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 5d1a7b8190c..5fad86fd6d1 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { release } from 'os'; +import { release, hostname } from 'os'; import { statSync } from 'fs'; import { app, ipcMain, systemPreferences, contentTracing, protocol, BrowserWindow, dialog, session } from 'electron'; import { IProcessEnvironment, isWindows, isMacintosh, isLinux, isLinuxSnap } from 'vs/base/common/platform'; @@ -604,7 +604,7 @@ export class CodeApplication extends Disposable { if (!this.environmentMainService.isExtensionDevelopment && !this.environmentMainService.args['disable-telemetry'] && !!this.productService.enableTelemetry) { const channel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('telemetryAppender'))); const appender = new TelemetryAppenderClient(channel); - const commonProperties = resolveCommonProperties(this.fileService, release(), process.arch, this.productService.commit, this.productService.version, machineId, this.productService.msftInternalDomains, this.environmentMainService.installSourcePath); + const commonProperties = resolveCommonProperties(this.fileService, release(), hostname(), process.arch, this.productService.commit, this.productService.version, machineId, this.productService.msftInternalDomains, this.environmentMainService.installSourcePath); const piiPaths = [this.environmentMainService.appRoot, this.environmentMainService.extensionsPath]; const config: ITelemetryServiceConfig = { appender, commonProperties, piiPaths, sendErrorTelemetry: true }; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index a2687622d9d..b4650d50702 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -204,7 +204,7 @@ class CodeMain { private initServices(environmentMainService: IEnvironmentMainService, configurationService: ConfigurationService, stateService: StateService): Promise { // Environment service (paths) - const environmentServiceInitialization = Promise.all([ + const environmentServiceInitialization = Promise.all([ environmentMainService.extensionsPath, environmentMainService.nodeCachedDataDir, environmentMainService.logsPath, diff --git a/src/vs/code/electron-sandbox/issue/issueReporter.js b/src/vs/code/electron-sandbox/issue/issueReporter.js index 2be3a5195fe..caa0b29f95b 100644 --- a/src/vs/code/electron-sandbox/issue/issueReporter.js +++ b/src/vs/code/electron-sandbox/issue/issueReporter.js @@ -14,7 +14,7 @@ return issueReporter.startup(configuration); }, { - configureDeveloperKeybindings: function () { + configureDeveloperSettings: function () { return { forceEnableDeveloperKeybindings: true, disallowReloadKeybinding: true @@ -31,7 +31,11 @@ * modules: string[], * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, * options?: { - * configureDeveloperKeybindings?: (config: ISandboxConfiguration) => {forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean}, + * configureDeveloperSettings?: (config: ISandboxConfiguration) => { + * forceEnableDeveloperKeybindings?: boolean, + * disallowReloadKeybinding?: boolean, + * removeDeveloperKeybindingsAfterLoad?: boolean + * }, * canModifyDOM?: (config: ISandboxConfiguration) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void diff --git a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js index 4161a8566a3..7aeb6f35d80 100644 --- a/src/vs/code/electron-sandbox/processExplorer/processExplorer.js +++ b/src/vs/code/electron-sandbox/processExplorer/processExplorer.js @@ -13,7 +13,7 @@ bootstrapWindow.load(['vs/code/electron-sandbox/processExplorer/processExplorerMain'], function (processExplorer, configuration) { return processExplorer.startup(configuration); }, { - configureDeveloperKeybindings: function () { + configureDeveloperSettings: function () { return { forceEnableDeveloperKeybindings: true }; @@ -28,7 +28,11 @@ * modules: string[], * resultCallback: (result, configuration: ISandboxConfiguration) => unknown, * options?: { - * configureDeveloperKeybindings?: (config: ISandboxConfiguration) => {forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean}, + * configureDeveloperSettings?: (config: ISandboxConfiguration) => { + * forceEnableDeveloperKeybindings?: boolean, + * disallowReloadKeybinding?: boolean, + * removeDeveloperKeybindingsAfterLoad?: boolean + * }, * canModifyDOM?: (config: ISandboxConfiguration) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js index 2777fcc4fed..38cd78a89b5 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.js +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -32,9 +32,13 @@ return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); }, { - configureDeveloperKeybindings: function (windowConfig) { + configureDeveloperSettings: function (windowConfig) { return { - forceEnableDeveloperKeybindings: Array.isArray(windowConfig.extensionDevelopmentPath), + // disable automated devtools opening on error when running extension tests + // as this can lead to undeterministic test exectuion (devtools steals focus) + forceDisableShowDevtoolsOnError: typeof windowConfig.extensionTestsPath === 'string', + // enable devtools keybindings in extension development window + forceEnableDeveloperKeybindings: Array.isArray(windowConfig.extensionDevelopmentPath) && windowConfig.extensionDevelopmentPath.length > 0, removeDeveloperKeybindingsAfterLoad: true }; }, @@ -91,7 +95,11 @@ * modules: string[], * resultCallback: (result, configuration: INativeWindowConfiguration) => unknown, * options?: { - * configureDeveloperKeybindings?: (config: INativeWindowConfiguration & object) => {forceEnableDeveloperKeybindings?: boolean, disallowReloadKeybinding?: boolean, removeDeveloperKeybindingsAfterLoad?: boolean}, + * configureDeveloperSettings?: (config: INativeWindowConfiguration & object) => { + * forceEnableDeveloperKeybindings?: boolean, + * disallowReloadKeybinding?: boolean, + * removeDeveloperKeybindingsAfterLoad?: boolean + * }, * canModifyDOM?: (config: INativeWindowConfiguration & object) => void, * beforeLoaderConfig?: (loaderConfig: object) => void, * beforeRequire?: () => void diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index f330f43c192..11716b334bc 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { release } from 'os'; +import { release, hostname } from 'os'; import * as fs from 'fs'; import { gracefulify } from 'graceful-fs'; import { isAbsolute, join } from 'vs/base/common/path'; @@ -158,7 +158,7 @@ class CliMain extends Disposable { const config: ITelemetryServiceConfig = { appender: combinedAppender(...appenders), sendErrorTelemetry: false, - commonProperties: resolveCommonProperties(fileService, release(), process.arch, productService.commit, productService.version, stateService.getItem('telemetry.machineId'), productService.msftInternalDomains, installSourcePath), + commonProperties: resolveCommonProperties(fileService, release(), hostname(), process.arch, productService.commit, productService.version, stateService.getItem('telemetry.machineId'), productService.msftInternalDomains, installSourcePath), piiPaths: [appRoot, extensionsPath] }; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 5abb19f3ce0..61452e16c78 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -57,9 +57,9 @@ interface ITextStream { on(event: string, callback: any): void; } -export function createTextBufferFactoryFromStream(stream: ITextStream, filter?: (chunk: string) => string, validator?: (chunk: string) => Error | undefined): Promise; -export function createTextBufferFactoryFromStream(stream: VSBufferReadableStream, filter?: (chunk: VSBuffer) => VSBuffer, validator?: (chunk: VSBuffer) => Error | undefined): Promise; -export function createTextBufferFactoryFromStream(stream: ITextStream | VSBufferReadableStream, filter?: (chunk: any) => string | VSBuffer, validator?: (chunk: any) => Error | undefined): Promise { +export function createTextBufferFactoryFromStream(stream: ITextStream): Promise; +export function createTextBufferFactoryFromStream(stream: VSBufferReadableStream): Promise; +export function createTextBufferFactoryFromStream(stream: ITextStream | VSBufferReadableStream): Promise { return new Promise((resolve, reject) => { const builder = createTextBufferBuilder(); @@ -67,18 +67,6 @@ export function createTextBufferFactoryFromStream(stream: ITextStream | VSBuffer listenStream(stream, { onData: chunk => { - if (validator) { - const error = validator(chunk); - if (error) { - done = true; - reject(error); - } - } - - if (filter) { - chunk = filter(chunk); - } - builder.acceptChunk((typeof chunk === 'string') ? chunk : chunk.toString()); }, onError: error => { diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index 515d11efe61..514ecdd22f6 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -440,6 +440,7 @@ export class SuggestWidget implements IDisposable { this._contentWidget.hide(); this._ctxSuggestWidgetVisible.reset(); this._ctxSuggestWidgetMultipleSuggestions.reset(); + this._showTimeout.cancel(); this.element.domNode.classList.remove('visible'); this._list.splice(0, this._list.length); this._focusedItem = undefined; diff --git a/src/vs/platform/debug/common/extensionHostDebug.ts b/src/vs/platform/debug/common/extensionHostDebug.ts index 8d8592c9c58..6b75bdf2a28 100644 --- a/src/vs/platform/debug/common/extensionHostDebug.ts +++ b/src/vs/platform/debug/common/extensionHostDebug.ts @@ -29,6 +29,7 @@ export interface ICloseSessionEvent { export interface IOpenExtensionWindowResult { rendererDebugPort?: number; + success: boolean; } /** diff --git a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts index c814220f558..5c814347fc4 100644 --- a/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts +++ b/src/vs/platform/debug/electron-main/extensionHostDebugIpc.ts @@ -30,7 +30,7 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens const extDevPaths = pargs.extensionDevelopmentPath; if (!extDevPaths) { - return {}; + return { success: false }; } // split INullableProcessEnvironment into a IProcessEnvironment and an array of keys to be deleted @@ -57,12 +57,12 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens }); if (!debugRenderer) { - return {}; + return { success: true }; } const win = codeWindow.win; if (!win) { - return {}; + return { success: true }; } const debug = win.webContents.debugger; @@ -127,6 +127,6 @@ export class ElectronExtensionHostDebugBroadcastChannel extends Extens await new Promise(r => server.listen(0, r)); win.on('close', () => server.close()); - return { rendererDebugPort: (server.address() as AddressInfo).port }; + return { rendererDebugPort: (server.address() as AddressInfo).port, success: true }; } } diff --git a/src/vs/platform/environment/node/shellEnv.ts b/src/vs/platform/environment/node/shellEnv.ts index 113ec039ece..8125c0baa50 100644 --- a/src/vs/platform/environment/node/shellEnv.ts +++ b/src/vs/platform/environment/node/shellEnv.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; -import { IProcessEnvironment, isWindows, platform } from 'vs/base/common/platform'; +import { IProcessEnvironment, isWindows, OS } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { NativeParsedArgs } from 'vs/platform/environment/common/argv'; import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper'; @@ -77,7 +77,7 @@ async function doResolveUnixShellEnv(logService: ILogService): Promise(); + private readonly directories = new Map(); + + readonly capabilities: FileSystemProviderCapabilities = + FileSystemProviderCapabilities.FileReadWrite + | FileSystemProviderCapabilities.PathCaseSensitive; + + readonly onDidChangeCapabilities = Event.None; + + private readonly _onDidChangeFile = new Emitter(); + readonly onDidChangeFile = this._onDidChangeFile.event; + + private readonly _onDidErrorOccur = new Emitter(); + readonly onDidErrorOccur = this._onDidErrorOccur.event; + + async readFile(resource: URI): Promise { + const handle = await this.getFileHandle(resource); + + if (!handle) { + throw new Error('File not found.'); + } + + const file = await handle.getFile(); + return new Uint8Array(await file.arrayBuffer()); + } + + async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise { + const handle = await this.getFileHandle(resource); + + if (!handle) { + throw new Error('File not found.'); + } + + const writable = await handle.createWritable(); + await writable.write(content); + await writable.close(); + } + + watch(resource: URI, opts: IWatchOptions): IDisposable { + return Disposable.None; + } + + async stat(resource: URI): Promise { + const handler = this.files.get(resource.authority); + + if (handler) { + const file = await handler.getFile(); + + return { + type: FileType.File, + mtime: file.lastModified, + ctime: 0, + size: file.size + }; + } + + if (isRoot(resource)) { + return { + type: FileType.Directory, + mtime: 0, + ctime: 0, + size: 0 + }; + } + + const parent = await this.getParentDirectoryHandle(resource); + + if (!parent) { + throw new Error('Stat error: no parent found'); + } + + const name = extUri.basename(resource); + for await (const [childName, child] of parent) { + if (childName === name) { + if (child.kind === 'file') { + const file = await child.getFile(); + + return { + type: FileType.File, + mtime: file.lastModified, + ctime: 0, + size: file.size + }; + } else { + return { + type: FileType.Directory, + mtime: 0, + ctime: 0, + size: 0 + }; + } + } + } + + throw new Error('Stat error: entry not found'); + } + + mkdir(resource: URI): Promise { + throw new Error('Method not implemented.'); + } + + async readdir(resource: URI): Promise<[string, FileType][]> { + const parent = await this.getDirectoryHandle(resource); + + if (!parent) { + throw new Error('Stat error: no parent found'); + } + + const result: [string, FileType][] = []; + + for await (const [name, child] of parent) { + result.push([name, child.kind === 'file' ? FileType.File : FileType.Directory]); + } + + return result; + } + + delete(resource: URI, opts: FileDeleteOptions): Promise { + throw new Error('Method not implemented: delete'); + } + + rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise { + throw new Error('Method not implemented: rename'); + } + + private async getDirectoryHandle(uri: URI): Promise { + if (isRoot(uri)) { + return this.directories.get(uri.authority); + } + + const splitResult = split(uri.path); + + if (!splitResult) { + return undefined; + } + + const parent = await this.getDirectoryHandle(URI.from({ ...uri, path: splitResult[0] })); + return await parent?.getDirectoryHandle(extUri.basename(uri)); + } + + private async getParentDirectoryHandle(uri: URI): Promise { + return this.getDirectoryHandle(URI.from({ ...uri, path: extUri.dirname(uri).path })); + } + + private async getFileHandle(uri: URI): Promise { + const result = this.files.get(uri.authority); + + if (result) { + return result; + } + + const parent = await this.getParentDirectoryHandle(uri); + const name = extUri.basename(uri); + return await parent?.getFileHandle(name); + } + + registerFileHandle(uuid: string, handle: FileSystemFileHandle): void { + this.files.set(uuid, handle); + } + + dispose(): void { + this._onDidChangeFile.dispose(); + } +} diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index e1f24089f39..0453e6f4c60 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -71,6 +71,10 @@ export class FileService extends Disposable implements IFileService { }); } + getProvider(scheme: string): IFileSystemProvider | undefined { + return this.provider.get(scheme); + } + async activateProvider(scheme: string): Promise { // Emit an event that we are about to activate a provider with the given scheme. diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index f99d94f0be7..f2543a85c6f 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -46,6 +46,11 @@ export interface IFileService { */ registerProvider(scheme: string, provider: IFileSystemProvider): IDisposable; + /** + * Returns a file system provider for a certain scheme. + */ + getProvider(scheme: string): IFileSystemProvider | undefined; + /** * Tries to activate a provider with the given scheme. */ @@ -199,7 +204,7 @@ export interface FileOverwriteOptions { * Set to `true` to overwrite a file if it exists. Will * throw an error otherwise if the file does exist. */ - overwrite: boolean; + readonly overwrite: boolean; } export interface FileUnlockOptions { @@ -209,7 +214,7 @@ export interface FileUnlockOptions { * have. A file that is write locked will throw an error for any * attempt to write to unless `unlock: true` is provided. */ - unlock: boolean; + readonly unlock: boolean; } export interface FileReadStreamOptions { @@ -241,7 +246,7 @@ export interface FileWriteOptions extends FileOverwriteOptions, FileUnlockOption * Set to `true` to create a file when it does not exist. Will * throw an error otherwise if the file does not exist. */ - create: boolean; + readonly create: boolean; } export type FileOpenOptions = FileOpenForReadOptions | FileOpenForWriteOptions; @@ -255,7 +260,7 @@ export interface FileOpenForReadOptions { /** * A hint that the file should be opened for reading only. */ - create: false; + readonly create: false; } export interface FileOpenForWriteOptions extends FileUnlockOptions { @@ -263,7 +268,7 @@ export interface FileOpenForWriteOptions extends FileUnlockOptions { /** * A hint that the file should be opened for reading and writing. */ - create: true; + readonly create: true; } export interface FileDeleteOptions { @@ -273,14 +278,14 @@ export interface FileDeleteOptions { * only applies to folders and can lead to an error unless provided * if the folder is not empty. */ - recursive: boolean; + readonly recursive: boolean; /** * Set to `true` to attempt to move the file to trash * instead of deleting it permanently from disk. This * option maybe not be supported on all providers. */ - useTrash: boolean; + readonly useTrash: boolean; } export enum FileType { @@ -315,17 +320,17 @@ export interface IStat { /** * The file type. */ - type: FileType; + readonly type: FileType; /** * The last modification date represented as millis from unix epoch. */ - mtime: number; + readonly mtime: number; /** * The creation date represented as millis from unix epoch. */ - ctime: number; + readonly ctime: number; /** * The size of the file in bytes. @@ -339,7 +344,7 @@ export interface IWatchOptions { * Set to `true` to watch for changes recursively in a folder * and all of its children. */ - recursive: boolean; + readonly recursive: boolean; /** * A set of paths to exclude from watching. @@ -561,18 +566,18 @@ export function toFileOperationResult(error: Error): FileOperationResult { } export interface IFileSystemProviderRegistrationEvent { - added: boolean; - scheme: string; - provider?: IFileSystemProvider; + readonly added: boolean; + readonly scheme: string; + readonly provider?: IFileSystemProvider; } export interface IFileSystemProviderCapabilitiesChangeEvent { - provider: IFileSystemProvider; - scheme: string; + readonly provider: IFileSystemProvider; + readonly scheme: string; } export interface IFileSystemProviderActivationEvent { - scheme: string; + readonly scheme: string; join(promise: Promise): void; } @@ -823,13 +828,13 @@ interface IBaseStat { /** * The unified resource identifier of this file or folder. */ - resource: URI; + readonly resource: URI; /** * The name which is the last segment * of the {{path}}. */ - name: string; + readonly name: string; /** * The size of the file. @@ -837,7 +842,7 @@ interface IBaseStat { * The value may or may not be resolved as * it is optional. */ - size?: number; + readonly size?: number; /** * The last modification date represented as millis from unix epoch. @@ -845,7 +850,7 @@ interface IBaseStat { * The value may or may not be resolved as * it is optional. */ - mtime?: number; + readonly mtime?: number; /** * The creation date represented as millis from unix epoch. @@ -853,7 +858,7 @@ interface IBaseStat { * The value may or may not be resolved as * it is optional. */ - ctime?: number; + readonly ctime?: number; /** * A unique identifier thet represents the @@ -862,7 +867,7 @@ interface IBaseStat { * The value may or may not be resolved as * it is optional. */ - etag?: string; + readonly etag?: string; } export interface IBaseStatWithMetadata extends Required { } @@ -875,12 +880,12 @@ export interface IFileStat extends IBaseStat { /** * The resource is a file. */ - isFile: boolean; + readonly isFile: boolean; /** * The resource is a directory. */ - isDirectory: boolean; + readonly isDirectory: boolean; /** * The resource is a symbolic link. Note: even when the @@ -888,7 +893,7 @@ export interface IFileStat extends IBaseStat { * and `FileType.Directory` to know the type of the target * the link points to. */ - isSymbolicLink: boolean; + readonly isSymbolicLink: boolean; /** * The children of the file stat or undefined if none. @@ -897,20 +902,20 @@ export interface IFileStat extends IBaseStat { } export interface IFileStatWithMetadata extends IFileStat, IBaseStatWithMetadata { - mtime: number; - ctime: number; - etag: string; - size: number; - children?: IFileStatWithMetadata[]; + readonly mtime: number; + readonly ctime: number; + readonly etag: string; + readonly size: number; + readonly children?: IFileStatWithMetadata[]; } export interface IResolveFileResult { - stat?: IFileStat; - success: boolean; + readonly stat?: IFileStat; + readonly success: boolean; } export interface IResolveFileResultWithMetadata extends IResolveFileResult { - stat?: IFileStatWithMetadata; + readonly stat?: IFileStatWithMetadata; } export interface IFileContent extends IBaseStatWithMetadata { @@ -918,7 +923,7 @@ export interface IFileContent extends IBaseStatWithMetadata { /** * The content of a file as buffer. */ - value: VSBuffer; + readonly value: VSBuffer; } export interface IFileStreamContent extends IBaseStatWithMetadata { @@ -926,7 +931,7 @@ export interface IFileStreamContent extends IBaseStatWithMetadata { /** * The content of a file as stream. */ - value: VSBufferReadableStream; + readonly value: VSBufferReadableStream; } export interface IBaseReadFileOptions extends FileReadStreamOptions { @@ -1126,6 +1131,7 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; * Helper to format a raw byte size into a human readable label. */ export class ByteSize { + static readonly KB = 1024; static readonly MB = ByteSize.KB * ByteSize.KB; static readonly GB = ByteSize.MB * ByteSize.KB; @@ -1159,8 +1165,8 @@ export class ByteSize { // Native only: Arch limits export interface IArchLimits { - maxFileSize: number; - maxHeapSize: number; + readonly maxFileSize: number; + readonly maxHeapSize: number; } export const enum Arch { diff --git a/src/vs/platform/files/common/io.ts b/src/vs/platform/files/common/io.ts index dab87ae0bc2..31ccfcacb6d 100644 --- a/src/vs/platform/files/common/io.ts +++ b/src/vs/platform/files/common/io.ts @@ -10,6 +10,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IFileSystemProviderWithOpenReadWriteCloseCapability, FileReadStreamOptions, createFileSystemProviderError, FileSystemProviderErrorCode, ensureFileSystemProviderError } from 'vs/platform/files/common/files'; import { canceled } from 'vs/base/common/errors'; import { IErrorTransformer, IDataTransformer, WriteableStream } from 'vs/base/common/stream'; +import product from 'vs/platform/product/common/product'; export interface ICreateReadStreamOptions extends FileReadStreamOptions { @@ -127,7 +128,7 @@ function throwIfTooLarge(totalBytesRead: number, options: ICreateReadStreamOptio // Return early if file is too large to load and we have configured limits if (options?.limits) { if (typeof options.limits.memory === 'number' && totalBytesRead > options.limits.memory) { - throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), FileSystemProviderErrorCode.FileExceedsMemoryLimit); + throw createFileSystemProviderError(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", product.nameShort), FileSystemProviderErrorCode.FileExceedsMemoryLimit); } if (typeof options.limits.size === 'number' && totalBytesRead > options.limits.size) { diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 7fa3f3fdb0f..cc36b01d8a7 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -22,6 +22,7 @@ suite('File Service', () => { const provider = new NullFileSystemProvider(); assert.strictEqual(service.canHandleResource(resource), false); + assert.strictEqual(service.getProvider(resource.scheme), undefined); const registrations: IFileSystemProviderRegistrationEvent[] = []; service.onDidChangeFileSystemProviderRegistrations(e => { @@ -50,6 +51,7 @@ suite('File Service', () => { await service.activateProvider('test'); assert.strictEqual(service.canHandleResource(resource), true); + assert.strictEqual(service.getProvider(resource.scheme), provider); assert.strictEqual(registrations.length, 1); assert.strictEqual(registrations[0].scheme, 'test'); diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index caf76f32f3b..e6be4645610 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -288,7 +288,7 @@ export class IssueMainService implements ICommonIssueService { backgroundColor: backgroundColor || IssueMainService.DEFAULT_BACKGROUND_COLOR, webPreferences: { preload: FileAccess.asFileUri('vs/base/parts/sandbox/electron-browser/preload.js', require).fsPath, - additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`], + additionalArguments: [`--vscode-window-config=${ipcObjectUrl.resource.toString()}`, '--context-isolation' /* TODO@bpasero: Use process.contextIsolateed when 13-x-y is adopted (https://github.com/electron/electron/pull/28030) */], v8CacheOptions: browserCodeLoadingCacheStrategy, enableWebSQL: false, enableRemoteModule: false, diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index 073308a7844..f45a42fb325 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -698,11 +698,14 @@ abstract class ResourceNavigator extends Disposable { class ListResourceNavigator extends ResourceNavigator { + protected override readonly widget: List | PagedList; + constructor( - protected readonly widget: List | PagedList, + widget: List | PagedList, options: IResourceNavigatorOptions ) { super(widget, options); + this.widget = widget; } getSelectedElement(): T | undefined { @@ -712,8 +715,10 @@ class ListResourceNavigator extends ResourceNavigator { class TableResourceNavigator extends ResourceNavigator { + protected override readonly widget!: Table; + constructor( - protected readonly widget: Table, + widget: Table, options: IResourceNavigatorOptions ) { super(widget, options); @@ -726,8 +731,10 @@ class TableResourceNavigator extends ResourceNavigator { class TreeResourceNavigator extends ResourceNavigator { + protected override readonly widget!: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree; + constructor( - protected readonly widget: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree, + widget: ObjectTree | CompressibleObjectTree | DataTree | AsyncDataTree | CompressibleAsyncDataTree, options: IResourceNavigatorOptions ) { super(widget, options); diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index a564507a4dc..f9ab5a1e3e5 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -569,10 +569,7 @@ export class Menubar { return [new MenuItem({ label: this.mnemonicLabel(nls.localize('miCheckForUpdates', "Check for &&Updates...")), click: () => setTimeout(() => { this.reportMenuActionTelemetry('CheckForUpdate'); - - const window = this.windowsMainService.getLastActiveWindow(); - const context = window && `window:${window.id}`; // sessionId - this.updateService.checkForUpdates(context); + this.updateService.checkForUpdates(true); }, 0) })]; diff --git a/src/vs/platform/notification/common/notification.ts b/src/vs/platform/notification/common/notification.ts index 130e513d134..6248169a381 100644 --- a/src/vs/platform/notification/common/notification.ts +++ b/src/vs/platform/notification/common/notification.ts @@ -72,6 +72,13 @@ export interface INeverShowAgainOptions { export interface INotification extends INotificationProperties { + /** + * The id of the notification. If provided, will be used to compare + * notifications with others to decide whether a notification is + * duplicate or not. + */ + readonly id?: string; + /** * The severity of the notification. Either `Info`, `Warning` or `Error`. */ diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index d2f056af402..52902edfcbc 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -15,8 +15,15 @@ export interface ResolvedAuthority { readonly connectionToken: string | undefined; } +export enum RemoteTrustOption { + Unknown = 0, + DisableTrust = 1, + MachineTrusted = 2 +} + export interface ResolvedOptions { readonly extensionHostEnv?: { [key: string]: string | null }; + readonly trust?: RemoteTrustOption; } export interface TunnelDescription { diff --git a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts index 5a9b92a156b..64168de41a6 100644 --- a/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts +++ b/src/vs/platform/sharedProcess/electron-main/sharedProcess.ts @@ -172,6 +172,7 @@ export class SharedProcess extends Disposable implements ISharedProcess { additionalArguments: [`--vscode-window-config=${configObjectUrl.resource.toString()}`], v8CacheOptions: browserCodeLoadingCacheStrategy, nodeIntegration: true, + contextIsolation: false, enableWebSQL: false, enableRemoteModule: false, spellcheck: false, diff --git a/src/vs/platform/telemetry/common/commonProperties.ts b/src/vs/platform/telemetry/common/commonProperties.ts index 46f08b5d3ce..454b873fde1 100644 --- a/src/vs/platform/telemetry/common/commonProperties.ts +++ b/src/vs/platform/telemetry/common/commonProperties.ts @@ -4,14 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { IFileService } from 'vs/platform/files/common/files'; -import { isLinuxSnap, PlatformToString, platform } from 'vs/base/common/platform'; +import { isLinuxSnap, PlatformToString, platform, Platform } from 'vs/base/common/platform'; import { platform as nodePlatform, env } from 'vs/base/common/process'; import { generateUuid } from 'vs/base/common/uuid'; import { URI } from 'vs/base/common/uri'; +function getPlatformDetail(hostname: string): string | undefined { + if (platform === Platform.Linux && /^penguin(\.|$)/i.test(hostname)) { + return 'chromebook'; + } + + return undefined; +} + export async function resolveCommonProperties( fileService: IFileService, release: string, + hostname: string, arch: string, commit: string | undefined, version: string | undefined, @@ -73,6 +82,13 @@ export async function resolveCommonProperties( result['common.snap'] = 'true'; } + const platformDetail = getPlatformDetail(hostname); + + if (platformDetail) { + // __GDPR__COMMON__ "common.platformDetail" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + result['common.platformDetail'] = platformDetail; + } + try { const contents = await fileService.readFile(URI.file(installSourcePath)); diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index f30cde48b64..bd57a847d2c 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -10,7 +10,7 @@ import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/t import { ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils'; import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { cloneAndChange, mixin } from 'vs/base/common/objects'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -223,6 +223,8 @@ Registry.as(Extensions.Configuration).registerConfigurat localize('telemetry.enableTelemetry', "Enable usage data and errors to be sent to a Microsoft online service.") : localize('telemetry.enableTelemetryMd', "Enable usage data and errors to be sent to a Microsoft online service. Read our privacy statement [here]({0}).", product.privacyStatementUrl), 'default': true, + 'requireTrust': true, + 'scope': ConfigurationScope.APPLICATION, 'tags': ['usesOnlineServices'] } } diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index ac1ba4f8ad2..4ed05490e0d 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -5,7 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; @@ -95,6 +95,8 @@ export interface IOffProcessTerminalService { attachToProcess(id: number): Promise; listProcesses(): Promise; + getDefaultSystemShell(osOverride?: OperatingSystem): Promise; + getShellEnvironment(): Promise; setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise; getTerminalLayoutInfo(): Promise; reduceConnectionGraceTime(): Promise; @@ -159,6 +161,8 @@ export interface IPtyService { /** Confirm the process is _not_ an orphan. */ orphanQuestionReply(id: number): Promise; + getDefaultSystemShell(osOverride?: OperatingSystem): Promise; + getShellEnvironment(): Promise; setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise; getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise; reduceConnectionGraceTime(): Promise; diff --git a/src/vs/platform/terminal/node/ptyHostService.ts b/src/vs/platform/terminal/node/ptyHostService.ts index 507586b21aa..6b1740731e9 100644 --- a/src/vs/platform/terminal/node/ptyHostService.ts +++ b/src/vs/platform/terminal/node/ptyHostService.ts @@ -9,7 +9,7 @@ import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensions import { Client } from 'vs/base/parts/ipc/node/ipc.cp'; import { FileAccess } from 'vs/base/common/network'; import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { Emitter } from 'vs/base/common/event'; import { LogLevelChannelClient } from 'vs/platform/log/common/logIpc'; import { IGetTerminalLayoutInfoArgs, IProcessDetails, IPtyHostProcessReplayEvent, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; @@ -170,6 +170,9 @@ export class PtyHostService extends Disposable implements IPtyService { input(id: number, data: string): Promise { return this._proxy.input(id, data); } + processBinary(id: number, data: string): Promise { + return this._proxy.processBinary(id, data); + } resize(id: number, cols: number, rows: number): Promise { return this._proxy.resize(id, cols, rows); } @@ -188,12 +191,17 @@ export class PtyHostService extends Disposable implements IPtyService { orphanQuestionReply(id: number): Promise { return this._proxy.orphanQuestionReply(id); } + + getDefaultSystemShell(osOverride?: OperatingSystem): Promise { + return this._proxy.getDefaultSystemShell(osOverride); + } + getShellEnvironment(): Promise { + return this._proxy.getShellEnvironment(); + } + setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { return this._proxy.setTerminalLayoutInfo(args); } - processBinary(id: number, data: string): Promise { - return this._proxy.processBinary(id, data); - } async getTerminalLayoutInfo(args: IGetTerminalLayoutInfoArgs): Promise { return await this._proxy.getTerminalLayoutInfo(args); } diff --git a/src/vs/platform/terminal/node/ptyService.ts b/src/vs/platform/terminal/node/ptyService.ts index d43d676c159..12f014b477b 100644 --- a/src/vs/platform/terminal/node/ptyService.ts +++ b/src/vs/platform/terminal/node/ptyService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, OperatingSystem, OS } from 'vs/base/common/platform'; import { IPtyService, IProcessDataEvent, IShellLaunchConfig, ITerminalDimensionsOverride, ITerminalLaunchError, LocalReconnectConstants, ITerminalsLayoutInfo, IRawTerminalInstanceLayoutInfo, ITerminalTabLayoutInfoById, ITerminalInstanceLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { AutoOpenBarrier, Queue, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter } from 'vs/base/common/event'; @@ -13,6 +13,7 @@ import { TerminalProcess } from 'vs/platform/terminal/node/terminalProcess'; import { ISetTerminalLayoutInfoArgs, ITerminalTabLayoutInfoDto, IProcessDetails, IGetTerminalLayoutInfoArgs, IPtyHostProcessReplayEvent } from 'vs/platform/terminal/common/terminalProcess'; import { ILogService } from 'vs/platform/log/common/log'; import { TerminalDataBufferer } from 'vs/platform/terminal/common/terminalDataBuffering'; +import { getSystemShell } from 'vs/base/node/shell'; type WorkspaceId = string; @@ -141,6 +142,9 @@ export class PtyService extends Disposable implements IPtyService { async input(id: number, data: string): Promise { return this._throwIfNoPty(id).input(data); } + async processBinary(id: number, data: string): Promise { + return this._throwIfNoPty(id).writeBinary(data); + } async resize(id: number, cols: number, rows: number): Promise { return this._throwIfNoPty(id).resize(cols, rows); } @@ -160,8 +164,12 @@ export class PtyService extends Disposable implements IPtyService { return this._throwIfNoPty(id).orphanQuestionReply(); } - async processBinary(id: number, data: string): Promise { - return this._throwIfNoPty(id).writeBinary(data); + async getDefaultSystemShell(osOverride: OperatingSystem = OS): Promise { + return getSystemShell(osOverride, process.env); + } + + async getShellEnvironment(): Promise { + return { ...process.env }; } async setTerminalLayoutInfo(args: ISetTerminalLayoutInfoArgs): Promise { diff --git a/src/vs/platform/terminal/node/terminalProcess.ts b/src/vs/platform/terminal/node/terminalProcess.ts index 920a9de05e2..f5f6feadc1d 100644 --- a/src/vs/platform/terminal/node/terminalProcess.ts +++ b/src/vs/platform/terminal/node/terminalProcess.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; import type * as pty from 'node-pty'; import * as fs from 'fs'; import * as os from 'os'; @@ -17,6 +16,7 @@ import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { WindowsShellHelper } from 'vs/platform/terminal/node/windowsShellHelper'; +import { IProcessEnvironment, isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; // Writing large amounts of data can be corrupted for some reason, after looking into this is // appears to be a race condition around writing to the FD which may be based on how powerful the @@ -91,17 +91,17 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess cwd: string, cols: number, rows: number, - env: platform.IProcessEnvironment, + env: IProcessEnvironment, /** * environment used for `findExecutable` */ - private readonly _executableEnv: platform.IProcessEnvironment, + private readonly _executableEnv: IProcessEnvironment, windowsEnableConpty: boolean, @ILogService private readonly _logService: ILogService ) { super(); let name: string; - if (platform.isWindows) { + if (isWindows) { name = path.basename(this._shellLaunchConfig.executable || ''); } else { // Using 'xterm-256color' here helps ensure that the majority of Linux distributions will use a @@ -122,7 +122,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess conptyInheritCursor: useConpty && !!_shellLaunchConfig.initialText }; // Delay resizes to avoid conpty not respecting very early resize calls - if (platform.isWindows) { + if (isWindows) { if (useConpty && cols === 0 && rows === 0 && this._shellLaunchConfig.executable?.endsWith('Git\\bin\\bash.exe')) { this._delayedResizer = new DelayedResizer(); this._register(this._delayedResizer.onTrigger(dimensions => { @@ -248,7 +248,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess // Send initial timeout async to give event listeners a chance to init setTimeout(() => this._sendProcessTitle(ptyProcess), 0); // Setup polling for non-Windows, for Windows `process` doesn't change - if (!platform.isWindows) { + if (!isWindows) { this._titleInterval = setInterval(() => { if (this._currentTitle !== ptyProcess.process) { this._sendProcessTitle(ptyProcess); @@ -425,7 +425,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } public getCwd(): Promise { - if (platform.isMacintosh) { + if (isMacintosh) { // Disable cwd lookup on macOS Big Sur due to spawn blocking thread (darwin v20 is macOS // Big Sur) https://github.com/Microsoft/vscode/issues/105446 const osRelease = os.release().split('.'); @@ -448,7 +448,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } } - if (platform.isLinux) { + if (isLinux) { return new Promise(resolve => { if (!this._ptyProcess) { resolve(this._initialCwd); diff --git a/src/vs/platform/terminal/node/windowsShellHelper.ts b/src/vs/platform/terminal/node/windowsShellHelper.ts index 1fed411ed95..64c3d205db2 100644 --- a/src/vs/platform/terminal/node/windowsShellHelper.ts +++ b/src/vs/platform/terminal/node/windowsShellHelper.ts @@ -3,13 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as platform from 'vs/base/common/platform'; import { Emitter, Event } from 'vs/base/common/event'; import type * as WindowsProcessTreeType from 'windows-process-tree'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { TerminalShellType, WindowsShellType } from 'vs/platform/terminal/common/terminal'; import { debounce } from 'vs/base/common/decorators'; import { timeout } from 'vs/base/common/async'; +import { isWindows, platform } from 'vs/base/common/platform'; export interface IWindowsShellHelper extends IDisposable { readonly onShellNameChanged: Event; @@ -51,8 +51,8 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe ) { super(); - if (!platform.isWindows) { - throw new Error(`WindowsShellHelper cannot be instantiated on ${platform.platform}`); + if (!isWindows) { + throw new Error(`WindowsShellHelper cannot be instantiated on ${platform}`); } this._isDisposed = false; @@ -69,7 +69,7 @@ export class WindowsShellHelper extends Disposable implements IWindowsShellHelpe @debounce(500) async checkShell(): Promise { - if (platform.isWindows) { + if (isWindows) { // Wait to give the shell some time to actually launch a process, this // could lead to a race condition but it would be recovered from when // data stops and should cover the majority of cases diff --git a/src/vs/platform/update/common/update.ts b/src/vs/platform/update/common/update.ts index 13e322a9115..cf66654c330 100644 --- a/src/vs/platform/update/common/update.ts +++ b/src/vs/platform/update/common/update.ts @@ -51,7 +51,7 @@ export const enum UpdateType { export type Uninitialized = { type: StateType.Uninitialized }; export type Idle = { type: StateType.Idle, updateType: UpdateType, error?: string }; -export type CheckingForUpdates = { type: StateType.CheckingForUpdates, context: any }; +export type CheckingForUpdates = { type: StateType.CheckingForUpdates, explicit: boolean }; export type AvailableForDownload = { type: StateType.AvailableForDownload, update: IUpdate }; export type Downloading = { type: StateType.Downloading, update: IUpdate }; export type Downloaded = { type: StateType.Downloaded, update: IUpdate }; @@ -63,7 +63,7 @@ export type State = Uninitialized | Idle | CheckingForUpdates | AvailableForDown export const State = { Uninitialized: { type: StateType.Uninitialized } as Uninitialized, Idle: (updateType: UpdateType, error?: string) => ({ type: StateType.Idle, updateType, error }) as Idle, - CheckingForUpdates: (context: any) => ({ type: StateType.CheckingForUpdates, context } as CheckingForUpdates), + CheckingForUpdates: (explicit: boolean) => ({ type: StateType.CheckingForUpdates, explicit } as CheckingForUpdates), AvailableForDownload: (update: IUpdate) => ({ type: StateType.AvailableForDownload, update } as AvailableForDownload), Downloading: (update: IUpdate) => ({ type: StateType.Downloading, update } as Downloading), Downloaded: (update: IUpdate) => ({ type: StateType.Downloaded, update } as Downloaded), @@ -86,7 +86,7 @@ export interface IUpdateService { readonly onStateChange: Event; readonly state: State; - checkForUpdates(context: any): Promise; + checkForUpdates(explicit: boolean): Promise; downloadUpdate(): Promise; applyUpdate(): Promise; quitAndInstall(): Promise; diff --git a/src/vs/platform/update/common/updateIpc.ts b/src/vs/platform/update/common/updateIpc.ts index 26dcaa321b1..6340754edbd 100644 --- a/src/vs/platform/update/common/updateIpc.ts +++ b/src/vs/platform/update/common/updateIpc.ts @@ -52,8 +52,8 @@ export class UpdateChannelClient implements IUpdateService { this.channel.call('_getInitialState').then(state => this.state = state); } - checkForUpdates(context: any): Promise { - return this.channel.call('checkForUpdates', context); + checkForUpdates(explicit: boolean): Promise { + return this.channel.call('checkForUpdates', explicit); } downloadUpdate(): Promise { diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 29e938df2ef..289b57cef16 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -97,7 +97,7 @@ export abstract class AbstractUpdateService implements IUpdateService { this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference'); // Check for updates only once after 30 seconds - setTimeout(() => this.checkForUpdates(null), 30 * 1000); + setTimeout(() => this.checkForUpdates(false), 30 * 1000); } else { // Start checking for updates after 30 seconds this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err)); @@ -110,21 +110,21 @@ export abstract class AbstractUpdateService implements IUpdateService { private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { return timeout(delay) - .then(() => this.checkForUpdates(null)) + .then(() => this.checkForUpdates(false)) .then(() => { // Check again after 1 hour return this.scheduleCheckForUpdates(60 * 60 * 1000); }); } - async checkForUpdates(context: any): Promise { + async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); if (this.state.type !== StateType.Idle) { return; } - this.doCheckForUpdates(context); + this.doCheckForUpdates(explicit); } async downloadUpdate(): Promise { diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index 8a057282a37..401da74a16b 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -50,7 +50,7 @@ export class DarwinUpdateService extends AbstractUpdateService { this.logService.error('UpdateService error:', err); // only show message when explicitly checking for updates - const shouldShowMessage = this.state.type === StateType.CheckingForUpdates ? !!this.state.context : true; + const shouldShowMessage = this.state.type === StateType.CheckingForUpdates ? this.state.explicit : true; const message: string | undefined = shouldShowMessage ? err : undefined; this.setState(State.Idle(UpdateType.Archive, message)); } @@ -103,7 +103,7 @@ export class DarwinUpdateService extends AbstractUpdateService { if (this.state.type !== StateType.CheckingForUpdates) { return; } - this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: !!this.state.context }); + this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: this.state.explicit }); this.setState(State.Idle(UpdateType.Archive)); } diff --git a/src/vs/platform/update/electron-main/updateService.snap.ts b/src/vs/platform/update/electron-main/updateService.snap.ts index 505adb5dd38..d14018a83d7 100644 --- a/src/vs/platform/update/electron-main/updateService.snap.ts +++ b/src/vs/platform/update/electron-main/updateService.snap.ts @@ -15,7 +15,7 @@ import { spawn } from 'child_process'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { UpdateNotAvailableClassification } from 'vs/platform/update/electron-main/abstractUpdateService'; -abstract class AbstractUpdateService2 implements IUpdateService { +abstract class AbstractUpdateService implements IUpdateService { declare readonly _serviceBrand: undefined; @@ -52,21 +52,21 @@ abstract class AbstractUpdateService2 implements IUpdateService { private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise { return timeout(delay) - .then(() => this.checkForUpdates(null)) + .then(() => this.checkForUpdates(false)) .then(() => { // Check again after 1 hour return this.scheduleCheckForUpdates(60 * 60 * 1000); }); } - async checkForUpdates(context: any): Promise { + async checkForUpdates(explicit: boolean): Promise { this.logService.trace('update#checkForUpdates, state = ', this.state.type); if (this.state.type !== StateType.Idle) { return; } - this.doCheckForUpdates(context); + this.doCheckForUpdates(explicit); } async downloadUpdate(): Promise { @@ -132,7 +132,7 @@ abstract class AbstractUpdateService2 implements IUpdateService { protected abstract doCheckForUpdates(context: any): void; } -export class SnapUpdateService extends AbstractUpdateService2 { +export class SnapUpdateService extends AbstractUpdateService { constructor( private snap: string, @@ -148,7 +148,7 @@ export class SnapUpdateService extends AbstractUpdateService2 { const onChange = Event.fromNodeEventEmitter(watcher, 'change', (_, fileName: string) => fileName); const onCurrentChange = Event.filter(onChange, n => n === 'current'); const onDebouncedCurrentChange = Event.debounce(onCurrentChange, (_, e) => e, 2000); - const listener = onDebouncedCurrentChange(this.checkForUpdates, this); + const listener = onDebouncedCurrentChange(() => this.checkForUpdates(false)); lifecycleMainService.onWillShutdown(() => { listener.dispose(); @@ -156,19 +156,19 @@ export class SnapUpdateService extends AbstractUpdateService2 { }); } - protected doCheckForUpdates(context: any): void { - this.setState(State.CheckingForUpdates(context)); + protected doCheckForUpdates(): void { + this.setState(State.CheckingForUpdates(false)); this.isUpdateAvailable().then(result => { if (result) { this.setState(State.Ready({ version: 'something', productVersion: 'something' })); } else { - this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: !!context }); + this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: false }); this.setState(State.Idle(UpdateType.Snap)); } }, err => { this.logService.error(err); - this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: !!context }); + this.telemetryService.publicLog2<{ explicit: boolean }, UpdateNotAvailableClassification>('update:notAvailable', { explicit: false }); this.setState(State.Idle(UpdateType.Snap, err.message || err)); }); } diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index a810dba1382..9fb6e838d52 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -219,8 +219,6 @@ export interface IColorScheme { } export interface IWindowConfiguration { - sessionId: string; - remoteAuthority?: string; colorScheme: IColorScheme; @@ -232,6 +230,7 @@ export interface IWindowConfiguration { export interface IOSConfiguration { readonly release: string; + readonly hostname: string; } export interface INativeWindowConfiguration extends IWindowConfiguration, NativeParsedArgs, ISandboxConfiguration { diff --git a/src/vs/platform/windows/electron-main/window.ts b/src/vs/platform/windows/electron-main/window.ts index 524765294c1..63d55023bf7 100644 --- a/src/vs/platform/windows/electron-main/window.ts +++ b/src/vs/platform/windows/electron-main/window.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { getMarks, mark } from 'vs/base/common/performance'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, Event, RenderProcessGoneDetails } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, Event, RenderProcessGoneDetails, WebFrameMain } from 'electron'; import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -195,13 +195,13 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Sandbox { - sandbox: true, - contextIsolation: true + sandbox: true } : // No Sandbox { - nodeIntegration: true + nodeIntegration: true, + contextIsolation: false } } }; @@ -431,12 +431,24 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Block all SVG requests from unsupported origins const supportedSvgSchemes = new Set([Schemas.file, Schemas.vscodeFileResource, Schemas.vscodeRemoteResource, 'devtools']); // TODO: handle webview origin + + // But allow them if the are made from inside an webview + const isSafeFrame = (requestFrame: WebFrameMain | undefined): boolean => { + for (let frame: WebFrameMain | null | undefined = requestFrame; frame; frame = frame.parent) { + if (frame.url.startsWith(`${Schemas.vscodeWebview}://`)) { + return true; + } + } + return false; + }; + this._win.webContents.session.webRequest.onBeforeRequest((details, callback) => { const uri = URI.parse(details.url); if (uri.path.endsWith('.svg')) { - const safeScheme = supportedSvgSchemes.has(uri.scheme) || uri.path.includes(Schemas.vscodeRemoteResource); - if (!safeScheme) { - return callback({ cancel: true }); + const isSafeResourceUrl = supportedSvgSchemes.has(uri.scheme) || uri.path.includes(Schemas.vscodeRemoteResource); + if (!isSafeResourceUrl) { + const isSafeContext = isSafeFrame(details.frame); + return callback({ cancel: !isSafeContext }); } } @@ -462,7 +474,8 @@ export class CodeWindow extends Disposable implements ICodeWindow { // remote extension schemes have the following format // http://127.0.0.1:/vscode-remote-resource?path= if (!uri.path.includes(Schemas.vscodeRemoteResource) && contentTypes.some(contentType => contentType.toLowerCase().includes('image/svg'))) { - return callback({ cancel: true }); + const isSafeContext = isSafeFrame(details.frame); + return callback({ cancel: !isSafeContext }); } } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 5a874111bcb..4f66b0be81f 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { statSync } from 'fs'; -import { release } from 'os'; +import { release, hostname } from 'os'; import product from 'vs/platform/product/common/product'; import { mark, getMarks } from 'vs/base/common/performance'; import { basename, normalize, join, posix } from 'vs/base/common/path'; @@ -341,7 +341,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // process can continue. We do this by deleting the waitMarkerFilePath. const waitMarkerFileURI = openConfig.waitMarkerFileURI; if (openConfig.context === OpenContext.CLI && waitMarkerFileURI && usedWindows.length === 1 && usedWindows[0]) { - usedWindows[0].whenClosedOrLoaded.then(() => this.fileService.del(waitMarkerFileURI), () => undefined); + (async () => { + await usedWindows[0].whenClosedOrLoaded; + + try { + await this.fileService.del(waitMarkerFileURI); + } catch (error) { + // ignore - could have been deleted from the window already + } + })(); } return usedWindows; @@ -1154,7 +1162,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic machineId: this.machineId, - sessionId: '', // Will be filled in by the window once loaded later windowId: -1, // Will be filled in by the window once loaded later mainPid: process.pid, @@ -1187,7 +1194,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic product, isInitialStartup: options.initialStartup, perfMarks: getMarks(), - os: { release: release() }, + os: { release: release(), hostname: hostname() }, zoomLevel: typeof windowConfig?.zoomLevel === 'number' ? windowConfig.zoomLevel : undefined, autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true, @@ -1268,7 +1275,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Update window identifier and session now // that we have the window object in hand. configuration.windowId = window.id; - configuration.sessionId = `window:${window.id}`; // If the window was already loaded, make sure to unload it // first and only load the new configuration if that was diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index 1e03bc1daad..fec04235be2 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -33,30 +33,23 @@ export interface WorkspaceTrustRequestOptions { } export type WorkspaceTrustChangeEvent = Event; -export const IWorkspaceTrustStorageService = createDecorator('workspaceTrustStorageService'); - -export interface IWorkspaceTrustStorageService { - _serviceBrand: undefined; - - readonly onDidStorageChange: Event; - - setFoldersTrust(folders: URI[], trusted: boolean): void; - getFoldersTrust(folders: URI[]): boolean; - - setTrustedFolders(folders: URI[]): void; - - getFolderTrustStateInfo(folder: URI): IWorkspaceTrustUriInfo; - getTrustStateInfo(): IWorkspaceTrustStateInfo; -} - export const IWorkspaceTrustManagementService = createDecorator('workspaceTrustManagementService'); export interface IWorkspaceTrustManagementService { readonly _serviceBrand: undefined; onDidChangeTrust: WorkspaceTrustChangeEvent; + onDidChangeTrustedFolders: Event; + isWorkpaceTrusted(): boolean; + canSetParentFolderTrust(): boolean; + setParentFolderTrust(trusted: boolean): void; + canSetWorkspaceTrust(): boolean; setWorkspaceTrust(trusted: boolean): void; + getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo; + setFoldersTrust(folders: URI[], trusted: boolean): void; + getTrustedFolders(): URI[]; + setTrustedFolders(folders: URI[]): void; } export const IWorkspaceTrustRequestService = createDecorator('workspaceTrustRequestService'); @@ -77,6 +70,6 @@ export interface IWorkspaceTrustUriInfo { trusted: boolean } -export interface IWorkspaceTrustStateInfo { +export interface IWorkspaceTrustInfo { uriTrustInfo: IWorkspaceTrustUriInfo[] } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 2fcc5cf1f20..382e84d3ffa 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -80,8 +80,16 @@ declare module 'vscode' { constructor(host: string, port: number, connectionToken?: string); } + export enum RemoteTrustOption { + Unknown = 0, + DisableTrust = 1, + MachineTrusted = 2 + } + export interface ResolvedOptions { extensionHostEnv?: { [key: string]: string | null; }; + + trust?: RemoteTrustOption; } export interface TunnelOptions { @@ -988,10 +996,10 @@ declare module 'vscode' { } export interface NotebookCellExecutionSummary { - executionOrder?: number; - success?: boolean; - startTime?: number; - endTime?: number; + readonly executionOrder?: number; + readonly success?: boolean; + readonly startTime?: number; + readonly endTime?: number; } // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md @@ -1035,17 +1043,67 @@ declare module 'vscode' { transientOutputs?: boolean; /** - * Controls if a meetadata property change will trigger notebook document content change and if it will be used in the diff editor - * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + * @deprecated use transientCellMetadata instead */ transientMetadata?: { [K in keyof NotebookCellMetadata]?: boolean }; + + /** + * Controls if a cell metadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientCellMetadata?: { [K in keyof NotebookCellMetadata]?: boolean }; + + /** + * Controls if a document metadata property change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit a metadata property in the file document, it should be set to true. + */ + transientDocumentMetadata?: { [K in keyof NotebookDocumentMetadata]?: boolean }; } + export interface NotebookDocumentContentOptions { + /** + * Not ready for production or development use yet. + */ + viewOptions?: { + displayName: string; + filenamePattern: NotebookFilenamePattern[]; + exclusive?: boolean; + }; + } + + /** + * Represents a notebook. Notebooks are composed of cells and metadata. + */ export interface NotebookDocument { + + /** + * The associated uri for this notebook. + * + * *Note* that most notebooks use the `file`-scheme, which means they are files on disk. However, **not** all notebooks are + * saved on disk and therefore the `scheme` must be checked before trying to access the underlying file or siblings on disk. + * + * @see [FileSystemProvider](#FileSystemProvider) + * @see [TextDocumentContentProvider](#TextDocumentContentProvider) + */ readonly uri: Uri; + + // todo@API should we really expose this? + readonly viewType: string; + + /** + * The version number of this notebook (it will strictly increase after each + * change, including undo/redo). + */ readonly version: number; + /** + * `true` if there are unpersisted changes. + */ readonly isDirty: boolean; + + /** + * Is this notebook representing an untitled file which has not been saved yet. + */ readonly isUntitled: boolean; /** @@ -1054,13 +1112,13 @@ declare module 'vscode' { */ readonly isClosed: boolean; + /** + * The [metadata](#NotebookDocumentMetadata) for this notebook. + */ readonly metadata: NotebookDocumentMetadata; - // todo@API should we really expose this? - readonly viewType: string; - /** - * The number of cells in the notebook document. + * The number of cells in the notebook. */ readonly cellCount: number; @@ -1091,17 +1149,43 @@ declare module 'vscode' { save(): Thenable; } + /** + * A notebook range represents on ordered pair of two cell indicies. + * It is guaranteed that start is less than or equal to end. + */ export class NotebookRange { - readonly start: number; + /** - * exclusive + * The zero-based start index of this range. + */ + readonly start: number; + + /** + * The exclusive end index of this range (zero-based). */ readonly end: number; + /** + * `true` if `start` and `end` are equals + */ readonly isEmpty: boolean; + /** + * Create a new notebook range. If `start` is not + * before or equal to `end`, the values will be swapped. + * + * @param start start index + * @param end end index. + */ constructor(start: number, end: number); + /** + * Derive a new range for this range. + * + * @param change An object that describes a change to this range. + * @return A range that reflects the given change. Will return `this` range if the change + * is not changing anything. + */ with(change: { start?: number, end?: number }): NotebookRange; } @@ -1145,6 +1229,12 @@ declare module 'vscode' { */ readonly visibleRanges: NotebookRange[]; + /** + * Scroll as indicated by `revealType` in order to reveal the given range. + * + * @param range A range. + * @param revealType The scrolling strategy for revealing `range`. + */ revealRange(range: NotebookRange, revealType?: NotebookEditorRevealType): void; /** @@ -1154,6 +1244,9 @@ declare module 'vscode' { } export interface NotebookDocumentMetadataChangeEvent { + /** + * The [notebook document](#NotebookDocument) for which the document metadata have changed. + */ readonly document: NotebookDocument; } @@ -1168,39 +1261,49 @@ declare module 'vscode' { } export interface NotebookCellsChangeEvent { - /** - * The affected document. + * The [notebook document](#NotebookDocument) for which the cells have changed. */ readonly document: NotebookDocument; readonly changes: ReadonlyArray; } export interface NotebookCellOutputsChangeEvent { - /** - * The affected document. + * The [notebook document](#NotebookDocument) for which the cell outputs have changed. */ readonly document: NotebookDocument; readonly cells: NotebookCell[]; } export interface NotebookCellMetadataChangeEvent { + /** + * The [notebook document](#NotebookDocument) for which the cell metadata have changed. + */ readonly document: NotebookDocument; readonly cell: NotebookCell; } export interface NotebookEditorSelectionChangeEvent { + /** + * The [notebook editor](#NotebookEditor) for which the selections have changed. + */ readonly notebookEditor: NotebookEditor; readonly selections: ReadonlyArray } export interface NotebookEditorVisibleRangesChangeEvent { + /** + * The [notebook editor](#NotebookEditor) for which the visible ranges have changed. + */ readonly notebookEditor: NotebookEditor; readonly visibleRanges: ReadonlyArray; } export interface NotebookCellExecutionStateChangeEvent { + /** + * The [notebook document](#NotebookDocument) for which the cell execution state has changed. + */ readonly document: NotebookDocument; readonly cell: NotebookCell; readonly executionState: NotebookCellExecutionState; @@ -1208,13 +1311,11 @@ declare module 'vscode' { // todo@API support ids https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md export class NotebookCellData { - // todo@API should they all be readonly? kind: NotebookCellKind; // todo@API better names: value? text? source: string; // todo@API how does language and MD relate? language: string; - // todo@API ReadonlyArray? outputs?: NotebookCellOutput[]; metadata?: NotebookCellMetadata; latestExecutionSummary?: NotebookCellExecutionSummary; @@ -1222,50 +1323,23 @@ declare module 'vscode' { } export class NotebookData { - // todo@API should they all be readonly? cells: NotebookCellData[]; metadata: NotebookDocumentMetadata; constructor(cells: NotebookCellData[], metadata?: NotebookDocumentMetadata); } - /** - * Communication object passed to the {@link NotebookContentProvider} and - * {@link NotebookOutputRenderer} to communicate with the webview. - */ + /** @deprecated used NotebookController */ export interface NotebookCommunication { - /** - * ID of the editor this object communicates with. A single notebook - * document can have multiple attached webviews and editors, when the - * notebook is split for instance. The editor ID lets you differentiate - * between them. - */ + /** @deprecated used NotebookController */ readonly editorId: string; - - /** - * Fired when the output hosting webview posts a message. - */ + /** @deprecated used NotebookController */ readonly onDidReceiveMessage: Event; - /** - * Post a message to the output hosting webview. - * - * Messages are only delivered if the editor is live. - * - * @param message Body of the message. This must be a string or other json serializable object. - */ + /** @deprecated used NotebookController */ postMessage(message: any): Thenable; - - /** - * Convert a uri for the local file system to one that can be used inside outputs webview. - */ + /** @deprecated used NotebookController */ asWebviewUri(localResource: Uri): Uri; - - // @rebornix - // readonly onDidDispose: Event; } - // export function registerNotebookKernel(selector: string, kernel: NotebookKernel): Disposable; - - export interface NotebookDocumentShowOptions { viewColumn?: ViewColumn; preserveFocus?: boolean; @@ -1411,6 +1485,18 @@ declare module 'vscode' { export type NotebookSelector = NotebookFilter | string | ReadonlyArray; + export interface NotebookExecutionHandler { + /** + * @param cells The notebook cells to execute + * @param controller The controller that the handler is attached to + */ + (this: NotebookController, cells: NotebookCell[], controller: NotebookController): void + } + + export interface NotebookInterruptHandler { + (this: NotebookController): void; + } + export interface NotebookController { readonly id: string; @@ -1427,23 +1513,21 @@ declare module 'vscode' { // UI properties (get/set) label: string; + detail?: string; description?: string; isPreferred?: boolean; supportedLanguages: string[]; hasExecutionOrder?: boolean; - preloads?: NotebookKernelPreload[]; /** * The execute handler is invoked when the run gestures in the UI are selected, e.g Run Cell, Run All, * Run Selection etc. */ - readonly executeHandler: (cells: NotebookCell[], controller: NotebookController) => void; + executeHandler: NotebookExecutionHandler; // optional kernel interrupt command - interruptHandler?: (notebook: NotebookDocument) => void - - // remove kernel + interruptHandler?: NotebookInterruptHandler dispose(): void; /** @@ -1457,24 +1541,24 @@ declare module 'vscode' { createNotebookCellExecutionTask(cell: NotebookCell): NotebookCellExecutionTask; // ipc + readonly preloads: NotebookKernelPreload[]; readonly onDidReceiveMessage: Event<{ editor: NotebookEditor, message: any }>; postMessage(message: any, editor?: NotebookEditor): Thenable; - asWebviewUri(localResource: Uri, editor: NotebookEditor): Uri; - } - - export interface NotebookControllerOptions { - id: string; - label: string; - description?: string; - selector: NotebookSelector; - supportedLanguages?: string[]; - hasExecutionOrder?: boolean; - executeHandler: (cells: NotebookCell[], controller: NotebookController) => void; - interruptHandler?: (notebook: NotebookDocument) => void + asWebviewUri(localResource: Uri): Uri; } export namespace notebook { - export function createNotebookController(options: NotebookControllerOptions): NotebookController; + + /** + * Creates a new notebook controller. + * + * @param id Unique identifier of the controller + * @param selector A notebook selector to narrow down notebook type or path + * @param label The label of the controller + * @param handler + * @param preloads + */ + export function createNotebookController(id: string, selector: NotebookSelector, label: string, handler?: NotebookExecutionHandler, preloads?: NotebookKernelPreload[]): NotebookController; } //#endregion @@ -1535,18 +1619,7 @@ declare module 'vscode' { // TODO@api use NotebookDocumentFilter instead of just notebookType:string? // TODO@API options duplicates the more powerful variant on NotebookContentProvider - export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, - options?: NotebookDocumentContentOptions & { - /** - * Not ready for production or development use yet. - */ - viewOptions?: { - displayName: string; - filenamePattern: NotebookFilenamePattern[]; - exclusive?: boolean; - }; - } - ): Disposable; + export function registerNotebookContentProvider(notebookType: string, provider: NotebookContentProvider, options?: NotebookDocumentContentOptions): Disposable; } //#endregion @@ -1558,6 +1631,7 @@ declare module 'vscode' { uri: Uri; } + /** @deprecated used NotebookController */ export interface NotebookKernel { // todo@API make this mandatory? @@ -1671,8 +1745,7 @@ declare module 'vscode' { filenamePattern?: NotebookFilenamePattern; } - // todo@API very unclear, provider MUST not return alive object but only data object - // todo@API unclear how the flow goes + /** @deprecated used NotebookController */ export interface NotebookKernelProvider { onDidChangeKernels?: Event; provideKernels(document: NotebookDocument, token: CancellationToken): ProviderResult; @@ -1680,9 +1753,6 @@ declare module 'vscode' { } export interface NotebookEditor { - - // todo@API unsure about that - // kernel, kernel selection, kernel provider /** @deprecated kernels are private object*/ readonly kernel?: NotebookKernel; } @@ -1690,7 +1760,7 @@ declare module 'vscode' { export namespace notebook { /** @deprecated */ export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined; }>; - /** @deprecated use createNotebookKernel */ + /** @deprecated used NotebookController */ export function registerNotebookKernelProvider(selector: NotebookDocumentFilter, provider: NotebookKernelProvider): Disposable; } @@ -1749,12 +1819,19 @@ declare module 'vscode' { } interface NotebookCellStatusBarItemProvider { + /** + * Implement and fire this event to signal that statusbar items have changed. The provide method will be called again. + */ onDidChangeCellStatusBarItems?: Event; + + /** + * The provider will be called when the cell scrolls into view, when its content, outputs, language, or metadata change, and when it changes execution state. + */ provideCellStatusBarItems(cell: NotebookCell, token: CancellationToken): ProviderResult; } export namespace notebook { - export function registerNotebookCellStatusBarItemProvider(selector: NotebookDocumentFilter, provider: NotebookCellStatusBarItemProvider): Disposable; + export function registerNotebookCellStatusBarItemProvider(selector: NotebookSelector, provider: NotebookCellStatusBarItemProvider): Disposable; } //#endregion @@ -2119,70 +2196,85 @@ declare module 'vscode' { //#endregion //#region https://github.com/microsoft/vscode/issues/107467 - /* - General activation events: - - `onLanguage:*` most test extensions will want to activate when their - language is opened to provide code lenses. - - `onTests:*` new activation event very simiular to `workspaceContains`, - but only fired when the user wants to run tests or opens the test explorer. - */ export namespace test { /** - * Registers a provider that discovers tests in workspaces and documents. + * Registers a controller that can discover and + * run tests in workspaces and documents. */ - export function registerTestProvider(testProvider: TestProvider): Disposable; + export function registerTestController(testController: TestController): Disposable; /** - * Runs tests. The "run" contains the list of tests to run as well as a - * method that can be used to update their state. At the point in time - * that "run" is called, all tests given in the run have their state - * automatically set to {@link TestRunState.Queued}. + * Requests that tests be run by their controller. + * @param run Run options to use + * @param token Cancellation token for the test run */ - export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; + export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable; /** * Returns an observer that retrieves tests in the given workspace folder. + * @stability experimental */ export function createWorkspaceTestObserver(workspaceFolder: WorkspaceFolder): TestObserver; /** * Returns an observer that retrieves tests in the given text document. + * @stability experimental */ export function createDocumentTestObserver(document: TextDocument): TestObserver; /** - * Inserts custom test results into the VS Code UI. The results are - * inserted and sorted based off the `completedAt` timestamp. If the - * results are being read from a file, for example, the `completedAt` - * time should generally be the modified time of the file if not more - * specific time is available. + * Creates a {@link TestRunTask}. This should be called by the + * {@link TestRunner} when a request is made to execute tests, and may also + * be called if a test run is detected externally. Once created, tests + * that are included in the results will be moved into the + * {@link TestResultState.Pending} state. * - * This will no-op if the inserted results are deeply equal to an - * existing result. - * - * @param results test results - * @param persist whether the test results should be saved by VS Code - * and persisted across reloads. Defaults to true. + * @param request Test run request. Only tests inside the `include` may be + * modified, and tests in its `exclude` are ignored. + * @param name The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + * @param persist Whether the results created by the run should be + * persisted in VS Code. This may be false if the results are coming from + * a file already saved externally, such as a coverage information file. */ - export function publishTestResult(results: TestRunResult, persist?: boolean): void; + export function createTestRunTask(request: TestRunRequest, name?: string, persist?: boolean): TestRunTask; /** - * List of test results stored by VS Code, sorted in descnding - * order by their `completedAt` time. - */ + * Creates a new managed {@link TestItem} instance. + * @param options Initial/required options for the item + * @param data Custom data to be stored in {@link TestItem.data} + */ + export function createTestItem(options: TestItemOptions, data: T): TestItem; + + /** + * Creates a new managed {@link TestItem} instance. + * @param options Initial/required options for the item + */ + export function createTestItem(options: TestItemOptions): TestItem; + + /** + * List of test results stored by VS Code, sorted in descnding + * order by their `completedAt` time. + * @stability experimental + */ export const testResults: ReadonlyArray; /** - * Event that fires when the {@link testResults} array is updated. - */ + * Event that fires when the {@link testResults} array is updated. + * @stability experimental + */ export const onDidChangeTestResults: Event; } + /** + * @stability experimental + */ export interface TestObserver { /** * List of tests returned by test provider for files in the workspace. */ - readonly tests: ReadonlyArray; + readonly tests: ReadonlyArray>; /** * An event that fires when an existing test in the collection changes, or @@ -2208,32 +2300,30 @@ declare module 'vscode' { dispose(): void; } + /** + * @stability experimental + */ export interface TestsChangeEvent { /** * List of all tests that are newly added. */ - readonly added: ReadonlyArray; + readonly added: ReadonlyArray>; /** * List of existing tests that have updated. */ - readonly updated: ReadonlyArray; + readonly updated: ReadonlyArray>; /** * List of existing tests that have been removed. */ - readonly removed: ReadonlyArray; + readonly removed: ReadonlyArray>; } /** - * Discovers and provides tests. - * - * Additionally, the UI may request it to discover tests for the workspace - * via `addWorkspaceTests`. - * - * @todo rename from provider + * Interface to discover and execute tests. */ - export interface TestProvider { + export interface TestController { /** * Requests that tests be provided for the given workspace. This will * be called when tests need to be enumerated for the workspace, such as @@ -2246,7 +2336,7 @@ declare module 'vscode' { * @param cancellationToken Token that signals the used asked to abort the test run. * @returns the root test item for the workspace */ - provideWorkspaceTestRoot(workspace: WorkspaceFolder, token: CancellationToken): ProviderResult; + createWorkspaceTestRoot(workspace: WorkspaceFolder, token: CancellationToken): ProviderResult>; /** * Requests that tests be provided for the given document. This will be @@ -2254,8 +2344,8 @@ declare module 'vscode' { * instance by code lens UI. * * It's suggested that the provider listen to change events for the text - * document to provide information for test that might not yet be - * saved, if possible. + * document to provide information for tests that might not yet be + * saved. * * If the test system is not able to provide or estimate for tests on a * per-file basis, this method may not be implemented. In that case, the @@ -2263,74 +2353,79 @@ declare module 'vscode' { * * @param document The document in which to observe tests * @param cancellationToken Token that signals the used asked to abort the test run. - * @returns the root test item for the workspace + * @returns the root test item for the document */ - provideDocumentTestRoot?(document: TextDocument, token: CancellationToken): ProviderResult; + createDocumentTestRoot?(document: TextDocument, token: CancellationToken): ProviderResult>; /** - * @todo this will move out of the provider soon - * @todo this will eventually need to be able to return a summary report, coverage for example. + * Starts a test run. When called, the controller should call + * {@link vscode.test.createTestRunTask}. All tasks associated with the + * run should be created before the function returns or the reutrned + * promise is resolved. * - * Starts a test run. This should cause {@link onDidChangeTest} to - * fire with update test states during the run. * @param options Options for this test run * @param cancellationToken Token that signals the used asked to abort the test run. */ - // eslint-disable-next-line vscode-dts-provider-naming - runTests(options: TestRunOptions, token: CancellationToken): ProviderResult; + runTests(options: TestRunRequest, token: CancellationToken): Thenable | void; } /** * Options given to {@link test.runTests}. */ - export interface TestRunRequest { + export interface TestRunRequest { /** - * Array of specific tests to run. The {@link TestProvider.testRoot} may - * be provided as an indication to run all tests. + * Array of specific tests to run. The controllers should run all of the + * given tests and all children of the given tests, excluding any tests + * that appear in {@link TestRunRequest.exclude}. */ - tests: T[]; + tests: TestItem[]; /** * An array of tests the user has marked as excluded in VS Code. May be - * omitted if no exclusions were requested. Test providers should not run + * omitted if no exclusions were requested. Test controllers should not run * excluded tests or any children of excluded tests. */ - exclude?: T[]; + exclude?: TestItem[]; /** - * Whether or not tests in this run should be debugged. + * Whether tests in this run should be debugged. */ debug: boolean; } /** - * Options given to {@link TestProvider.runTests} + * Options given to {@link TestController.runTests} */ - export interface TestRunOptions extends TestRunRequest { + export interface TestRunTask { /** - * Updates the state of the test in the run. By default, all tests involved - * in the run will have a "queued" state until they are updated by this method. - * - * Calling with method with nodes outside the {@link TestRunRequesttests} - * or in the {@link TestRunRequestexclude} array will no-op. + * The human-readable name of the run. This can be used to + * disambiguate multiple sets of results in a test run. It is useful if + * tests are run across multiple platforms, for example. + */ + readonly name?: string; + + /** + * Updates the state of the test in the run. Calling with method with nodes + * outside the {@link TestRunRequest.tests} or in the + * {@link TestRunRequest.exclude} array will no-op. * * @param test The test to update * @param state The state to assign to the test * @param duration Optionally sets how long the test took to run */ - setState(test: T, state: TestResultState, duration?: number): void; + setState(test: TestItem, state: TestResultState, duration?: number): void; /** * Appends a message, such as an assertion error, to the test item. * - * Calling with method with nodes outside the {@link TestRunRequesttests} - * or in the {@link TestRunRequestexclude} array will no-op. + * Calling with method with nodes outside the {@link TestRunRequest.tests} + * or in the {@link TestRunRequest.exclude} array will no-op. * * @param test The test to update * @param state The state to assign to the test * */ - appendMessage(test: T, message: TestMessage): void; + appendMessage(test: TestItem, message: TestMessage): void; /** * Appends raw output from the test runner. On the user's request, the @@ -2341,44 +2436,56 @@ declare module 'vscode' { * @param associateTo Optionally, associate the given segment of output */ appendOutput(output: string): void; + + /** + * Signals that the end of the test run. Any tests whose states have not + * been updated will be moved into the {@link TestResultState.Unset} state. + */ + end(): void; } - export interface TestChildrenCollection extends Iterable { + /** + * Indicates the the activity state of the {@link TestItem}. + */ + export enum TestItemStatus { /** - * Gets the number of children in the collection. + * All children of the test item, if any, have been discovered. */ - readonly size: number; + Resolved = 1, /** - * Gets an existing TestItem by its ID, if it exists. - * @param id ID of the test. - * @returns the TestItem instance if it exists. + * The test item may have children who have not been discovered yet. */ - get(id: string): T | undefined; + Pending = 0, + } + + /** + * Options initially passed into `vscode.test.createTestItem` + */ + export interface TestItemOptions { + /** + * Unique identifier for the TestItem. This is used to correlate + * test results and tests in the document with those in the workspace + * (test explorer). This cannot change for the lifetime of the TestItem. + */ + id: string; /** - * Adds a new child test item. No-ops if the test was already a child. - * @param child The test item to add. + * URI this TestItem is associated with. May be a file or directory. */ - add(child: T): void; + uri: Uri; /** - * Removes the child test item by reference or ID from the collection. - * @param child Child ID or instance to remove. + * Display name describing the test item. */ - delete(child: T | string): void; - - /** - * Removes all children from the collection. - */ - clear(): void; + label: string; } /** * A test item is an item shown in the "test explorer" view. It encompasses * both a suite and a test, since they have almost or identical capabilities. */ - export class TestItem { + export interface TestItem { /** * Unique identifier for the TestItem. This is used to correlate * test results and tests in the document with those in the workspace @@ -2392,10 +2499,28 @@ declare module 'vscode' { readonly uri: Uri; /** - * A set of children this item has. You can add new children to it, which - * will propagate to the editor UI. + * A mapping of children by ID to the associated TestItem instances. */ - readonly children: TestChildrenCollection; + readonly children: ReadonlyMap>; + + /** + * The parent of this item, if any. Assigned automatically when calling + * {@link TestItem.addChild}. + */ + readonly parent?: TestItem; + + /** + * Indicates the state of the test item's children. The editor will show + * TestItems in the `Pending` state and with a `resolveHandler` as being + * expandable, and will call the `resolveHandler` to request items. + * + * A TestItem in the `Resolved` state is assumed to have discovered and be + * watching for changes in its children if applicable. TestItems are in the + * `Resolved` state when initially created; if the editor should call + * the `resolveHandler` to discover children, set the state to `Pending` + * after creating the item. + */ + status: TestItemStatus; /** * Display name describing the test case. @@ -2413,6 +2538,13 @@ declare module 'vscode' { */ range?: Range; + /** + * May be set to an error associated with loading the test. Note that this + * is not a test result and should only be used to represent errors in + * discovery, such as syntax errors. + */ + error?: string | MarkdownString; + /** * Whether this test item can be run by providing it in the * {@link TestRunRequest.tests} array. Defaults to `true`. @@ -2426,20 +2558,10 @@ declare module 'vscode' { debuggable: boolean; /** - * Whether this test item can be expanded in the tree view, implying it - * has (or may have) children. If this is true, VS Code may call - * the {@link TestItem.discoverChildren} method. + * Custom extension data on the item. This data will never be serialized + * or shared outside the extenion who created the item. */ - expandable: boolean; - - /** - * Creates a new TestItem instance. - * @param id Value of the "id" property - * @param label Value of the "label" property. - * @param uri Value of the "uri" property. - * @param expandable Value of the "expandable" property. - */ - constructor(id: string, label: string, uri: Uri, expandable: boolean); + data: T; /** * Marks the test as outdated. This can happen as a result of file changes, @@ -2452,17 +2574,18 @@ declare module 'vscode' { invalidate(): void; /** - * Requests the children of the test item. Extensions should override this - * method for any test that can discover children. + * A function provided by the extension that the editor may call to request + * children of the item, if the {@link TestItem.status} is `Pending`. * - * When called, the item should discover tests and update its's `children`. - * The provider will be marked as 'busy' when this method is called, and - * the provider should report `{ busy: false }` to {@link Progress.report} - * once discovery is complete. + * When called, the item should discover tests and call {@link TestItem.addChild}. + * The items should set its {@link TestItem.status} to `Resolved` when + * discovery is finished. * * The item should continue watching for changes to the children and * firing updates until the token is cancelled. The process of watching - * the tests may involve creating a file watcher, for example. + * the tests may involve creating a file watcher, for example. After the + * token is cancelled and watching stops, the TestItem should set its + * {@link TestItem.status} back to `Pending`. * * The editor will only call this method when it's interested in refreshing * the children of the item, and will not call it again while there's an @@ -2470,9 +2593,20 @@ declare module 'vscode' { * * @param token Cancellation for the request. Cancellation will be * requested if the test changes before the previous call completes. - * @returns a provider result of child test items */ - discoverChildren(progress: Progress<{ busy: boolean }>, token: CancellationToken): void; + resolveHandler?: (token: CancellationToken) => void; + + /** + * Attaches a child, created from the {@link test.createTestItem} function, + * to this item. A `TestItem` may be a child of at most one other item. + */ + addChild(child: TestItem): void; + + /** + * Removes the test and its children from the tree. Any tokens passed to + * child `resolveHandler` methods will be cancelled. + */ + dispose(): void; } /** @@ -2612,6 +2746,19 @@ declare module 'vscode' { */ readonly range?: Range; + /** + * State of the test in each task. In the common case, a test will only + * be executed in a single task and the length of this array will be 1. + */ + readonly taskStates: ReadonlyArray; + + /** + * Optional list of nested tests for this item. + */ + readonly children: Readonly[]; + } + + export interface TestSnapshoptTaskState { /** * Current result of the test. */ @@ -2628,11 +2775,6 @@ declare module 'vscode' { * failure information if the test fails. */ readonly messages: ReadonlyArray; - - /** - * Optional list of nested tests for this item. - */ - readonly children: Readonly[]; } //#endregion @@ -2834,6 +2976,9 @@ declare module 'vscode' { * Prompt the user to chose whether to trust the current workspace * @param options Optional object describing the properties of the * workspace trust request. Defaults to { modal: false } + * When using a non-modal request, the promise will return immediately. + * Any time trust is not given, it is recommended to use the + * `onDidReceiveWorkspaceTrust` event to listen for trust changes. */ export function requestWorkspaceTrust(options?: WorkspaceTrustRequestOptions): Thenable; diff --git a/src/vs/workbench/api/browser/mainThreadCLICommands.ts b/src/vs/workbench/api/browser/mainThreadCLICommands.ts index 43241ad99e2..62002c1f80e 100644 --- a/src/vs/workbench/api/browser/mainThreadCLICommands.ts +++ b/src/vs/workbench/api/browser/mainThreadCLICommands.ts @@ -20,7 +20,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { IOpenWindowOptions, IWindowOpenable } from 'vs/platform/windows/common/windows'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { IExtensionManifest } from 'vs/workbench/workbench.web.api'; @@ -88,22 +88,19 @@ class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService private _location: string | undefined; - private readonly _extensionKindController: ExtensionKindController; - constructor( @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, @ILabelService labelService: ILabelService, - @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService, + @IExtensionManifestPropertiesService private readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(extensionManagementService, extensionGalleryService); const remoteAuthority = envService.remoteAuthority; this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined; - - this._extensionKindController = new ExtensionKindController(productService, configurationService); } protected override get location(): string | undefined { @@ -111,7 +108,7 @@ class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService } protected override validateExtensionKind(manifest: IExtensionManifest, output: CLIOutput): boolean { - if (!this._extensionKindController.canExecuteOnWorkspace(manifest)) { + if (!this._extensionManifestPropertiesService.canExecuteOnWorkspace(manifest)) { output.log(localize('cannot be installed', "Cannot install the '{0}' extension because it is declared to not run in this setup.", getExtensionId(manifest.publisher, manifest.name))); return false; } diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 24cc045670e..00e0592719e 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -31,13 +31,13 @@ import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/ import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInput'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; -import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; const enum CustomEditorModelType { Custom, @@ -60,8 +60,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @IBackupFileService private readonly _backupService: IBackupFileService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { super(); @@ -229,7 +228,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxyCustomEditors, viewType, resource, options, () => { return Array.from(this.mainThreadWebviewPanels.webviewInputs) .filter(editor => editor instanceof CustomEditorInput && isEqual(editor.resource, resource)) as CustomEditorInput[]; - }, cancellation, this._backupService); + }, cancellation); return this._customEditorService.models.add(resource, viewType, model); } } @@ -296,6 +295,18 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod private readonly _onDidChangeOrphaned = this._register(new Emitter()); public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + // TODO@mjbvz consider to enable a `typeId` that is specific for custom + // editors. Using a distinct `typeId` allows the working copy to have + // any resource (including file based resources) even if other working + // copies exist with the same resource. + // + // IMPORTANT: changing the `typeId` has an impact on backups for this + // working copy. Any value that is not the empty string will be used + // as seed to the backup. Only change the `typeId` if you have implemented + // a fallback solution to resolve any existing backups that do not have + // this seed. + readonly typeId = NO_TYPE_ID; + public static async create( instantiationService: IInstantiationService, proxy: extHostProtocol.ExtHostCustomEditorsShape, @@ -304,7 +315,6 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod options: { backupId?: string }, getEditors: () => CustomEditorInput[], cancellation: CancellationToken, - _backupFileService: IBackupFileService, ): Promise { const editors = getEditors(); let untitledDocumentData: VSBuffer | undefined; @@ -652,23 +662,25 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } const primaryEditor = editors[0]; - const backupData: IWorkingCopyBackup = { - meta: { - viewType: this.viewType, - editorResource: this._editorResource, - backupId: '', - extension: primaryEditor.extension ? { - id: primaryEditor.extension.id.value, - location: primaryEditor.extension.location, - } : undefined, - webview: { - id: primaryEditor.id, - options: primaryEditor.webview.options, - state: primaryEditor.webview.state, - } + const backupMeta: CustomDocumentBackupData = { + viewType: this.viewType, + editorResource: this._editorResource, + backupId: '', + extension: primaryEditor.extension ? { + id: primaryEditor.extension.id.value, + location: primaryEditor.extension.location, + } : undefined, + webview: { + id: primaryEditor.id, + options: primaryEditor.webview.options, + state: primaryEditor.webview.state, } }; + const backupData: IWorkingCopyBackup = { + meta: backupMeta + }; + if (!this._editable) { return backupData; } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index be31750123b..d0809171f6f 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -14,7 +14,8 @@ import { ILogService } from 'vs/platform/log/common/log'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/notebookEditorService'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { ICellRange, INotebookCellStatusBarItemProvider, INotebookDocumentFilter, INotebookExclusiveDocumentFilter, INotebookKernel, NotebookDataDto, TransientMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICellRange, INotebookCellStatusBarItemProvider, INotebookDocumentFilter, INotebookExclusiveDocumentFilter, INotebookKernel, NotebookDataDto, TransientCellMetadata, TransientDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, MainContext, MainThreadNotebookShape, NotebookExtensionDescription } from '../common/extHost.protocol'; @@ -65,17 +66,19 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: { transientOutputs: boolean; - transientMetadata: TransientMetadata; + transientCellMetadata: TransientCellMetadata; + transientDocumentMetadata: TransientDocumentMetadata; viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; }; }): Promise { - let contentOptions = { transientOutputs: options.transientOutputs, transientMetadata: options.transientMetadata }; + let contentOptions = { transientOutputs: options.transientOutputs, transientCellMetadata: options.transientCellMetadata, transientDocumentMetadata: options.transientDocumentMetadata }; const controller: IMainNotebookController = { get options() { return contentOptions; }, set options(newOptions) { - contentOptions.transientMetadata = newOptions.transientMetadata; + contentOptions.transientCellMetadata = newOptions.transientCellMetadata; + contentOptions.transientDocumentMetadata = newOptions.transientDocumentMetadata; contentOptions.transientOutputs = newOptions.transientOutputs; }, viewOptions: options.viewOptions, @@ -107,7 +110,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { this._notebookProviders.set(viewType, { controller, disposable }); } - async $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientMetadata: TransientMetadata; }): Promise { + async $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata; }): Promise { const provider = this._notebookProviders.get(viewType); if (provider && options) { @@ -202,7 +205,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } } - async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, documentFilter: INotebookDocumentFilter): Promise { + async $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, selector: NotebookSelector): Promise { const that = this; const provider: INotebookCellStatusBarItemProvider = { async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken) { @@ -216,7 +219,7 @@ export class MainThreadNotebooks implements MainThreadNotebookShape { } }; }, - selector: documentFilter + selector: selector }; if (typeof eventHandle === 'number') { diff --git a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts index 95c87645843..3125c4a5dcd 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebookKernels.ts @@ -51,6 +51,7 @@ abstract class MainThreadKernel implements INotebookKernel2 { this.implementsInterrupt = data.supportsInterrupt ?? false; this.label = data.label; this.description = data.description; + this.detail = data.detail; this.isPreferred = data.isPreferred; this.supportedLanguages = data.supportedLanguages; this.implementsExecutionOrder = data.hasExecutionOrder ?? false; @@ -69,6 +70,10 @@ abstract class MainThreadKernel implements INotebookKernel2 { this.description = data.description; event.description = true; } + if (data.detail !== undefined) { + this.detail = data.detail; + event.detail = true; + } if (data.isPreferred !== undefined) { this.isPreferred = data.isPreferred; event.isPreferred = true; @@ -84,8 +89,13 @@ abstract class MainThreadKernel implements INotebookKernel2 { this._onDidChange.fire(event); } - abstract executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): void; - abstract cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): void; + abstract executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): Promise; + abstract cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): Promise; + + // old stuff + readonly resolve = () => Promise.resolve(); + get friendlyId() { return this.id; } + get providerHandle() { return undefined; } } @extHostNamedCustomer(MainContext.MainThreadNotebookKernels) @@ -177,14 +187,14 @@ export class MainThreadNotebookKernels implements MainThreadNotebookKernelsShape // --- kernel adding/updating/removal - $addKernel(handle: number, data: INotebookKernelDto2): void { + async $addKernel(handle: number, data: INotebookKernelDto2): Promise { const that = this; const kernel = new class extends MainThreadKernel { - executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): void { - that._proxy.$executeCells(handle, uri, ranges); + async executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): Promise { + await that._proxy.$executeCells(handle, uri, ranges); } - cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): void { - that._proxy.$cancelCells(handle, uri, ranges); + async cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): Promise { + await that._proxy.$cancelCells(handle, uri, ranges); } }(data); const registration = this._notebookKernelService.registerKernel(kernel); diff --git a/src/vs/workbench/api/browser/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index 246982f9bc3..3ba387186db 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -10,12 +10,12 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { dispose } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/modes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; +import { getCodiconAriaLabel } from 'vs/base/common/codicons'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { private readonly entries: Map = new Map(); - private static readonly CODICON_REGEXP = /\$\((.*?)\)/g; constructor( _extHostContext: IExtHostContext, @@ -35,7 +35,7 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { ariaLabel = accessibilityInformation.label; role = accessibilityInformation.role; } else { - ariaLabel = text ? text.replace(MainThreadStatusBar.CODICON_REGEXP, (_match, codiconName) => codiconName) : ''; + ariaLabel = getCodiconAriaLabel(text); } const entry: IStatusbarEntry = { text, tooltip, command, color, backgroundColor, ariaLabel, role }; diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 49ad9f65a50..428f24124f6 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -70,7 +70,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._toDispose.add(_terminalService.onInstanceRequestStartExtensionTerminal(e => this._onRequestStartExtensionTerminal(e))); this._toDispose.add(_terminalService.onActiveInstanceChanged(instance => this._onActiveTerminalChanged(instance ? instance.instanceId : null))); this._toDispose.add(_terminalService.onInstanceTitleChanged(instance => instance && this._onTitleChanged(instance.instanceId, instance.title))); - this._toDispose.add(_terminalService.configHelper.onWorkspacePermissionsChanged(isAllowed => this._onWorkspacePermissionsChanged(isAllowed))); this._toDispose.add(_terminalService.onRequestAvailableProfiles(e => this._onRequestAvailableProfiles(e))); // ITerminalInstanceService listeners @@ -209,10 +208,6 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape this._proxy.$acceptTerminalTitleChange(terminalId, name); } - private _onWorkspacePermissionsChanged(isAllowed: boolean): void { - this._proxy.$acceptWorkspacePermissionsChanged(isAllowed); - } - private _onTerminalDisposed(terminalInstance: ITerminalInstance): void { this._proxy.$acceptTerminalClosed(terminalInstance.instanceId, terminalInstance.exitCode); } diff --git a/src/vs/workbench/api/browser/mainThreadTesting.ts b/src/vs/workbench/api/browser/mainThreadTesting.ts index daf1e90856e..64f6a4a307c 100644 --- a/src/vs/workbench/api/browser/mainThreadTesting.ts +++ b/src/vs/workbench/api/browser/mainThreadTesting.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; -import { emptyStream } from 'vs/base/common/stream'; import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Range } from 'vs/editor/common/core/range'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { getTestSubscriptionKey, ISerializedTestResults, ITestMessage, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; -import { HydratedTestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; +import { ExtensionRunTestsRequest, getTestSubscriptionKey, ITestItem, ITestMessage, ITestRunTask, RunTestsRequest, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { ITestRootProvider, ITestService } from 'vs/workbench/contrib/testing/common/testService'; import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol'; @@ -48,7 +47,6 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri))); this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri))); - const prevResults = resultService.results.map(r => r.toJSON()).filter(isDefined); if (prevResults.length) { this.proxy.$publishTestResults(prevResults); @@ -72,43 +70,64 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void { - this.resultService.push(new HydratedTestResult( - results, - () => Promise.resolve( - results.output - ? bufferToStream(VSBuffer.fromString(results.output)) - : emptyStream(), - ), - persist, - )); - } - - /** - * @inheritdoc - */ - public $updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void { - const r = this.resultService.getResult(runId); - if (r && r instanceof LiveTestResult) { - r.updateState(testId, state, duration); + $addTestsToRun(runId: string, tests: ITestItem[]): void { + for (const test of tests) { + test.uri = URI.revive(test.uri); + if (test.range) { + test.range = Range.lift(test.range); + } } + + this.withLiveRun(runId, r => r.addTestChainToRun(tests)); } /** * @inheritdoc */ - public $appendOutputToRun(runId: string, output: VSBuffer): void { - const r = this.resultService.getResult(runId); - if (r && r instanceof LiveTestResult) { - r.output.append(output); - } + $startedExtensionTestRun(req: ExtensionRunTestsRequest): void { + this.resultService.createLiveResult(req); + } + + /** + * @inheritdoc + */ + $startedTestRunTask(runId: string, task: ITestRunTask): void { + this.withLiveRun(runId, r => r.addTask(task)); + } + + /** + * @inheritdoc + */ + $finishedTestRunTask(runId: string, taskId: string): void { + this.withLiveRun(runId, r => r.markTaskComplete(taskId)); + } + + /** + * @inheritdoc + */ + $finishedExtensionTestRun(runId: string): void { + this.withLiveRun(runId, r => r.markComplete()); + } + + /** + * @inheritdoc + */ + public $updateTestStateInRun(runId: string, taskId: string, testId: string, state: TestResultState, duration?: number): void { + this.withLiveRun(runId, r => r.updateState(testId, taskId, state, duration)); + } + + /** + * @inheritdoc + */ + public $appendOutputToRun(runId: string, _taskId: string, output: VSBuffer): void { + this.withLiveRun(runId, r => r.output.append(output)); } /** * @inheritdoc */ - public $appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void { + public $appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void { const r = this.resultService.getResult(runId); if (r && r instanceof LiveTestResult) { if (message.location) { @@ -116,14 +135,14 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh message.location.range = Range.lift(message.location.range); } - r.appendMessage(testId, message); + r.appendMessage(testId, taskId, message); } } /** * @inheritdoc */ - public $registerTestProvider(id: string) { + public $registerTestController(id: string) { const disposable = this.testService.registerTestController(id, { runTests: (req, token) => this.proxy.$runTestsForProvider(req, token), lookupTest: test => this.proxy.$lookupTest(test), @@ -136,7 +155,7 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh /** * @inheritdoc */ - public $unregisterTestProvider(id: string) { + public $unregisterTestController(id: string) { this.testProviderRegistrations.get(id)?.dispose(); this.testProviderRegistrations.delete(id); } @@ -180,4 +199,9 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh } this.testSubscriptions.clear(); } + + private withLiveRun(runId: string, fn: (run: LiveTestResult) => T): T | undefined { + const r = this.resultService.getResult(runId); + return r && r instanceof LiveTestResult ? fn(r) : undefined; + } } diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index ebd8fa253fe..668e0fae130 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -12,10 +12,12 @@ import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderF import { Disposable } from 'vs/base/common/lifecycle'; import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape, PortAttributesProvider { @@ -175,19 +177,13 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun this.remoteAgentService.getEnvironment().then(() => { switch (source) { case CandidatePortSource.None: { - const autoDetectionEnablement = this.configurationService.inspect(PORT_AUTO_FORWARD_SETTING); - if (autoDetectionEnablement.userRemote === undefined) { - // Only update the remote setting if the user hasn't already set it. - this.configurationService.updateValue(PORT_AUTO_FORWARD_SETTING, false, ConfigurationTarget.USER_REMOTE); - } + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ 'remote.autoForwardPorts': false }]); break; } case CandidatePortSource.Output: { - const candidatePortSourceSetting = this.configurationService.inspect(PORT_AUTO_SOURCE_SETTING); - if (candidatePortSourceSetting.userRemote === undefined) { - // Only update the remote setting if the user hasn't already set it. - this.configurationService.updateValue(PORT_AUTO_SOURCE_SETTING, PORT_AUTO_SOURCE_SETTING_OUTPUT, ConfigurationTarget.USER_REMOTE); - } + Registry.as(ConfigurationExtensions.Configuration) + .registerDefaultConfigurations([{ 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT }]); break; } default: // Do nothing, the defaults for these settings should be used. diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index cd9fbb04c2e..9ee98013773 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -85,6 +85,7 @@ import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState'; import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs'; import { IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry'; import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels'; +import { RemoteTrustOption } from 'vs/platform/remote/common/remoteAuthorityResolver'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -335,9 +336,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I : extHostTypes.ExtensionKind.UI; const test: typeof vscode.test = { - registerTestProvider(provider) { + registerTestController(provider) { checkProposedApiEnabled(extension); - return extHostTesting.registerTestProvider(provider); + return extHostTesting.registerTestController(extension.identifier.value, provider); }, createDocumentTestObserver(document) { checkProposedApiEnabled(extension); @@ -351,9 +352,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostTesting.runTests(provider); }, - publishTestResult(results, persist = true) { + createTestItem(options: vscode.TestItemOptions, data?: T) { + return new extHostTypes.TestItemImpl(options.id, options.label, options.uri, data); + }, + createTestRunTask(request, name, persist) { checkProposedApiEnabled(extension); - return extHostTesting.publishExtensionProvidedResults(results, persist); + return extHostTesting.createTestRunTask(extension.identifier.value, request, name, persist); }, get onDidChangeTestResults() { checkProposedApiEnabled(extension); @@ -1069,7 +1073,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookKernelProvider(extension, selector, provider); }, - registerNotebookCellStatusBarItemProvider: (selector: vscode.NotebookDocumentFilter, provider: vscode.NotebookCellStatusBarItemProvider) => { + registerNotebookCellStatusBarItemProvider: (selector: vscode.NotebookSelector, provider: vscode.NotebookCellStatusBarItemProvider) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookCellStatusBarItemProvider(extension, selector, provider); }, @@ -1105,9 +1109,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.createNotebookCellExecution(uri, index, kernelId); }, - createNotebookController(options) { + createNotebookController(id, selector, label, executeHandler, preloads) { checkProposedApiEnabled(extension); - return extHostNotebookKernels.createKernel(extension, options); + return extHostNotebookKernels.createNotebookController(extension, id, selector, label, executeHandler, preloads); } }; @@ -1240,6 +1244,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlineHint: extHostTypes.InlineHint, InlineHintKind: extHostTypes.InlineHintKind, RemoteAuthorityResolverError: extHostTypes.RemoteAuthorityResolverError, + RemoteTrustOption: RemoteTrustOption, ResolvedAuthority: extHostTypes.ResolvedAuthority, SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, ExtensionRuntime: extHostTypes.ExtensionRuntime, @@ -1257,7 +1262,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I NotebookCellOutputItem: extHostTypes.NotebookCellOutputItem, NotebookCellStatusBarItem: extHostTypes.NotebookCellStatusBarItem, LinkedEditingRanges: extHostTypes.LinkedEditingRanges, - TestItem: extHostTypes.TestItem, + TestItemStatus: extHostTypes.TestItemStatus, TestResultState: extHostTypes.TestResultState, TestMessage: extHostTypes.TestMessage, TestMessageSeverity: extHostTypes.TestMessageSeverity, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 8d2b7126b29..eb4dc4e7ace 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -50,13 +50,13 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, ProvidedPortAttributes } from 'vs/platform/remote/common/tunnel'; import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline'; import { revive } from 'vs/base/common/marshalling'; -import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, TransientMetadata, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto, TransientOptions, IImmediateCellEditOperation, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, TransientCellMetadata, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter, IOutputDto, TransientOptions, IImmediateCellEditOperation, INotebookCellStatusBarItem, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { DebugConfigurationProviderTriggerKind, TestResultState } from 'vs/workbench/api/common/extHostTypes'; import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync'; -import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage } from 'vs/workbench/contrib/testing/common/testCollection'; +import { InternalTestItem, RunTestForProviderRequest, RunTestsRequest, TestIdWithSrc, TestsDiff, ISerializedTestResults, ITestMessage, ITestItem, ITestRunTask, ExtensionRunTestsRequest } from 'vs/workbench/contrib/testing/common/testCollection'; import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { WorkspaceTrustRequestOptions } from 'vs/platform/workspace/common/workspaceTrust'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -874,10 +874,11 @@ export interface INotebookCellStatusBarListDto { export interface MainThreadNotebookShape extends IDisposable { $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, options: { transientOutputs: boolean; - transientMetadata: TransientMetadata; + transientCellMetadata: TransientCellMetadata; + transientDocumentMetadata: TransientDocumentMetadata; viewOptions?: { displayName: string; filenamePattern: (string | IRelativePattern | INotebookExclusiveDocumentFilter)[]; exclusive: boolean; }; }): Promise; - $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientMetadata: TransientMetadata; }): Promise; + $updateNotebookProviderOptions(viewType: string, options?: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata; }): Promise; $unregisterNotebookProvider(viewType: string): Promise; $registerNotebookSerializer(handle: number, extension: NotebookExtensionDescription, viewType: string, options: TransientOptions): void; @@ -885,7 +886,7 @@ export interface MainThreadNotebookShape extends IDisposable { $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise; $unregisterNotebookKernelProvider(handle: number): Promise; - $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, documentFilter: INotebookDocumentFilter): Promise; + $registerNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined, selector: NotebookSelector): Promise; $unregisterNotebookCellStatusBarItemProvider(handle: number, eventHandle: number | undefined): Promise; $emitCellStatusBarEvent(eventHandle: number): void; $onNotebookKernelChange(handle: number, uri: UriComponents | undefined): void; @@ -913,6 +914,7 @@ export interface INotebookKernelDto2 { extensionId: ExtensionIdentifier; extensionLocation: UriComponents; label: string; + detail?: string; description?: string; isPreferred?: boolean; supportedLanguages: string[]; @@ -923,7 +925,7 @@ export interface INotebookKernelDto2 { export interface MainThreadNotebookKernelsShape extends IDisposable { $postMessage(handle: number, editorId: string | undefined, message: any): Promise; - $addKernel(handle: number, data: INotebookKernelDto2): void; + $addKernel(handle: number, data: INotebookKernelDto2): Promise; $updateKernel(handle: number, data: Partial): void; $removeKernel(handle: number): void; } @@ -1718,7 +1720,6 @@ export interface ExtHostTerminalServiceShape { $acceptProcessRequestInitialCwd(id: number): void; $acceptProcessRequestCwd(id: number): void; $acceptProcessRequestLatency(id: number): number; - $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; $getAvailableProfiles(configuredProfilesOnly: boolean): Promise; $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; $provideLinks(id: number, line: string): Promise; @@ -1963,8 +1964,8 @@ export interface ExtHostNotebookEditorsShape { export interface ExtHostNotebookKernelsShape { $acceptSelection(handle: number, uri: UriComponents, value: boolean): void; - $executeCells(handle: number, uri: UriComponents, ranges: ICellRange[]): void; - $cancelCells(handle: number, uri: UriComponents, ranges: ICellRange[]): void; + $executeCells(handle: number, uri: UriComponents, ranges: ICellRange[]): Promise; + $cancelCells(handle: number, uri: UriComponents, ranges: ICellRange[]): Promise; $acceptRendererMessage(handle: number, editorId: string, message: any): void; } @@ -2008,16 +2009,39 @@ export interface ExtHostTestingShape { } export interface MainThreadTestingShape { - $registerTestProvider(id: string): void; - $unregisterTestProvider(id: string): void; + /** Registeres that there's a test controller with the given ID */ + $registerTestController(id: string): void; + /** Diposes of the test controller with the given ID */ + $unregisterTestController(id: string): void; + /** Requests tests from the given resource/uri, from the observer API. */ $subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; + /** Stops requesting tests from the given resource/uri, from the observer API. */ $unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void; + /** Publishes that new tests were available on the given source. */ $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void; - $updateTestStateInRun(runId: string, testId: string, state: TestResultState, duration?: number): void; - $appendTestMessageInRun(runId: string, testId: string, message: ITestMessage): void; - $appendOutputToRun(runId: string, output: VSBuffer): void; + /** Request by an extension to run tests. */ $runTests(req: RunTestsRequest, token: CancellationToken): Promise; - $publishExtensionProvidedResults(results: ISerializedTestResults, persist: boolean): void; + + // --- test run handling: + /** + * Adds tests to the run. The tests are given in descending depth. The first + * item will be a previously-known test, or a test root. + */ + $addTestsToRun(runId: string, tests: ITestItem[]): void; + /** Updates the state of a test run in the given run. */ + $updateTestStateInRun(runId: string, taskId: string, testId: string, state: TestResultState, duration?: number): void; + /** Appends a message to a test in the run. */ + $appendTestMessageInRun(runId: string, taskId: string, testId: string, message: ITestMessage): void; + /** Appends raw output to the test run.. */ + $appendOutputToRun(runId: string, taskId: string, output: VSBuffer): void; + /** Signals a task in a test run started. */ + $startedTestRunTask(runId: string, task: ITestRunTask): void; + /** Signals a task in a test run ended. */ + $finishedTestRunTask(runId: string, taskId: string): void; + /** Start a new extension-provided test run. */ + $startedExtensionTestRun(req: ExtensionRunTestsRequest): void; + /** Signals that an extension-provided test run finished. */ + $finishedExtensionTestRun(runId: string): void; } // --- proxy identifiers diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 7c2367c41f2..2c44e40c33f 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -18,7 +18,7 @@ import { ICommandsExecutor, RemoveFromRecentlyOpenedAPICommand, OpenIssueReporte import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IRange } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; -import { TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { TransientCellMetadata, TransientDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { VSBuffer } from 'vs/base/common/buffer'; import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto'; @@ -30,13 +30,13 @@ const newCommands: ApiCommand[] = [ new ApiCommand( 'vscode.executeDocumentHighlights', '_executeDocumentHighlights', 'Execute document highlight provider.', [ApiCommandArgument.Uri, ApiCommandArgument.Position], - new ApiCommandResult('A promise that resolves to an array of SymbolInformation and DocumentSymbol instances.', tryMapWith(typeConverters.DocumentHighlight.to)) + new ApiCommandResult('A promise that resolves to an array of DocumentHighlight-instances.', tryMapWith(typeConverters.DocumentHighlight.to)) ), // -- document symbols new ApiCommand( 'vscode.executeDocumentSymbolProvider', '_executeDocumentSymbolProvider', 'Execute document symbol provider.', [ApiCommandArgument.Uri], - new ApiCommandResult('A promise that resolves to an array of DocumentHighlight-instances.', (value, apiArgs) => { + new ApiCommandResult('A promise that resolves to an array of SymbolInformation and DocumentSymbol instances.', (value, apiArgs) => { if (isFalsyOrEmpty(value)) { return undefined; @@ -343,7 +343,7 @@ const newCommands: ApiCommand[] = [ new ApiCommandResult<{ viewType: string; displayName: string; - options: { transientOutputs: boolean; transientMetadata: TransientMetadata }; + options: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata; }; filenamePattern: (string | types.RelativePattern | { include: string | types.RelativePattern, exclude: string | types.RelativePattern })[] }[], { viewType: string; @@ -354,7 +354,12 @@ const newCommands: ApiCommand[] = [ return { viewType: item.viewType, displayName: item.displayName, - options: { transientOutputs: item.options.transientOutputs, transientMetadata: item.options.transientMetadata }, + options: { + transientOutputs: item.options.transientOutputs, + transientMetadata: item.options.transientCellMetadata, + transientCellMetadata: item.options.transientCellMetadata, + transientDocumentMetadata: item.options.transientDocumentMetadata + }, filenamePattern: item.filenamePattern.map(pattern => typeConverters.NotebookExclusiveDocumentPattern.to(pattern)) }; })) diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 422df0e5b15..14ed86bd515 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -667,7 +667,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme connectionToken: result.connectionToken }; const options: ResolvedOptions = { - extensionHostEnv: result.extensionHostEnv + extensionHostEnv: result.extensionHostEnv, + trust: result.trust }; return { diff --git a/src/vs/workbench/api/common/extHostMemento.ts b/src/vs/workbench/api/common/extHostMemento.ts index bac8ab5fd3b..31c1676e696 100644 --- a/src/vs/workbench/api/common/extHostMemento.ts +++ b/src/vs/workbench/api/common/extHostMemento.ts @@ -7,6 +7,7 @@ import type * as vscode from 'vscode'; import { IDisposable } from 'vs/base/common/lifecycle'; import { ExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { DeferredPromise, RunOnceScheduler } from 'vs/base/common/async'; export class ExtensionMemento implements vscode.Memento { @@ -18,6 +19,9 @@ export class ExtensionMemento implements vscode.Memento { private _value?: { [n: string]: any; }; private readonly _storageListener: IDisposable; + private _deferredPromises: Map> = new Map(); + private _scheduler: RunOnceScheduler; + constructor(id: string, global: boolean, storage: ExtHostStorage) { this._id = id; this._shared = global; @@ -33,6 +37,23 @@ export class ExtensionMemento implements vscode.Memento { this._value = e.value; } }); + + this._scheduler = new RunOnceScheduler(() => { + const records = this._deferredPromises; + this._deferredPromises = new Map(); + (async () => { + try { + await this._storage.setValue(this._shared, this._id, this._value!); + for (const value of records.values()) { + value.complete(); + } + } catch (e) { + for (const value of records.values()) { + value.error(e); + } + } + })(); + }, 0); } get whenReady(): Promise { @@ -51,7 +72,20 @@ export class ExtensionMemento implements vscode.Memento { update(key: string, value: any): Promise { this._value![key] = value; - return this._storage.setValue(this._shared, this._id, this._value!); + + let record = this._deferredPromises.get(key); + if (record !== undefined) { + return record.p; + } + + const promise = new DeferredPromise(); + this._deferredPromises.set(key, promise); + + if (!this._scheduler.isScheduled()) { + this._scheduler.schedule(); + } + + return promise.p; } dispose(): void { diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 76d2868f15f..d3df4ca0d17 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -376,7 +376,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { const internalOptions = typeConverters.NotebookDocumentContentOptions.from(options); this._notebookProxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, { transientOutputs: internalOptions.transientOutputs, - transientMetadata: internalOptions.transientMetadata, + transientCellMetadata: internalOptions.transientCellMetadata, + transientDocumentMetadata: internalOptions.transientDocumentMetadata, viewOptions: options?.viewOptions && viewOptionsFilenamePattern ? { displayName: options.viewOptions.displayName, filenamePattern: viewOptionsFilenamePattern, exclusive: options.viewOptions.exclusive || false } : undefined }); @@ -403,15 +404,12 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { }); } - registerNotebookCellStatusBarItemProvider(extension: IExtensionDescription, selector: vscode.NotebookDocumentFilter, provider: vscode.NotebookCellStatusBarItemProvider) { + registerNotebookCellStatusBarItemProvider(extension: IExtensionDescription, selector: vscode.NotebookSelector, provider: vscode.NotebookCellStatusBarItemProvider) { const handle = ExtHostNotebookController._notebookStatusBarItemProviderHandlePool++; const eventHandle = typeof provider.onDidChangeCellStatusBarItems === 'function' ? ExtHostNotebookController._notebookStatusBarItemProviderHandlePool++ : undefined; this._notebookStatusBarItemProviders.set(handle, provider); - this._notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, { - viewType: selector.viewType, - filenamePattern: selector.filenamePattern ? typeConverters.NotebookExclusiveDocumentPattern.from(selector.filenamePattern) : undefined - }); + this._notebookProxy.$registerNotebookCellStatusBarItemProvider(handle, eventHandle, selector); let subscription: vscode.Disposable | undefined; if (eventHandle !== undefined) { @@ -623,7 +621,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape { } } - private cancelOneNotebookCellExecution(cell: ExtHostCell): void { + cancelOneNotebookCellExecution(cell: ExtHostCell): void { const execution = this._activeExecutions.get(cell.uri); execution?.cancel(); } diff --git a/src/vs/workbench/api/common/extHostNotebookKernels.ts b/src/vs/workbench/api/common/extHostNotebookKernels.ts index 2629fb98062..94ca379c78e 100644 --- a/src/vs/workbench/api/common/extHostNotebookKernels.ts +++ b/src/vs/workbench/api/common/extHostNotebookKernels.ts @@ -8,7 +8,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { ExtHostNotebookKernelsShape, IMainContext, INotebookKernelDto2, MainContext, MainThreadNotebookKernelsShape } from 'vs/workbench/api/common/extHost.protocol'; import * as vscode from 'vscode'; import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as extHostTypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; @@ -16,10 +16,8 @@ import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; import { asWebviewUri } from 'vs/workbench/api/common/shared/webview'; -type ExecuteHandler = (cells: vscode.NotebookCell[], controller: vscode.NotebookController) => void; -type InterruptHandler = (notebook: vscode.NotebookDocument) => void; - interface IKernelData { + extensionId: ExtensionIdentifier, controller: vscode.NotebookController; onDidChangeSelection: Emitter<{ selected: boolean; notebook: vscode.NotebookDocument; }>; onDidReceiveMessage: Emitter<{ editor: vscode.NotebookEditor, message: any }>; @@ -40,11 +38,20 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebookKernels); } - createKernel(extension: IExtensionDescription, options: vscode.NotebookControllerOptions): vscode.NotebookController { + createNotebookController(extension: IExtensionDescription, id: string, selector: vscode.NotebookSelector, label: string, handler?: vscode.NotebookExecutionHandler, preloads?: vscode.NotebookKernelPreload[]): vscode.NotebookController { + + for (let data of this._kernelData.values()) { + if (data.controller.id === id) { + throw new Error(`notebook controller with id '${id}' ALREADY exist`); + } + } const handle = this._handlePool++; const that = this; + const _defaultSupportedLanguages = ['plaintext']; + const _defaultExecutHandler = () => console.warn(`NO execute handler from notebook controller '${data.id}' of extension: '${extension.identifier}'`); + let isDisposed = false; const commandDisposables = new DisposableStore(); @@ -52,20 +59,25 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { const onDidReceiveMessage = new Emitter<{ editor: vscode.NotebookEditor, message: any }>(); const data: INotebookKernelDto2 = { - id: options.id, - selector: options.selector, + id: id, + selector: selector, extensionId: extension.identifier, extensionLocation: extension.extensionLocation, - label: options.label, - supportedLanguages: [], + label: label || extension.identifier.value, + supportedLanguages: _defaultSupportedLanguages, + preloads: preloads ? preloads.map(extHostTypeConverters.NotebookKernelPreload.from) : [] }; // - let _executeHandler: ExecuteHandler = options.executeHandler; - let _interruptHandler: InterruptHandler | undefined = options.interruptHandler; + let _executeHandler: vscode.NotebookExecutionHandler = handler ?? _defaultExecutHandler; + let _interruptHandler: vscode.NotebookInterruptHandler | undefined; // todo@jrieken the selector needs to be massaged - this._proxy.$addKernel(handle, data); + this._proxy.$addKernel(handle, data).catch(err => { + // this can happen when a kernel with that ID is already registered + console.log(err); + isDisposed = true; + }); // update: all setters write directly into the dto object // and trigger an update. the actual update will only happen @@ -91,7 +103,14 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return data.label; }, set label(value) { - data.label = value; + data.label = value ?? extension.displayName ?? extension.name; + _update(); + }, + get detail() { + return data.detail ?? ''; + }, + set detail(value) { + data.detail = value; _update(); }, get description() { @@ -112,7 +131,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { return data.supportedLanguages; }, set supportedLanguages(value) { - data.supportedLanguages = isNonEmptyArray(value) ? value : ['plaintext']; + data.supportedLanguages = isNonEmptyArray(value) ? value : _defaultSupportedLanguages; _update(); }, get hasExecutionOrder() { @@ -123,15 +142,14 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { _update(); }, get preloads() { - return data.preloads && data.preloads.map(extHostTypeConverters.NotebookKernelPreload.to); - }, - set preloads(value) { - data.preloads = value && value.map(extHostTypeConverters.NotebookKernelPreload.from); - _update(); + return data.preloads ? data.preloads.map(extHostTypeConverters.NotebookKernelPreload.to) : []; }, get executeHandler() { return _executeHandler; }, + set executeHandler(value) { + _executeHandler = value ?? _defaultExecutHandler; + }, get interruptHandler() { return _interruptHandler; }, @@ -162,17 +180,12 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { postMessage(message, editor) { return that._proxy.$postMessage(handle, editor && that._extHostNotebook.getIdByEditor(editor), message); }, - asWebviewUri(uri: URI, editor) { - return asWebviewUri(that._initData.environment, that._extHostNotebook.getIdByEditor(editor)!, uri); + asWebviewUri(uri: URI) { + return asWebviewUri(that._initData.environment, data.id, uri); } }; - this._kernelData.set(handle, { controller, onDidChangeSelection, onDidReceiveMessage }); - - controller.supportedLanguages = options.supportedLanguages ?? []; - controller.interruptHandler = options.interruptHandler; - controller.hasExecutionOrder = options.hasExecutionOrder ?? false; - + this._kernelData.set(handle, { extensionId: extension.identifier, controller, onDidChangeSelection, onDidReceiveMessage }); return controller; } @@ -186,7 +199,7 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } } - $executeCells(handle: number, uri: UriComponents, ranges: ICellRange[]): void { + async $executeCells(handle: number, uri: UriComponents, ranges: ICellRange[]): Promise { const obj = this._kernelData.get(handle); if (!obj) { // extension can dispose kernels in the meantime @@ -203,14 +216,14 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { } try { - obj.controller.executeHandler(cells, obj.controller); + obj.controller.executeHandler.call(obj.controller, cells, obj.controller); } catch (err) { // console.error(err); } } - $cancelCells(handle: number, uri: UriComponents, ranges: ICellRange[]): void { + async $cancelCells(handle: number, uri: UriComponents, ranges: ICellRange[]): Promise { const obj = this._kernelData.get(handle); if (!obj) { // extension can dispose kernels in the meantime @@ -221,7 +234,17 @@ export class ExtHostNotebookKernels implements ExtHostNotebookKernelsShape { throw new Error('MISSING notebook'); } if (obj.controller.interruptHandler) { - obj.controller.interruptHandler(document.notebookDocument); + obj.controller.interruptHandler.call(obj.controller); + } + + // we do both? interrupt and cancellation or should we be selective? + for (const range of ranges) { + for (let i = range.start; i < range.end; i++) { + const cell = document.getCellFromIndex(i); + if (cell) { + this._extHostNotebook.cancelOneNotebookCellExecution(cell); + } + } } } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 01b3d87a346..a10d6b973b4 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -336,7 +336,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public abstract getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string; public abstract $getAvailableProfiles(configuredProfilesOnly: boolean): Promise; public abstract $getDefaultShellAndArgs(useAutomationShell: boolean): Promise; - public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void; public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); @@ -791,8 +790,4 @@ export class WorkerExtHostTerminalService extends BaseExtHostTerminalService { public async $getDefaultShellAndArgs(useAutomationShell: boolean): Promise { throw new NotSupportedError(); } - - public $acceptWorkspacePermissionsChanged(isAllowed: boolean): void { - throw new NotSupportedError(); - } } diff --git a/src/vs/workbench/api/common/extHostTesting.ts b/src/vs/workbench/api/common/extHostTesting.ts index 142aa312694..e8aad5e6eec 100644 --- a/src/vs/workbench/api/common/extHostTesting.ts +++ b/src/vs/workbench/api/common/extHostTesting.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { mapFind } from 'vs/base/common/arrays'; -import { disposableTimeout } from 'vs/base/common/async'; +import { Barrier, DeferredPromise, disposableTimeout, isThenable } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; +import { Iterable } from 'vs/base/common/iterator'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { deepFreeze } from 'vs/base/common/objects'; import { isDefined } from 'vs/base/common/types'; @@ -18,24 +19,29 @@ import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTes import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { ExtHostTestItemEventType, getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { Disposable, TestItem as TestItemImpl, TestItemHookProperty } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, TestItemImpl } from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { OwnedTestCollection, SingleUseTestCollection, TestPosition } from 'vs/workbench/contrib/testing/common/ownedTestCollection'; -import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; +import { AbstractIncrementalTestCollection, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, ISerializedTestResults, ITestItem, RunTestForProviderRequest, TestDiffOpType, TestIdWithSrc, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection'; import type * as vscode from 'vscode'; const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`; export class ExtHostTesting implements ExtHostTestingShape { private readonly resultsChangedEmitter = new Emitter(); - private readonly providers = new Map(); + private readonly controllers = new Map + }>(); private readonly proxy: MainThreadTestingShape; private readonly ownedTests = new OwnedTestCollection(); - private readonly testSubscriptions = new Map void; + subscribeFn: (id: string, provider: vscode.TestController) => void; }>(); private workspaceObservers: WorkspaceFolderTestObserverFactory; @@ -46,6 +52,7 @@ export class ExtHostTesting implements ExtHostTestingShape { constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) { this.proxy = rpc.getProxy(MainContext.MainThreadTesting); + this.runQueue = new TestRunQueue(this.proxy); this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy); this.textDocumentObservers = new TextDocumentTestObserverFactory(this.proxy, documents); } @@ -53,22 +60,22 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Implements vscode.test.registerTestProvider */ - public registerTestProvider(provider: vscode.TestProvider): vscode.Disposable { - const providerId = generateUuid(); - this.providers.set(providerId, provider); - this.proxy.$registerTestProvider(providerId); + public registerTestController(extensionId: string, controller: vscode.TestController): vscode.Disposable { + const controllerId = generateUuid(); + this.controllers.set(controllerId, { instance: controller, extensionId }); + this.proxy.$registerTestController(controllerId); // give the ext a moment to register things rather than synchronously invoking within activate() - const toSubscribe = [...this.testSubscriptions.keys()]; + const toSubscribe = [...this.testControllers.keys()]; setTimeout(() => { for (const subscription of toSubscribe) { - this.testSubscriptions.get(subscription)?.subscribeFn(providerId, provider); + this.testControllers.get(subscription)?.subscribeFn(controllerId, controller); } }, 0); return new Disposable(() => { - this.providers.delete(providerId); - this.proxy.$unregisterTestProvider(providerId); + this.controllers.delete(controllerId); + this.proxy.$unregisterTestController(controllerId); }); } @@ -89,8 +96,8 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Implements vscode.test.runTests */ - public async runTests(req: vscode.TestRunRequest, token = CancellationToken.None) { - const testListToProviders = (tests: ReadonlyArray) => + public async runTests(req: vscode.TestRunRequest, token = CancellationToken.None) { + const testListToProviders = (tests: ReadonlyArray>) => tests .map(this.getInternalTestForReference, this) .filter(isDefined) @@ -104,10 +111,10 @@ export class ExtHostTesting implements ExtHostTestingShape { } /** - * Implements vscode.test.publishTestResults + * Implements vscode.test.createTestRunTask */ - public publishExtensionProvidedResults(results: vscode.TestRunResult, persist: boolean): void { - this.proxy.$publishExtensionProvidedResults(Convert.TestResults.from(generateUuid(), results), persist); + public createTestRunTask(extensionId: string, request: vscode.TestRunRequest, name: string | undefined, persist = true): vscode.TestRunTask { + return this.runQueue.createTestRunTask(extensionId, request, name, persist); } /** @@ -133,12 +140,12 @@ export class ExtHostTesting implements ExtHostTestingShape { public async $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const subscriptionKey = getTestSubscriptionKey(resource, uri); - if (this.testSubscriptions.has(subscriptionKey)) { + if (this.testControllers.has(subscriptionKey)) { return; } const cancellation = new CancellationTokenSource(); - let method: undefined | ((p: vscode.TestProvider) => vscode.ProviderResult); + let method: undefined | ((p: vscode.TestController) => vscode.ProviderResult>); if (resource === ExtHostTestingResource.TextDocument) { let document = this.documents.getDocument(uri); @@ -157,14 +164,14 @@ export class ExtHostTesting implements ExtHostTestingShape { if (document) { const folder = await this.workspace.getWorkspaceFolder2(uri, false); - method = p => p.provideDocumentTestRoot - ? p.provideDocumentTestRoot(document!.document, cancellation.token) + method = p => p.createDocumentTestRoot + ? p.createDocumentTestRoot(document!.document, cancellation.token) : createDefaultDocumentTestRoot(p, document!.document, folder, cancellation.token); } } else { const folder = await this.workspace.getWorkspaceFolder2(uri, false); if (folder) { - method = p => p.provideWorkspaceTestRoot(folder, cancellation.token); + method = p => p.createWorkspaceTestRoot(folder, cancellation.token); } } @@ -172,7 +179,7 @@ export class ExtHostTesting implements ExtHostTestingShape { return; } - const subscribeFn = async (id: string, provider: vscode.TestProvider) => { + const subscribeFn = async (id: string, provider: vscode.TestController) => { try { const root = await method!(provider); if (root) { @@ -187,15 +194,15 @@ export class ExtHostTesting implements ExtHostTestingShape { const collection = disposable.add(this.ownedTests.createForHierarchy( diff => this.proxy.$publishDiff(resource, uriComponents, diff))); disposable.add(toDisposable(() => cancellation.dispose(true))); - for (const [id, provider] of this.providers) { - subscribeFn(id, provider); + for (const [id, controller] of this.controllers) { + subscribeFn(id, controller.instance); } // note: we don't increment the root count initially -- this is done by the // main thread, incrementing once per extension host. We just push the // diff to signal that roots have been discovered. collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]); - this.testSubscriptions.set(subscriptionKey, { store: disposable, collection, subscribeFn }); + this.testControllers.set(subscriptionKey, { store: disposable, collection, subscribeFn }); } /** @@ -204,7 +211,7 @@ export class ExtHostTesting implements ExtHostTestingShape { * @override */ public async $expandTest(test: TestIdWithSrc, levels: number) { - const sub = mapFind(this.testSubscriptions.values(), s => s.collection.treeId === test.src.tree ? s : undefined); + const sub = mapFind(this.testControllers.values(), s => s.collection.treeId === test.src.tree ? s : undefined); await sub?.collection.expand(test.testId, levels < 0 ? Infinity : levels); this.flushCollectionDiffs(); } @@ -216,8 +223,8 @@ export class ExtHostTesting implements ExtHostTestingShape { public $unsubscribeFromTests(resource: ExtHostTestingResource, uriComponents: UriComponents) { const uri = URI.revive(uriComponents); const subscriptionKey = getTestSubscriptionKey(resource, uri); - this.testSubscriptions.get(subscriptionKey)?.store.dispose(); - this.testSubscriptions.delete(subscriptionKey); + this.testControllers.get(subscriptionKey)?.store.dispose(); + this.testControllers.delete(subscriptionKey); } /** @@ -238,14 +245,14 @@ export class ExtHostTesting implements ExtHostTestingShape { * providers to be run. * @override */ - public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise { - const provider = this.providers.get(req.tests[0].src.provider); - if (!provider) { + public async $runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise { + const controller = this.controllers.get(req.tests[0].src.controller); + if (!controller) { return; } const includeTests = req.tests - .map(({ testId, src }) => this.ownedTests.getTestById(testId, src.tree)) + .map(({ testId, src }) => this.ownedTests.getTestById(testId, src?.tree)) .filter(isDefined) .map(([_tree, test]) => test); @@ -260,50 +267,19 @@ export class ExtHostTesting implements ExtHostTestingShape { return; } - const isExcluded = (test: vscode.TestItem) => { - // for test providers that don't support excluding natively, - // make sure not to report excluded result otherwise summaries will be off. - for (const [tree, exclude] of excludeTests) { - const e = tree.comparePositions(exclude, test.id); - if (e === TestPosition.IsChild || e === TestPosition.IsSame) { - return true; - } - } - - return false; + const publicReq: vscode.TestRunRequest = { + tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)), + exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)), + debug: req.debug, }; - try { - await provider.runTests({ - appendOutput: message => { - this.proxy.$appendOutputToRun(req.runId, VSBuffer.fromString(message)); - }, - appendMessage: (test, message) => { - if (!isExcluded(test)) { - this.flushCollectionDiffs(); - this.proxy.$appendTestMessageInRun(req.runId, test.id, Convert.TestMessage.from(message)); - } - }, - setState: (test, state, duration) => { - if (!isExcluded(test)) { - this.flushCollectionDiffs(); - this.proxy.$updateTestStateInRun(req.runId, test.id, state, duration); - } - }, - tests: includeTests.map(t => TestItemFilteredWrapper.unwrap(t.actual)), - exclude: excludeTests.map(([, t]) => TestItemFilteredWrapper.unwrap(t.actual)), - debug: req.debug, - }, cancellation); - - for (const { collection } of this.testSubscriptions.values()) { - collection.flushDiff(); // ensure all states are updated - } - - return; - } catch (e) { - console.error(e); // so it appears to attached debuggers - throw e; - } + await this.runQueue.enqueueRun({ + dto: TestRunDto.fromInternal(req), + token, + extensionId: controller.extensionId, + req: publicReq, + doRun: () => controller!.instance.runTests(publicReq, token) + }); } public $lookupTest(req: TestIdWithSrc): Promise { @@ -321,7 +297,7 @@ export class ExtHostTesting implements ExtHostTestingShape { * main thread is updated. */ private flushCollectionDiffs() { - for (const { collection } of this.testSubscriptions.values()) { + for (const { collection } of this.testControllers.values()) { collection.flushDiff(); } } @@ -329,18 +305,236 @@ export class ExtHostTesting implements ExtHostTestingShape { /** * Gets the internal test item associated with the reference from the extension. */ - private getInternalTestForReference(test: vscode.TestItem) { + private getInternalTestForReference(test: vscode.TestItem) { // Find workspace items first, then owned tests, then document tests. // If a test instance exists in both the workspace and document, prefer // the workspace because it's less ephemeral. return this.workspaceObservers.getMirroredTestDataByReference(test) - ?? mapFind(this.testSubscriptions.values(), c => c.collection.getTestByReference(test)) + ?? mapFind(this.testControllers.values(), c => c.collection.getTestByReference(test)) ?? this.textDocumentObservers.getMirroredTestDataByReference(test); } } -export const createDefaultDocumentTestRoot = async ( - provider: vscode.TestProvider, +/** + * Queues runs for a single extension and provides the currently-executing + * run so that `createTestRunTask` can be properly correlated. + */ +class TestRunQueue { + private readonly state = new Map, + factory: (name: string | undefined) => TestRunTask, + }, + queue: (() => (Promise | void))[]; + }>(); + + constructor(private readonly proxy: MainThreadTestingShape) { } + + /** + * Registers and enqueues a test run. `doRun` will be called when an + * invokation to {@link TestController.runTests} should be called. + */ + public enqueueRun(opts: { + extensionId: string, + req: vscode.TestRunRequest, + dto: TestRunDto, + token: CancellationToken, + doRun: () => Thenable | void, + }, + ) { + let record = this.state.get(opts.extensionId); + if (!record) { + record = { queue: [], current: undefined as any }; + this.state.set(opts.extensionId, record); + } + + const deferred = new DeferredPromise(); + const runner = () => { + const tasks: TestRunTask[] = []; + const shared = new Set(); + record!.current = { + publicReq: opts.req, + factory: name => { + const task = new TestRunTask(name, opts.dto, shared, this.proxy); + tasks.push(task); + opts.token.onCancellationRequested(() => task.end()); + return task; + }, + }; + + this.invokeRunner(opts.extensionId, opts.dto.id, opts.doRun, tasks).finally(() => deferred.complete()); + }; + + record.queue.push(runner); + if (record.queue.length === 1) { + runner(); + } + + return deferred.p; + } + + /** + * Implements the public `createTestRunTask` API. + */ + public createTestRunTask(extensionId: string, request: vscode.TestRunRequest, name: string | undefined, persist: boolean): vscode.TestRunTask { + const state = this.state.get(extensionId); + // If the request is for the currently-executing `runTests`, then correlate + // it to that existing run. Otherwise return a new, detached run. + if (state?.current.publicReq === request) { + return state.current.factory(name); + } + + const dto = TestRunDto.fromPublic(request); + const task = new TestRunTask(name, dto, new Set(), this.proxy); + this.proxy.$startedExtensionTestRun({ + debug: request.debug, + exclude: request.exclude?.map(t => t.id) ?? [], + id: dto.id, + tests: request.tests.map(t => t.id), + persist: persist + }); + task.onEnd.wait().then(() => this.proxy.$finishedExtensionTestRun(dto.id)); + return task; + } + + private invokeRunner(extensionId: string, runId: string, fn: () => Thenable | void, tasks: TestRunTask[]): Promise { + try { + const res = fn(); + if (isThenable(res)) { + return res + .then(() => this.handleInvokeResult(extensionId, runId, tasks, undefined)) + .catch(err => this.handleInvokeResult(extensionId, runId, tasks, err)); + } else { + return this.handleInvokeResult(extensionId, runId, tasks, undefined); + } + } catch (e) { + return this.handleInvokeResult(extensionId, runId, tasks, e); + } + } + + private async handleInvokeResult(extensionId: string, runId: string, tasks: TestRunTask[], error?: Error) { + const record = this.state.get(extensionId); + if (!record) { + return; + } + + record.queue.shift(); + if (record.queue.length > 0) { + record.queue[0](); + } else { + this.state.delete(extensionId); + } + + await Promise.all(tasks.map(t => t.onEnd.wait())); + } +} + +class TestRunDto { + public static fromPublic(request: vscode.TestRunRequest) { + return new TestRunDto( + generateUuid(), + new Set(request.tests.map(t => t.id)), + new Set(request.exclude?.map(t => t.id) ?? Iterable.empty()), + ); + } + + public static fromInternal(request: RunTestForProviderRequest) { + return new TestRunDto( + request.runId, + new Set(request.tests.map(t => t.testId)), + new Set(request.excludeExtIds), + ); + } + + constructor( + public readonly id: string, + private readonly include: ReadonlySet, + private readonly exclude: ReadonlySet, + ) { } + + public isIncluded(test: vscode.TestItem) { + for (let t: vscode.TestItem | undefined = test; t; t = t.parent) { + if (this.include.has(t.id)) { + return true; + } else if (this.exclude.has(t.id)) { + return false; + } + } + + return true; + } +} + +class TestRunTask implements vscode.TestRunTask { + readonly #proxy: MainThreadTestingShape; + readonly #req: TestRunDto; + readonly #taskId = generateUuid(); + readonly #sharedIds: Set; + public readonly onEnd = new Barrier(); + + constructor( + public readonly name: string | undefined, + dto: TestRunDto, + sharedTestIds: Set, + proxy: MainThreadTestingShape, + ) { + this.#proxy = proxy; + this.#req = dto; + this.#sharedIds = sharedTestIds; + proxy.$startedTestRunTask(dto.id, { id: this.#taskId, name, running: true }); + } + + setState(test: vscode.TestItem, state: vscode.TestResultState, duration?: number): void { + if (this.#req.isIncluded(test)) { + this.ensureTestIsKnown(test); + this.#proxy.$updateTestStateInRun(this.#req.id, this.#taskId, test.id, state, duration); + } + } + + appendMessage(test: vscode.TestItem, message: vscode.TestMessage): void { + if (this.#req.isIncluded(test)) { + this.ensureTestIsKnown(test); + this.#proxy.$appendTestMessageInRun(this.#req.id, this.#taskId, test.id, Convert.TestMessage.from(message)); + } + } + + appendOutput(output: string): void { + this.#proxy.$appendOutputToRun(this.#req.id, this.#taskId, VSBuffer.fromString(output)); + } + + end(): void { + this.#proxy.$finishedTestRunTask(this.#req.id, this.#taskId); + this.onEnd.open(); + } + + private ensureTestIsKnown(test: vscode.TestItem) { + const sent = this.#sharedIds; + if (sent.has(test.id)) { + return; + } + + const chain: ITestItem[] = []; + while (true) { + chain.unshift(Convert.TestItem.from(test)); + + if (sent.has(test.id)) { + break; + } + + sent.add(test.id); + if (!test.parent) { + break; + } + + test = test.parent; + } + + this.#proxy.$addTestsToRun(this.#req.id, chain); + } +} + +export const createDefaultDocumentTestRoot = async ( + provider: vscode.TestController, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined, token: CancellationToken, @@ -349,7 +543,7 @@ export const createDefaultDocumentTestRoot = async ( return; } - const root = await provider.provideWorkspaceTestRoot(folder, token); + const root = await provider.createWorkspaceTestRoot(folder, token); if (!root) { return; } @@ -365,25 +559,26 @@ export const createDefaultDocumentTestRoot = async ( * A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children * to only the children that are located in a certain vscode.Uri. */ -export class TestItemFilteredWrapper extends TestItemImpl { - private static wrapperMap = new WeakMap>(); +export class TestItemFilteredWrapper extends TestItemImpl { + private static wrapperMap = new WeakMap, TestItemFilteredWrapper>>(); + public static removeFilter(document: vscode.TextDocument): void { this.wrapperMap.delete(document); } // Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists. - public static getWrapperForTestItem( - item: T, + public static getWrapperForTestItem( + item: vscode.TestItem, filterDocument: vscode.TextDocument, - parent?: TestItemFilteredWrapper, - ): TestItemFilteredWrapper { + parent?: TestItemFilteredWrapper, + ): TestItemFilteredWrapper { let innerMap = this.wrapperMap.get(filterDocument); if (innerMap?.has(item)) { - return innerMap.get(item) as TestItemFilteredWrapper; + return innerMap.get(item) as TestItemFilteredWrapper; } if (!innerMap) { - innerMap = new WeakMap(); + innerMap = new WeakMap(); this.wrapperMap.set(filterDocument, innerMap); } @@ -396,8 +591,8 @@ export class TestItemFilteredWrapper(item: vscode.TestItem | TestItemFilteredWrapper) { + return item instanceof TestItemFilteredWrapper ? item.actual as vscode.TestItem : item; } private _cachedMatchesFilter: boolean | undefined; @@ -414,21 +609,37 @@ export class TestItemFilteredWrapper, private filterDocument: vscode.TextDocument, - public readonly parent?: TestItemFilteredWrapper, + public readonly actualParent?: TestItemFilteredWrapper, ) { - super(actual.id, actual.label, actual.uri, actual.expandable); + super(actual.id, actual.label, actual.uri, undefined); if (!(actual instanceof TestItemImpl)) { throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); } - (actual as TestItemImpl)[TestItemHookProperty] = { - setProp: (key, value) => (this as Record)[key] = value, - created: child => TestItemFilteredWrapper.getWrapperForTestItem(child, this.filterDocument, this).refreshMatch(), - invalidate: () => this.invalidate(), - delete: child => this.children.delete(child), - }; + this.debuggable = actual.debuggable; + this.runnable = actual.runnable; + this.description = actual.description; + this.error = actual.error; + this.status = actual.status; + + const wrapperApi = getPrivateApiFor(this); + const actualApi = getPrivateApiFor(actual); + actualApi.bus.event(evt => { + switch (evt[0]) { + case ExtHostTestItemEventType.SetProp: + (this as Record)[evt[1]] = evt[2]; + break; + case ExtHostTestItemEventType.NewChild: + const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(evt[1], this.filterDocument, this); + getPrivateApiFor(wrapper).parent = actual; + wrapper.refreshMatch(); + break; + default: + wrapperApi.bus.fire(evt); + } + }); } /** @@ -441,12 +652,12 @@ export class TestItemFilteredWrapper; depth: number; } @@ -562,7 +773,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection) { - let output: vscode.TestItem[] = []; + let output: vscode.TestItem[] = []; for (const itemId of itemIds) { const item = this.items.get(itemId); if (item) { @@ -584,7 +795,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection) { return this.items.get(item.id); } @@ -595,7 +806,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection, depth: parent ? parent.depth + 1 : 0, children: new Set(), }; @@ -644,7 +855,7 @@ abstract class AbstractTestObserverFactory { /** * Gets the internal test data by its reference, in any observer. */ - public getMirroredTestDataByReference(ref: vscode.TestItem) { + public getMirroredTestDataByReference(ref: vscode.TestItem) { for (const { tests } of this.resources.values()) { const v = tests.getMirroredTestDataByReference(ref); if (v) { diff --git a/src/vs/workbench/api/common/extHostTestingPrivateApi.ts b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts new file mode 100644 index 00000000000..66394266b4b --- /dev/null +++ b/src/vs/workbench/api/common/extHostTestingPrivateApi.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter } from 'vs/base/common/event'; +import { TestItemImpl } from 'vs/workbench/api/common/extHostTypes'; +import * as vscode from 'vscode'; + +export const enum ExtHostTestItemEventType { + NewChild, + Disposed, + Invalidated, + SetProp, +} + +export type ExtHostTestItemEvent = + | [evt: ExtHostTestItemEventType.NewChild, item: TestItemImpl] + | [evt: ExtHostTestItemEventType.Disposed] + | [evt: ExtHostTestItemEventType.Invalidated] + | [evt: ExtHostTestItemEventType.SetProp, key: keyof vscode.TestItem, value: any]; + +export interface IExtHostTestItemApi { + children: Map; + parent?: TestItemImpl; + bus: Emitter; +} + +const eventPrivateApis = new WeakMap(); + +/** + * Gets the private API for a test item implementation. This implementation + * is a managed object, but we keep a weakmap to avoid exposing any of the + * internals to extensions. + */ +export const getPrivateApiFor = (impl: TestItemImpl) => { + let api = eventPrivateApis.get(impl); + if (!api) { + api = { children: new Map(), bus: new Emitter() }; + eventPrivateApis.set(impl, api); + } + + return api; +}; diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 66a4a2096c7..f9e8d73a13a 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -29,7 +29,7 @@ import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebo import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor'; import * as notebooks from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as search from 'vs/workbench/contrib/search/common/search'; -import { ISerializedTestResults, ITestItem, ITestMessage, SerializedTestResultItem, TestItemExpandState } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ISerializedTestResults, ITestItem, ITestMessage, SerializedTestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import type * as vscode from 'vscode'; import * as types from './extHostTypes'; @@ -1419,7 +1419,17 @@ export namespace NotebookRange { export namespace NotebookCellMetadata { export function to(data: notebooks.NotebookCellMetadata): types.NotebookCellMetadata { - return new types.NotebookCellMetadata(data.inputCollapsed, data.outputCollapsed).with({ custom: data.custom }); + return new types.NotebookCellMetadata().with({ + ...data, + ...{ + executionOrder: null, + lastRunSuccess: null, + runState: null, + runStartTime: null, + runStartTimeAdjustment: null, + runEndTime: null + } + }); } } @@ -1430,7 +1440,7 @@ export namespace NotebookDocumentMetadata { } export function to(data: notebooks.NotebookDocumentMetadata): types.NotebookDocumentMetadata { - return new types.NotebookDocumentMetadata(data.trusted, data.custom); + return new types.NotebookDocumentMetadata().with(data); } } @@ -1633,15 +1643,16 @@ export namespace NotebookDocumentContentOptions { export function from(options: vscode.NotebookDocumentContentOptions | undefined): notebooks.TransientOptions { return { transientOutputs: options?.transientOutputs ?? false, - transientMetadata: { - ...options?.transientMetadata, + transientCellMetadata: { + ...(options?.transientCellMetadata ?? options?.transientMetadata), executionOrder: true, runState: true, runStartTime: true, runStartTimeAdjustment: true, runEndTime: true, lastRunSuccess: true - } + }, + transientDocumentMetadata: options?.transientDocumentMetadata ?? {} }; } } @@ -1684,9 +1695,9 @@ export namespace TestMessage { } export namespace TestItem { - export type Raw = vscode.TestItem; + export type Raw = vscode.TestItem; - export function from(item: vscode.TestItem): ITestItem { + export function from(item: vscode.TestItem): ITestItem { return { extId: item.id, label: item.label, @@ -1695,7 +1706,6 @@ export namespace TestItem { debuggable: item.debuggable ?? false, description: item.description, runnable: item.runnable ?? true, - expandable: item.expandable, }; } @@ -1708,25 +1718,27 @@ export namespace TestItem { debuggable: false, description: item.description, runnable: true, - expandable: true, }; } - export function toPlain(item: ITestItem): Omit { + export function toPlain(item: ITestItem): Omit, 'children' | 'invalidate' | 'discoverChildren'> { return { id: item.extId, label: item.label, uri: URI.revive(item.uri), range: Range.to(item.range), - expandable: item.expandable, + addChild: () => undefined, + dispose: () => undefined, + status: types.TestItemStatus.Pending, + data: undefined as never, debuggable: item.debuggable, description: item.description, runnable: item.runnable, }; } - export function to(item: ITestItem): types.TestItem { - const testItem = new types.TestItem(item.extId, item.label, URI.revive(item.uri), item.expandable); + export function to(item: ITestItem): types.TestItemImpl { + const testItem = new types.TestItemImpl(item.extId, item.label, URI.revive(item.uri), undefined); testItem.range = Range.to(item.range); testItem.debuggable = item.debuggable; testItem.description = item.description; @@ -1736,52 +1748,13 @@ export namespace TestItem { } export namespace TestResults { - export function from(id: string, results: vscode.TestRunResult): ISerializedTestResults { - const serialized: ISerializedTestResults = { - completedAt: results.completedAt, - id, - output: results.output, - items: [], - }; - - const queue: [parent: SerializedTestResultItem | null, children: Iterable][] = [ - [null, results.results], - ]; - - while (queue.length) { - const [parent, children] = queue.pop()!; - for (const item of children) { - const serializedItem: SerializedTestResultItem = { - children: item.children?.map(c => c.id) ?? [], - computedState: item.state, - item: TestItem.fromResultSnapshot(item), - state: { - state: item.state, - duration: item.duration, - messages: item.messages.map(TestMessage.from), - }, - retired: undefined, - expand: TestItemExpandState.Expanded, - parent: parent?.item.extId ?? null, - src: { provider: '', tree: -1 }, - direct: !parent, - }; - - serialized.items.push(serializedItem); - if (item.children) { - queue.push([serializedItem, item.children]); - } - } - } - - return serialized; - } - const convertTestResultItem = (item: SerializedTestResultItem, byInternalId: Map): vscode.TestResultSnapshot => ({ ...TestItem.toPlain(item.item), - state: item.state.state, - duration: item.state.duration, - messages: item.state.messages.map(TestMessage.to), + taskStates: item.tasks.map(t => ({ + state: t.state, + duration: t.duration, + messages: t.messages.map(TestMessage.to), + })), children: item.children .map(c => byInternalId.get(c)) .filter(isDefined) diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0ffbab2f9be..4157a949b15 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -7,12 +7,13 @@ import { coalesceInPlace, equals } from 'vs/base/common/arrays'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString, MarkdownString as BaseMarkdownString } from 'vs/base/common/htmlContent'; -import { ResourceMap } from 'vs/base/common/map'; +import { ReadonlyMapView, ResourceMap } from 'vs/base/common/map'; import { isStringArray } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { generateUuid } from 'vs/base/common/uuid'; import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files'; import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { getPrivateApiFor, ExtHostTestItemEventType, IExtHostTestItemApi } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import { CellEditType, ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; @@ -2916,11 +2917,16 @@ export class NotebookRange { if (start < 0) { throw illegalArgument('start must be positive'); } - if (end < start) { - throw illegalArgument('end cannot be smaller than start'); + if (end < 0) { + throw illegalArgument('end must be positive'); + } + if (start <= end) { + this._start = start; + this._end = end; + } else { + this._start = end; + this._end = start; } - this._start = start; - this._end = end; } with(change: { start?: number, end?: number }): NotebookRange { @@ -3231,16 +3237,17 @@ export enum TestMessageSeverity { Hint = 3 } -export const TestItemHookProperty = Symbol('TestItemHookProperty'); - -export interface ITestItemHook { - created(item: vscode.TestItem): void; - setProp(key: K, value: vscode.TestItem[K]): void; - invalidate(id: string): void; - delete(id: string): void; +export enum TestItemStatus { + Pending = 0, + Resolved = 1, } -const testItemPropAccessor = (item: TestItem, key: K, defaultValue: vscode.TestItem[K]) => { +const testItemPropAccessor = >( + api: IExtHostTestItemApi, + key: K, + defaultValue: vscode.TestItem[K], + equals: (a: vscode.TestItem[K], b: vscode.TestItem[K]) => boolean +) => { let value = defaultValue; return { enumerable: true, @@ -3248,80 +3255,41 @@ const testItemPropAccessor = (item: TestItem, k get() { return value; }, - set(newValue: vscode.TestItem[K]) { - item[TestItemHookProperty]?.setProp(key, newValue); - value = newValue; + set(newValue: vscode.TestItem[K]) { + if (!equals(value, newValue)) { + value = newValue; + api.bus.fire([ExtHostTestItemEventType.SetProp, key, newValue]); + } }, }; }; -export class TestChildrenCollection implements vscode.TestChildrenCollection { - #map = new Map(); - #hookRef: () => ITestItemHook | undefined; +const strictEqualComparator = (a: T, b: T) => a === b; +const rangeComparator = (a: vscode.Range | undefined, b: vscode.Range | undefined) => { + if (a === b) { return true; } + if (!a || !b) { return false; } + return a.isEqual(b); +}; - public get size() { - return this.#map.size; - } +export class TestItemImpl implements vscode.TestItem { + public readonly id!: string; + public readonly uri!: vscode.Uri; + public readonly children!: ReadonlyMap; + public readonly parent!: TestItemImpl | undefined; - constructor(hookRef: () => ITestItemHook | undefined) { - this.#hookRef = hookRef; - } - - public add(child: vscode.TestItem) { - const map = this.#map; - const hook = this.#hookRef(); - - const existing = map.get(child.id); - if (existing === child) { - return; - } - - if (existing) { - hook?.delete(child.id); - } - - map.set(child.id, child); - hook?.created(child); - } - - public get(id: string) { - return this.#map.get(id); - } - - public clear() { - for (const key of this.#map.keys()) { - this.delete(key); - } - } - - public delete(childOrId: vscode.TestItem | string) { - const id = typeof childOrId === 'string' ? childOrId : childOrId.id; - if (this.#map.has(id)) { - this.#map.delete(id); - this.#hookRef()?.delete(id); - } - } - - public toJSON() { - return [...this.#map.values()]; - } - - public [Symbol.iterator]() { - return this.#map.values(); - } -} - -export class TestItem implements vscode.TestItem { - public id!: string; public range!: vscode.Range | undefined; public description!: string | undefined; public runnable!: boolean; public debuggable!: boolean; - public children!: TestChildrenCollection; - public uri!: vscode.Uri; - public [TestItemHookProperty]!: ITestItemHook | undefined; + public error!: string | vscode.MarkdownString; + public status!: vscode.TestItemStatus; + + /** Extension-owned resolve handler */ + public resolveHandler?: (token: vscode.CancellationToken) => void; + + constructor(id: string, public label: string, uri: vscode.Uri, public data: unknown) { + const api = getPrivateApiFor(this); - constructor(id: string, public label: string, uri: vscode.Uri, public expandable: boolean) { Object.defineProperties(this, { id: { value: id, @@ -3333,32 +3301,52 @@ export class TestItem implements vscode.TestItem { enumerable: true, writable: false, }, + parent: { + enumerable: false, + get: () => api.parent, + }, children: { - value: new TestChildrenCollection(() => this[TestItemHookProperty]), + value: new ReadonlyMapView(api.children), enumerable: true, writable: false, }, - [TestItemHookProperty]: { - enumerable: false, - writable: true, - configurable: false, - }, - range: testItemPropAccessor(this, 'range', undefined), - description: testItemPropAccessor(this, 'description', undefined), - runnable: testItemPropAccessor(this, 'runnable', true), - debuggable: testItemPropAccessor(this, 'debuggable', true), + range: testItemPropAccessor(api, 'range', undefined, rangeComparator), + description: testItemPropAccessor(api, 'description', undefined, strictEqualComparator), + runnable: testItemPropAccessor(api, 'runnable', true, strictEqualComparator), + debuggable: testItemPropAccessor(api, 'debuggable', true, strictEqualComparator), + status: testItemPropAccessor(api, 'status', TestItemStatus.Resolved, strictEqualComparator), }); } public invalidate() { - this[TestItemHookProperty]?.invalidate(this.id); + getPrivateApiFor(this).bus.fire([ExtHostTestItemEventType.Invalidated]); } - public discoverChildren(progress: vscode.Progress<{ busy: boolean }>, _token: vscode.CancellationToken) { - progress.report({ busy: false }); + public dispose() { + const api = getPrivateApiFor(this); + if (api.parent) { + getPrivateApiFor(api.parent).children.delete(this.id); + } + + api.bus.fire([ExtHostTestItemEventType.Disposed]); + } + + public addChild(child: vscode.TestItem) { + if (!(child instanceof TestItemImpl)) { + throw new Error('Test child must be created through vscode.test.createTestItem()'); + } + + const api = getPrivateApiFor(this); + if (api.children.has(child.id)) { + throw new Error(`Attempted to insert a duplicate test item ID ${child.id}`); + } + + api.children.set(child.id, child); + api.bus.fire([ExtHostTestItemEventType.NewChild, child]); } } + export class TestMessage implements vscode.TestMessage { public severity = TestMessageSeverity.Error; public expectedOutput?: string; diff --git a/src/vs/workbench/api/common/extHostWorkspace.ts b/src/vs/workbench/api/common/extHostWorkspace.ts index e89ad13926b..5f0bc4cea9a 100644 --- a/src/vs/workbench/api/common/extHostWorkspace.ts +++ b/src/vs/workbench/api/common/extHostWorkspace.ts @@ -563,7 +563,8 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac } requestWorkspaceTrust(options?: vscode.WorkspaceTrustRequestOptions): Promise { - return this._proxy.$requestWorkspaceTrust(options); + const promise = this._proxy.$requestWorkspaceTrust(options); + return options?.modal ? promise : Promise.resolve(this._trusted); } $onDidReceiveWorkspaceTrust(): void { diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 8f58c1c96c3..66b252be651 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -27,8 +27,6 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { private _variableResolverPromise: Promise; private _lastActiveWorkspace: IWorkspaceFolder | undefined; - // TODO: Pull this from main side - private _isWorkspaceShellAllowed: boolean = false; private _defaultShell: string | undefined; constructor( @@ -43,7 +41,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { // Getting the SystemShell is an async operation, however, the ExtHost terminal service is mostly synchronous // and the API `vscode.env.shell` is also synchronous. The default shell _should_ be set when extensions are // starting up but if not, we run getSystemShellSync below which gets a sane default. - getSystemShell(platform.platform, process.env as platform.IProcessEnvironment).then(s => this._defaultShell = s); + getSystemShell(platform.OS, process.env as platform.IProcessEnvironment).then(s => this._defaultShell = s); this._updateLastActiveWorkspace(); this._variableResolverPromise = this._updateVariableResolver(); @@ -77,17 +75,15 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { } public getDefaultShell(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string { - const fetchSetting = (key: string): { userValue: string | string[] | undefined, value: string | string[] | undefined, defaultValue: string | string[] | undefined } => { - const setting = configProvider + const fetchSetting = (key: string): string | undefined => { + return configProvider .getConfiguration(key.substr(0, key.lastIndexOf('.'))) - .inspect(key.substr(key.lastIndexOf('.') + 1)); - return this._apiInspectConfigToPlain(setting); + .get(key.substr(key.lastIndexOf('.') + 1)); }; return terminalEnvironment.getDefaultShell( fetchSetting, - this._isWorkspaceShellAllowed, - this._defaultShell ?? getSystemShellSync(platform.platform, process.env as platform.IProcessEnvironment), + this._defaultShell ?? getSystemShellSync(platform.OS, process.env as platform.IProcessEnvironment), process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), process.env.windir, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), @@ -97,24 +93,13 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { } public getDefaultShellArgs(useAutomationShell: boolean, configProvider: ExtHostConfigProvider): string[] | string { - const fetchSetting = (key: string): { userValue: string | string[] | undefined, value: string | string[] | undefined, defaultValue: string | string[] | undefined } => { - const setting = configProvider + const fetchSetting = (key: string): string | string[] | undefined => { + return configProvider .getConfiguration(key.substr(0, key.lastIndexOf('.'))) - .inspect(key.substr(key.lastIndexOf('.') + 1)); - return this._apiInspectConfigToPlain(setting); + .get(key.substr(key.lastIndexOf('.') + 1)); }; - return terminalEnvironment.getDefaultShellArgs(fetchSetting, this._isWorkspaceShellAllowed, useAutomationShell, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), this._logService); - } - - private _apiInspectConfigToPlain( - config: { key: string; defaultValue?: T; globalValue?: T; workspaceValue?: T, workspaceFolderValue?: T } | undefined - ): { userValue: T | undefined, value: T | undefined, defaultValue: T | undefined } { - return { - userValue: config ? config.globalValue : undefined, - value: config ? config.workspaceValue : undefined, - defaultValue: config ? config.defaultValue : undefined, - }; + return terminalEnvironment.getDefaultShellArgs(fetchSetting, useAutomationShell, terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver), this._logService); } private _registerListeners(): void { @@ -150,8 +135,4 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService { args: this.getDefaultShellArgs(useAutomationShell, configProvider) }; } - - public $acceptWorkspacePermissionsChanged(isAllowed: boolean): void { - this._isWorkspaceShellAllowed = isAllowed; - } } diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index 94160516029..1ba0978d7a9 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -268,10 +268,10 @@ class LogWorkingCopiesAction extends Action2 { const logService = accessor.get(ILogService); const msg = [ `Dirty Working Copies:`, - ...workingCopyService.dirtyWorkingCopies.map(workingCopy => workingCopy.resource.toString(true)), + ...workingCopyService.dirtyWorkingCopies.map(workingCopy => `${workingCopy.resource.toString(true)} (typeId: ${workingCopy.typeId || ''})`), ``, `All Working Copies:`, - ...workingCopyService.workingCopies.map(workingCopy => workingCopy.resource.toString(true)), + ...workingCopyService.workingCopies.map(workingCopy => `${workingCopy.resource.toString(true)} (typeId: ${workingCopy.typeId || ''})`), ]; logService.info(msg.join('\n')); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index bfb5933b877..6839635871f 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -9,7 +9,8 @@ import { basename, isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; -import { ITextFileService, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { FileAccess, Schemas } from 'vs/base/common/network'; import { ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { DataTransfers, IDragAndDropData } from 'vs/base/browser/dnd'; @@ -27,8 +28,9 @@ import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsSe import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Emitter } from 'vs/base/common/event'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; export interface IDraggedResource { resource: URI; @@ -110,7 +112,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array r.resource.fsPath === file.path) /* prevent duplicates */) { + if (file?.path /* Electron only */ && !resources.some(resource => resource.resource.fsPath === file.path) /* prevent duplicates */) { try { resources.push({ resource: URI.file(file.path), isExternal: true }); } catch (error) { @@ -126,7 +128,7 @@ export function extractResources(e: DragEvent, externalOnly?: boolean): Array { - if (!resources.some(r => r.resource.fsPath === codeFile) /* prevent duplicates */) { + if (!resources.some(resource => resource.resource.fsPath === codeFile) /* prevent duplicates */) { resources.push({ resource: URI.file(codeFile), isExternal: true }); } }); @@ -159,7 +161,7 @@ export class ResourcesDropHandler { @IFileService private readonly fileService: IFileService, @IWorkspacesService private readonly workspacesService: IWorkspacesService, @ITextFileService private readonly textFileService: ITextFileService, - @IBackupFileService private readonly backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @IEditorService private readonly editorService: IEditorService, @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService, @IHostService private readonly hostService: IHostService @@ -167,7 +169,7 @@ export class ResourcesDropHandler { } async handleDrop(event: DragEvent, resolveTargetGroup: () => IEditorGroup | undefined, afterDrop: (targetGroup: IEditorGroup | undefined) => void, targetIndex?: number): Promise { - const untitledOrFileResources = extractResources(event).filter(r => this.fileService.canHandleResource(r.resource) || r.resource.scheme === Schemas.untitled); + const untitledOrFileResources = extractResources(event).filter(resource => this.fileService.canHandleResource(resource.resource) || resource.resource.scheme === Schemas.untitled); if (!untitledOrFileResources.length) { return; } @@ -245,7 +247,7 @@ export class ResourcesDropHandler { // content and turn it into a backup so that it loads the contents if (typeof droppedDirtyEditor.dirtyContent === 'string') { try { - await this.backupFileService.backup(droppedDirtyEditor.resource, stringToSnapshot(droppedDirtyEditor.dirtyContent)); + await this.workingCopyBackupService.backup({ resource: droppedDirtyEditor.resource, typeId: NO_TYPE_ID }, bufferToReadable(VSBuffer.fromString(droppedDirtyEditor.dirtyContent))); } catch (e) { // Ignore error } diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 1d0e6b90007..eb7d81c37b7 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -5,16 +5,23 @@ import { localize } from 'vs/nls'; import { Event } from 'vs/base/common/event'; -import { EditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, EditorResourceAccessor, IEditorInput, IEditorInputFactoryRegistry, SideBySideEditorInput, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; +import { IConstructorSignature0, IInstantiationService, BrandedService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { Extensions as ConfigurationExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; +import { Promises } from 'vs/base/common/async'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { URI } from 'vs/workbench/workbench.web.api'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; export const Extensions = { Editors: 'workbench.contributions.editors', @@ -328,3 +335,108 @@ Registry.add(Extensions.Associations, new EditorAssociationsRegistry()); configurationRegistry.registerConfiguration(editorAssociationsConfigurationNode); //#endregion + + +//#region Text Editor Close Tracker + +export function whenTextEditorClosed(accessor: ServicesAccessor, resources: URI[]): Promise { + const editorService = accessor.get(IEditorService); + const uriIdentityService = accessor.get(IUriIdentityService); + const workingCopyService = accessor.get(IWorkingCopyService); + + const fileEditorInputFactory = Registry.as(EditorInputExtensions.EditorInputFactories).getFileEditorInputFactory(); + + return new Promise(resolve => { + let remainingResources = [...resources]; + + // Observe any editor closing from this moment on + const listener = editorService.onDidCloseEditor(async event => { + let primaryResource: URI | undefined = undefined; + let secondaryResource: URI | undefined = undefined; + + // Resolve the resources from the editor that closed + // but only consider file editor inputs, given we + // are only tracking text editors. + if (event.editor instanceof SideBySideEditorInput) { + if (fileEditorInputFactory.isFileEditorInput(event.editor.primary)) { + primaryResource = EditorResourceAccessor.getOriginalUri(event.editor.primary); + } + + if (fileEditorInputFactory.isFileEditorInput(event.editor.secondary)) { + secondaryResource = EditorResourceAccessor.getOriginalUri(event.editor.secondary); + } + } else { + if (fileEditorInputFactory.isFileEditorInput(event.editor)) { + primaryResource = EditorResourceAccessor.getOriginalUri(event.editor); + } + } + + // Remove from resources to wait for being closed based on the + // resources from editors that got closed + remainingResources = remainingResources.filter(resource => { + if (uriIdentityService.extUri.isEqual(resource, primaryResource) || uriIdentityService.extUri.isEqual(resource, secondaryResource)) { + return false; // remove - the closing editor matches this resource + } + + return true; // keep - not yet closed + }); + + // All resources to wait for being closed are closed + if (remainingResources.length === 0) { + + // If auto save is configured with the default delay (1s) it is possible + // to close the editor while the save still continues in the background. As such + // we have to also check if the editors to track for are dirty and if so wait + // for them to get saved. + const dirtyResources = resources.filter(resource => workingCopyService.isDirty(resource, NO_TYPE_ID /* only check on text file working copies */)); + if (dirtyResources.length > 0) { + await Promises.settled(dirtyResources.map(async resource => await new Promise(resolve => { + if (!workingCopyService.isDirty(resource, NO_TYPE_ID /* only check on text file working copies */)) { + return resolve(); // return early if resource is not dirty + } + + // Otherwise resolve promise when resource is saved + const listener = workingCopyService.onDidChangeDirty(workingCopy => { + if (!workingCopy.isDirty() && uriIdentityService.extUri.isEqual(resource, workingCopy.resource)) { + listener.dispose(); + + return resolve(); + } + }); + }))); + } + + listener.dispose(); + + return resolve(); + } + }); + }); +} + +//#endregion + + +//#region ARIA + +export function computeEditorAriaLabel(input: IEditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number): string { + let ariaLabel = input.getAriaLabel(); + if (group && !group.isPinned(input)) { + ariaLabel = localize('preview', "{0}, preview", ariaLabel); + } + + if (group?.isSticky(index ?? input)) { + ariaLabel = localize('pinned', "{0}, pinned", ariaLabel); + } + + // Apply group information to help identify in + // which group we are (only if more than one group + // is actually opened) + if (group && groupCount > 1) { + ariaLabel = `${ariaLabel}, ${group.ariaLabel}`; + } + + return ariaLabel; +} + +//#endregion diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 94461bac6b3..02ff8b3f52a 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -7,7 +7,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { EventType, addDisposableListener, getClientArea, Dimension, position, size, IDimension, isAncestorUsingFlowTo } from 'vs/base/browser/dom'; import { onDidChangeFullscreen, isFullscreen } from 'vs/base/browser/browser'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Registry } from 'vs/platform/registry/common/platform'; import { isWindows, isLinux, isMacintosh, isWeb, isNative } from 'vs/base/common/platform'; import { pathsToEditors, SideBySideEditorInput } from 'vs/workbench/common/editor'; @@ -168,7 +168,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private viewletService!: IViewletService; private viewDescriptorService!: IViewDescriptorService; private contextService!: IWorkspaceContextService; - private backupFileService!: IBackupFileService; + private workingCopyBackupService!: IWorkingCopyBackupService; private notificationService!: INotificationService; private themeService!: IThemeService; private activityBarService!: IActivityBarService; @@ -248,7 +248,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.hostService = accessor.get(IHostService); this.contextService = accessor.get(IWorkspaceContextService); this.storageService = accessor.get(IStorageService); - this.backupFileService = accessor.get(IBackupFileService); + this.workingCopyBackupService = accessor.get(IWorkingCopyBackupService); this.themeService = accessor.get(IThemeService); this.extensionService = accessor.get(IExtensionService); this.logService = accessor.get(ILogService); @@ -616,7 +616,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return []; // do not open any empty untitled file if we restored groups/editors from previous session } - return this.backupFileService.hasBackups().then(hasBackups => { + return this.workingCopyBackupService.hasBackups().then(hasBackups => { if (hasBackups) { return []; // do not open any empty untitled file if we have backups to restore } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 105de1515a8..ff528d84ab8 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -14,7 +14,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { IMenuService, MenuId, IMenu, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { activeContrastBorder, focusBorder, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActivityAction, ActivityActionViewItem, IActivityHoverOptions, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { CATEGORIES } from 'vs/workbench/common/actions'; @@ -112,16 +112,16 @@ class MenuActivityActionViewItem extends ActivityActionViewItem { action: ActivityAction, private contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, - hoverOptions: IActivityHoverOptions | undefined, + hoverOptions: IActivityHoverOptions, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, @IMenuService protected readonly menuService: IMenuService, @IContextMenuService protected readonly contextMenuService: IContextMenuService, @IContextKeyService protected readonly contextKeyService: IContextKeyService, - @IConfigurationService protected readonly configurationService: IConfigurationService, + @IConfigurationService configurationService: IConfigurationService, @IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService, ) { - super(action, { draggable: false, colors, icon: true, hasPopup: true, hoverOptions }, themeService, hoverService); + super(action, { draggable: false, colors, icon: true, hasPopup: true, hoverOptions }, themeService, hoverService, configurationService); } override render(container: HTMLElement): void { @@ -193,7 +193,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem { action: ActivityAction, contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, - activityHoverOptions: IActivityHoverOptions | undefined, + activityHoverOptions: IActivityHoverOptions, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, @IContextMenuService contextMenuService: IContextMenuService, @@ -302,7 +302,7 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem { action: ActivityAction, contextMenuActionsProvider: () => IAction[], colors: (theme: IColorTheme) => ICompositeBarColors, - activityHoverOptions: IActivityHoverOptions | undefined, + activityHoverOptions: IActivityHoverOptions, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, @IMenuService menuService: IMenuService, @@ -386,15 +386,6 @@ registerAction2( ); registerThemingParticipant((theme, collector) => { - const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); - if (toolbarHoverBackgroundColor) { - collector.addRule(` - .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item:hover { - background-color: ${toolbarHoverBackgroundColor} !important; - } - `); - } - const activityBarForegroundColor = theme.getColor(ACTIVITY_BAR_FOREGROUND); if (activityBarForegroundColor) { collector.addRule(` diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 9bab513def0..3aee53a7e63 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -219,7 +219,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { })); } - private getActivityHoverOptions(): IActivityHoverOptions | undefined { + private getActivityHoverOptions(): IActivityHoverOptions { return { alignment: () => this.layoutService.getSideBarPosition() === Position.LEFT ? ActivityHoverAlignment.RIGHT : ActivityHoverAlignment.LEFT, delay: () => 0 diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 7e758293257..bb7970f5b36 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -145,7 +145,7 @@ export interface ICompositeBarOptions { readonly compositeSize: number; readonly overflowActionSize: number; readonly dndHandler: ICompositeDragAndDrop; - readonly activityHoverOptions?: IActivityHoverOptions; + readonly activityHoverOptions: IActivityHoverOptions; readonly preventLoopNavigation?: boolean; getActivityAction: (compositeId: string) => ActivityAction; diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 95cef40665a..b9f576c8097 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { Action, IAction, Separator } from 'vs/base/common/actions'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType, getDomNodePagePosition, hide, show } from 'vs/base/browser/dom'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { dispose, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, toDisposable, MutableDisposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TextBadge, NumberBadge, IBadge, IconBadge, ProgressBadge } from 'vs/workbench/services/activity/common/activity'; @@ -25,6 +25,7 @@ import { IHoverService, IHoverTarget } from 'vs/workbench/services/hover/browser import { domEvent } from 'vs/base/browser/event'; import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview'; import { RunOnceScheduler } from 'vs/base/common/async'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export interface ICompositeActivity { badge: IBadge; @@ -138,7 +139,7 @@ export interface IActivityHoverOptions { export interface IActivityActionViewItemOptions extends IBaseActionViewItemOptions { icon?: boolean; colors: (theme: IColorTheme) => ICompositeBarColors; - hoverOptions?: IActivityHoverOptions; + hoverOptions: IActivityHoverOptions; hasPopup?: boolean; } @@ -153,6 +154,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { private mouseUpTimeout: any; private title: string = ''; + private readonly hoverDisposables = this._register(new DisposableStore()); private readonly hover = this._register(new MutableDisposable()); private readonly showHoverScheduler = new RunOnceScheduler(() => this.showHover(), 0); @@ -161,6 +163,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { options: IActivityActionViewItemOptions, @IThemeService protected readonly themeService: IThemeService, @IHoverService private readonly hoverService: IHoverService, + @IConfigurationService protected readonly configurationService: IConfigurationService, ) { super(null, action, options); @@ -168,6 +171,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { this._register(this.themeService.onDidColorThemeChange(this.onThemeChange, this)); this._register(action.onDidChangeActivity(this.updateActivity, this)); + this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('workbench.experimental.useCustomHover'))(() => this.updateHover())); this._register(action.onDidChangeBadge(this.updateBadge, this)); this._register(toDisposable(() => this.showHoverScheduler.cancel())); } @@ -264,41 +268,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { this.updateActivity(); this.updateStyles(); - - if (this.options.hoverOptions) { - this.container.setAttribute('title', ''); - this.container.removeAttribute('title'); - this._register(domEvent(container, EventType.MOUSE_OVER, true)(() => { - if (!this.showHoverScheduler.isScheduled()) { - this.showHoverScheduler.schedule(this.options.hoverOptions!.delay()); - } - })); - this._register(domEvent(container, EventType.MOUSE_LEAVE, true)(() => { - this.hover.value = undefined; - this.showHoverScheduler.cancel(); - })); - } - - } - - private showHover(): void { - if (this.hover.value) { - return; - } - const { left, right, bottom } = this.container.getBoundingClientRect(); - const hoverAlignment = this.options.hoverOptions!.alignment(); - const anchorPosition: AnchorPosition | undefined = hoverAlignment === ActivityHoverAlignment.ABOVE ? AnchorPosition.ABOVE : hoverAlignment === ActivityHoverAlignment.BELOW ? AnchorPosition.BELOW : undefined; - const target: IHoverTarget | HTMLElement = anchorPosition === undefined ? { - targetElements: [this.container], - x: hoverAlignment === ActivityHoverAlignment.RIGHT ? right + 2 : left - 2, - y: bottom - 10, - dispose: () => { } - } : this.container; - this.hover.value = this.hoverService.showHover({ - target, - anchorPosition, - text: this.title, - }); + this.updateHover(); } private onThemeChange(theme: IColorTheme): void { @@ -408,13 +378,61 @@ export class ActivityActionViewItem extends BaseActionViewItem { [this.label, this.badge, this.container].forEach(element => { if (element) { element.setAttribute('aria-label', title); - if (!this.options.hoverOptions) { + if (this.useCustomHover) { + element.setAttribute('title', ''); + element.removeAttribute('title'); + } else { element.setAttribute('title', title); } } }); } + private updateHover(): void { + this.hoverDisposables.clear(); + + this.updateTitle(this.title); + if (this.useCustomHover) { + this.hoverDisposables.add(domEvent(this.container, EventType.MOUSE_OVER, true)(() => { + if (!this.showHoverScheduler.isScheduled()) { + this.showHoverScheduler.schedule(this.options.hoverOptions!.delay() || 150); + } + })); + this.hoverDisposables.add(domEvent(this.container, EventType.MOUSE_LEAVE, true)(() => { + this.hover.value = undefined; + this.showHoverScheduler.cancel(); + })); + this.hoverDisposables.add(toDisposable(() => { + this.hover.value = undefined; + this.showHoverScheduler.cancel(); + })); + } + } + + private showHover(): void { + if (this.hover.value) { + return; + } + const { left, right, bottom } = this.container.getBoundingClientRect(); + const hoverAlignment = this.options.hoverOptions!.alignment(); + const anchorPosition: AnchorPosition | undefined = hoverAlignment === ActivityHoverAlignment.ABOVE ? AnchorPosition.ABOVE : hoverAlignment === ActivityHoverAlignment.BELOW ? AnchorPosition.BELOW : undefined; + const target: IHoverTarget | HTMLElement = anchorPosition === undefined ? { + targetElements: [this.container], + x: hoverAlignment === ActivityHoverAlignment.RIGHT ? right + 2 : left - 2, + y: bottom - 10, + dispose: () => { } + } : this.container; + this.hover.value = this.hoverService.showHover({ + target, + anchorPosition, + text: this.title, + }); + } + + private get useCustomHover(): boolean { + return !!this.configurationService.getValue('workbench.experimental.useCustomHover'); + } + override dispose(): void { super.dispose(); @@ -453,12 +471,13 @@ export class CompositeOverflowActivityActionViewItem extends ActivityActionViewI private getBadge: (compositeId: string) => IBadge, private getCompositeOpenAction: (compositeId: string) => IAction, colors: (theme: IColorTheme) => ICompositeBarColors, - hoverOptions: IActivityHoverOptions | undefined, + hoverOptions: IActivityHoverOptions, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IThemeService themeService: IThemeService, - @IHoverService hoverService: IHoverService + @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(action, { icon: true, colors, hasPopup: true, hoverOptions }, themeService, hoverService); + super(action, { icon: true, colors, hasPopup: true, hoverOptions }, themeService, hoverService, configurationService); } showMenu(): void { @@ -540,8 +559,9 @@ export class CompositeActionViewItem extends ActivityActionViewItem { @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, @IHoverService hoverService: IHoverService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(compositeActivityAction, options, themeService, hoverService); + super(compositeActivityAction, options, themeService, hoverService, configurationService); if (!CompositeActionViewItem.manageExtensionAction) { CompositeActionViewItem.manageExtensionAction = instantiationService.createInstance(ManageExtensionAction); diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 8bc795a28a1..a0d43504614 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -13,18 +13,15 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { URI } from 'vs/base/common/uri'; import { Dimension, size, clearNode, append, addDisposableListener, EventType, $ } from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; import { ByteSize } from 'vs/platform/files/common/files'; export interface IOpenCallbacks { openInternal: (input: EditorInput, options: EditorOptions | undefined) => Promise; - openExternal: (uri: URI) => void; } /* @@ -42,15 +39,14 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { private metadata: string | undefined; private binaryContainer: HTMLElement | undefined; private scrollbar: DomScrollableElement | undefined; - private resourceViewerContext: ResourceViewerContext | undefined; + private inputDisposable = this._register(new MutableDisposable()); constructor( id: string, callbacks: IOpenCallbacks, telemetryService: ITelemetryService, themeService: IThemeService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IStorageService storageService: IStorageService, + @IStorageService storageService: IStorageService ) { super(id, telemetryService, themeService, storageService); @@ -65,7 +61,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { // Container for Binary this.binaryContainer = document.createElement('div'); - this.binaryContainer.className = 'binary-container'; + this.binaryContainer.className = 'monaco-binary-resource-editor'; this.binaryContainer.style.outline = 'none'; this.binaryContainer.tabIndex = 0; // enable focus support from the editor part (do not remove) @@ -89,23 +85,38 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { } // Render Input - if (this.resourceViewerContext) { - this.resourceViewerContext.dispose(); - } - - const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); - this.resourceViewerContext = ResourceViewer.show({ name: model.getName(), resource: model.resource, size: model.getSize(), etag: model.getETag(), mime: model.getMime() }, binaryContainer, scrollbar, { - openInternalClb: () => this.handleOpenInternalCallback(input, options), - openExternalClb: this.environmentService.remoteAuthority ? undefined : resource => this.callbacks.openExternal(resource), - metadataClb: meta => this.handleMetadataChanged(meta) - }); + this.inputDisposable.value = this.renderInput(input, options, model); } - private async handleOpenInternalCallback(input: EditorInput, options: EditorOptions | undefined): Promise { - await this.callbacks.openInternal(input, options); + private renderInput(input: EditorInput, options: EditorOptions | undefined, model: BinaryEditorModel): IDisposable { + const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); - // Signal to listeners that the binary editor has been opened in-place - this._onDidOpenInPlace.fire(); + clearNode(binaryContainer); + + const disposables = new DisposableStore(); + + const label = document.createElement('p'); + label.textContent = localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."); + binaryContainer.appendChild(label); + + const link = append(label, $('a.embedded-link')); + link.setAttribute('role', 'button'); + link.textContent = localize('openAsText', "Do you want to open it anyway?"); + + disposables.add(addDisposableListener(link, EventType.CLICK, async () => { + await this.callbacks.openInternal(input, options); + + // Signal to listeners that the binary editor has been opened in-place + this._onDidOpenInPlace.fire(); + })); + + scrollbar.scanDomNode(); + + // Update metadata + const size = model.getSize(); + this.handleMetadataChanged(typeof size === 'number' ? ByteSize.formatSize(size) : ''); + + return disposables; } private handleMetadataChanged(meta: string | undefined): void { @@ -127,8 +138,7 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { if (this.binaryContainer) { clearNode(this.binaryContainer); } - dispose(this.resourceViewerContext); - this.resourceViewerContext = undefined; + this.inputDisposable.clear(); super.clearInput(); } @@ -139,9 +149,6 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { const [binaryContainer, scrollbar] = assertAllDefined(this.binaryContainer, this.scrollbar); size(binaryContainer, dimension.width, dimension.height); scrollbar.scanDomNode(); - if (typeof this.resourceViewerContext?.layout === 'function') { - this.resourceViewerContext.layout(dimension); - } } override focus(): void { @@ -151,106 +158,8 @@ export abstract class BaseBinaryResourceEditor extends EditorPane { } override dispose(): void { - if (this.binaryContainer) { - this.binaryContainer.remove(); - } - - dispose(this.resourceViewerContext); - this.resourceViewerContext = undefined; + this.binaryContainer?.remove(); super.dispose(); } } - -export interface IResourceDescriptor { - readonly resource: URI; - readonly name: string; - readonly size?: number; - readonly etag?: string; - readonly mime: string; -} - -interface ResourceViewerContext extends IDisposable { - layout?(dimension: Dimension): void; -} - -interface ResourceViewerDelegate { - openInternalClb(uri: URI): void; - openExternalClb?(uri: URI): void; - metadataClb(meta: string): void; -} - -class ResourceViewer { - - private static readonly MAX_OPEN_INTERNAL_SIZE = ByteSize.MB * 200; // max size until we offer an action to open internally - - static show( - descriptor: IResourceDescriptor, - container: HTMLElement, - scrollbar: DomScrollableElement, - delegate: ResourceViewerDelegate, - ): ResourceViewerContext { - - // Ensure CSS class - container.className = 'monaco-binary-resource-editor'; - - // Large Files - if (typeof descriptor.size === 'number' && descriptor.size > ResourceViewer.MAX_OPEN_INTERNAL_SIZE) { - return FileTooLargeFileView.create(container, descriptor.size, scrollbar, delegate); - } - - // Seemingly Binary Files - return FileSeemsBinaryFileView.create(container, descriptor, scrollbar, delegate); - } -} - -class FileTooLargeFileView { - static create( - container: HTMLElement, - descriptorSize: number, - scrollbar: DomScrollableElement, - delegate: ResourceViewerDelegate - ) { - const size = ByteSize.formatSize(descriptorSize); - delegate.metadataClb(size); - - clearNode(container); - - const label = document.createElement('span'); - label.textContent = localize('nativeFileTooLargeError', "The file is not displayed in the editor because it is too large ({0}).", size); - container.appendChild(label); - - scrollbar.scanDomNode(); - - return Disposable.None; - } -} - -class FileSeemsBinaryFileView { - static create( - container: HTMLElement, - descriptor: IResourceDescriptor, - scrollbar: DomScrollableElement, - delegate: ResourceViewerDelegate - ) { - delegate.metadataClb(typeof descriptor.size === 'number' ? ByteSize.formatSize(descriptor.size) : ''); - - clearNode(container); - - const disposables = new DisposableStore(); - - const label = document.createElement('p'); - label.textContent = localize('nativeBinaryError', "The file is not displayed in the editor because it is either binary or uses an unsupported text encoding."); - container.appendChild(label); - - const link = append(label, $('a.embedded-link')); - link.setAttribute('role', 'button'); - link.textContent = localize('openAsText', "Do you want to open it anyway?"); - - disposables.add(addDisposableListener(link, EventType.CLICK, () => delegate.openInternalClb(descriptor.resource))); - - scrollbar.scanDomNode(); - - return disposables; - } -} diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index b61b8d45d41..8bddd352872 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -11,7 +11,8 @@ import { SaveReason, IEditorIdentifier, IEditorInput, GroupIdentifier, ISaveOpti import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ILogService } from 'vs/platform/log/common/log'; export class EditorAutoSave extends Disposable implements IWorkbenchContribution { @@ -184,7 +185,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Clear any running auto save operation this.discardAutoSave(workingCopy); - this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString()); + this.logService.trace(`[editor auto save] scheduling auto save after ${this.autoSaveAfterDelay}ms`, workingCopy.resource.toString(true), workingCopy.typeId); // Schedule new auto save const handle = setTimeout(() => { @@ -194,7 +195,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Save if dirty if (workingCopy.isDirty()) { - this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString()); + this.logService.trace(`[editor auto save] running auto save`, workingCopy.resource.toString(true), workingCopy.typeId); workingCopy.save({ reason: SaveReason.AUTO }); } @@ -202,7 +203,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution // Keep in map for disposal as needed this.pendingAutoSavesAfterDelay.set(workingCopy, toDisposable(() => { - this.logService.trace(`[editor auto save] clearing pending auto save`, workingCopy.resource.toString()); + this.logService.trace(`[editor auto save] clearing pending auto save`, workingCopy.resource.toString(true), workingCopy.typeId); clearTimeout(handle); })); diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index fb118890b85..073efd02ef1 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -1068,6 +1068,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } const handle = this.notificationService.notify({ + id: `${hash(editor.resource?.toString())}`, // unique per editor severity: Severity.Error, message: localize('editorOpenError', "Unable to open '{0}': {1}.", editor.getName(), toErrorMessage(error)), actions diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index 99b311a671d..9c86a843d7c 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { Action, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { IFileEditorInput, EncodingMode, IEncodingSupport, EditorResourceAccessor, SideBySideEditorInput, IEditorPane, IEditorInput, SideBySideEditor, IModeSupport } from 'vs/workbench/common/editor'; +import { IFileEditorInput, EditorResourceAccessor, SideBySideEditorInput, IEditorPane, IEditorInput, SideBySideEditor } from 'vs/workbench/common/editor'; import { Disposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorAction } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; @@ -31,7 +31,7 @@ import { Selection } from 'vs/editor/common/core/selection'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { EncodingMode, IEncodingSupport, IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { SUPPORTED_ENCODINGS } from 'vs/workbench/services/textfile/common/encoding'; import { ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ConfigurationChangedEvent, IEditorOptions, EditorOption } from 'vs/editor/common/config/editorOptions'; diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 2d64cc3f46b..5ca4311be8b 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -6,7 +6,8 @@ import 'vs/css!./media/tabstitlecontrol'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { shorten } from 'vs/base/common/labels'; -import { EditorResourceAccessor, GroupIdentifier, IEditorInput, Verbosity, EditorCommandsContextActionRunner, IEditorPartOptions, SideBySideEditor, computeEditorAriaLabel } from 'vs/workbench/common/editor'; +import { EditorResourceAccessor, GroupIdentifier, IEditorInput, Verbosity, EditorCommandsContextActionRunner, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; import { KeyCode } from 'vs/base/common/keyCodes'; diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index d3737140996..06e461ce36e 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -10,7 +10,8 @@ import { Event } from 'vs/base/common/event'; import { isObject, assertIsDefined, withNullAsUndefined, isFunction } from 'vs/base/common/types'; import { Dimension } from 'vs/base/browser/dom'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, computeEditorAriaLabel, IEditorOpenContext, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, IEditorOpenContext, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorViewState, IEditor, ScrollType } from 'vs/editor/common/editorCommon'; import { IStorageService } from 'vs/platform/storage/common/storage'; diff --git a/src/vs/workbench/browser/parts/editor/untitledHint.ts b/src/vs/workbench/browser/parts/editor/untitledHint.ts index 2a326232b60..04a3dc37cee 100644 --- a/src/vs/workbench/browser/parts/editor/untitledHint.ts +++ b/src/vs/workbench/browser/parts/editor/untitledHint.ts @@ -7,7 +7,6 @@ import * as dom from 'vs/base/browser/dom'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { localize } from 'vs/nls'; -import { DEFAULT_FONT_FAMILY } from 'vs/workbench/browser/style'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { inputPlaceholderForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { ChangeModeAction } from 'vs/workbench/browser/parts/editor/editorStatus'; @@ -17,6 +16,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { Schemas } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; const $ = dom.$; const untitledHintSetting = 'workbench.editor.untitled.hint'; @@ -82,6 +82,11 @@ class UntitledHintContentWidget implements IContentWidget { ) { this.toDispose = []; this.toDispose.push(editor.onDidChangeModelContent(() => this.onDidChangeModelContent())); + this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => { + if (this.domNode && e.hasChanged(EditorOption.fontInfo)) { + this.editor.applyFontInfo(this.domNode); + } + })); this.onDidChangeModelContent(); } @@ -137,9 +142,9 @@ class UntitledHintContentWidget implements IContentWidget { this.editor.focus(); })); - this.domNode.style.fontFamily = DEFAULT_FONT_FAMILY; this.domNode.style.fontStyle = 'italic'; this.domNode.style.paddingLeft = '4px'; + this.editor.applyFontInfo(this.domNode); } return this.domNode; diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index 5bc852fe601..afbec62f5ec 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { PanelActivityAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; -import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground, toolbarHoverBackground } from 'vs/platform/theme/common/colorRegistry'; +import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { ActivityHoverAlignment, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; @@ -856,15 +856,6 @@ registerThemingParticipant((theme, collector) => { `); } - const toolbarHoverBackgroundColor = theme.getColor(toolbarHoverBackground); - if (toolbarHoverBackgroundColor) { - collector.addRule(` - .monaco-workbench .part.panel > .title > .panel-switcher-container > .monaco-action-bar .action-item:hover:not(.icon) { - background-color: ${toolbarHoverBackgroundColor} !important; - } - `); - } - // Styling with Outline color (e.g. high contrast theme) const outline = theme.getColor(activeContrastBorder); if (outline) { diff --git a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts index e46d6becdd3..ac94cca192e 100644 --- a/src/vs/workbench/browser/parts/titlebar/menubarControl.ts +++ b/src/vs/workbench/browser/parts/titlebar/menubarControl.ts @@ -464,7 +464,7 @@ export class CustomMenubarControl extends MenubarControl { case StateType.Idle: return new Action('update.check', localize({ key: 'checkForUpdates', comment: ['&& denotes a mnemonic'] }, "Check for &&Updates..."), undefined, true, () => - this.updateService.checkForUpdates(this.environmentService.sessionId)); + this.updateService.checkForUpdates(true)); case StateType.CheckingForUpdates: return new Action('update.checking', localize('checkingForUpdates', "Checking for Updates..."), undefined, false); diff --git a/src/vs/workbench/browser/parts/views/media/views.css b/src/vs/workbench/browser/parts/views/media/views.css index 93e3ef2e0bd..d82afb63ebd 100644 --- a/src/vs/workbench/browser/parts/views/media/views.css +++ b/src/vs/workbench/browser/parts/views/media/views.css @@ -131,6 +131,11 @@ overflow: hidden; } +.customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item .monaco-icon-label-container::after { + content: ''; + display: block; +} + .customview-tree .monaco-list .monaco-list-row .custom-view-tree-node-item > .custom-view-tree-node-item-icon { background-size: 16px; background-position: left center; diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 75c0b760423..bed7bae394e 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -60,8 +60,9 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService'; import { BrowserWindow } from 'vs/workbench/browser/window'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; -import { WorkspaceTrustManagementService, WorkspaceTrustStorageService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustStorageService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; class BrowserMain extends Disposable { @@ -203,9 +204,7 @@ class BrowserMain extends Disposable { // Workspace Trust Service // TODO @lszomoru: Following two services shall be merged into single service - const workspaceTrustStorageService = new WorkspaceTrustStorageService(storageService, uriIdentityService); - serviceCollection.set(IWorkspaceTrustStorageService, workspaceTrustStorageService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, configurationService, workspaceTrustStorageService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, storageService, uriIdentityService, configurationService, environmentService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkpaceTrusted()); this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkpaceTrusted()))); @@ -309,6 +308,8 @@ class BrowserMain extends Disposable { } }); } + + fileService.registerProvider(Schemas.file, new HTMLFileSystemProvider()); } private async createStorageService(payload: IWorkspaceInitializationPayload, environmentService: IWorkbenchEnvironmentService, fileService: IFileService, logService: ILogService): Promise { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index ce7f2020594..30687a95725 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -9,10 +9,14 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions, Configur import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { isStandalone } from 'vs/base/browser/browser'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; + +const registry = Registry.as(ConfigurationExtensions.Configuration); // Configuration (function registerConfiguration(): void { - const registry = Registry.as(ConfigurationExtensions.Configuration); // Workbench registry.registerConfiguration({ @@ -511,3 +515,23 @@ import { isStandalone } from 'vs/base/browser/browser'; } }); })(); + +class ExperimentalCustomHoverConfigContribution implements IWorkbenchContribution { + constructor(@ITASExperimentService tasExperimentService: ITASExperimentService) { + tasExperimentService.getTreatment('customHovers').then(useCustomHoversAsDefault => { + registry.registerConfiguration({ + ...workbenchConfigurationNodeBase, + 'properties': { + 'workbench.experimental.useCustomHover': { + 'type': 'boolean', + 'description': localize('workbench.experimental.useCustomHover', "Enable/disable custom hovers on Activity Bar & Panel. Note this configuration is experimental and subjected to be removed at any time."), + 'default': !!useCustomHoversAsDefault + } + } + }); + }); + } +} + +const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); +workbenchRegistry.registerWorkbenchContribution(ExperimentalCustomHoverConfigContribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 64c6f868a1c..edb366557e1 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -13,7 +13,7 @@ import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceEditorIn import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ITextModel } from 'vs/editor/common/model'; +import { IEncodingSupport, IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { GroupsOrder, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl, IComposite } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; @@ -626,40 +626,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } } -export const enum EncodingMode { - - /** - * Instructs the encoding support to encode the current input with the provided encoding - */ - Encode, - - /** - * Instructs the encoding support to decode the current input with the provided encoding - */ - Decode -} - -export interface IEncodingSupport { - - /** - * Gets the encoding of the type if known. - */ - getEncoding(): string | undefined; - - /** - * Sets the encoding for the type for saving. - */ - setEncoding(encoding: string, mode: EncodingMode): void; -} - -export interface IModeSupport { - - /** - * Sets the language mode of the type. - */ - setMode(mode: string): void; -} - export interface IEditorInputWithPreferredResource { /** @@ -865,10 +831,6 @@ export class SideBySideEditorInput extends EditorInput { } } -export interface ITextEditorModel extends IEditorModel { - textEditorModel: ITextModel; -} - /** * The editor model is the heavyweight counterpart of editor input. Depending on the editor input, it * resolves from a file system retrieve content and may allow for saving it back or reverting it. @@ -1597,29 +1559,6 @@ export const enum EditorsOrder { SEQUENTIAL } -export function computeEditorAriaLabel(input: IEditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number): string { - let ariaLabel = input.getAriaLabel(); - if (group && !group.isPinned(input)) { - ariaLabel = localize('preview', "{0}, preview", ariaLabel); - } - - if (group?.isSticky(index ?? input)) { - ariaLabel = localize('pinned', "{0}, pinned", ariaLabel); - } - - // Apply group information to help identify in - // which group we are (only if more than one group - // is actually opened) - if (group && groupCount > 1) { - ariaLabel = `${ariaLabel}, ${group.ariaLabel}`; - } - - return ariaLabel; -} - - -//#region Editor Group Column - /** * A way to address editor groups through a column based system * where `0` is the first column. Will fallback to `SIDE_GROUP` @@ -1653,5 +1592,3 @@ export function editorGroupToViewColumn(editorGroupService: IEditorGroupsService return editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).indexOf(group); } - -//#endregion diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index b0f5565e636..2077aad5a16 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITextEditorModel, IModeSupport } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { IReference } from 'vs/base/common/lifecycle'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 8ff1fcee778..e7bd48733ce 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { ITextModel, ITextBufferFactory, ITextSnapshot, ModelConstants } from 'vs/editor/common/model'; -import { EditorModel, IModeSupport } from 'vs/workbench/common/editor'; +import { EditorModel } from 'vs/workbench/common/editor'; +import { IModeSupport } from 'vs/workbench/services/textfile/common/textfiles'; import { URI } from 'vs/base/common/uri'; import { ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; diff --git a/src/vs/workbench/common/notifications.ts b/src/vs/workbench/common/notifications.ts index 20e8e36d33b..fb465bf963c 100644 --- a/src/vs/workbench/common/notifications.ts +++ b/src/vs/workbench/common/notifications.ts @@ -275,6 +275,7 @@ export class NotificationsModel extends Disposable implements INotificationsMode } export interface INotificationViewItem { + readonly id: string | undefined; readonly severity: Severity; readonly sticky: boolean; readonly silent: boolean; @@ -468,7 +469,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie actions = { primary: notification.message.actions }; } - return new NotificationViewItem(severity, notification.sticky, notification.silent || filter === NotificationsFilter.SILENT || (filter === NotificationsFilter.ERROR && notification.severity !== Severity.Error), message, notification.source, notification.progress, actions); + return new NotificationViewItem(notification.id, severity, notification.sticky, notification.silent || filter === NotificationsFilter.SILENT || (filter === NotificationsFilter.ERROR && notification.severity !== Severity.Error), message, notification.source, notification.progress, actions); } private static parseNotificationMessage(input: NotificationMessage): INotificationMessage | undefined { @@ -500,6 +501,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie } private constructor( + readonly id: string | undefined, private _severity: Severity, private _sticky: boolean | undefined, private _silent: boolean | undefined, @@ -684,6 +686,10 @@ export class NotificationViewItem extends Disposable implements INotificationVie return false; } + if (typeof this.id === 'string' || typeof other.id === 'string') { + return this.id === other.id; + } + if (typeof this._source === 'object') { if (this._source.label !== other.source || this._source.id !== other.sourceId) { return false; @@ -698,7 +704,7 @@ export class NotificationViewItem extends Disposable implements INotificationVie const primaryActions = (this._actions && this._actions.primary) || []; const otherPrimaryActions = (other.actions && other.actions.primary) || []; - return equals(primaryActions, otherPrimaryActions, (a, b) => (a.id + a.label) === (b.id + b.label)); + return equals(primaryActions, otherPrimaryActions, (action, otherAction) => (action.id + action.label) === (otherAction.id + otherAction.label)); } } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 8d1385fe409..b933e01319f 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -10,9 +10,10 @@ import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/cus import { IWebviewService, WebviewContentOptions, WebviewContentPurpose, WebviewExtensionDescription, WebviewOptions } from 'vs/workbench/contrib/webview/browser/webview'; import { SerializedWebviewOptions, DeserializedWebview, reviveWebviewExtensionDescription, SerializedWebview, WebviewEditorInputSerializer, restoreWebviewContentOptions, restoreWebviewOptions } from 'vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer'; import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webviewPanel/browser/webviewWorkbenchService'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService, IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; -export interface CustomDocumentBackupData { +export interface CustomDocumentBackupData extends IWorkingCopyBackupMeta { readonly viewType: string; readonly editorResource: UriComponents; backupId: string; @@ -107,9 +108,9 @@ export const customEditorInputFactory = new class implements ICustomEditorInputF public createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { return instantiationService.invokeFunction(async accessor => { const webviewService = accessor.get(IWebviewService); - const backupFileService = accessor.get(IBackupFileService); + const workingCopyBackupService = accessor.get(IWorkingCopyBackupService); - const backup = await backupFileService.resolve(resource); + const backup = await workingCopyBackupService.resolve({ resource, typeId: NO_TYPE_ID }); if (!backup?.meta) { throw new Error(`No backup found for custom editor: ${resource}`); } diff --git a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts index 5c2ed83d10d..859e0a95380 100644 --- a/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugAdapterManager.ts @@ -24,6 +24,7 @@ import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/c import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; +import { IModeService } from 'vs/editor/common/services/modeService'; const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); export class AdapterManager implements IAdapterManager { @@ -43,7 +44,8 @@ export class AdapterManager implements IAdapterManager { @IInstantiationService private readonly instantiationService: IInstantiationService, @ICommandService private readonly commandService: ICommandService, @IExtensionService private readonly extensionService: IExtensionService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IModeService private readonly modeService: IModeService ) { this.adapterDescriptorFactories = []; this.debuggers = []; @@ -225,10 +227,14 @@ export class AdapterManager implements IAdapterManager { } const activeTextEditorControl = this.editorService.activeTextEditorControl; - let candidates: Debugger[] | undefined; + let candidates: Debugger[] = []; + let languageLabel: string | null = null; if (isCodeEditor(activeTextEditorControl)) { const model = activeTextEditorControl.getModel(); const language = model ? model.getLanguageIdentifier().language : undefined; + if (language) { + languageLabel = this.modeService.getLanguageName(language); + } const adapters = this.debuggers.filter(a => language && a.languages && a.languages.indexOf(language) >= 0); if (adapters.length === 1) { return adapters[0]; @@ -236,22 +242,27 @@ export class AdapterManager implements IAdapterManager { if (adapters.length > 1) { candidates = adapters; } - } - - if (!candidates) { + } else { await this.activateDebuggers('onDebugInitialConfigurations'); candidates = this.debuggers.filter(dbg => dbg.hasInitialConfiguration() || dbg.hasConfigurationProvider()); } candidates.sort((first, second) => first.label.localeCompare(second.label)); - const picks = candidates.map(c => ({ label: c.label, debugger: c })); - return this.quickInputService.pick<{ label: string, debugger: Debugger | undefined }>([...picks, { type: 'separator' }, { label: nls.localize('more', "More..."), debugger: undefined }], { placeHolder: nls.localize('selectDebug', "Select Environment") }) + const picks: { label: string, debugger?: Debugger, type?: string }[] = candidates.map(c => ({ label: c.label, debugger: c })); + let placeHolder = languageLabel ? nls.localize('CouldNotFindLanguage', "Can not find an extension to debug {0}", languageLabel) : nls.localize('CouldNotFind', "Can not find extension to debug"); + if (picks.length > 0) { + placeHolder = nls.localize('selectDebug', "Select environment"); + picks.push({ type: 'separator', label: '' }); + } + + picks.push({ label: languageLabel ? nls.localize('installLanguage', "Install an extension for {0}...", languageLabel) : nls.localize('installExt', "Install extension...") }); + return this.quickInputService.pick<{ label: string, debugger?: Debugger }>(picks, { activeItem: picks[0], placeHolder }) .then(picked => { if (picked && picked.debugger) { return picked.debugger; } if (picked) { - this.commandService.executeCommand('debug.installAdditionalDebuggers'); + this.commandService.executeCommand('debug.installAdditionalDebuggers', languageLabel); } return undefined; }); diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index e30b535fed7..15d2a42dc2d 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -552,10 +552,14 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.WorkbenchContrib, when: undefined, primary: undefined, - handler: async (accessor) => { + handler: async (accessor, query: string) => { const viewletService = accessor.get(IViewletService); const viewlet = (await viewletService.openViewlet(EXTENSIONS_VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; - viewlet.search('tag:debuggers @sort:installs'); + let searchFor = `tag:debuggers @sort:installs`; + if (typeof query === 'string') { + searchFor += ` ${query}`; + } + viewlet.search(searchFor); viewlet.focus(); } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 3c43ab2954f..fc116d861a7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -465,7 +465,7 @@ export class DebugService implements IDebugService { nls.localize({ key: 'installAdditionalDebuggers', comment: ['Placeholder is the debug type, so for example "node", "python"'] }, "Install {0} Extension", resolvedConfig.type), undefined, true, - async () => this.commandService.executeCommand('debug.installAdditionalDebuggers') + async () => this.commandService.executeCommand('debug.installAdditionalDebuggers', resolvedConfig?.type) )); await this.showError(message, actionList); @@ -675,6 +675,39 @@ export class DebugService implements IDebugService { return; } + // Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration + let needsToSubstitute = false; + let unresolved: IConfig | undefined; + const launch = session.root ? this.configurationManager.getLaunch(session.root.uri) : undefined; + if (launch) { + unresolved = launch.getConfiguration(session.configuration.name); + if (unresolved && !equals(unresolved, session.unresolvedConfiguration)) { + // Take the type from the session since the debug extension might overwrite it #21316 + unresolved.type = session.configuration.type; + unresolved.noDebug = session.configuration.noDebug; + needsToSubstitute = true; + } + } + + let resolved: IConfig | undefined | null = session.configuration; + if (launch && needsToSubstitute && unresolved) { + const initCancellationToken = new CancellationTokenSource(); + this.sessionCancellationTokens.set(session.getId(), initCancellationToken); + const resolvedByProviders = await this.configurationManager.resolveConfigurationByProviders(launch.workspace ? launch.workspace.uri : undefined, unresolved.type, unresolved, initCancellationToken.token); + if (resolvedByProviders) { + resolved = await this.substituteVariables(launch, resolvedByProviders); + if (resolved && !initCancellationToken.token.isCancellationRequested) { + resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, unresolved.type, resolved, initCancellationToken.token); + } + } else { + resolved = resolvedByProviders; + } + } + if (resolved) { + session.setConfiguration({ resolved, unresolved }); + } + session.configuration.__restart = restartData; + if (session.capabilities.supportsRestartRequest) { const taskResult = await runTasks(); if (taskResult === TaskRunResult.Success) { @@ -699,42 +732,10 @@ export class DebugService implements IDebugService { return; } - // Read the configuration again if a launch.json has been changed, if not just use the inmemory configuration - let needsToSubstitute = false; - let unresolved: IConfig | undefined; - const launch = session.root ? this.configurationManager.getLaunch(session.root.uri) : undefined; - if (launch) { - unresolved = launch.getConfiguration(session.configuration.name); - if (unresolved && !equals(unresolved, session.unresolvedConfiguration)) { - // Take the type from the session since the debug extension might overwrite it #21316 - unresolved.type = session.configuration.type; - unresolved.noDebug = session.configuration.noDebug; - needsToSubstitute = true; - } - } - - let resolved: IConfig | undefined | null = session.configuration; - if (launch && needsToSubstitute && unresolved) { - const initCancellationToken = new CancellationTokenSource(); - this.sessionCancellationTokens.set(session.getId(), initCancellationToken); - const resolvedByProviders = await this.configurationManager.resolveConfigurationByProviders(launch.workspace ? launch.workspace.uri : undefined, unresolved.type, unresolved, initCancellationToken.token); - if (resolvedByProviders) { - resolved = await this.substituteVariables(launch, resolvedByProviders); - if (resolved && !initCancellationToken.token.isCancellationRequested) { - resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, unresolved.type, resolved, initCancellationToken.token); - } - } else { - resolved = resolvedByProviders; - } - } - if (!resolved) { return c(undefined); } - session.setConfiguration({ resolved, unresolved }); - session.configuration.__restart = restartData; - try { await this.launchOrAttachToSession(session, shouldFocus); this._onDidNewSession.fire(session); diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 1c7f06bc92e..504d9c7b2c7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -21,13 +21,11 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { RunOnceScheduler, Queue } from 'vs/base/common/async'; import { generateUuid } from 'vs/base/common/uuid'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; import { ICustomEndpointTelemetryService, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ReplModel } from 'vs/workbench/contrib/debug/common/replModel'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; import { INotificationService } from 'vs/platform/notification/common/notification'; @@ -37,6 +35,7 @@ import { canceled } from 'vs/base/common/errors'; import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class DebugSession implements IDebugSession { @@ -80,11 +79,10 @@ export class DebugSession implements IDebugSession { @IViewletService private readonly viewletService: IViewletService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IProductService private readonly productService: IProductService, - @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, - @IOpenerService private readonly openerService: IOpenerService, @INotificationService private readonly notificationService: INotificationService, @ILifecycleService lifecycleService: ILifecycleService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IInstantiationService private readonly instantiationService: IInstantiationService, @ICustomEndpointTelemetryService private readonly customEndpointTelemetryService: ICustomEndpointTelemetryService ) { this._options = options || {}; @@ -235,7 +233,7 @@ export class DebugSession implements IDebugSession { try { const debugAdapter = await dbgr.createDebugAdapter(this); - this.raw = new RawDebugSession(debugAdapter, dbgr, this.id, this.telemetryService, this.customEndpointTelemetryService, this.extensionHostDebugService, this.openerService, this.notificationService); + this.raw = this.instantiationService.createInstance(RawDebugSession, debugAdapter, dbgr, this.id); await this.raw.start(); this.registerListeners(); @@ -337,7 +335,7 @@ export class DebugSession implements IDebugSession { } this.cancelAllRequests(); - await this.raw.restart(); + await this.raw.restart({ arguments: this.configuration }); } async sendBreakpoints(modelUri: URI, breakpointsToSend: IBreakpoint[], sourceModified: boolean): Promise { diff --git a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts index 6a2e78375f3..582c66ce916 100644 --- a/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/browser/extensionHostDebugService.ts @@ -55,7 +55,7 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i if (environmentService.options && environmentService.options.workspaceProvider) { this.workspaceProvider = environmentService.options.workspaceProvider; } else { - this.workspaceProvider = { open: async () => undefined, workspace: undefined, trusted: undefined }; + this.workspaceProvider = { open: async () => true, workspace: undefined, trusted: undefined }; logService.warn('Extension Host Debugging not available due to missing workspace provider.'); } @@ -161,12 +161,12 @@ class BrowserExtensionHostDebugService extends ExtensionHostDebugChannelClient i } // Open debug window as new window. Pass arguments over. - await this.workspaceProvider.open(debugWorkspace, { + const success = await this.workspaceProvider.open(debugWorkspace, { reuse: false, // debugging always requires a new window payload: Array.from(environment.entries()) // mandatory properties to enable debugging }); - return {}; + return { success }; } private findArgument(key: string, args: string[]): string | undefined { diff --git a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index 0e405a986de..08fc6c9a192 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -16,7 +16,8 @@ import { URI } from 'vs/base/common/uri'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { INotificationService } from 'vs/platform/notification/common/notification'; +import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; /** * This interface represents a single command line argument split into a "prefix" and a "path" half. @@ -80,11 +81,12 @@ export class RawDebugSession implements IDisposable { debugAdapter: IDebugAdapter, public readonly dbgr: IDebugger, private readonly sessionId: string, - private readonly telemetryService: ITelemetryService, - private readonly customTelemetryService: ICustomEndpointTelemetryService, - private readonly extensionHostDebugService: IExtensionHostDebugService, - private readonly openerService: IOpenerService, - private readonly notificationService: INotificationService + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ICustomEndpointTelemetryService private readonly customTelemetryService: ICustomEndpointTelemetryService, + @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, + @IOpenerService private readonly openerService: IOpenerService, + @INotificationService private readonly notificationService: INotificationService, + @IDialogService private readonly dialogSerivce: IDialogService, ) { this.debugAdapter = debugAdapter; this._capabilities = Object.create(null); @@ -303,9 +305,9 @@ export class RawDebugSession implements IDisposable { return Promise.reject(new Error('terminated not supported')); } - restart(): Promise { + restart(args: DebugProtocol.RestartArguments): Promise { if (this.capabilities.supportsRestartRequest) { - return this.send('restart', null); + return this.send('restart', args); } return Promise.reject(new Error('restart not supported')); } @@ -565,17 +567,27 @@ export class RawDebugSession implements IDisposable { switch (request.command) { case 'launchVSCode': - this.launchVsCode(request.arguments).then(result => { + try { + let result = await this.launchVsCode(request.arguments); + if (!result.success) { + const showResult = await this.dialogSerivce.show(Severity.Warning, nls.localize('canNotStart', "The debugger needs to open a new tab or window for the debuggee but the browser prevented this. You must give permission to continue."), [nls.localize('continue', "Continue"), nls.localize('cancel', "Cancel")]); + if (showResult.choice === 0) { + result = await this.launchVsCode(request.arguments); + } else { + response.success = false; + safeSendResponse(response); + await this.shutdown(); + } + } response.body = { rendererDebugPort: result.rendererDebugPort, - //processId: pid }; safeSendResponse(response); - }, err => { + } catch (err) { response.success = false; response.message = err.message; safeSendResponse(response); - }); + } break; case 'runInTerminal': try { diff --git a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts index c63fb145838..7c79be55bfc 100644 --- a/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts +++ b/src/vs/workbench/contrib/debug/common/debugProtocol.d.ts @@ -586,6 +586,11 @@ declare module DebugProtocol { The attribute is only honored by a debug adapter if the capability 'supportTerminateDebuggee' is true. */ terminateDebuggee?: boolean; + /** Indicates whether the debuggee should stay suspended when the debugger is disconnected. + If unspecified, the debuggee should resume execution. + The attribute is only honored by a debug adapter if the capability 'supportSuspendDebuggee' is true. + */ + suspendDebuggee?: boolean; } /** Response to 'disconnect' request. This is just an acknowledgement, so no body field is required. */ @@ -1609,6 +1614,8 @@ declare module DebugProtocol { supportsExceptionInfoRequest?: boolean; /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ supportTerminateDebuggee?: boolean; + /** The debug adapter supports the 'suspendDebuggee' attribute on the 'disconnect' request. */ + supportSuspendDebuggee?: boolean; /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and an optional 'totalFrames' result of the 'StackTrace' request are supported. */ supportsDelayedStackTraceLoading?: boolean; /** The debug adapter supports the 'loadedSources' request. */ diff --git a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts index e2eb1fb4f10..66760beb2cb 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -11,7 +11,6 @@ import { Source } from 'vs/workbench/contrib/debug/common/debugSource'; import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; import { Range } from 'vs/editor/common/core/range'; import { IDebugSessionOptions, State, IDebugService } from 'vs/workbench/contrib/debug/common/debug'; -import { NullOpenerService } from 'vs/platform/opener/common/opener'; import { createDecorationsForStackFrame } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; import { Constants } from 'vs/base/common/uint'; import { getContext, getContextForContributedActions, getSpecificSourceName } from 'vs/workbench/contrib/debug/browser/callStackView'; @@ -20,6 +19,7 @@ import { generateUuid } from 'vs/base/common/uuid'; import { debugStackframe, debugStackframeFocused } from 'vs/workbench/contrib/debug/browser/debugIcons'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; const mockWorkspaceContextService = { getWorkspace: () => { @@ -38,7 +38,7 @@ export function createMockSession(model: DebugModel, name = 'mockSession', optio } }; } - } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService, undefined!); + } as IDebugService, undefined!, undefined!, new TestConfigurationService({ debug: { console: { collapseIdenticalLines: true } } }), undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!); } function createTwoStackFrames(session: DebugSession): { firstStackFrame: StackFrame, secondStackFrame: StackFrame } { @@ -378,7 +378,7 @@ suite('Debug - CallStack', () => { override get state(): State { return State.Stopped; } - }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, NullOpenerService, undefined!, undefined!, mockUriIdentityService, undefined!); + }(generateUuid(), { resolved: { name: 'stoppedSession', type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, mockWorkspaceContextService, undefined!, undefined!, undefined!, mockUriIdentityService, new TestInstantiationService(), undefined!); const runningSession = createMockSession(model); model.addSession(runningSession); diff --git a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts index 3044104191a..50567b6a93f 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -176,7 +176,7 @@ suite('Debug - REPL', () => { model.addSession(session); const adapter = new MockDebugAdapter(); - const raw = new RawDebugSession(adapter, undefined!, '', undefined!, undefined!, undefined!, undefined!, undefined!); + const raw = new RawDebugSession(adapter, undefined!, '', undefined!, undefined!, undefined!, undefined!, undefined!, undefined!); session.initializeForTest(raw); await session.addReplExpression(undefined, 'before.1'); diff --git a/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts index b13961ea6b6..3f75a0cb79a 100644 --- a/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts +++ b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; -import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; registerSingleton(IExperimentService, ExperimentService, true); @@ -27,6 +27,8 @@ registry.registerConfiguration({ 'type': 'boolean', 'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."), 'default': true, + 'scope': ConfigurationScope.APPLICATION, + 'requireTrust': true, 'tags': ['usesOnlineServices'] } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 3def3319cf7..fd1b3971949 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -85,6 +85,11 @@ function removeEmbeddedSVGs(documentContent: string): string { ], img: ['src', 'alt', 'title', 'aria-label', 'width', 'height'], }, + allowedSchemes: [ + Schemas.http, + Schemas.https, + Schemas.command, // We only allow executing the release notes command + ], filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { return token.tag !== 'svg'; } @@ -663,7 +668,7 @@ export class ExtensionEditor extends EditorPane { const contents = await this.loadContents(() => cacheResult, template); const content = await renderMarkdownDocument(contents, this.extensionService, this.modeService); const sanitizedContent = removeEmbeddedSVGs(content); - return await this.renderBody(sanitizedContent); + return this.renderBody(sanitizedContent); } private async renderBody(body: string): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index f9baabc1c92..dc1e3227c0b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -174,6 +174,19 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', description: localize('extensionsWebWorker', "Enable web worker extension host."), default: false + }, + 'extensions.supportsVirtualWorkspace': { + type: 'object', + markdownDescription: localize('extensions.supportsVirtualWorkspace', "Override the virtual workspace support of an extension"), + patternProperties: { + '([a-z0-9A-Z][a-z0-9\-A-Z]*)\\.([a-z0-9A-Z][a-z0-9\-A-Z]*)$': { + type: 'boolean', + default: false + } + }, + default: { + 'pub.name': false + } } } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 0f1116116fc..7dca6e89f64 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -45,7 +45,6 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -61,6 +60,8 @@ import * as Constants from 'vs/workbench/contrib/logs/common/logConstants'; import { infoIcon, manageExtensionIcon, syncEnabledIcon, syncIgnoredIcon, trustIcon, warningIcon } from 'vs/workbench/contrib/extensions/browser/extensionsIcons'; import { isWeb } from 'vs/base/common/platform'; import { isWorkspaceTrustEnabled } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { getVirtualWorkspaceLocation } from 'vs/platform/remote/common/remoteHosts'; function getRelativeDateLabel(date: Date): string { const delta = new Date().getTime() - date.getTime(); @@ -448,8 +449,6 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { private static readonly Class = `${ExtensionAction.LABEL_ACTION_CLASS} prominent install`; private static readonly InstallingClass = `${ExtensionAction.LABEL_ACTION_CLASS} install installing`; - private _extensionKindController: ExtensionKindController; - updateWhenCounterExtensionChanges: boolean = true; constructor( @@ -460,11 +459,10 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(id, InstallInOtherServerAction.INSTALL_LABEL, InstallInOtherServerAction.Class, false); this.update(); - - this._extensionKindController = new ExtensionKindController(productService, configurationService); } update(): void { @@ -506,28 +504,28 @@ export abstract class InstallInOtherServerAction extends ExtensionAction { } // Prefers to run on UI - if (this.server === this.extensionManagementServerService.localExtensionManagementServer && this._extensionKindController.prefersExecuteOnUI(this.extension.local.manifest)) { + if (this.server === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local.manifest)) { return true; } // Prefers to run on Workspace - if (this.server === this.extensionManagementServerService.remoteExtensionManagementServer && this._extensionKindController.prefersExecuteOnWorkspace(this.extension.local.manifest)) { + if (this.server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local.manifest)) { return true; } // Prefers to run on Web - if (this.server === this.extensionManagementServerService.webExtensionManagementServer && this._extensionKindController.prefersExecuteOnWeb(this.extension.local.manifest)) { + if (this.server === this.extensionManagementServerService.webExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWeb(this.extension.local.manifest)) { return true; } if (this.canInstallAnyWhere) { // Can run on UI - if (this.server === this.extensionManagementServerService.localExtensionManagementServer && this._extensionKindController.canExecuteOnUI(this.extension.local.manifest)) { + if (this.server === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.canExecuteOnUI(this.extension.local.manifest)) { return true; } // Can run on Workspace - if (this.server === this.extensionManagementServerService.remoteExtensionManagementServer && this._extensionKindController.canExecuteOnWorkspace(this.extension.local.manifest)) { + if (this.server === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.canExecuteOnWorkspace(this.extension.local.manifest)) { return true; } } @@ -562,8 +560,9 @@ export class RemoteInstallAction extends InstallInOtherServerAction { @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(`extensions.remoteinstall`, extensionManagementServerService.remoteExtensionManagementServer, canInstallAnyWhere, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService); + super(`extensions.remoteinstall`, extensionManagementServerService.remoteExtensionManagementServer, canInstallAnyWhere, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService, extensionManifestPropertiesService); } protected getInstallLabel(): string { @@ -581,8 +580,9 @@ export class LocalInstallAction extends InstallInOtherServerAction { @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(`extensions.localinstall`, extensionManagementServerService.localExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService); + super(`extensions.localinstall`, extensionManagementServerService.localExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService, extensionManifestPropertiesService); } protected getInstallLabel(): string { @@ -599,8 +599,9 @@ export class WebInstallAction extends InstallInOtherServerAction { @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, @IWebExtensionsScannerService private readonly webExtensionsScannerService: IWebExtensionsScannerService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(`extensions.webInstall`, extensionManagementServerService.webExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService); + super(`extensions.webInstall`, extensionManagementServerService.webExtensionManagementServer, false, extensionsWorkbenchService, extensionManagementServerService, productService, configurationService, extensionManifestPropertiesService); } protected getInstallLabel(): string { @@ -1210,22 +1211,19 @@ export class ReloadAction extends ExtensionAction { updateWhenCounterExtensionChanges: boolean = true; private _runningExtensions: IExtensionDescription[] | null = null; - private _extensionKindController: ExtensionKindController; - constructor( @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IHostService private readonly hostService: IHostService, @IExtensionService private readonly extensionService: IExtensionService, @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, ) { super('extensions.reload', localize('reloadAction', "Reload"), ReloadAction.DisabledClass, false); this._register(this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this)); this.updateRunningExtensions(); - - this._extensionKindController = new ExtensionKindController(productService, configurationService); } private updateRunningExtensions(): void { @@ -1293,7 +1291,7 @@ export class ReloadAction extends ExtensionAction { const extensionInOtherServer = this.extensionsWorkbenchService.installed.filter(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)[0]; if (extensionInOtherServer) { // This extension prefers to run on UI/Local side but is running in remote - if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this._extensionKindController.prefersExecuteOnUI(this.extension.local!.manifest)) { + if (runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local!.manifest)) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('enable locally', "Please reload Visual Studio Code to enable this extension locally."); @@ -1301,7 +1299,7 @@ export class ReloadAction extends ExtensionAction { } // This extension prefers to run on Workspace/Remote side but is running in local - if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this._extensionKindController.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { + if (runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer && this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('enable remote', "Please reload Visual Studio Code to enable this extension in {0}.", this.extensionManagementServerService.remoteExtensionManagementServer?.label); @@ -1313,7 +1311,7 @@ export class ReloadAction extends ExtensionAction { if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { // This extension prefers to run on UI/Local side but is running in remote - if (this._extensionKindController.prefersExecuteOnUI(this.extension.local!.manifest)) { + if (this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local!.manifest)) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); @@ -1321,7 +1319,7 @@ export class ReloadAction extends ExtensionAction { } if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { // This extension prefers to run on Workspace/Remote side but is running in local - if (this._extensionKindController.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { + if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('postEnableTooltip', "Please reload Visual Studio Code to enable this extension."); @@ -2085,23 +2083,20 @@ export class SystemDisabledWarningAction extends ExtensionAction { updateWhenCounterExtensionChanges: boolean = true; private _runningExtensions: IExtensionDescription[] | null = null; - private _extensionKindController: ExtensionKindController; - constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @ILabelService private readonly labelService: ILabelService, + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionService private readonly extensionService: IExtensionService, - @IProductService productService: IProductService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super('extensions.install', '', `${SystemDisabledWarningAction.CLASS} hide`, false); this._register(this.labelService.onDidChangeFormatters(() => this.update(), this)); this._register(this.extensionService.onDidChangeExtensions(this.updateRunningExtensions, this)); this.updateRunningExtensions(); this.update(); - - this._extensionKindController = new ExtensionKindController(productService, configurationService); } private updateRunningExtensions(): void { @@ -2120,6 +2115,12 @@ export class SystemDisabledWarningAction extends ExtensionAction { ) { return; } + const virtualWorkspaceLocation = getVirtualWorkspaceLocation(this.workspaceContextService.getWorkspace()); + if (virtualWorkspaceLocation && this.extension.enablementState === EnablementState.DisabledByVirtualWorkspace) { + this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; + this.tooltip = localize('disabled because of virtual workspace', "This extension has defined that it cannot run in {0} workspace", this.labelService.getHostLabel(virtualWorkspaceLocation.scheme, virtualWorkspaceLocation.authority)); + return; + } if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) { if (isLanguagePackExtension(this.extension.local.manifest)) { if (!this.extensionsWorkbenchService.installed.some(e => areSameExtensions(e.identifier, this.extension!.identifier) && e.server !== this.extension!.server)) { @@ -2147,14 +2148,14 @@ export class SystemDisabledWarningAction extends ExtensionAction { const runningExtension = this._runningExtensions.filter(e => areSameExtensions({ id: e.identifier.value, uuid: e.uuid }, this.extension!.identifier))[0]; const runningExtensionServer = runningExtension ? this.extensionManagementServerService.getExtensionManagementServer(toExtension(runningExtension)) : null; if (this.extension.server === this.extensionManagementServerService.localExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.remoteExtensionManagementServer) { - if (this._extensionKindController.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { + if (this.extensionManifestPropertiesService.prefersExecuteOnWorkspace(this.extension.local!.manifest)) { this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; this.tooltip = localize('disabled locally', "Extension is enabled on '{0}' and disabled locally.", this.extensionManagementServerService.remoteExtensionManagementServer.label); } return; } if (this.extension.server === this.extensionManagementServerService.remoteExtensionManagementServer && runningExtensionServer === this.extensionManagementServerService.localExtensionManagementServer) { - if (this._extensionKindController.prefersExecuteOnUI(this.extension.local!.manifest)) { + if (this.extensionManifestPropertiesService.prefersExecuteOnUI(this.extension.local!.manifest)) { this.class = `${SystemDisabledWarningAction.INFO_CLASS}`; this.tooltip = localize('disabled remotely', "Extension is enabled locally and disabled on '{0}'.", this.extensionManagementServerService.remoteExtensionManagementServer.label); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts index 260b670ea9e..1dcc14dcc56 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsList.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsList.ts @@ -26,7 +26,7 @@ import { foreground, listActiveSelectionForeground, listActiveSelectionBackgroun import { WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { localize } from 'vs/nls'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export const EXTENSION_LIST_ELEMENT_HEIGHT = 62; @@ -67,7 +67,7 @@ export class Renderer implements IPagedRenderer { @IExtensionService private readonly extensionService: IExtensionService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionWorkspaceTrustRequestService private readonly extensionWorkspaceTrustRequestService: IExtensionWorkspaceTrustRequestService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { } @@ -207,7 +207,7 @@ export class Renderer implements IPagedRenderer { if (extension.local?.manifest.workspaceTrust?.request) { const trustRequirement = extension.local.manifest.workspaceTrust; - const requestType = this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(extension.local.manifest); + const requestType = this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(extension.local.manifest); if (requestType !== 'never' && trustRequirement.request !== 'never') { data.workspaceTrustDescription.textContent = trustRequirement.description; } else if (requestType === 'onStart') { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 90597fb181d..95b2a5d2baa 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -48,7 +48,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -119,7 +119,7 @@ export class ExtensionsListView extends ViewPane { @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, - @IExtensionWorkspaceTrustRequestService private readonly extensionWorkspaceTrustRequestService: IExtensionWorkspaceTrustRequestService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IWorkbenchExtensionManagementService protected readonly extensionManagementService: IWorkbenchExtensionManagementService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @@ -561,15 +561,15 @@ export class ExtensionsListView extends ViewPane { value = value.replace(/@trustRequired/g, '').replace(/@sort:(\w+)(-\w*)?/g, '').trim().toLowerCase(); - const result = local.filter(extension => extension.local && this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(extension.local.manifest) !== 'never' && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); + const result = local.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(extension.local.manifest) !== 'never' && (extension.name.toLowerCase().indexOf(value) > -1 || extension.displayName.toLowerCase().indexOf(value) > -1)); if (onStartOnly) { - const onStartExtensions = result.filter(extension => extension.local && this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(extension.local.manifest) === 'onStart'); + const onStartExtensions = result.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(extension.local.manifest) === 'onStart'); return this.sortExtensions(onStartExtensions, options); } if (onDemandOnly) { - const onDemandExtensions = result.filter(extension => extension.local && this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(extension.local.manifest) === 'onDemand'); + const onDemandExtensions = result.filter(extension => extension.local && this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(extension.local.manifest) === 'onDemand'); return this.sortExtensions(onDemandExtensions, options); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index c114b95be95..02ec9138ccc 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -36,12 +36,12 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension } from 'vs/platform/extensions/common/extensions'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IProductService } from 'vs/platform/product/common/productService'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { FileAccess } from 'vs/base/common/network'; import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions'; import { IUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataSync'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { isBoolean } from 'vs/base/common/types'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; interface IExtensionStateProvider { (extension: Extension): T; @@ -518,8 +518,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension private installing: IExtension[] = []; - private readonly extensionKindController: ExtensionKindController; - constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, @@ -538,7 +536,8 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension @IIgnoredExtensionsManagementService private readonly extensionsSyncManagementService: IIgnoredExtensionsManagementService, @IUserDataAutoSyncService private readonly userDataAutoSyncService: IUserDataAutoSyncService, @IProductService private readonly productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(); this.hasOutdatedExtensionsContextKey = HasOutdatedExtensionsContext.bindTo(contextKeyService); @@ -588,8 +587,6 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension this.updateContexts(); this.updateActivity(); })); - - this.extensionKindController = new ExtensionKindController(productService, configurationService); } get local(): IExtension[] { @@ -716,7 +713,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension return extensionsToChoose[0]; } - const extensionKinds = this.extensionKindController.getExtensionKind(manifest); + const extensionKinds = this.extensionManifestPropertiesService.getExtensionKind(manifest); let extension = extensionsToChoose.find(extension => { for (const extensionKind of extensionKinds) { diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css index 03c0e7d1a9b..4b20fae19e6 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionActions.css @@ -29,7 +29,8 @@ width: auto; height: auto; line-height: 14px; - margin-top: 2px; + margin-top: 2px; /* margin for outline */ + margin-bottom: 2px; /* margin for outline */ } .monaco-action-bar .action-item .action-label.extension-action.multiserver.install:after, diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 3550b140655..a7567d850b4 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -158,8 +158,15 @@ min-width: 0; } +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action:not(.icon) { + margin-left: 2px; /* margin for outline */ +} + +.extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item > .monaco-dropdown .extension-action.action-dropdown { + margin-right: 2px; /* margin for outline */ +} + .extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item .extension-action:not(.icon) { - margin-top: 0px; /* overrides from extension actions */ border-radius: 0; padding-top: 0; padding-bottom: 0; @@ -172,7 +179,7 @@ .extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item.action-dropdown-item, .extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item:not(.action-dropdown-item) > .extension-action { - margin-right: 8px; + margin-right: 6px; } .extension-editor > .header > .details > .actions > .monaco-action-bar > .actions-container > .action-item > .extension-action.label, diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index f1916c9f3ee..99d2c924f4c 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -57,6 +57,11 @@ display: none; } +.extensions-viewlet > .extensions .extension-list-item .monaco-action-bar > .actions-container > .action-item.action-dropdown-item, +.extensions-viewlet > .extensions .extension-list-item .monaco-action-bar > .actions-container > .action-item:not(.action-dropdown-item) > .extension-action { + margin-left: 6px; +} + .extensions-viewlet > .extensions .extensions-list.hidden, .extensions-viewlet > .extensions .message-container.hidden { display: none; diff --git a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts index dc9ec77a603..ed0a2a144a4 100644 --- a/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/binaryFileEditor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; +import { localize } from 'vs/nls'; import { BaseBinaryResourceEditor } from 'vs/workbench/browser/parts/editor/binaryEditor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -11,8 +11,6 @@ import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { BINARY_FILE_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOverride } from 'vs/platform/editor/common/editor'; @@ -26,20 +24,16 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, - @IOpenerService private readonly openerService: IOpenerService, @IEditorService private readonly editorService: IEditorService, - @IStorageService storageService: IStorageService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IStorageService storageService: IStorageService ) { super( BinaryFileEditor.ID, { - openInternal: (input, options) => this.openInternal(input, options), - openExternal: resource => this.openerService.open(resource, { openExternal: true }) + openInternal: (input, options) => this.openInternal(input, options) }, telemetryService, themeService, - environmentService, storageService ); } @@ -56,6 +50,6 @@ export class BinaryFileEditor extends BaseBinaryResourceEditor { } override getTitle(): string { - return this.input ? this.input.getName() : nls.localize('binaryFileEditor', "Binary File Viewer"); + return this.input ? this.input.getName() : localize('binaryFileEditor', "Binary File Viewer"); } } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 2efcb9b9418..2a8a5bab95e 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -31,6 +31,7 @@ import { Schemas } from 'vs/base/common/network'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IEditorIdentifier, SaveReason } from 'vs/workbench/common/editor'; import { GroupsOrder, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { hash } from 'vs/base/common/hash'; export const CONFLICT_RESOLUTION_CONTEXT = 'saveConflictResolutionContext'; export const CONFLICT_RESOLUTION_SCHEME = 'conflictResolution'; @@ -136,7 +137,7 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa const isWriteLocked = fileOperationError.fileOperationResult === FileOperationResult.FILE_WRITE_LOCKED; const triedToUnlock = isWriteLocked && fileOperationError.options?.unlock; const isPermissionDenied = fileOperationError.fileOperationResult === FileOperationResult.FILE_PERMISSION_DENIED; - const canSaveElevated = resource.scheme === Schemas.file; // https://github.com/microsoft/vscode/issues/48659 TODO + const canSaveElevated = resource.scheme === Schemas.file; // currently only supported for local schemes (https://github.com/microsoft/vscode/issues/48659) // Save Elevated if (canSaveElevated && (isPermissionDenied || triedToUnlock)) { @@ -175,7 +176,12 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // Show message and keep function to hide in case the file gets saved/reverted const actions: INotificationActions = { primary: primaryActions, secondary: secondaryActions }; - const handle = this.notificationService.notify({ severity: Severity.Error, message, actions }); + const handle = this.notificationService.notify({ + id: `${hash(model.resource.toString())}`, // unique per model (https://github.com/microsoft/vscode/issues/121539) + severity: Severity.Error, + message, + actions + }); Event.once(handle.onDidClose)(() => { dispose(primaryActions); dispose(secondaryActions); }); this.messages.set(model.resource, handle); } @@ -249,6 +255,7 @@ class ResolveSaveConflictAction extends Action { // Show additional help how to resolve the save conflict const actions = { primary: [this.instantiationService.createInstance(ResolveConflictLearnMoreAction)] }; const handle = this.notificationService.notify({ + id: `${hash(resource.toString())}`, // unique per model severity: Severity.Info, message: conflictEditorHelp, actions, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index a8eecafced1..49dcd5eee7f 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -40,7 +40,8 @@ import { getErrorMessage } from 'vs/base/common/errors'; import { WebFileSystemAccess, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { RunOnceWorker, sequence, timeout } from 'vs/base/common/async'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { once } from 'vs/base/common/functional'; @@ -1015,7 +1016,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { fileBytesDownloaded: number; } - async function downloadFileBuffered(resource: URI, target: WebFileSystemAccess.FileSystemWritableFileStream, operation: IDownloadOperation): Promise { + async function downloadFileBuffered(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation): Promise { const contents = await fileService.readFileStream(resource); if (cts.token.isCancellationRequested) { target.close(); @@ -1055,7 +1056,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { }); } - async function downloadFileUnbuffered(resource: URI, target: WebFileSystemAccess.FileSystemWritableFileStream, operation: IDownloadOperation): Promise { + async function downloadFileUnbuffered(resource: URI, target: FileSystemWritableFileStream, operation: IDownloadOperation): Promise { const contents = await fileService.readFile(resource); if (!cts.token.isCancellationRequested) { target.write(contents.value.buffer); @@ -1065,7 +1066,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { target.close(); } - async function downloadFile(targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle, file: IFileStatWithMetadata, operation: IDownloadOperation): Promise { + async function downloadFile(targetFolder: FileSystemDirectoryHandle, file: IFileStatWithMetadata, operation: IDownloadOperation): Promise { // Report progress operation.filesDownloaded++; @@ -1085,7 +1086,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { return downloadFileUnbuffered(file.resource, targetFileWriter, operation); } - async function downloadFolder(folder: IFileStatWithMetadata, targetFolder: WebFileSystemAccess.FileSystemDirectoryHandle, operation: IDownloadOperation): Promise { + async function downloadFolder(folder: IFileStatWithMetadata, targetFolder: FileSystemDirectoryHandle, operation: IDownloadOperation): Promise { if (folder.children) { operation.filesTotal += (folder.children.map(child => child.isFile)).length; @@ -1132,7 +1133,7 @@ const downloadFileHandler = async (accessor: ServicesAccessor) => { } try { - const parentFolder: WebFileSystemAccess.FileSystemDirectoryHandle = await window.showDirectoryPicker(); + const parentFolder: FileSystemDirectoryHandle = await window.showDirectoryPicker(); const operation: IDownloadOperation = { startTime: Date.now(), progressScheduler: new RunOnceWorker(steps => { progress.report(steps[steps.length - 1]); }, 1000), diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index e44004ee03e..560c2702fe6 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -44,6 +44,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { isPromiseCanceledError } from 'vs/base/common/errors'; import { toAction } from 'vs/base/common/actions'; import { EditorOverride } from 'vs/platform/editor/common/editor'; +import { hash } from 'vs/base/common/hash'; // Commands @@ -445,6 +446,7 @@ async function doSaveEditors(accessor: ServicesAccessor, editors: IEditorIdentif } catch (error) { if (!isPromiseCanceledError(error)) { notificationService.notify({ + id: editors.map(({ editor }) => hash(editor.resource?.toString())).join(), // ensure unique notification ID per set of editor severity: Severity.Error, message: nls.localize({ key: 'genericSaveError', comment: ['{0} is the resource that failed to save and {1} the error message'] }, "Failed to save '{0}': {1}", editors.map(({ editor }) => editor.getName()).join(', '), toErrorMessage(error, false)), actions: { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 71f7d47cddd..5f77591b054 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -41,7 +41,8 @@ import { ElementsDragAndDropData, NativeDragAndDropData } from 'vs/base/browser/ import { URI } from 'vs/base/common/uri'; import { withUndefinedAsNull } from 'vs/base/common/types'; import { isWeb } from 'vs/base/common/platform'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; diff --git a/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts b/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts index 4daff378eec..6dfdf2560f2 100644 --- a/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts +++ b/src/vs/workbench/contrib/files/common/dirtyFilesIndicator.ts @@ -9,7 +9,8 @@ import { VIEWLET_ID } from 'vs/workbench/contrib/files/common/files'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export class DirtyFilesIndicator extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index bcddcf3addf..78a357629d7 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -5,11 +5,11 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { EncodingMode, IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, isTextEditorPane } from 'vs/workbench/common/editor'; +import { IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, isTextEditorPane } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; -import { ITextFileService, TextFileEditorModelState, TextFileResolveReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { ITextFileService, TextFileEditorModelState, TextFileResolveReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel, EncodingMode } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IReference, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; diff --git a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index c5a07f93a9d..a47e3abe916 100644 --- a/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -24,6 +24,7 @@ import { IPreferencesService } from 'vs/workbench/services/preferences/common/pr import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { IProductService } from 'vs/platform/product/common/productService'; /** * An implementation of editor for file system resources. @@ -45,7 +46,8 @@ export class NativeTextFileEditor extends TextFileEditor { @INativeHostService private readonly nativeHostService: INativeHostService, @IPreferencesService private readonly preferencesService: IPreferencesService, @IExplorerService explorerService: IExplorerService, - @IUriIdentityService uriIdentityService: IUriIdentityService + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IProductService private readonly productService: IProductService ) { super(telemetryService, fileService, viewletService, instantiationService, contextService, storageService, textResourceConfigurationService, editorService, themeService, editorGroupService, textFileService, explorerService, uriIdentityService); } @@ -56,7 +58,7 @@ export class NativeTextFileEditor extends TextFileEditor { if ((error).fileOperationResult === FileOperationResult.FILE_EXCEEDS_MEMORY_LIMIT) { const memoryLimit = Math.max(MIN_MAX_MEMORY_SIZE_MB, +this.textResourceConfigurationService.getValue(undefined, 'files.maxMemoryForLargeFilesMB') || FALLBACK_MAX_MEMORY_SIZE_MB); - throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow it to use more memory"), { + throw createErrorWithActions(localize('fileTooLargeForHeapError', "To open a file of this size, you need to restart and allow {0} to use more memory", this.productService.nameShort), { actions: [ toAction({ id: 'workbench.window.action.relaunchWithIncreasedMemoryLimit', label: localize('relaunchWithIncreasedMemoryLimit', "Restart with {0} MB", memoryLimit), run: () => { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 5d615d3f5ab..e6a6b183b40 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -9,8 +9,8 @@ import { toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { workbenchInstantiationService, TestServiceAccessor, TestEditorService, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EncodingMode, IEditorInputFactoryRegistry, Verbosity, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; -import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorInputFactoryRegistry, Verbosity, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; +import { EncodingMode, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { timeout } from 'vs/base/common/async'; diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index 01cf917692d..08a38c9f09e 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -24,6 +24,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { whenTextEditorClosed } from 'vs/workbench/browser/editor'; suite('Files - TextFileEditorTracker', () => { @@ -180,4 +181,26 @@ suite('Files - TextFileEditorTracker', () => { }); }); } + + test('whenTextEditorClosed (single editor)', async function () { + return testWhenTextEditorClosed(toResource.call(this, '/path/index.txt')); + }); + + test('whenTextEditorClosed (multiple editor)', async function () { + return testWhenTextEditorClosed(toResource.call(this, '/path/index.txt'), toResource.call(this, '/test.html')); + }); + + async function testWhenTextEditorClosed(...resources: URI[]): Promise { + const accessor = await createTracker(false); + + for (const resource of resources) { + await accessor.editorService.openEditor({ resource, options: { pinned: true } }); + } + + const closedPromise = accessor.instantitionService.invokeFunction(accessor => whenTextEditorClosed(accessor, resources)); + + accessor.editorGroupService.activeGroup.closeAllEditors(); + + await closedPromise; + } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 1630f25609b..24c56ad1139 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -18,8 +18,8 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellExecutionState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientMetadata, SelectionStateType, ICellReplaceEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BaseCellRenderTemplate, CellEditState, CellFocusMode, EXECUTE_CELL_COMMAND_ID, EXPAND_CELL_INPUT_COMMAND_ID, getNotebookEditorFromEditorPane, IActiveNotebookEditor, ICellViewModel, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_HAS_OUTPUTS, NOTEBOOK_CELL_INPUT_COLLAPSED, NOTEBOOK_CELL_LIST_FOCUSED, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_OUTPUT_COLLAPSED, NOTEBOOK_CELL_EXECUTION_STATE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_KERNEL_COUNT, NOTEBOOK_OUTPUT_FOCUSED, NOTEBOOK_INTERRUPTIBLE_KERNEL, NOTEBOOK_HAS_RUNNING_CELL, CHANGE_CELL_LANGUAGE, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditType, CellKind, ICellEditOperation, ICellRange, INotebookDocumentFilter, isDocumentExcludePattern, NotebookCellMetadata, NotebookCellExecutionState, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, TransientCellMetadata, TransientDocumentMetadata, SelectionStateType, ICellReplaceEdit } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -53,14 +53,12 @@ const CHANGE_CELL_TO_CODE_COMMAND_ID = 'notebook.cell.changeToCode'; const CHANGE_CELL_TO_MARKDOWN_COMMAND_ID = 'notebook.cell.changeToMarkdown'; const EDIT_CELL_COMMAND_ID = 'notebook.cell.edit'; -const QUIT_EDIT_CELL_COMMAND_ID = 'notebook.cell.quitEdit'; const DELETE_CELL_COMMAND_ID = 'notebook.cell.delete'; const CANCEL_CELL_COMMAND_ID = 'notebook.cell.cancelExecution'; const EXECUTE_CELL_SELECT_BELOW = 'notebook.cell.executeAndSelectBelow'; const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; -const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; const CENTER_ACTIVE_CELL = 'notebook.centerActiveCell'; const FOCUS_IN_OUTPUT_COMMAND_ID = 'notebook.cell.focusInOutput'; @@ -1339,7 +1337,7 @@ interface IChangeCellContext extends INotebookCellActionContext { language?: string; } -export class ChangeCellLanguageAction extends NotebookCellAction { +registerAction2(class ChangeCellLanguageAction extends NotebookCellAction { constructor() { super({ id: CHANGE_CELL_LANGUAGE, @@ -1495,8 +1493,7 @@ export class ChangeCellLanguageAction extends NotebookCellAction { return fakeResource; } -} -registerAction2(ChangeCellLanguageAction); +}); registerAction2(class extends NotebookAction { constructor() { @@ -1695,7 +1692,7 @@ CommandsRegistry.registerCommand('notebook.trust', (accessor, args) => { CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, args): { viewType: string; displayName: string; - options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; + options: { transientOutputs: boolean; transientCellMetadata: TransientCellMetadata; transientDocumentMetadata: TransientDocumentMetadata; }; filenamePattern: (string | glob.IRelativePattern | { include: string | glob.IRelativePattern, exclude: string | glob.IRelativePattern; })[]; }[] => { const notebookService = accessor.get(INotebookService); @@ -1724,7 +1721,11 @@ CommandsRegistry.registerCommand('_resolveNotebookContentProvider', (accessor, a viewType: provider.id, displayName: provider.displayName, filenamePattern: filenamePatterns, - options: { transientMetadata: provider.options.transientMetadata, transientOutputs: provider.options.transientOutputs } + options: { + transientCellMetadata: provider.options.transientCellMetadata, + transientDocumentMetadata: provider.options.transientDocumentMetadata, + transientOutputs: provider.options.transientOutputs + } }; }); }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts index 13911ac1194..7ac92e61f2d 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/cellStatusBar.ts @@ -6,7 +6,7 @@ import { flatten } from 'vs/base/common/arrays'; import { Throttler } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { registerNotebookContribution } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; @@ -18,6 +18,8 @@ export class NotebookStatusBarController extends Disposable implements INotebook private readonly _visibleCells = new Map(); + private readonly _viewModelDisposables = new DisposableStore(); + constructor( private readonly _notebookEditor: INotebookEditor, @INotebookCellStatusBarService private readonly _notebookCellStatusBarService: INotebookCellStatusBarService @@ -25,11 +27,22 @@ export class NotebookStatusBarController extends Disposable implements INotebook super(); this._updateVisibleCells(); this._register(this._notebookEditor.onDidChangeVisibleRanges(this._updateVisibleCells, this)); - this._register(this._notebookEditor.onDidChangeModel(this._updateEverything, this)); + this._register(this._notebookEditor.onDidChangeModel(this._onModelChange, this)); this._register(this._notebookCellStatusBarService.onDidChangeProviders(this._updateEverything, this)); this._register(this._notebookCellStatusBarService.onDidChangeItems(this._updateEverything, this)); } + private _onModelChange() { + this._viewModelDisposables.clear(); + const vm = this._notebookEditor.viewModel; + if (!vm) { + return; + } + + this._viewModelDisposables.add(vm.onDidChangeViewCells(() => this._updateEverything())); + this._updateEverything(); + } + private _updateEverything(): void { this._visibleCells.forEach(cell => cell.dispose()); this._visibleCells.clear(); @@ -64,7 +77,7 @@ export class NotebookStatusBarController extends Disposable implements INotebook } } - dispose(): void { + override dispose(): void { this._visibleCells.forEach(cell => cell.dispose()); this._visibleCells.clear(); } @@ -117,7 +130,7 @@ class CellStatusBarHelper extends Disposable { this._currentItemIds = newIds; } - dispose() { + override dispose() { super.dispose(); this._notebookViewModel.deltaCellStatusBarItems(this._currentItemIds, [{ handle: this._cell.handle, items: [] }]); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts index 0429bd8d2af..52babeda60a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/statusBar/statusBarProviders.ts @@ -3,36 +3,56 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider, INotebookDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem, INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { CHANGE_CELL_LANGUAGE, EXECUTE_CELL_COMMAND_ID, QUIT_EDIT_CELL_COMMAND_ID } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; class CellStatusBarPlaceholderProvider implements INotebookCellStatusBarItemProvider { - readonly selector: INotebookDocumentFilter = { - filenamePattern: '**/*' + readonly selector: NotebookSelector = { + pattern: '**/*' }; - constructor(@INotebookService private readonly _notebookService: INotebookService) { } + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + ) { } - onDidChangeStatusBarItems?: Event | undefined; - - async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise { + async provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise { const doc = this._notebookService.getNotebookTextModel(uri); const cell = doc?.cells[index]; - if (typeof cell?.metadata.runState !== 'undefined' || typeof cell?.metadata.lastRunSuccess !== 'undefined') { - return { items: [] }; + if (!cell || typeof cell.metadata.runState !== 'undefined' || typeof cell.metadata.lastRunSuccess !== 'undefined') { + return; + } + + let text: string; + if (cell.cellKind === CellKind.Code) { + const keybinding = this._keybindingService.lookupKeybinding(EXECUTE_CELL_COMMAND_ID)?.getLabel(); + if (!keybinding) { + return; + } + + text = localize('notebook.cell.status.codeExecuteTip', "Press {0} to execute cell", keybinding); + } else { + const keybinding = this._keybindingService.lookupKeybinding(QUIT_EDIT_CELL_COMMAND_ID)?.getLabel(); + if (!keybinding) { + return; + } + + text = localize('notebook.cell.status.markdownExecuteTip', "Press {0} to stop editing", keybinding); } - const text = isWindows ? 'Ctrl + Alt + Enter to run' : 'Ctrl + Enter to run'; const item = { text, tooltip: text, @@ -46,12 +66,47 @@ class CellStatusBarPlaceholderProvider implements INotebookCellStatusBarItemProv } } +class CellStatusBarLanguagePickerProvider implements INotebookCellStatusBarItemProvider { + readonly selector: NotebookSelector = { + pattern: '**/*' + }; + + constructor( + @INotebookService private readonly _notebookService: INotebookService, + @IModeService private readonly _modeService: IModeService, + ) { } + + async provideCellStatusBarItems(uri: URI, index: number, _token: CancellationToken): Promise { + const doc = this._notebookService.getNotebookTextModel(uri); + const cell = doc?.cells[index]; + if (!cell) { + return; + } + + const modeId = cell.cellKind === CellKind.Markdown ? + 'markdown' : + (this._modeService.getModeIdForLanguageName(cell.language) || cell.language); + const text = this._modeService.getLanguageName(modeId) || this._modeService.getLanguageName('plaintext'); + const item = { + text, + command: CHANGE_CELL_LANGUAGE, + tooltip: localize('notebook.cell.status.language', "Select Cell Language Mode"), + alignment: CellStatusbarAlignment.Right, + priority: -Number.MAX_SAFE_INTEGER + }; + return { + items: [item] + }; + } +} + class BuiltinCellStatusBarProviders extends Disposable { constructor( @IInstantiationService instantiationService: IInstantiationService, @INotebookCellStatusBarService notebookCellStatusBarService: INotebookCellStatusBarService) { super(); this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(CellStatusBarPlaceholderProvider))); + this._register(notebookCellStatusBarService.registerCellStatusBarItemProvider(instantiationService.createInstance(CellStatusBarLanguagePickerProvider))); } } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts index 53c6484ba10..925415cd8ba 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/diffElementViewModel.ts @@ -493,11 +493,11 @@ export function getFormatedMetadataJSON(documentTextModel: NotebookTextModel, me let filteredMetadata: { [key: string]: any } = {}; if (documentTextModel) { - const transientMetadata = documentTextModel.transientOptions.transientMetadata; + const transientCellMetadata = documentTextModel.transientOptions.transientCellMetadata; const keys = new Set([...Object.keys(metadata)]); for (let key of keys) { - if (!(transientMetadata[key as keyof NotebookCellMetadata]) + if (!(transientCellMetadata[key as keyof NotebookCellMetadata]) ) { filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; } diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts index 13f04c088e2..58b0fae01cd 100644 --- a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -483,7 +483,28 @@ export class NotebookTextDiffEditor extends EditorPane implements INotebookTextD } } - scheduleOutputHeightAck() { } + scheduleOutputHeightAck(cellInfo: IDiffCellInfo, outputId: string, height: number) { + const diffElement = cellInfo.diffElement; + // const activeWebview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + let diffSide = DiffSide.Original; + + if (diffElement instanceof SideBySideDiffElementViewModel) { + const info = CellUri.parse(cellInfo.cellUri); + if (!info) { + return; + } + + diffSide = info.notebook.toString() === this._model?.original.resource.toString() ? DiffSide.Original : DiffSide.Modified; + } else { + diffSide = diffElement.type === 'insert' ? DiffSide.Modified : DiffSide.Original; + } + + const webview = diffSide === DiffSide.Modified ? this._modifiedWebview : this._originalWebview; + + DOM.scheduleAtNextAnimationFrame(() => { + webview?.ackHeight(cellInfo.cellId, outputId, height); + }, 10); + } private _computeModifiedLCS(change: IDiffChange, originalModel: NotebookTextModel, modifiedModel: NotebookTextModel) { const result: DiffElementViewModelBase[] = []; diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 278cabbc997..08f00a78366 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -420,7 +420,6 @@ .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left, .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { - padding-right: 12px; display: flex; z-index: 26; } @@ -448,24 +447,15 @@ cursor: pointer; } -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker { - cursor: pointer; -} - .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-duration { margin-right: 8px; } -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-duration, -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-message { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-duration { display: flex; align-items: center; } -.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-message { - margin-right: 6px; -} - .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index b25cfe8a66c..c6db38500ca 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -23,7 +23,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorInput, Extensions as EditorInputExtensions, ICustomEditorInputFactory, IEditorInput, IEditorInputSerializer, IEditorInputFactoryRegistry, IEditorInputWithOptions } from 'vs/workbench/common/editor'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/common/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; @@ -47,6 +47,7 @@ import { getFormatedMetadataJSON } from 'vs/workbench/contrib/notebook/browser/d import { NotebookModelResolverServiceImpl } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl'; import { INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { NotebookKernelService } from 'vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; // Editor Contribution import 'vs/workbench/contrib/notebook/browser/contrib/clipboard/notebookClipboard'; @@ -172,9 +173,9 @@ Registry.as(EditorInputExtensions.EditorInputFactor new class implements ICustomEditorInputFactory { async createCustomEditorInput(resource: URI, instantiationService: IInstantiationService): Promise { return instantiationService.invokeFunction(async accessor => { - const backupFileService = accessor.get(IBackupFileService); + const workingCopyBackupService = accessor.get(IWorkingCopyBackupService); - const backup = await backupFileService.resolve(resource); + const backup = await workingCopyBackupService.resolve({ resource, typeId: NO_TYPE_ID }); if (!backup?.meta) { throw new Error(`No backup found for Notebook editor: ${resource}`); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index b022a7314f1..de7f895509b 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -72,6 +72,8 @@ export const NOTEBOOK_INTERRUPTIBLE_KERNEL = new RawContextKey('noteboo //#region Shared commands export const EXPAND_CELL_INPUT_COMMAND_ID = 'notebook.cell.expandCellInput'; export const EXECUTE_CELL_COMMAND_ID = 'notebook.cell.execute'; +export const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; +export const QUIT_EDIT_CELL_COMMAND_ID = 'notebook.cell.quitEdit'; //#endregion @@ -171,7 +173,7 @@ export interface ICommonNotebookEditor { focusNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output', options?: IFocusNotebookCellOptions): void; focusNextNotebookCell(cell: IGenericCellViewModel, focus: 'editor' | 'container' | 'output'): void; updateOutputHeight(cellInfo: ICommonCellInfo, output: IDisplayOutputViewModel, height: number, isInit: boolean, source?: string): void; - scheduleOutputHeightAck(cellId: string, outputId: string, height: number): void; + scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number): void; updateMarkdownCellHeight(cellId: string, height: number, isInit: boolean): void; setMarkdownCellEditState(cellId: string, editState: CellEditState): void; markdownCellDragStart(cellId: string, position: { clientY: number }): void; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts index 35b74591b6a..05d2a1fc52a 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts @@ -9,7 +9,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; -import { INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider, notebookDocumentFilterMatch } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellStatusBarItemList, INotebookCellStatusBarItemProvider } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { score } from 'vs/workbench/contrib/notebook/common/notebookSelector'; export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { @@ -42,10 +43,10 @@ export class NotebookCellStatusBarService extends Disposable implements INoteboo } async getStatusBarItemsForCell(docUri: URI, cellIndex: number, viewType: string, token: CancellationToken): Promise { - const providers = this._providers.filter(p => notebookDocumentFilterMatch(p.selector, viewType, docUri)); - return await Promise.all(providers.map(p => { + const providers = this._providers.filter(p => score(p.selector, docUri, viewType) > 0); + return await Promise.all(providers.map(async p => { try { - return p.provideCellStatusBarItems(docUri, cellIndex, token); + return await p.provideCellStatusBarItems(docUri, cellIndex, token) ?? { items: [] }; } catch (e) { onUnexpectedExternalError(e); return { items: [] }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 65475c2c541..47676ed3841 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -2355,12 +2355,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - scheduleOutputHeightAck(cellId: string, outputId: string, height: number) { + scheduleOutputHeightAck(cellInfo: ICommonCellInfo, outputId: string, height: number) { DOM.scheduleAtNextAnimationFrame(() => { this.updateScrollHeight(); this._debug('ack height', height); - this._webview?.ackHeight(cellId, outputId, height); + this._webview?.ackHeight(cellInfo.cellId, outputId, height); }, 10); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts index 6ceaa6b68f6..578672986f0 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookKernelServiceImpl.ts @@ -5,7 +5,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; -import { ICellRange, INotebookKernel, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookKernelBindEvent, INotebookKernel2, INotebookKernelService } from 'vs/workbench/contrib/notebook/common/notebookKernelService'; import { score } from 'vs/workbench/contrib/notebook/common/notebookSelector'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; @@ -40,7 +40,7 @@ export class NotebookKernelService implements INotebookKernelService { registerKernel(kernel: INotebookKernel2): IDisposable { if (this._kernels.has(kernel.id)) { - throw new Error(`KERNEL with id '${kernel.id}' already exists`); + throw new Error(`NOTEBOOK CONTROLLER with id '${kernel.id}' already exists`); } this._kernels.set(kernel.id, kernel); @@ -143,31 +143,11 @@ class KernelAdaptorBridge implements IWorkbenchContribution { providerExtensionId: 'notAnExtension', selector: { filenamePattern: '**/*' }, async provideKernels(uri: URI) { - const model = notebookService.getNotebookTextModel(uri); if (!model) { return []; } - return notebookKernelService.getMatchingKernels(model).map((kernel: INotebookKernel2): INotebookKernel => { - return { - id: kernel.id, - friendlyId: kernel.id, - label: kernel.label, - description: kernel.description, - detail: kernel.detail, - isPreferred: kernel.isPreferred, - preloadUris: kernel.preloadUris, - preloadProvides: kernel.preloadProvides, - localResourceRoot: kernel.localResourceRoot, - supportedLanguages: kernel.supportedLanguages, - implementsInterrupt: kernel.implementsInterrupt, - implementsExecutionOrder: kernel.implementsExecutionOrder, - extension: kernel.extension, - async resolve() { }, - async executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): Promise { kernel.executeNotebookCellsRequest(uri, ranges); }, - async cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): Promise { kernel.cancelNotebookCellExecution(uri, ranges); }, - }; - }); + return notebookKernelService.getMatchingKernels(model); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index 400ebd17df7..55aa4501c5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -83,18 +83,23 @@ export class NotebookKernelProviderInfoStore { } export class NotebookProviderInfoStore extends Disposable { + private static readonly CUSTOM_EDITORS_STORAGE_ID = 'notebookEditors'; private static readonly CUSTOM_EDITORS_ENTRY_ID = 'editors'; private readonly _memento: Memento; private _handled: boolean = false; + + private readonly _contributedEditors = new Map(); + private readonly _contributedEditorDisposables = new DisposableStore(); + constructor( - storageService: IStorageService, - extensionService: IExtensionService, - private readonly _editorOverrideService: IEditorOverrideService, - private readonly _instantiationService: IInstantiationService, - private readonly _configurationService: IConfigurationService, - private readonly _accessibilityService: IAccessibilityService, + @IStorageService storageService: IStorageService, + @IExtensionService extensionService: IExtensionService, + @IEditorOverrideService private readonly _editorOverrideService: IEditorOverrideService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, + @IInstantiationService private readonly _instantiationService: IInstantiationService, ) { super(); this._memento = new Memento(NotebookProviderInfoStore.CUSTOM_EDITORS_STORAGE_ID, storageService); @@ -110,18 +115,25 @@ export class NotebookProviderInfoStore extends Disposable { if (!this._handled) { // there is no extension point registered for notebook content provider // clear the memento and cache - this.clear(); + this._clear(); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = []; this._memento.saveMemento(); this._updateProviderExtensionsInfo(); } })); + + notebookProviderExtensionPoint.setHandler(extensions => this._setupHandler(extensions)); } - setupHandler(extensions: readonly IExtensionPointUser[]) { + override dispose(): void { + this._clear(); + super.dispose(); + } + + private _setupHandler(extensions: readonly IExtensionPointUser[]) { this._handled = true; - this.clear(); + this._clear(); for (const extension of extensions) { for (const notebookContribution of extension.value) { @@ -174,10 +186,10 @@ export class NotebookProviderInfoStore extends Disposable { } - private registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): void { + private _registerContributionPoint(notebookProviderInfo: NotebookProviderInfo): void { for (const selector of notebookProviderInfo.selectors) { const globPattern = (selector as INotebookExclusiveDocumentFilter).include || selector as glob.IRelativePattern | string; - this._register(this._editorOverrideService.registerContributionPoint( + this._contributedEditorDisposables.add(this._editorOverrideService.registerContributionPoint( globPattern, priorityToRank(notebookProviderInfo.exclusive ? ContributedEditorPriority.exclusive : notebookProviderInfo.priority), { @@ -215,10 +227,10 @@ export class NotebookProviderInfoStore extends Disposable { } } - private readonly _contributedEditors = new Map(); - clear() { + private _clear(): void { this._contributedEditors.clear(); + this._contributedEditorDisposables.clear(); } get(viewType: string): NotebookProviderInfo | undefined { @@ -230,7 +242,7 @@ export class NotebookProviderInfoStore extends Disposable { return; } this._contributedEditors.set(info.id, info); - this.registerContributionPoint(info); + this._registerContributionPoint(info); const mementoObject = this._memento.getMemento(StorageScope.GLOBAL, StorageTarget.MACHINE); mementoObject[NotebookProviderInfoStore.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._contributedEditors.values()); @@ -318,8 +330,6 @@ class ModelData implements IDisposable { } } - - export class NotebookService extends Disposable implements INotebookService, IEditorTypesHandler { declare readonly _serviceBrand: undefined; @@ -352,25 +362,15 @@ export class NotebookService extends Disposable implements INotebookService, IEd @IExtensionService private readonly _extensionService: IExtensionService, @IConfigurationService private readonly _configurationService: IConfigurationService, @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IStorageService private readonly _storageService: IStorageService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IEditorOverrideService private readonly _editorOverrideService: IEditorOverrideService, ) { super(); - this._notebookProviderInfoStore = new NotebookProviderInfoStore( - this._storageService, - this._extensionService, this._editorOverrideService, - this._instantiationService, this._configurationService, - this._accessibilityService - ); + this._notebookProviderInfoStore = _instantiationService.createInstance(NotebookProviderInfoStore); this._register(this._notebookProviderInfoStore); - notebookProviderExtensionPoint.setHandler((extensions) => { - this._notebookProviderInfoStore.setupHandler(extensions); - }); notebookRendererExtensionPoint.setHandler((renderers) => { this._notebookRenderersInfoStore.clear(); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index 145e8f28e02..ad5f137f8fd 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { getExtensionForMimeType } from 'vs/base/common/mime'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { isWeb } from 'vs/base/common/platform'; +import { isMacintosh, isWeb } from 'vs/base/common/platform'; import { dirname, joinPath } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; @@ -436,6 +436,8 @@ export class BackLayerWebView extends Disposable { } private generateContent(coreDependencies: string, baseUrl: string) { const markdownRenderersSrc = this.getMarkdownRendererScripts(); + const outputWidth = `calc(100% - ${this.options.leftMargin + (this.options.cellMargin * 2) + this.options.runGutter}px)`; + const outputMarginLeft = `${this.options.leftMargin + this.options.runGutter}px`; return html` @@ -597,8 +599,8 @@ export class BackLayerWebView extends Disposable { } #container > div > div > div.output { - width: calc(100% - ${this.options.leftMargin + (this.options.cellMargin * 2) + this.options.runGutter}px); - margin-left: ${this.options.leftMargin + this.options.runGutter}px; + width: ${outputWidth}; + margin-left: ${outputMarginLeft}; padding: ${this.options.outputNodePadding}px ${this.options.outputNodePadding}px ${this.options.outputNodePadding}px ${this.options.outputNodeLeftPadding}px; box-sizing: border-box; background-color: var(--vscode-notebook-outputContainerBackgroundColor); @@ -872,7 +874,7 @@ var requirejs = (function() { if (resolvedResult) { const { cellInfo, output } = resolvedResult; this.notebookEditor.updateOutputHeight(cellInfo, output, height, !!update.init, 'webview#dimension'); - this.notebookEditor.scheduleOutputHeightAck(cellInfo.cellId, update.id, height); + this.notebookEditor.scheduleOutputHeightAck(cellInfo, update.id, height); } } else { this.notebookEditor.updateMarkdownCellHeight(update.id, height, !!update.init); @@ -971,7 +973,7 @@ var requirejs = (function() { { const cell = this.notebookEditor.getCellById(data.cellId); if (cell) { - if (data.shiftKey || data.metaKey) { + if (data.shiftKey || (isMacintosh ? data.metaKey : data.ctrlKey)) { // Add to selection this.notebookEditor.toggleNotebookCellSelection(cell); } else { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts index fbac41335a3..edf7f821806 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellEditorOptions.ts @@ -11,7 +11,7 @@ import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'v import { IEditorOptions, LineNumbersType } from 'vs/editor/common/config/editorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EDITOR_BOTTOM_PADDING, EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR } from 'vs/workbench/contrib/notebook/browser/constants'; -import { EditorTopPaddingChangeEvent, getEditorTopPadding, getNotebookEditorFromEditorPane, ICellViewModel, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EditorTopPaddingChangeEvent, getEditorTopPadding, getNotebookEditorFromEditorPane, ICellViewModel, NOTEBOOK_CELL_LINE_NUMBERS, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { ShowCellStatusBarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { localize } from 'vs/nls'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; @@ -143,7 +143,8 @@ registerAction2(class ToggleLineNumberAction extends Action2 { menu: [{ id: MenuId.EditorTitle, group: 'LineNumber', - order: 0 + order: 0, + when: NOTEBOOK_IS_ACTIVE_EDITOR }], category: NOTEBOOK_ACTIONS_CATEGORY, f1: true, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts index b82b758f9bf..8598cac1a98 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -7,22 +7,19 @@ import * as DOM from 'vs/base/browser/dom'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { SimpleIconLabel } from 'vs/base/browser/ui/iconLabel/simpleIconLabel'; import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; -import { stripIcons } from 'vs/base/common/iconLabels'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { Emitter, Event } from 'vs/base/common/event'; +import { stripIcons } from 'vs/base/common/iconLabels'; import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { IDimension } from 'vs/editor/common/editorCommon'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ChangeCellLanguageAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { ICellViewModel, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { CellStatusbarAlignment, INotebookCellStatusBarItem } from 'vs/workbench/contrib/notebook/common/notebookCommon'; const $ = DOM.$; @@ -41,7 +38,6 @@ export const enum ClickTargetType { export class CellEditorStatusBar extends Disposable { readonly cellRunStatusContainer: HTMLElement; readonly statusBarContainer: HTMLElement; - readonly languageStatusBarItem: CellLanguageStatusBarItem; readonly durationContainer: HTMLElement; private readonly leftContributedItemsContainer: HTMLElement; @@ -65,7 +61,6 @@ export class CellEditorStatusBar extends Disposable { this.durationContainer = DOM.append(leftItemsContainer, $('.cell-run-duration')); this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items.cell-contributed-items-left')); this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items.cell-contributed-items-right')); - this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightItemsContainer); this.itemsDisposable = this._register(new DisposableStore()); @@ -104,7 +99,6 @@ export class CellEditorStatusBar extends Disposable { update(context: INotebookCellActionContext) { this.currentContext = context; this.itemsDisposable.clear(); - this.languageStatusBarItem.update(context.cell, context.notebookEditor); this.updateStatusBarItems(); } @@ -224,61 +218,6 @@ class CellStatusBarItem extends Disposable { } } -export class CellLanguageStatusBarItem extends Disposable { - private readonly labelElement: HTMLElement; - - private cell: ICellViewModel | undefined; - private editor: INotebookEditor | undefined; - - private cellDisposables: DisposableStore; - - constructor( - readonly container: HTMLElement, - @IModeService private readonly modeService: IModeService, - @IInstantiationService private readonly instantiationService: IInstantiationService - ) { - super(); - this.labelElement = DOM.append(container, $('.cell-language-picker.cell-status-item')); - this.labelElement.tabIndex = 0; - this.labelElement.classList.add('cell-status-item-has-command'); - - this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { - this.run(); - })); - this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.KEY_DOWN, e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { - this.run(); - } - })); - this._register(this.cellDisposables = new DisposableStore()); - } - - private run() { - this.instantiationService.invokeFunction(accessor => { - if (!this.editor || !this.editor.hasModel() || !this.cell) { - return; - } - new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor, cell: this.cell }); - }); - } - - update(cell: ICellViewModel, editor: INotebookEditor): void { - this.cellDisposables.clear(); - this.cell = cell; - this.editor = editor; - - this.render(); - this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render())); - } - - private render(): void { - const modeId = this.cell?.cellKind === CellKind.Markdown ? 'markdown' : this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language; - this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext'); - this.labelElement.title = localize('notebook.cell.status.language', "Select Cell Language Mode"); - } -} - declare const ResizeObserver: any; export interface IResizeObserver { diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 31d4fb746b9..b5e6a193a79 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -159,11 +159,11 @@ export class NotebookCellTextModel extends Disposable implements ICell { private _getPersisentMetadata() { let filteredMetadata: { [key: string]: any } = {}; - const transientMetadata = this.transientOptions.transientMetadata; + const transientCellMetadata = this.transientOptions.transientCellMetadata; const keys = new Set([...Object.keys(this.metadata)]); for (let key of keys) { - if (!(transientMetadata[key as keyof NotebookCellMetadata]) + if (!(transientCellMetadata[key as keyof NotebookCellMetadata]) ) { filteredMetadata[key] = this.metadata[key as keyof NotebookCellMetadata]; } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index 2db4d08fc7e..fd36077f502 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -193,7 +193,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private _cells: NotebookCellTextModel[] = []; metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; - transientOptions: TransientOptions = { transientMetadata: {}, transientOutputs: false }; + transientOptions: TransientOptions = { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false }; private _versionId = 0; /** @@ -515,25 +515,28 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel private _updateNotebookMetadata(metadata: NotebookDocumentMetadata, computeUndoRedo: boolean) { const oldMetadata = this.metadata; - this.metadata = metadata; + const triggerDirtyChange = this._isDocumentMetadataChanged(this.metadata, metadata); - if (computeUndoRedo) { - const that = this; - this._operationManager.pushEditOperation(new class implements IResourceUndoRedoElement { - readonly type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; - get resource() { - return that.uri; - } - readonly label = 'Update Notebook Metadata'; - undo() { - that._updateNotebookMetadata(oldMetadata, false); - } - redo() { - that._updateNotebookMetadata(metadata, false); - } - }(), undefined, undefined); + if (triggerDirtyChange) { + if (computeUndoRedo) { + const that = this; + this._operationManager.pushEditOperation(new class implements IResourceUndoRedoElement { + readonly type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + get resource() { + return that.uri; + } + readonly label = 'Update Notebook Metadata'; + undo() { + that._updateNotebookMetadata(oldMetadata, false); + } + redo() { + that._updateNotebookMetadata(metadata, false); + } + }(), undefined, undefined); + } } + this.metadata = metadata; this._eventEmitter.emit({ kind: NotebookCellsChangeType.ChangeDocumentMetadata, metadata: this.metadata, transient: this._isDocumentMetadataChangeTransient(oldMetadata, metadata) }, true); } @@ -595,20 +598,42 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } + private _isDocumentMetadataChanged(a: NotebookDocumentMetadata, b: NotebookDocumentMetadata) { + const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); + for (let key of keys) { + if (key === 'custom') { + if (!this._customMetadataEqual(a[key], b[key]) + && + !(this.transientOptions.transientCellMetadata[key as keyof NotebookCellMetadata]) + ) { + return true; + } + } else if ( + (a[key as keyof NotebookDocumentMetadata] !== b[key as keyof NotebookDocumentMetadata]) + && + !(this.transientOptions.transientDocumentMetadata[key as keyof NotebookDocumentMetadata]) + ) { + return true; + } + } + + return false; + } + private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); for (let key of keys) { if (key === 'custom') { if (!this._customMetadataEqual(a[key], b[key]) && - !(this.transientOptions.transientMetadata[key as keyof NotebookCellMetadata]) + !(this.transientOptions.transientCellMetadata[key as keyof NotebookCellMetadata]) ) { return true; } } else if ( (a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata]) && - !(this.transientOptions.transientMetadata[key as keyof NotebookCellMetadata]) + !(this.transientOptions.transientCellMetadata[key as keyof NotebookCellMetadata]) ) { return true; } diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 0bf4ecb9c44..45ba3687efe 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -21,6 +21,8 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { ThemeColor } from 'vs/platform/theme/common/themeService'; +import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; export enum CellKind { Markdown = 1, @@ -92,11 +94,13 @@ export interface NotebookCellMetadata { custom?: { [key: string]: unknown }; } -export type TransientMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; +export type TransientCellMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; +export type TransientDocumentMetadata = { [K in keyof NotebookDocumentMetadata]?: boolean }; export interface TransientOptions { transientOutputs: boolean; - transientMetadata: TransientMetadata; + transientCellMetadata: TransientCellMetadata; + transientDocumentMetadata: TransientDocumentMetadata; } export interface INotebookMimeTypeSelector { @@ -679,7 +683,7 @@ export interface INotebookTextModelBackup { cells: ICellDto2[] } -export interface NotebookDocumentBackupData { +export interface NotebookDocumentBackupData extends IWorkingCopyBackupMeta { readonly viewType: string; readonly backupId?: string; readonly mtime?: number; @@ -762,12 +766,17 @@ export function notebookDocumentFilterMatch(filter: INotebookDocumentFilter, vie } export interface INotebookKernel { + + /** @deprecated */ + providerHandle?: number; + /** @deprecated */ + resolve(uri: URI, editorId: string, token: CancellationToken): Promise; + id?: string; friendlyId: string; label: string; extension: ExtensionIdentifier; localResourceRoot: URI; - providerHandle?: number; description?: string; detail?: string; isPreferred?: boolean; @@ -777,7 +786,6 @@ export interface INotebookKernel { implementsInterrupt?: boolean; implementsExecutionOrder?: boolean; - resolve(uri: URI, editorId: string, token: CancellationToken): Promise; executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): Promise; cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): Promise; } @@ -791,9 +799,9 @@ export interface INotebookKernelProvider { } export interface INotebookCellStatusBarItemProvider { - selector: INotebookDocumentFilter; + selector: NotebookSelector; onDidChangeStatusBarItems?: Event; - provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise; + provideCellStatusBarItems(uri: URI, index: number, token: CancellationToken): Promise; } export class CellSequence implements ISequence { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index 1258268eb67..b5e50f65ffa 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -10,9 +10,10 @@ import { INotebookEditorModel, INotebookLoadOptions, IResolvedNotebookEditorMode import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMainNotebookController, INotebookSerializer, INotebookService, SimpleNotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookService'; import { URI } from 'vs/base/common/uri'; -import { IWorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities, NO_TYPE_ID, IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Schemas } from 'vs/base/common/network'; import { IFileStatWithMetadata, IFileService, FileChangeType, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -43,7 +44,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook private _lastResolvedFileStat?: IFileStatWithMetadata; private readonly _name: string; - private readonly _workingCopyResource: URI; + private readonly _workingCopyIdentifier: IWorkingCopyIdentifier; private readonly _saveSequentializer = new TaskSequentializer(); private _dirty: boolean = false; @@ -55,7 +56,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook @IInstantiationService private readonly _instantiationService: IInstantiationService, @INotebookService private readonly _notebookService: INotebookService, @IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService, - @IBackupFileService private readonly _backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly _workingCopyBackupService: IWorkingCopyBackupService, @IFileService private readonly _fileService: IFileService, @INotificationService private readonly _notificationService: INotificationService, @ILogService private readonly _logService: ILogService, @@ -67,9 +68,23 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook this._name = labelService.getUriBasenameLabel(resource); const that = this; - this._workingCopyResource = URI.from({ scheme: Schemas.vscodeNotebook, path: resource.toString() }); + this._workingCopyIdentifier = { + // TODO@jrieken TODO@rebornix consider to enable a `typeId` that is + // specific for custom editors. Using a distinct `typeId` allows the + // working copy to have any resource (including file based resources) + // even if other working copies exist with the same resource. + // + // IMPORTANT: changing the `typeId` has an impact on backups for this + // working copy. Any value that is not the empty string will be used + // as seed to the backup. Only change the `typeId` if you have implemented + // a fallback solution to resolve any existing backups that do not have + // this seed. + typeId: NO_TYPE_ID, + resource: URI.from({ scheme: Schemas.vscodeNotebook, path: resource.toString() }) + }; const workingCopyAdapter = new class implements IWorkingCopy { - readonly resource = that._workingCopyResource; + readonly typeId = that._workingCopyIdentifier.typeId; + readonly resource = that._workingCopyIdentifier.resource; get name() { return that._name; } readonly capabilities = that._isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = that.onDidChangeDirty; @@ -126,7 +141,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook } } - async backup(token: CancellationToken): Promise> { + async backup(token: CancellationToken): Promise { if (!this.isResolved()) { return {}; @@ -173,7 +188,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook return this; } - const backup = await this._backupFileService.resolve(this._workingCopyResource); + const backup = await this._workingCopyBackupService.resolve(this._workingCopyIdentifier); if (this.isResolved()) { return this; // Make sure meanwhile someone else did not succeed in loading @@ -247,7 +262,7 @@ export class ComplexNotebookEditorModel extends EditorModel implements INotebook } if (backupId) { - this._backupFileService.discardBackup(this._workingCopyResource); + this._workingCopyBackupService.discardBackup(this._workingCopyIdentifier); this.setDirty(true); } else { this.setDirty(false); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts index 61912f63c42..1848e7139f6 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverServiceImpl.ts @@ -16,6 +16,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; import { ResourceMap } from 'vs/base/common/map'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; class NotebookModelReferenceCollection extends ReferenceCollection> { @@ -40,6 +41,17 @@ class NotebookModelReferenceCollection extends ReferenceCollection_instantiationService.createInstance( FileWorkingCopyManager, + // TODO@jrieken TODO@rebornix consider to enable a `typeId` that is + // specific for custom editors. Using a distinct `typeId` allows the + // working copy to have any resource (including file based resources) + // even if other working copies exist with the same resource. + // + // IMPORTANT: changing the `typeId` has an impact on backups for this + // working copy. Any value that is not the empty string will be used + // as seed to the backup. Only change the `typeId` if you have implemented + // a fallback solution to resolve any existing backups that do not have + // this seed. + NO_TYPE_ID, new NotebookFileWorkingCopyModelFactory(_notebookService) ); } diff --git a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts index 397aa81b707..61ed85828d5 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookKernelService.ts @@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ICellRange, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookKernel, INotebookTextModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookSelector } from 'vs/workbench/contrib/notebook/common/notebookSelector'; export interface INotebookKernel2ChangeEvent { @@ -20,28 +20,13 @@ export interface INotebookKernel2ChangeEvent { hasExecutionOrder?: true; } -export interface INotebookKernel2 { +export interface INotebookKernel2 extends INotebookKernel { readonly id: string; readonly selector: NotebookSelector readonly extension: ExtensionIdentifier; readonly onDidChange: Event; - - label: string; - description?: string; - detail?: string; - isPreferred?: boolean; - supportedLanguages: string[]; - implementsExecutionOrder: boolean; - implementsInterrupt: boolean; - - localResourceRoot: URI; - preloadUris: URI[]; - preloadProvides: string[]; - - executeNotebookCellsRequest(uri: URI, ranges: ICellRange[]): void; - cancelNotebookCellExecution(uri: URI, ranges: ICellRange[]): void } export interface INotebookKernelBindEvent { diff --git a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts index a47f5375465..57e8aab2e4f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookProvider.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookProvider.ts @@ -9,7 +9,7 @@ import { basename } from 'vs/base/common/path'; import { INotebookExclusiveDocumentFilter, isDocumentExcludePattern, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ContributedEditorPriority } from 'vs/workbench/services/editor/common/editorOverrideService'; -export type NotebookSelector = string | glob.IRelativePattern | INotebookExclusiveDocumentFilter; +type NotebookSelector = string | glob.IRelativePattern | INotebookExclusiveDocumentFilter; export interface NotebookEditorDescriptor { readonly id: string; @@ -61,7 +61,8 @@ export class NotebookProviderInfo { this.dynamicContribution = descriptor.dynamicContribution; this.exclusive = descriptor.exclusive; this._options = { - transientMetadata: {}, + transientCellMetadata: {}, + transientDocumentMetadata: {}, transientOutputs: false }; } diff --git a/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts index 7728628c114..3792dc84d36 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookEditorModel.test.ts @@ -16,15 +16,16 @@ import { NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ComplexNotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; -import { IWorkingCopy, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; suite('NotebookEditorModel', function () { const instaService = new InstantiationService(); const notebokService = new class extends mock() { }; - const backupService = new class extends mock() { }; + const backupService = new class extends mock() { }; const notificationService = new class extends mock() { }; const untitledTextEditorService = new class extends mock() { }; const fileService = new class extends mock() { diff --git a/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts b/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts index cac96403a87..f7db87dad57 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookServiceImpl.test.ts @@ -31,11 +31,9 @@ suite('NotebookProviderInfoStore', function () { override onDidRegisterExtensions = Event.None; }, instantiationService.createInstance(EditorOverrideService), - instantiationService, new TestConfigurationService(), - new class extends mock() { - - } + new class extends mock() { }, + instantiationService, ); const fooInfo = new NotebookProviderInfo({ diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 213b6c019be..86556d7032b 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -34,7 +34,7 @@ suite('NotebookViewModel', () => { instantiationService.stub(IThemeService, new TestThemeService()); test('ctor', function () { - const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false }, undoRedoService, modelService, modeService); + const notebook = new NotebookTextModel('notebook', URI.parse('test'), [], notebookDocumentMetadataDefaults, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false }, undoRedoService, modelService, modeService); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel('notebook', model.notebook, eventDispatcher, null, instantiationService, bulkEditService, undoRedoService, textModelService); diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index ddc046d8f79..70a88c92cb3 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -47,7 +47,7 @@ export class TestCell extends NotebookCellTextModel { outputs: IOutputDto[], modeService: IModeService, ) { - super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modeService); + super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false }, modeService); } } @@ -152,7 +152,7 @@ function _createTestNotebookEditor(instantiationService: TestInstantiationServic outputs: cell[3] ?? [], metadata: cell[4] }; - }), notebookDocumentMetadataDefaults, { transientMetadata: {}, transientOutputs: false }); + }), notebookDocumentMetadataDefaults, { transientCellMetadata: {}, transientDocumentMetadata: {}, transientOutputs: false }); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index ac02b175756..662ee097931 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -213,6 +213,13 @@ export class SettingsEditor2 extends EditorPane { if (this.settingsTreeModel) { this.settingsTreeModel.updateWorkspaceTrust(workspaceTrustManagementService.isWorkpaceTrusted()); } + this.renderTree(); + })); + + this._register(configurationService.onDidChangeUntrustdSettings(e => { + if (e.default.length) { + this.updateElementsByKey([...e.default]); + } })); this.modelDisposables = this._register(new DisposableStore()); @@ -283,18 +290,16 @@ export class SettingsEditor2 extends EditorPane { })); this.defaultSettingsEditorModel = model; + options = options || SettingsEditorOptions.create({}); + if (!this.viewState.settingsTarget) { + if (!options.target) { + options.target = ConfigurationTarget.USER_LOCAL; + } + } + this._setOptions(options); + // Don't block setInput on render (which can trigger an async search) this.onConfigUpdate(undefined, true).then(() => { - options = options || SettingsEditorOptions.create({}); - - if (!this.viewState.settingsTarget) { - if (!options.target) { - options.target = ConfigurationTarget.USER_LOCAL; - } - } - - this._setOptions(options); - this._register(input.onWillDispose(() => { this.searchWidget.setValue(''); })); diff --git a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts index 9aa879ad2cb..af0ff69f350 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteExplorer.ts @@ -198,10 +198,10 @@ export class AutomaticPortForwarding extends Disposable implements IWorkbenchCon Registry.as(ConfigurationExtensions.Configuration) .registerDefaultConfigurations([{ 'remote.autoForwardPortsSource': PORT_AUTO_SOURCE_SETTING_OUTPUT }]); this._register(new OutputAutomaticPortForwarding(terminalService, notificationService, openerService, externalOpenerService, - remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, logService, false)); + remoteExplorerService, configurationService, debugService, tunnelService, remoteAgentService, hostService, logService, () => false)); } else { - const useProc = (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS); - if (useProc) { + const useProc = () => (this.configurationService.getValue(PORT_AUTO_SOURCE_SETTING) === PORT_AUTO_SOURCE_SETTING_PROCESS); + if (useProc()) { this._register(new ProcAutomaticPortForwarding(configurationService, remoteExplorerService, notificationService, openerService, externalOpenerService, tunnelService, hostService, logService)); } @@ -405,7 +405,7 @@ class OutputAutomaticPortForwarding extends Disposable { private readonly remoteAgentService: IRemoteAgentService, readonly hostService: IHostService, readonly logService: ILogService, - readonly privilegedOnly: boolean + readonly privilegedOnly: () => boolean ) { super(); this.notifier = new OnAutoForwardedAction(notificationService, remoteExplorerService, openerService, externalOpenerService, tunnelService, hostService, logService); @@ -444,7 +444,7 @@ class OutputAutomaticPortForwarding extends Disposable { if ((await this.remoteExplorerService.tunnelModel.getAttributes([localUrl.port]))?.get(localUrl.port)?.onAutoForward === OnPortForward.Ignore) { return; } - if (this.privilegedOnly && !isPortPrivileged(localUrl.port, (await this.remoteAgentService.getEnvironment())?.os)) { + if (this.privilegedOnly() && !isPortPrivileged(localUrl.port, (await this.remoteAgentService.getEnvironment())?.os)) { return; } const forwarded = await this.remoteExplorerService.forward(localUrl, undefined, undefined, undefined, undefined, undefined, false); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 85c6c254585..4014ba998f6 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -26,6 +26,7 @@ import { once } from 'vs/base/common/functional'; import { truncate } from 'vs/base/common/strings'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { getVirtualWorkspaceLocation } from 'vs/platform/remote/common/remoteHosts'; +import { getCodiconAriaLabel } from 'vs/base/common/codicons'; export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution { @@ -34,7 +35,6 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr private static readonly SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command private static readonly REMOTE_STATUS_LABEL_MAX_LENGTH = 40; - private static readonly CODICON_REGEXP = /\$\((.*?)\)/g; private remoteStatusEntry: IStatusbarEntryAccessor | undefined; @@ -261,7 +261,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID; } - const ariaLabel = text.replace(RemoteStatusIndicator.CODICON_REGEXP, (_match, codiconName) => codiconName); + const ariaLabel = getCodiconAriaLabel(text); const properties: IStatusbarEntry = { backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index f9a808c423b..d8705fde875 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -46,8 +46,7 @@ import { IMarginData } from 'vs/editor/browser/controller/mouseTarget'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ISplice } from 'vs/base/common/sequence'; import { createStyleSheet } from 'vs/base/browser/dom'; -import { ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; -import { EncodingMode } from 'vs/workbench/common/editor'; +import { EncodingMode, ITextFileEditorModel, IResolvedTextFileEditorModel, ITextFileService, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { gotoNextLocation, gotoPreviousLocation } from 'vs/platform/theme/common/iconRegistry'; import { Codicon } from 'vs/base/common/codicons'; diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index cfb84f1fecb..4099394a207 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -25,8 +25,9 @@ import { SearchEditorModel } from 'vs/workbench/contrib/searchEditor/browser/sea import { defaultSearchConfig, extractSearchQueryFromModel, parseSavedSearchEditor, serializeSearchConfiguration } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ISearchConfigurationProperties } from 'vs/workbench/services/search/common/search'; -import { ITextFileSaveOptions, ITextFileService, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; -import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { ITextFileSaveOptions, ITextFileService, toBufferOrReadable } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, NO_TYPE_ID, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { CancellationToken } from 'vs/base/common/cancellation'; export type SearchConfiguration = { @@ -111,6 +112,17 @@ export class SearchEditorInput extends EditorInput { const input = this; const workingCopyAdapter = new class implements IWorkingCopy { + // TODO@JacksonKearl consider to enable a `typeId` that is specific for custom + // editors. Using a distinct `typeId` allows the working copy to have + // any resource (including file based resources) even if other working + // copies exist with the same resource. + // + // IMPORTANT: changing the `typeId` has an impact on backups for this + // working copy. Any value that is not the empty string will be used + // as seed to the backup. Only change the `typeId` if you have implemented + // a fallback solution to resolve any existing backups that do not have + // this seed. + readonly typeId = NO_TYPE_ID; readonly resource = input.modelUri; get name() { return input.getName(); } readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; @@ -255,8 +267,12 @@ export class SearchEditorInput extends EditorInput { } private async backup(token: CancellationToken): Promise { - const content = stringToSnapshot((await this.model).getValue()); - return { content }; + const model = await this.model; + if (token.isCancellationRequested) { + return {}; + } + + return { content: toBufferOrReadable(model.createSnapshot(true /* preserve BOM */)) }; } private async suggestFileName(): Promise { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts index fd868994e08..be30a47e0a0 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorModel.ts @@ -9,9 +9,11 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { parseSavedSearchEditor } from 'vs/workbench/contrib/searchEditor/browser/searchEditorSerialization'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { SearchConfiguration } from './searchEditorInput'; import { assertIsDefined } from 'vs/base/common/types'; +import { NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; export class SearchEditorModel { @@ -29,13 +31,24 @@ export class SearchEditorModel { { text: string; modelUri?: never; } | { backingUri: URI; text?: never; modelUri?: never; })), @IInstantiationService private readonly instantiationService: IInstantiationService, - @IBackupFileService readonly backupService: IBackupFileService, + @IWorkingCopyBackupService readonly workingCopyBackupService: IWorkingCopyBackupService, @IModelService private readonly modelService: IModelService, @IModeService private readonly modeService: IModeService) { this.onModelResolved = new Promise(resolve => this.resolveContents = resolve); this.onModelResolved.then(model => this.cachedContentsModel = model); - this.ongoingResolve = backupService.resolve(modelUri) - .then(backup => modelService.getModel(modelUri) ?? (backup ? modelService.createModel(backup.value, modeService.create('search-result'), modelUri) : undefined)) + this.ongoingResolve = workingCopyBackupService.resolve({ resource: modelUri, typeId: NO_TYPE_ID }) + .then(backup => { + const model = modelService.getModel(modelUri); + if (model) { + return model; + } + + if (backup) { + return createTextBufferFactoryFromStream(backup.value).then(factory => modelService.createModel(factory, modeService.create('search-result'), modelUri)); + } + + return undefined; + }) .then(model => { if (model) { this.resolveContents(model); } }); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 22a84f1c689..cc58de36a24 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -45,7 +45,8 @@ import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder, IWorkspace, import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IOutputService, IOutputChannel } from 'vs/workbench/contrib/output/common/output'; -import { ITerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { ITaskSystem, ITaskResolver, ITaskSummary, TaskExecuteKind, TaskError, TaskErrors, TaskTerminateResponse, TaskSystemInfo, ITaskExecuteResult } from 'vs/workbench/contrib/tasks/common/taskSystem'; import { @@ -252,7 +253,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @INotificationService private readonly notificationService: INotificationService, @IContextKeyService protected readonly contextKeyService: IContextKeyService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @ITerminalInstanceService private readonly terminalInstanceService: ITerminalInstanceService, + @ITerminalProfileResolverService private readonly terminalProfileResolverService: ITerminalProfileResolverService, @IPathService private readonly pathService: IPathService, @ITextModelService private readonly textModelResolverService: ITextModelService, @IPreferencesService private readonly preferencesService: IPreferencesService, @@ -1639,7 +1640,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.terminalService, this.outputService, this.panelService, this.viewsService, this.markerService, this.modelService, this.configurationResolverService, this.telemetryService, this.contextService, this.environmentService, - AbstractTaskService.OutputChannelId, this.fileService, this.terminalInstanceService, + AbstractTaskService.OutputChannelId, this.fileService, this.terminalProfileResolverService, this.pathService, this.viewDescriptorService, this.logService, this.configurationService, (workspaceFolder: IWorkspaceFolder | undefined) => { if (workspaceFolder) { diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 7699679e10f..2cf87022efe 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -27,8 +27,8 @@ import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ITerminalService, ITerminalInstanceService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalProfileResolverService, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalService, ITerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; import { StartStopProblemCollector, WatchingProblemCollector, ProblemCollectorEventKind, ProblemHandlingStrategy } from 'vs/workbench/contrib/tasks/common/problemCollectors'; import { @@ -211,7 +211,7 @@ export class TerminalTaskSystem implements ITaskSystem { private environmentService: IWorkbenchEnvironmentService, private outputChannelId: string, private fileService: IFileService, - private terminalInstanceService: ITerminalInstanceService, + private terminalProfileResolverService: ITerminalProfileResolverService, private pathService: IPathService, private viewDescriptorService: IViewDescriptorService, private logService: ILogService, @@ -1010,7 +1010,20 @@ export class TerminalTaskSystem implements ITaskSystem { let terminalName = this.createTerminalName(task); let originalCommand = task.command.name; if (isShellCommand) { - const defaultConfig = variableResolver.taskSystemInfo ? await variableResolver.taskSystemInfo.getDefaultShellAndArgs() : await this.terminalInstanceService.getDefaultShellAndArgs(true, platform); + let defaultConfig: { shell: string, args: string[] | string | undefined }; + if (variableResolver.taskSystemInfo) { + defaultConfig = await variableResolver.taskSystemInfo.getDefaultShellAndArgs(); + } else { + const defaultProfile = await this.terminalProfileResolverService.getDefaultProfile({ + allowAutomationShell: true, + os: Platform.OS, + remoteAuthority: this.environmentService.remoteAuthority + }); + defaultConfig = { + shell: defaultProfile.path, + args: defaultProfile.args + }; + } shellLaunchConfig = { name: terminalName, executable: defaultConfig.shell, args: defaultConfig.args, waitOnExit }; let shellSpecified: boolean = false; let shellOptions: ShellConfiguration | undefined = task.command.options && task.command.options.shell; diff --git a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts index 6ae6dc5307e..4760211ffbe 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/taskService.ts @@ -35,7 +35,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IViewsService, IViewDescriptorService } from 'vs/workbench/common/views'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import { ITerminalService, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -47,6 +47,7 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; interface WorkspaceFolderConfigurationResult { workspaceFolder: IWorkspaceFolder; @@ -81,7 +82,7 @@ export class TaskService extends AbstractTaskService { @INotificationService notificationService: INotificationService, @IContextKeyService contextKeyService: IContextKeyService, @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, - @ITerminalInstanceService terminalInstanceService: ITerminalInstanceService, + @ITerminalProfileResolverService terminalProfileResolverService: ITerminalProfileResolverService, @IPathService pathService: IPathService, @ITextModelService textModelResolverService: ITextModelService, @IPreferencesService preferencesService: IPreferencesService, @@ -112,7 +113,7 @@ export class TaskService extends AbstractTaskService { notificationService, contextKeyService, environmentService, - terminalInstanceService, + terminalProfileResolverService, pathService, textModelResolverService, preferencesService, diff --git a/src/vs/workbench/contrib/terminal/browser/media/terminal.css b/src/vs/workbench/contrib/terminal/browser/media/terminal.css index 7a7cf41a3cc..65b8792e372 100644 --- a/src/vs/workbench/contrib/terminal/browser/media/terminal.css +++ b/src/vs/workbench/contrib/terminal/browser/media/terminal.css @@ -226,9 +226,14 @@ .monaco-workbench .pane-body.integrated-terminal .tabs-container.has-text .tabs-widget .terminal-tabs-entry { padding-left: 10px; + padding-right: 10px; text-align: left; } +.monaco-workbench .pane-body.integrated-terminal .tabs-container.has-text .tabs-widget .terminal-tabs-entry .monaco-icon-label::after { + padding-right: 0 +} + .monaco-workbench .pane-body.integrated-terminal .tabs-widget .codicon { vertical-align: text-bottom; } diff --git a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts index 5edcc9dae1f..a5ffba224e5 100644 --- a/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/remoteTerminalService.ts @@ -6,6 +6,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { revive } from 'vs/base/common/marshalling'; +import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; @@ -145,14 +146,12 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal cwd: shellLaunchConfig.cwd, env: shellLaunchConfig.env }; - const isWorkspaceShellAllowed = configHelper.checkIsProcessLaunchSafe(remoteEnv.os); const result = await this._remoteTerminalChannel.createProcess( shellLaunchConfigDto, activeWorkspaceRootUri, shouldPersist, cols, rows, - isWorkspaceShellAllowed, ); const pty = new RemotePty(result.persistentTerminalId, shouldPersist, this._remoteTerminalChannel, this._remoteAgentService, this._logService); this._ptys.set(result.persistentTerminalId, pty); @@ -190,6 +189,14 @@ export class RemoteTerminalService extends Disposable implements IRemoteTerminal }); } + public async getDefaultSystemShell(osOverride?: OperatingSystem): Promise { + return this._remoteTerminalChannel?.getDefaultSystemShell(osOverride) || ''; + } + + public async getShellEnvironment(): Promise { + return this._remoteTerminalChannel?.getShellEnvironment() || {}; + } + public setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise { if (!this._remoteTerminalChannel) { throw new Error(`Cannot call setActiveInstanceId when there is no remote`); diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts index d7f48b35904..01d2e84825f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.contribution.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import * as platform from 'vs/base/common/platform'; import 'vs/css!./media/scrollbar'; import 'vs/css!./media/terminal'; import 'vs/css!./media/widgets'; @@ -37,6 +36,7 @@ import { terminalViewIcon } from 'vs/workbench/contrib/terminal/browser/terminal import { RemoteTerminalService } from 'vs/workbench/contrib/terminal/browser/remoteTerminalService'; import { isIPad } from 'vs/base/browser/browser'; import { WindowsShellType } from 'vs/platform/terminal/common/terminal'; +import { isWindows } from 'vs/base/common/platform'; // Register services registerSingleton(ITerminalService, TerminalService, true); @@ -114,7 +114,7 @@ const CTRL_LETTER_OFFSET = 64; // shell, this gets handled by PSReadLine which properly handles multi-line pastes. This is // disabled in accessibility mode as PowerShell does not run PSReadLine when it detects a screen // reader. This works even when clipboard.readText is not supported. -if (platform.isWindows) { +if (isWindows) { registerSendSequenceKeybinding(String.fromCharCode('V'.charCodeAt(0) - CTRL_LETTER_OFFSET), { // ctrl+v when: ContextKeyExpr.and(KEYBINDING_CONTEXT_TERMINAL_FOCUS, ContextKeyExpr.equals(KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE_KEY, WindowsShellType.PowerShell), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), primary: KeyMod.CtrlCmd | KeyCode.KEY_V @@ -134,7 +134,7 @@ registerSendSequenceKeybinding(String.fromCharCode('W'.charCodeAt(0) - CTRL_LETT primary: KeyMod.CtrlCmd | KeyCode.Backspace, mac: { primary: KeyMod.Alt | KeyCode.Backspace } }); -if (platform.isWindows) { +if (isWindows) { // Delete word left: ctrl+h // Windows cmd.exe requires ^H to delete full word left registerSendSequenceKeybinding(String.fromCharCode('H'.charCodeAt(0) - CTRL_LETTER_OFFSET), { diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.ts b/src/vs/workbench/contrib/terminal/browser/terminal.ts index 35fdd258bf8..3806f25f5dd 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.ts @@ -6,17 +6,17 @@ import { Codicon } from 'vs/base/common/codicons'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IProcessEnvironment, Platform } from 'vs/base/common/platform'; +import { IProcessEnvironment } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import { FindReplaceState } from 'vs/editor/contrib/find/findState'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IOffProcessTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerminalDimensions, ITerminalLaunchError, ITerminalTabLayoutInfoById, TerminalShellType } from 'vs/platform/terminal/common/terminal'; -import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; import { IAvailableProfilesRequest, ICommandTracker, IDefaultShellAndArgsRequest, INavigationMode, IRemoteTerminalAttachTarget, ITerminalProfile, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, LinuxDistro, TitleEventSource } from 'vs/workbench/contrib/terminal/common/terminal'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; +import { ITerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; export const ITerminalService = createDecorator('terminalService'); export const ITerminalInstanceService = createDecorator('terminalInstanceService'); @@ -37,7 +37,6 @@ export interface ITerminalInstanceService { getXtermSearchConstructor(): Promise; getXtermUnicode11Constructor(): Promise; getXtermWebglConstructor(): Promise; - getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride?: Platform): Promise<{ shell: string, args: string[] | string | undefined }>; getMainProcessParentEnv(): Promise; } @@ -97,6 +96,7 @@ export interface ITerminalService { onInstanceRequestStartExtensionTerminal: Event; onInstancesChanged: Event; onInstanceTitleChanged: Event; + onInstancePrimaryStatusChanged: Event; onActiveInstanceChanged: Event; onRequestAvailableProfiles: Event; onDidRegisterProcessSupport: Event; @@ -167,14 +167,19 @@ export interface ITerminalService { showProfileQuickPick(type: 'setDefault' | 'createInstance'): Promise; /** - * Gets the detected terminal profiles for the platform + * Gets the detected terminal profiles for the platform, this will queue an update of the + * available profiles but will not wait for it to complete. */ getAvailableProfiles(): ITerminalProfile[]; + /** + * Gets the detected terminal profiles for the platform. + */ + getAvailableProfilesAsync(): Promise; + getTabForInstance(instance: ITerminalInstance): ITerminalTab | undefined; setContainers(panelContainer: HTMLElement, terminalContainer: HTMLElement): void; - manageWorkspaceShellPermissions(): void; /** * Injects native Windows functionality into the service. @@ -571,4 +576,14 @@ export interface ITerminalInstance { * Triggers a quick pick to rename this terminal. */ rename(): Promise; + + /** + * Triggers a quick pick to rename this terminal. + */ + changeIcon(): Promise; + + /** + * Allows the user to configure this terminal. + */ + configure(): Promise; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts index 6773ae9e4aa..2eb6ec2de2b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminal.web.contribution.ts @@ -6,9 +6,13 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProfileResolverService, TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; import { IConfigurationRegistry, Extensions } from 'vs/platform/configuration/common/configurationRegistry'; import { getNoDefaultTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/common/terminalConfiguration'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { BrowserTerminalProfileResolverService } from 'vs/workbench/contrib/terminal/browser/terminalProfileResolverService'; + +registerSingleton(ITerminalProfileResolverService, BrowserTerminalProfileResolverService, true); // Desktop shell configuration are registered in electron-browser as their default values rely // on process.env diff --git a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts index c58a308e72f..2c097abb47b 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalActions.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalActions.ts @@ -17,7 +17,7 @@ import { localize } from 'vs/nls'; import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from 'vs/platform/accessibility/common/accessibility'; import { Action2, ICommandActionTitle, ILocalizedString, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyAndExpr, ContextKeyEqualsExpr, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -619,15 +619,29 @@ export function registerTerminalActions() { registerAction2(class extends Action2 { constructor() { super({ - id: TERMINAL_COMMAND_ID.MANAGE_WORKSPACE_SHELL_PERMISSIONS, - title: { value: localize('workbench.action.terminal.manageWorkspaceShellPermissions', "Manage Workspace Shell Permissions"), original: 'Manage Workspace Shell Permissions' }, + id: TERMINAL_COMMAND_ID.CONFIGURE_ACTIVE, + title: { value: localize('workbench.action.terminal.configureActive', "Configure Active Terminal"), original: 'Configure Active Terminal' }, f1: true, category, precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED }); } - run(accessor: ServicesAccessor) { - accessor.get(ITerminalService).manageWorkspaceShellPermissions(); + async run(accessor: ServicesAccessor) { + return accessor.get(ITerminalService).getActiveInstance()?.configure(); + } + }); + registerAction2(class extends Action2 { + constructor() { + super({ + id: TERMINAL_COMMAND_ID.CHANGE_ICON, + title: { value: localize('workbench.action.terminal.changeIcon', "Change Icon"), original: 'Change Icon' }, + f1: true, + category, + precondition: KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED + }); + } + async run(accessor: ServicesAccessor) { + return accessor.get(ITerminalService).getActiveInstance()?.changeIcon(); } }); registerAction2(class extends Action2 { @@ -1173,7 +1187,10 @@ export function registerTerminalActions() { id: MenuId.ViewTitle, group: 'navigation', order: 2, - when: ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + when: ContextKeyAndExpr.create([ + ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + ContextKeyExpr.not('config.terminal.integrated.showTabs') + ]), }] }); } @@ -1263,7 +1280,10 @@ export function registerTerminalActions() { id: MenuId.ViewTitle, group: 'navigation', order: 1, - when: ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID) + when: ContextKeyAndExpr.create([ + ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + ContextKeyExpr.not('config.terminal.integrated.showTabs') + ]), } }); } @@ -1330,7 +1350,10 @@ export function registerTerminalActions() { id: MenuId.ViewTitle, group: 'navigation', order: 3, - when: ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID) + when: ContextKeyAndExpr.create([ + ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + ContextKeyExpr.not('config.terminal.integrated.showTabs') + ]), } }); } @@ -1557,11 +1580,8 @@ export function registerTerminalActions() { if (quickSelectProfiles) { const profile = quickSelectProfiles.find(profile => profile.profileName === profileSelection); if (profile) { - const workspaceShellAllowed = terminalService.configHelper.checkIsProcessLaunchSafe(undefined, profile); - if (workspaceShellAllowed) { - const instance = terminalService.createTerminal(profile); - terminalService.setActiveInstance(instance); - } + const instance = terminalService.createTerminal(profile); + terminalService.setActiveInstance(instance); } else { console.warn(`No profile with name "${profileSelection}"`); } @@ -1578,7 +1598,10 @@ export function registerTerminalActions() { }, group: 'navigation', order: 0, - when: ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID) + when: ContextKeyAndExpr.create([ + ContextKeyEqualsExpr.create('view', TERMINAL_VIEW_ID), + ContextKeyExpr.not('config.terminal.integrated.showTabs') + ]), }); } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts index cbde1c98f18..fba304d7d48 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalConfigHelper.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import * as platform from 'vs/base/common/platform'; import { EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { ITerminalConfiguration, ITerminalFont, IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, TERMINAL_CONFIG_SECTION, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MINIMUM_LETTER_SPACING, LinuxDistro, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_FONT_WEIGHT, DEFAULT_BOLD_FONT_WEIGHT, FontWeight, ITerminalProfile } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalConfiguration, TERMINAL_CONFIG_SECTION, DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, MINIMUM_LETTER_SPACING, LinuxDistro, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_FONT_WEIGHT, DEFAULT_BOLD_FONT_WEIGHT, FontWeight, ITerminalFont } from 'vs/workbench/contrib/terminal/common/terminal'; import Severity from 'vs/base/common/severity'; import { INotificationService, NeverShowAgainScope } from 'vs/platform/notification/common/notification'; import { IBrowserTerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -21,6 +19,7 @@ import { InstallRecommendedExtensionAction } from 'vs/workbench/contrib/extensio import { IProductService } from 'vs/platform/product/common/productService'; import { XTermCore } from 'vs/workbench/contrib/terminal/browser/xterm-private'; import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; +import { isWindows } from 'vs/base/common/platform'; const MINIMUM_FONT_SIZE = 6; const MAXIMUM_FONT_SIZE = 25; @@ -37,9 +36,6 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { private _linuxDistro: LinuxDistro = LinuxDistro.Unknown; public config!: ITerminalConfiguration; - private readonly _onWorkspacePermissionsChanged = new Emitter(); - public get onWorkspacePermissionsChanged(): Event { return this._onWorkspacePermissionsChanged.event; } - private readonly _onConfigChanged = new Emitter(); public get onConfigChanged(): Event { return this._onConfigChanged.event; } @@ -47,7 +43,6 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { @IConfigurationService private readonly _configurationService: IConfigurationService, @IExtensionManagementService private readonly _extensionManagementService: IExtensionManagementService, @INotificationService private readonly _notificationService: INotificationService, - @IStorageService private readonly _storageService: IStorageService, @ITelemetryService private readonly telemetryService: ITelemetryService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IProductService private readonly productService: IProductService, @@ -205,86 +200,6 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { return this._measureFont(fontFamily, fontSize, letterSpacing, lineHeight); } - public setWorkspaceShellAllowed(isAllowed: boolean): void { - this._onWorkspacePermissionsChanged.fire(isAllowed); - this._storageService.store(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, isAllowed, StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - public isWorkspaceShellAllowed(defaultValue: boolean | undefined = undefined): boolean | undefined { - return this._storageService.getBoolean(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, StorageScope.WORKSPACE, defaultValue); - } - - public checkIsProcessLaunchSafe(osOverride: platform.OperatingSystem = platform.OS, profile?: ITerminalProfile): boolean { - // Check whether there is a workspace setting - const platformKey = osOverride === platform.OperatingSystem.Windows ? 'windows' : osOverride === platform.OperatingSystem.Macintosh ? 'osx' : 'linux'; - const shellConfigValue = this._configurationService.inspect(`terminal.integrated.shell.${platformKey}`); - const shellArgsConfigValue = this._configurationService.inspect(`terminal.integrated.shellArgs.${platformKey}`); - const envConfigValue = this._configurationService.inspect<{ [key: string]: string }>(`terminal.integrated.env.${platformKey}`); - - // Check if the workspace terminals are allowed: - // - undefined: Unknown - always ask - // - false: Disallowed - never ask - // - true: Allowed - never ask - let isTerminalLaunchSafe: boolean | undefined = false; - - // Check whether the shell is defined in workspace settings - const workspaceDefinedShell = profile?.isWorkspaceProfile ? profile.path : shellConfigValue.workspaceValue; - - // Check whether the shell args is defined in workspace settings - let workspaceDefinedShellArgs = profile?.isWorkspaceProfile ? profile.args : shellArgsConfigValue.workspaceValue; - if (typeof workspaceDefinedShellArgs === 'string') { - workspaceDefinedShellArgs = [workspaceDefinedShellArgs]; - } - - // Check whether the environment is defined in workspace settings - const workspaceDefinedEnv = envConfigValue.workspaceValue; - - // Return safe if no workspace settings are used - if ( - workspaceDefinedShell === undefined && - workspaceDefinedShellArgs === undefined && - workspaceDefinedEnv === undefined - ) { - return true; - } - - // Check whether the user has already been prompted about this workspace's permissions - isTerminalLaunchSafe = this.isWorkspaceShellAllowed(undefined); - - // If the user has already answered, return the response - if (isTerminalLaunchSafe !== undefined) { - return isTerminalLaunchSafe; - } - - // Format string message for workspace settings, note that this is not localized because - // they reference settings keys - const shellString = workspaceDefinedShell ? `shell: "${workspaceDefinedShell}"` : undefined; - const argsString = workspaceDefinedShellArgs ? `shellArgs: [${workspaceDefinedShellArgs.map(v => '"' + v + '"').join(', ')}]` : undefined; - const envString = workspaceDefinedEnv ? `env: {${Object.keys(workspaceDefinedEnv).map(k => `${k}:${workspaceDefinedEnv![k]}`).join(', ')}}` : undefined; - const workspaceConfigStrings: string[] = []; - if (shellString) { workspaceConfigStrings.push(shellString); } - if (argsString) { workspaceConfigStrings.push(argsString); } - if (envString) { workspaceConfigStrings.push(envString); } - const workspaceConfigString = workspaceConfigStrings.join(', '); - - // Ask the user's permissions whether to allow or disallow this workspace and remember their - // selection - this._notificationService.prompt(Severity.Info, nls.localize('terminal.integrated.allowWorkspaceShell', "Do you allow this workspace to modify your terminal shell? {0}", workspaceConfigString), - [{ - label: nls.localize('allow', "Allow"), - run: () => this.setWorkspaceShellAllowed(true) - }, - { - label: nls.localize('disallow', "Disallow"), - run: () => this.setWorkspaceShellAllowed(false) - }] - ); - - // TODO: We should await this instead when trusted workspaces modal is adopted - // Always return false when asking, since we don't await the notification - return false; - } - private _clampInt(source: any, minimum: number, maximum: number, fallback: T): number | T { let r = parseInt(source, 10); if (isNaN(r)) { @@ -307,7 +222,7 @@ export class TerminalConfigHelper implements IBrowserTerminalConfigHelper { } this.recommendationsShown = true; - if (platform.isWindows && shellLaunchConfig.executable && basename(shellLaunchConfig.executable).toLowerCase() === 'wsl.exe') { + if (isWindows && shellLaunchConfig.executable && basename(shellLaunchConfig.executable).toLowerCase() === 'wsl.exe') { const exeBasedExtensionTips = this.productService.exeBasedExtensionTips; if (!exeBasedExtensionTips || !exeBasedExtensionTips.wsl) { return; diff --git a/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts b/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts new file mode 100644 index 00000000000..b07fd9e915d --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalDecorationsProvider.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import Severity from 'vs/base/common/severity'; +import { URI } from 'vs/base/common/uri'; +import { localize } from 'vs/nls'; +import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Codicon } from 'vs/base/common/codicons'; +import { listErrorForeground, listWarningForeground } from 'vs/platform/theme/common/colorRegistry'; +import { TERMINAL_DECORATIONS_SCHEME } from 'vs/workbench/contrib/terminal/common/terminal'; + +export interface ITerminalDecorationData { + tooltip: string, + statusIcon: string, + color: string +} + +export class TerminalDecorationsProvider implements IDecorationsProvider { + readonly label: string = localize('label', "Terminal"); + private readonly _onDidChange = new Emitter(); + + constructor( + @ITerminalService private readonly _terminalService: ITerminalService + ) { + } + + get onDidChange(): Event { + return this._onDidChange.event; + } + + provideDecorations(resource: URI): IDecorationData | undefined { + if (resource.scheme !== TERMINAL_DECORATIONS_SCHEME || !parseInt(resource.path)) { + return; + } + + const instance = this._terminalService.getInstanceFromId(parseInt(resource.path)); + if (!instance?.statusList?.primary?.icon) { + return; + } + + return { + color: this.getColorForSeverity(instance.statusList.primary.severity), + letter: this.getStatusIcon(instance.statusList.primary.icon, instance.statusList.statuses.length), + // Commenting out this line to unblock build + // tooltip: localize(instance.statusList.primary.id, '{0}', instance.statusList.primary.id) + }; + } + + getColorForSeverity(severity: Severity): string { + switch (severity) { + case Severity.Error: + return listErrorForeground; + case Severity.Warning: + return listWarningForeground; + default: + return ''; + } + } + + getStatusIcon(icon: Codicon, statusCount: number): string { + let statusIcon; + switch (icon) { + case Codicon.warning: + statusIcon = '⚠'; + break; + case Codicon.bell: + statusIcon = 'B'; + break; + case Codicon.debugDisconnect: + statusIcon = 'D'; + break; + default: + statusIcon = ''; + break; + } + return statusCount > 1 ? `${statusCount}, ${statusIcon}` : statusIcon; + } + + dispose(): void { + this.dispose(); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index d56ac86aef5..bc803e721a1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -10,7 +10,6 @@ import { debounce } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle'; -import * as platform from 'vs/base/common/platform'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import * as nls from 'vs/nls'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -25,7 +24,7 @@ import { activeContrastBorder, scrollbarSliderActiveBackground, scrollbarSliderB import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { TerminalWidgetManager } from 'vs/workbench/contrib/terminal/browser/widgets/widgetManager'; -import { ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, SUGGESTED_RENDERER_TYPE } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ITerminalProcessManager, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, NEVER_MEASURE_RENDER_TIME_STORAGE_KEY, ProcessState, TERMINAL_VIEW_ID, KEYBINDING_CONTEXT_TERMINAL_A11Y_TREE_FOCUS, INavigationMode, TitleEventSource, DEFAULT_COMMANDS_TO_SKIP_SHELL, TERMINAL_CREATION_COMMANDS, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, SUGGESTED_RENDERER_TYPE, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { ansiColorIdentifiers, TERMINAL_BACKGROUND_COLOR, TERMINAL_CURSOR_BACKGROUND_COLOR, TERMINAL_CURSOR_FOREGROUND_COLOR, TERMINAL_FOREGROUND_COLOR, TERMINAL_SELECTION_BACKGROUND_COLOR } from 'vs/workbench/contrib/terminal/common/terminalColorRegistry'; import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalLinkManager } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager'; @@ -53,7 +52,9 @@ import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/t import { AutoOpenBarrier } from 'vs/base/common/async'; import { Codicon, iconRegistry } from 'vs/base/common/codicons'; import { ITerminalStatusList, TerminalStatus, TerminalStatusList } from 'vs/workbench/contrib/terminal/browser/terminalStatusList'; -import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; // How long in milliseconds should an average frame take to render for a notification to appear // which suggests the fallback DOM-based renderer @@ -210,6 +211,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { private _container: HTMLElement | undefined, private _shellLaunchConfig: IShellLaunchConfig, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, + @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IKeybindingService private readonly _keybindingService: IKeybindingService, @INotificationService private readonly _notificationService: INotificationService, @@ -224,7 +226,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, @IViewDescriptorService private readonly _viewDescriptorService: IViewDescriptorService, @IProductService private readonly _productService: IProductService, - @IQuickInputService private readonly _quickInputService: IQuickInputService + @IQuickInputService private readonly _quickInputService: IQuickInputService, + @IWorkbenchEnvironmentService workbenchEnvironmentService: IWorkbenchEnvironmentService ) { super(); @@ -247,6 +250,14 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._logService.trace(`terminalInstance#ctor (instanceId: ${this.instanceId})`, this._shellLaunchConfig); + // Resolve just the icon ahead of time so that it shows up immediately in the tabs. This is + // disabled in remote because this needs to be sync and the OS may differ on the remote + // which would result in the wrong profile being selected and the wrong icon being + // permanently attached to the terminal. + if (!this.shellLaunchConfig.executable && !workbenchEnvironmentService.remoteAuthority) { + this._terminalProfileResolverService.resolveIcon(this._shellLaunchConfig, OS); + } + this._initDimensions(); this._createProcessManager(); @@ -477,7 +488,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (this._shellLaunchConfig.initialText) { this._xterm.writeln(this._shellLaunchConfig.initialText); } - this._xterm.onBell(() => this.statusList.add({ id: TerminalStatus.Bell, severity: Severity.Warning }, 3000)); + this._xterm.onBell(() => this.statusList.add({ id: TerminalStatus.Bell, severity: Severity.Warning, icon: Codicon.bell }, 3000)); this._xterm.onLineFeed(() => this._onLineFeed()); this._xterm.onKey(e => this._onKey(e.key, e.domEvent)); this._xterm.onSelectionChange(async () => this._onSelectionChange()); @@ -494,7 +505,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Init winpty compat and link handler after process creation as they rely on the // underlying process OS this._processManager.onProcessReady(() => { - if (this._processManager.os === platform.OperatingSystem.Windows) { + if (this._processManager.os === OperatingSystem.Windows) { xterm.setOption('windowsMode', true); // Force line data to be sent when the cursor is moved, the main purpose for // this is because ConPTY will often not do a line feed but instead move the @@ -640,7 +651,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // Skip processing by xterm.js of keyboard events that match menu bar mnemonics - if (this._configHelper.config.allowMnemonics && !platform.isMacintosh && event.altKey) { + if (this._configHelper.config.allowMnemonics && !isMacintosh && event.altKey) { return false; } @@ -651,7 +662,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // Always have alt+F4 skip the terminal on Windows and allow it to be handled by the // system - if (platform.isWindows && event.altKey && event.key === 'F4' && !event.ctrlKey) { + if (isWindows && event.altKey && event.key === 'F4' && !event.ctrlKey) { return false; } @@ -1023,7 +1034,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this._processManager.onPtyDisconnect(() => { this._safeSetOption('disableStdin', true); - this.statusList.add({ id: TerminalStatus.Disconnected, severity: Severity.Error }); + this.statusList.add({ id: TerminalStatus.Disconnected, severity: Severity.Error, icon: Codicon.debugDisconnect }); this._onTitleChanged.fire(this); }); this._processManager.onPtyReconnect(() => { @@ -1552,7 +1563,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } switch (eventSource) { case TitleEventSource.Process: - if (platform.isWindows) { + if (isWindows) { // Remove the .exe extension title = path.basename(title); title = title.split('.exe')[0]; @@ -1652,10 +1663,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } // (Re-)create the widget - this.statusList.add({ id: TerminalStatus.RelaunchNeeded, severity: Severity.Error }); this._environmentInfo?.disposable.dispose(); const widget = this._instantiationService.createInstance(EnvironmentVariableInfoWidget, info); const disposable = this._widgetManager.attachWidget(widget); + if (info.requiresAction) { + this.statusList.add({ id: TerminalStatus.RelaunchNeeded, severity: Severity.Warning, icon: Codicon.warning }); + } if (disposable) { this._environmentInfo = { widget, disposable }; } @@ -1732,6 +1745,33 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { this.setTitle(name, TitleEventSource.Api); } } + + public async changeIcon() { + const items: IQuickPickItem[] = []; + for (const icon of iconRegistry.all) { + items.push({ label: `$(${icon.id})`, description: `${icon.id}` }); + } + const result = await this._quickInputService.pick(items, { + title: nls.localize('changeTerminalIcon', "Change Icon"), + matchOnDescription: true + }); + if (result) { + this.shellLaunchConfig.icon = result.description; + this._onTitleChanged.fire(this); + } + } + + public async configure(): Promise { + const changeIcon: IQuickPickItem = { label: nls.localize('changeIconTerminal', 'Change Icon') }; + const rename: IQuickPickItem = { label: nls.localize('renameTerminal', 'Rename') }; + const result = await this._quickInputService.pick([changeIcon, rename], { + title: nls.localize('configureTerminalTitle', "Configure Terminal") + }); + switch (result) { + case changeIcon: return this.changeIcon(); + case rename: return this.rename(); + } + } } registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts index 450bb3d64d5..64771ba0971 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstanceService.ts @@ -53,13 +53,6 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst return WebglAddon; } - public getDefaultShellAndArgs(useAutomationShell: boolean,): Promise<{ shell: string, args: string[] | string | undefined }> { - return new Promise(r => this._onRequestDefaultShellAndArgs.fire({ - useAutomationShell, - callback: (shell, args) => r({ shell, args }) - })); - } - public async getMainProcessParentEnv(): Promise { return {}; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts index a8d7a1754e7..463b4ca43b5 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProcessManager.ts @@ -3,10 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as platform from 'vs/base/common/platform'; import * as terminalEnvironment from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; -import { env as processEnv } from 'vs/base/common/process'; -import { ProcessState, ITerminalProcessManager, ITerminalConfigHelper, IBeforeProcessDataEvent } from 'vs/workbench/contrib/terminal/common/terminal'; +import { ProcessState, ITerminalProcessManager, ITerminalConfigHelper, IBeforeProcessDataEvent, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; import { ILogService } from 'vs/platform/log/common/log'; import { Emitter, Event } from 'vs/base/common/event'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; @@ -30,6 +28,7 @@ import { IProcessDataEvent, IShellLaunchConfig, ITerminalChildProcess, ITerminal import { TerminalRecorder } from 'vs/platform/terminal/common/terminalRecorder'; import { localize } from 'vs/nls'; import { formatMessageForTerminal } from 'vs/workbench/contrib/terminal/common/terminalStrings'; +import { IProcessEnvironment, isMacintosh, isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -57,7 +56,7 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce public ptyProcessReady: Promise; public shellProcessId: number | undefined; public remoteAuthority: string | undefined; - public os: platform.OperatingSystem | undefined; + public os: OperatingSystem | undefined; public userHome: string | undefined; public isDisconnected: boolean = false; public environmentVariableInfo: IEnvironmentVariableInfo | undefined; @@ -119,14 +118,15 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce @ILogService private readonly _logService: ILogService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, - @IConfigurationService private readonly _workspaceConfigurationService: IConfigurationService, - @IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService, + @IConfigurationService private readonly _configurationService: IConfigurationService, + @IWorkbenchEnvironmentService private readonly _workbenchEnvironmentService: IWorkbenchEnvironmentService, @IProductService private readonly _productService: IProductService, @ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService, @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IPathService private readonly _pathService: IPathService, @IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService, @IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService, + @ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService, @optional(ILocalTerminalService) localTerminalService: ILocalTerminalService, ) { super(); @@ -194,30 +194,26 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce this._processType = ProcessType.PsuedoTerminal; newProcess = shellLaunchConfig.customPtyImplementation(this._instanceId, cols, rows); } else { - const forceExtHostProcess = (this._configHelper.config as any).extHostProcess; if (shellLaunchConfig.cwd && typeof shellLaunchConfig.cwd === 'object') { this.remoteAuthority = getRemoteAuthority(shellLaunchConfig.cwd); } else { - this.remoteAuthority = this._environmentService.remoteAuthority; + this.remoteAuthority = this._workbenchEnvironmentService.remoteAuthority; } const hasRemoteAuthority = !!this.remoteAuthority; - let launchRemotely = hasRemoteAuthority || forceExtHostProcess; // resolvedUserHome is needed here as remote resolvers can launch local terminals before // they're connected to the remote. this.userHome = this._pathService.resolvedUserHome?.fsPath; - this.os = platform.OS; - if (launchRemotely) { + this.os = OS; + if (hasRemoteAuthority) { const userHomeUri = await this._pathService.userHome(); this.userHome = userHomeUri.path; - if (hasRemoteAuthority) { - this._remoteAgentService.getEnvironment().then(remoteEnv => { - if (remoteEnv) { - this.userHome = remoteEnv.userHome.path; - this.os = remoteEnv.os; - } - }); + const remoteEnv = await this._remoteAgentService.getEnvironment(); + if (!remoteEnv) { + throw new Error(`Failed to get remote environment for remote authority "${this.remoteAuthority}"`); } + this.userHome = remoteEnv.userHome.path; + this.os = remoteEnv.os; const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); @@ -234,6 +230,10 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce return undefined; } } else { + await this._terminalProfileResolverService.resolveShellLaunchConfig(shellLaunchConfig, { + remoteAuthority: this.remoteAuthority, + os: this.os + }); newProcess = await this._remoteTerminalService.createProcess(shellLaunchConfig, activeWorkspaceRootUri, cols, rows, shouldPersist, this._configHelper); } if (!this._isDisposed) { @@ -332,15 +332,16 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce } // Fetch any extension environment additions and apply them - private async _setupEnvVariableInfo(activeWorkspaceRootUri: URI | undefined, shellLaunchConfig: IShellLaunchConfig): Promise { - const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); + private async _setupEnvVariableInfo(activeWorkspaceRootUri: URI | undefined, shellLaunchConfig: IShellLaunchConfig): Promise { + const platformKey = isWindows ? 'windows' : (isMacintosh ? 'osx' : 'linux'); const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - const envFromConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.env.${platformKey}`); - const isWorkspaceShellAllowed = this._configHelper.checkIsProcessLaunchSafe(); + const envFromConfigValue = this._configurationService.getValue(`terminal.integrated.env.${platformKey}`); this._configHelper.showRecommendations(shellLaunchConfig); - const baseEnv = this._configHelper.config.inheritEnv ? processEnv : await this._terminalInstanceService.getMainProcessParentEnv(); + const baseEnv = await (this._configHelper.config.inheritEnv + ? this._terminalProfileResolverService.getShellEnvironment(this.remoteAuthority) + : this._terminalInstanceService.getMainProcessParentEnv()); const variableResolver = terminalEnvironment.createVariableResolver(lastActiveWorkspace, this._configurationResolverService); - const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, isWorkspaceShellAllowed, this._productService.version, this._configHelper.config.detectLocale, baseEnv); + const env = terminalEnvironment.createTerminalEnvironment(shellLaunchConfig, envFromConfigValue, variableResolver, this._productService.version, this._configHelper.config.detectLocale, baseEnv); if (!shellLaunchConfig.strictEnv && !shellLaunchConfig.hideFromUser) { this._extEnvironmentVariableCollection = this._environmentVariableService.mergedCollection; @@ -368,26 +369,13 @@ export class TerminalProcessManager extends Disposable implements ITerminalProce userHome: string | undefined, isScreenReaderModeEnabled: boolean ): Promise { + await this._terminalProfileResolverService.resolveShellLaunchConfig(shellLaunchConfig, { + remoteAuthority: undefined, + os: OS + }); + const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; - if (!shellLaunchConfig.executable) { - const defaultConfig = await this._terminalInstanceService.getDefaultShellAndArgs(false); - shellLaunchConfig.executable = defaultConfig.shell; - shellLaunchConfig.args = defaultConfig.args; - } else { - shellLaunchConfig.executable = await this._configurationResolverService.resolveAsync(lastActiveWorkspace, shellLaunchConfig.executable); - if (shellLaunchConfig.args) { - if (Array.isArray(shellLaunchConfig.args)) { - const resolvedArgs: string[] = []; - for (const arg of shellLaunchConfig.args) { - resolvedArgs.push(await this._configurationResolverService.resolveAsync(lastActiveWorkspace, arg)); - } - shellLaunchConfig.args = resolvedArgs; - } else { - shellLaunchConfig.args = await this._configurationResolverService.resolveAsync(lastActiveWorkspace, shellLaunchConfig.args); - } - } - } const initialCwd = terminalEnvironment.getCwd( shellLaunchConfig, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts new file mode 100644 index 00000000000..e652fbd3b67 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileResolverService.ts @@ -0,0 +1,287 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Schemas } from 'vs/base/common/network'; +import { env } from 'vs/base/common/process'; +import { withNullAsUndefined } from 'vs/base/common/types'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; +import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; +import { IShellLaunchConfig } from 'vs/platform/terminal/common/terminal'; +import { IShellLaunchConfigResolveOptions, ITerminalProfile, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; +import * as path from 'vs/base/common/path'; +import { Codicon } from 'vs/base/common/codicons'; + +export interface IProfileContextProvider { + getDefaultSystemShell: (remoteAuthority: string | undefined, os: OperatingSystem) => Promise; + getShellEnvironment: (remoteAuthority: string | undefined) => Promise; +} + +const generatedProfileName = 'Generated'; + +export abstract class BaseTerminalProfileResolverService implements ITerminalProfileResolverService { + declare _serviceBrand: undefined; + + constructor( + private readonly _context: IProfileContextProvider, + private readonly _configurationService: IConfigurationService, + private readonly _configurationResolverService: IConfigurationResolverService, + private readonly _historyService: IHistoryService, + private readonly _logService: ILogService, + private readonly _terminalService: ITerminalService, + private readonly _workspaceContextService: IWorkspaceContextService, + ) { + } + + resolveIcon(shellLaunchConfig: IShellLaunchConfig, os: OperatingSystem): void { + if (shellLaunchConfig.executable) { + return; + } + + const defaultProfile = this._getRealDefaultProfile(true, os); + if (defaultProfile) { + shellLaunchConfig.icon = defaultProfile.icon; + } + } + + async resolveShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig, options: IShellLaunchConfigResolveOptions): Promise { + // Resolve the shell and shell args + let resolvedProfile: ITerminalProfile; + if (shellLaunchConfig.executable) { + resolvedProfile = await this._resolveProfile({ + path: shellLaunchConfig.executable, + args: shellLaunchConfig.args, + profileName: generatedProfileName + }, options); + } else { + resolvedProfile = await this.getDefaultProfile(options); + } + shellLaunchConfig.executable = resolvedProfile.path; + shellLaunchConfig.args = resolvedProfile.args; + shellLaunchConfig.icon = shellLaunchConfig.icon || resolvedProfile.icon; + } + + async getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { + return (await this.getDefaultProfile(options)).path; + } + + async getDefaultShellArgs(options: IShellLaunchConfigResolveOptions): Promise { + return (await this.getDefaultProfile(options)).args || []; + } + + async getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { + return this._resolveProfile(await this._getUnresolvedDefaultProfile(options), options); + } + + getShellEnvironment(remoteAuthority: string | undefined): Promise { + return this._context.getShellEnvironment(remoteAuthority); + } + + private async _getUnresolvedDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { + // If automation shell is allowed, prefer that + if (options.allowAutomationShell) { + const automationShellProfile = this._getAutomationShellProfile(options); + if (automationShellProfile) { + return automationShellProfile; + } + } + + // Return the real default profile if it exists and is valid + const defaultProfile = await this._getRealDefaultProfile(false, options.os); + if (defaultProfile) { + return defaultProfile; + } + + // If there is no real default profile, create a fallback default profile based on the shell + // and shellArgs settings in addition to the current environment. + return this._getFallbackDefaultProfile(options); + } + + private _getRealDefaultProfile(sync: true, os: OperatingSystem): ITerminalProfile | undefined; + private _getRealDefaultProfile(sync: false, os: OperatingSystem): Promise; + private _getRealDefaultProfile(sync: boolean, os: OperatingSystem): ITerminalProfile | undefined | Promise { + const defaultProfileName = this._configurationService.getValue(`terminal.integrated.defaultProfile.${this._getOsKey(os)}`); + if (defaultProfileName && typeof defaultProfileName === 'string') { + if (sync) { + const profiles = this._terminalService.getAvailableProfiles(); + return profiles.find(e => e.profileName === defaultProfileName); + } else { + return this._terminalService.getAvailableProfilesAsync().then(profiles => { + return profiles.find(e => e.profileName === defaultProfileName); + }); + } + } + return undefined; + } + + private async _getFallbackDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { + let executable: string; + let args: string | string[] | undefined; + const shellSetting = this._configurationService.getValue(`terminal.integrated.shell.${this._getOsKey(options.os)}`); + if (this._isValidShell(shellSetting)) { + executable = shellSetting; + const shellArgsSetting = this._configurationService.getValue(`terminal.integrated.shellArgs.${this._getOsKey(options.os)}`); + if (this._isValidShellArgs(shellArgsSetting, options.os)) { + args = shellArgsSetting || []; + } + } else { + executable = await this._context.getDefaultSystemShell(options.remoteAuthority, options.os); + } + + const icon = this._guessProfileIcon(executable); + + return { + profileName: generatedProfileName, + path: executable, + args, + icon + }; + } + + private _getAutomationShellProfile(options: IShellLaunchConfigResolveOptions): ITerminalProfile | undefined { + const automationShell = this._configurationService.getValue(`terminal.integrated.automationShell.${this._getOsKey(options.os)}`); + if (!automationShell || typeof automationShell !== 'string') { + return undefined; + } + return { + path: automationShell, + profileName: generatedProfileName + }; + } + + private async _resolveProfile(profile: ITerminalProfile, options: IShellLaunchConfigResolveOptions): Promise { + if (options.os === OperatingSystem.Windows) { + // Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's + // safe to assume that this was used by accident as Sysnative does not + // exist and will break the terminal in non-WoW64 environments. + const env = await this._context.getShellEnvironment(options.remoteAuthority); + const isWoW64 = !!env.hasOwnProperty('PROCESSOR_ARCHITEW6432'); + const windir = env.windir; + if (!isWoW64 && windir) { + const sysnativePath = path.join(windir, 'Sysnative').replace(/\//g, '\\').toLowerCase(); + if (profile.path && profile.path.toLowerCase().indexOf(sysnativePath) === 0) { + profile.path = path.join(windir, 'System32', profile.path.substr(sysnativePath.length + 1)); + } + } + + // Convert / to \ on Windows for convenience + if (profile.path) { + profile.path = profile.path.replace(/\//g, '\\'); + } + } + + // Resolve path variables + const env = await this._context.getShellEnvironment(options.remoteAuthority); + const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(Schemas.file); + const lastActiveWorkspace = activeWorkspaceRootUri ? withNullAsUndefined(this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri)) : undefined; + profile.path = this._resolveVariables(profile.path, env, lastActiveWorkspace); + + // Resolve args variables + if (profile.args) { + if (typeof profile.args === 'string') { + profile.args = this._resolveVariables(profile.args, env, lastActiveWorkspace); + } else { + for (let i = 0; i < profile.args.length; i++) { + profile.args[i] = this._resolveVariables(profile.args[i], env, lastActiveWorkspace); + } + } + } + + return profile; + } + + private _resolveVariables(value: string, env: IProcessEnvironment, lastActiveWorkspace: IWorkspaceFolder | undefined) { + try { + value = this._configurationResolverService.resolveWithEnvironment(env, lastActiveWorkspace, value); + } catch (e) { + this._logService.error(`Could not resolve shell`, e); + } + return value; + } + + private _getOsKey(os: OperatingSystem): string { + switch (os) { + case OperatingSystem.Linux: return 'linux'; + case OperatingSystem.Macintosh: return 'osx'; + case OperatingSystem.Windows: return 'windows'; + } + } + + private _guessProfileIcon(shell: string): string | undefined { + const file = path.parse(shell).name; + switch (file) { + case 'pwsh': + case 'powershell': + return Codicon.terminalPowershell.id; + case 'tmux': + return Codicon.terminalTmux.id; + case 'cmd': + return Codicon.terminalCmd.id; + default: + return undefined; + } + } + + private _isValidShell(shell: unknown): shell is string { + if (!shell) { + return false; + } + return typeof shell === 'string'; + } + + private _isValidShellArgs(shellArgs: unknown, os: OperatingSystem): shellArgs is string | string[] | undefined { + if (shellArgs === undefined) { + return true; + } + if (os === OperatingSystem.Windows && typeof shellArgs === 'string') { + return true; + } + if (Array.isArray(shellArgs) && shellArgs.every(e => typeof e === 'string')) { + return true; + } + return false; + } +} + +export class BrowserTerminalProfileResolverService extends BaseTerminalProfileResolverService { + + constructor( + @IConfigurationResolverService configurationResolverService: IConfigurationResolverService, + @IConfigurationService configurationService: IConfigurationService, + @IHistoryService historyService: IHistoryService, + @ILogService logService: ILogService, + @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, + @ITerminalService terminalService: ITerminalService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, + ) { + super( + { + getDefaultSystemShell: async (remoteAuthority, os) => { + if (!remoteAuthority) { + // Just return basic values, this is only for serverless web and wouldn't be used + return os === OperatingSystem.Windows ? 'pwsh' : 'bash'; + } + return remoteTerminalService.getDefaultSystemShell(os); + }, + getShellEnvironment: async (remoteAuthority) => { + if (!remoteAuthority) { + return env; + } + return remoteTerminalService.getShellEnvironment(); + } + }, + configurationService, + configurationResolverService, + historyService, + logService, + terminalService, + workspaceContextService + ); + } +} diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index b73e7b8ebf9..e381469ee12 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -25,7 +25,7 @@ import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/term import { TerminalInstance } from 'vs/workbench/contrib/terminal/browser/terminalInstance'; import { TerminalTab } from 'vs/workbench/contrib/terminal/browser/terminalTab'; import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView'; -import { IAvailableProfilesRequest, IRemoteTerminalAttachTarget, ITerminalProfile, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID, ITerminalProfileObject, ITerminalExecutable, ITerminalProfileSource, ITerminalTypeContribution } from 'vs/workbench/contrib/terminal/common/terminal'; +import { IAvailableProfilesRequest, IRemoteTerminalAttachTarget, ITerminalProfile, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalNativeWindowsDelegate, ITerminalProcessExtHostProxy, KEYBINDING_CONTEXT_TERMINAL_ALT_BUFFER_ACTIVE, KEYBINDING_CONTEXT_TERMINAL_FOCUS, KEYBINDING_CONTEXT_TERMINAL_IS_OPEN, KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED, KEYBINDING_CONTEXT_TERMINAL_SHELL_TYPE, LinuxDistro, TERMINAL_VIEW_ID, ITerminalProfileObject, ITerminalTypeContribution } from 'vs/workbench/contrib/terminal/common/terminal'; import { escapeNonWindowsPath } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -100,6 +100,8 @@ export class TerminalService implements ITerminalService { public get onInstanceTitleChanged(): Event { return this._onInstanceTitleChanged.event; } private readonly _onActiveInstanceChanged = new Emitter(); public get onActiveInstanceChanged(): Event { return this._onActiveInstanceChanged.event; } + private readonly _onInstancePrimaryStatusChanged = new Emitter(); + public get onInstancePrimaryStatusChanged(): Event { return this._onInstancePrimaryStatusChanged.event; } private readonly _onTabDisposed = new Emitter(); public get onTabDisposed(): Event { return this._onTabDisposed.event; } private readonly _onRequestAvailableProfiles = new Emitter(); @@ -308,54 +310,14 @@ export class TerminalService implements ITerminalService { return this._availableProfiles || []; } - private async _getWorkspaceProfilePermissions(profile: ITerminalProfile): Promise { - const platformKey = await this._getPlatformKey(); - const profiles = this._configurationService.inspect<{ [key: string]: ITerminalProfileObject }>(`terminal.integrated.profiles.${platformKey}`); - if (!profiles || !profiles.workspaceValue || !profiles.defaultValue) { - return false; - } - const workspaceProfile = Object.entries(profiles.workspaceValue).find(p => p[0] === profile.profileName); - const defaultProfile = Object.entries(profiles.defaultValue).find(p => p[0] === profile.profileName); - if (workspaceProfile && defaultProfile && workspaceProfile[0] === defaultProfile[0]) { - let result = !this._terminalProfileObjectEqual(workspaceProfile[1], defaultProfile[1]); - return result; - } else if (!workspaceProfile && !defaultProfile) { - // user profile - return false; - } else { - // this key is missing from either default or the workspace config - return true; - } - } - - private _terminalProfileObjectEqual(one?: ITerminalProfileObject, two?: ITerminalProfileObject): boolean { - if (one === null && two === null) { - return true; - } else if ((one as ITerminalExecutable).path && (two as ITerminalExecutable).path) { - const oneExec = (one as ITerminalExecutable); - const twoExec = (two as ITerminalExecutable); - return ((Array.isArray(oneExec.path) && Array.isArray(twoExec.path) && oneExec.path.length === twoExec.path.length && oneExec.path.every((p, index) => p === twoExec.path[index])) || - (oneExec.path === twoExec.path) - ) && ((Array.isArray(oneExec.args) && Array.isArray(twoExec.args) && oneExec.args?.every((a, index) => a === twoExec.args?.[index])) || - (oneExec.args === twoExec.args) - ); - } else if ((one as ITerminalProfileSource).source && (two as ITerminalProfileSource).source) { - const oneSource = (one as ITerminalProfileSource); - const twoSource = (two as ITerminalProfileSource); - return oneSource.source === twoSource.source - && ((Array.isArray(oneSource.args) && Array.isArray(twoSource.args) && oneSource.args?.every((a, index) => a === twoSource.args?.[index])) || - (oneSource.args === twoSource.args) - ); - } - return false; + public async getAvailableProfilesAsync(): Promise { + await this._updateAvailableProfilesNow(); + return this._availableProfiles || []; } // when relevant config changes, update without debouncing private async _updateAvailableProfilesNow(): Promise { const result = await this._detectProfiles(true); - for (const p of result) { - p.isWorkspaceProfile = await this._getWorkspaceProfilePermissions(p); - } if (!equals(result, this._availableProfiles)) { this._availableProfiles = result; this._onProfilesConfigChanged.fire(); @@ -668,6 +630,7 @@ export class TerminalService implements ITerminalService { instance.addDisposable(instance.onDisposed(this._onInstanceDisposed.fire, this._onInstanceDisposed)); instance.addDisposable(instance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); instance.addDisposable(instance.onProcessIdReady(this._onInstanceProcessIdReady.fire, this._onInstanceProcessIdReady)); + instance.addDisposable(instance.statusList.onDidChangePrimaryStatus(() => this._onInstancePrimaryStatusChanged.fire(instance))); instance.addDisposable(instance.onLinksReady(this._onInstanceLinksReady.fire, this._onInstanceLinksReady)); instance.addDisposable(instance.onDimensionsChanged(() => { this._onInstanceDimensionsChanged.fire(instance); @@ -748,16 +711,6 @@ export class TerminalService implements ITerminalService { return terminalIndex; } - public async manageWorkspaceShellPermissions(): Promise { - const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.terminal.allowWorkspaceShell', "Allow Workspace Shell Configuration") }; - const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.terminal.disallowWorkspaceShell', "Disallow Workspace Shell Configuration") }; - const value = await this._quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); - if (!value) { - return; - } - this.configHelper.setWorkspaceShellAllowed(value === allowItem); - } - protected async _showTerminalCloseConfirmation(): Promise { let message: string; if (this.terminalInstances.length === 1) { @@ -860,8 +813,8 @@ export class TerminalService implements ITerminalService { return; } const configKey = `terminal.integrated.profiles.${platformKey}`; - const configProfiles = this._configurationService.inspect<{ [key: string]: ITerminalProfileObject }>(configKey); - const existingProfiles = configProfiles.userValue ? Object.keys(configProfiles.userValue) : []; + const configProfiles = this._configurationService.getValue<{ [key: string]: ITerminalProfileObject }>(configKey); + const existingProfiles = configProfiles ? Object.keys(configProfiles) : []; const name = await this._quickInputService.input({ prompt: nls.localize('enterTerminalProfileName', "Enter terminal profile name"), value: context.item.profile.profileName, @@ -875,7 +828,7 @@ export class TerminalService implements ITerminalService { if (!name) { return; } - const newConfigValue: { [key: string]: ITerminalProfileObject } = { ...configProfiles.userValue } ?? {}; + const newConfigValue: { [key: string]: ITerminalProfileObject } = { ...configProfiles } ?? {}; newConfigValue[name] = { path: context.item.profile.path, args: context.item.profile.args @@ -978,6 +931,7 @@ export class TerminalService implements ITerminalService { } private _convertProfileToShellLaunchConfig(shellLaunchConfigOrProfile?: IShellLaunchConfig | ITerminalProfile): IShellLaunchConfig { + // Profile was provided if (shellLaunchConfigOrProfile && 'profileName' in shellLaunchConfigOrProfile) { const profile = shellLaunchConfigOrProfile; return { @@ -988,7 +942,14 @@ export class TerminalService implements ITerminalService { name: profile.overrideName ? profile.profileName : undefined }; } - return shellLaunchConfigOrProfile || {}; + + // Shell launch config was provided + if (shellLaunchConfigOrProfile) { + return shellLaunchConfigOrProfile; + } + + // Return empty shell launch config + return {}; } public createTerminal(shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance; @@ -1014,11 +975,12 @@ export class TerminalService implements ITerminalService { terminalTab.addDisposable(terminalTab.onDisposed(this._onTabDisposed.fire, this._onTabDisposed)); terminalTab.addDisposable(terminalTab.onInstancesChanged(this._onInstancesChanged.fire, this._onInstancesChanged)); this._initInstanceListeners(instance); + this._onInstancesChanged.fire(); if (this.terminalInstances.length === 1) { - // It's the first instance so it should be made active automatically + // It's the first instance so it should be made active automatically, this must fire + // after onInstancesChanged so consumers can react to the instance being added first this.setActiveInstanceByIndex(0); } - this._onInstancesChanged.fire(); return instance; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts index 207abd140b3..ab674f48341 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalStatusList.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Codicon } from 'vs/base/common/codicons'; import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import Severity from 'vs/base/common/severity'; @@ -25,6 +26,11 @@ export interface ITerminalStatus { * the "primary status". */ severity: Severity; + /** + * An icon representing the status, if this is not specified it will not show up on the terminal + * tab and will use the generic `info` icon when hovering. + */ + icon?: Codicon; } export interface ITerminalStatusList { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts index ba5ccb9e2e2..77f8b25f229 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabbedView.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { LayoutPriority, Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal'; import { TerminalFindWidget } from 'vs/workbench/contrib/terminal/browser/terminalFindWidget'; -import { TerminalTabsWidget } from 'vs/workbench/contrib/terminal/browser/terminalTabsWidget'; +import { DEFAULT_TABS_WIDGET_WIDTH, MIDPOINT_WIDGET_WIDTH, MIN_TABS_WIDGET_WIDTH, TerminalTabsWidget } from 'vs/workbench/contrib/terminal/browser/terminalTabsWidget'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; import * as nls from 'vs/nls'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; @@ -28,14 +28,13 @@ import { KEYBINDING_CONTEXT_TERMINAL_FIND_VISIBLE, TERMINAL_COMMAND_ID } from 'v import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { Codicon } from 'vs/base/common/codicons'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; +import { ILogService } from 'vs/platform/log/common/log'; const $ = dom.$; const FIND_FOCUS_CLASS = 'find-focused'; const TABS_WIDGET_WIDTH_KEY = 'tabs-widget-width'; -const MIN_TABS_WIDGET_WIDTH = 46; -const DEFAULT_TABS_WIDGET_WIDTH = 124; -const MIDPOINT_WIDGET_WIDTH = (MIN_TABS_WIDGET_WIDTH + DEFAULT_TABS_WIDGET_WIDTH) / 2; +const MAX_TABS_WIDGET_WIDTH = 500; export class TerminalTabbedView extends Disposable { @@ -48,6 +47,7 @@ export class TerminalTabbedView extends Disposable { private _tabsWidget: TerminalTabsWidget; private _findWidget: TerminalFindWidget; + private _sashDisposables: IDisposable[] | undefined; private _plusButton: HTMLElement | undefined; @@ -74,7 +74,8 @@ export class TerminalTabbedView extends Disposable { @IConfigurationService configurationService: IConfigurationService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, - @IStorageService private readonly _storageService: IStorageService + @IStorageService private readonly _storageService: IStorageService, + @ILogService private readonly _logService: ILogService ) { super(); @@ -89,8 +90,8 @@ export class TerminalTabbedView extends Disposable { this._instanceMenu = this._register(menuService.createMenu(MenuId.TerminalContext, contextKeyService)); // this._dropdownMenu = this._register(menuService.createMenu(MenuId.TerminalTabsContext, contextKeyService)); - this._tabsWidget = this._instantiationService.createInstance(TerminalTabsWidget, this._terminalTabTree); - this._findWidget = this._instantiationService.createInstance(TerminalFindWidget, this._terminalService.getFindState()); + this._register(this._tabsWidget = this._instantiationService.createInstance(TerminalTabsWidget, this._terminalTabTree)); + this._register(this._findWidget = this._instantiationService.createInstance(TerminalFindWidget, this._terminalService.getFindState())); parentElement.appendChild(this._findWidget.getDomNode()); this._terminalContainer = document.createElement('div'); @@ -111,17 +112,20 @@ export class TerminalTabbedView extends Disposable { this._showTabs = this._terminalService.configHelper.config.showTabs; if (this._showTabs) { this._addTabTree(); + this._addSashListener(); } else { this._splitView.removeView(this._tabTreeIndex); if (this._plusButton) { this._tabTreeContainer.removeChild(this._plusButton); } + this._removeSashListener(); } } else if (e.affectsConfiguration('terminal.integrated.tabsLocation')) { this._tabTreeIndex = this._terminalService.configHelper.config.tabsLocation === 'left' ? 0 : 1; this._terminalContainerIndex = this._terminalService.configHelper.config.tabsLocation === 'left' ? 1 : 0; if (this._showTabs) { this._splitView.swapViews(0, 1); + this._splitView.resizeView(this._tabTreeIndex, DEFAULT_TABS_WIDGET_WIDTH); } } }); @@ -147,15 +151,13 @@ export class TerminalTabbedView extends Disposable { } private _handleOnDidSashChange(): void { - this._refreshHasTextClass(); let widgetWidth = this._splitView.getViewSize(this._tabTreeIndex); if (!this._width || widgetWidth <= 0) { return; } widgetWidth = this._updateWidgetWidth(widgetWidth); - for (const instance of this._terminalService.terminalInstances) { - this._tabsWidget.rerender(instance); - } + this._refreshHasTextClass(); + this._rerenderTabs(); this._storageService.store(TABS_WIDGET_WIDTH_KEY, widgetWidth, StorageScope.WORKSPACE, StorageTarget.USER); } @@ -163,7 +165,7 @@ export class TerminalTabbedView extends Disposable { if (width < MIDPOINT_WIDGET_WIDTH && width > MIN_TABS_WIDGET_WIDTH) { width = MIN_TABS_WIDGET_WIDTH; this._splitView.resizeView(this._tabTreeIndex, width); - } else if (width > MIDPOINT_WIDGET_WIDTH && width < DEFAULT_TABS_WIDGET_WIDTH) { + } else if (width >= MIDPOINT_WIDGET_WIDTH && width < DEFAULT_TABS_WIDGET_WIDTH) { width = DEFAULT_TABS_WIDGET_WIDTH; this._splitView.resizeView(this._tabTreeIndex, width); } @@ -185,6 +187,10 @@ export class TerminalTabbedView extends Disposable { onDidChange: () => Disposable.None, priority: LayoutPriority.High }, Sizing.Distribute, this._terminalContainerIndex); + + if (this._showTabs) { + this._addSashListener(); + } } private _addTabTree() { @@ -192,11 +198,46 @@ export class TerminalTabbedView extends Disposable { element: this._tabTreeContainer, layout: width => this._tabsWidget.layout(this._height ? this._height - 28 : 0, width), minimumSize: MIN_TABS_WIDGET_WIDTH, - maximumSize: Number.POSITIVE_INFINITY, + maximumSize: MAX_TABS_WIDGET_WIDTH, onDidChange: () => Disposable.None, priority: LayoutPriority.Low }, Sizing.Distribute, this._tabTreeIndex); this._createButton(); + this._refreshHasTextClass(); + this._rerenderTabs(); + } + + private _rerenderTabs() { + for (const instance of this._terminalService.terminalInstances) { + try { + this._tabsWidget.rerender(instance); + } catch (e) { + this._logService.warn('Exception when rerendering new tab widget', e); + } + } + } + + private _addSashListener() { + let interval: number; + this._sashDisposables = [ + this._splitView.sashes[0].onDidStart(e => { + interval = window.setInterval(() => { + this._refreshHasTextClass(); + this._rerenderTabs(); + }, 100); + }), + this._splitView.sashes[0].onDidEnd(e => { + window.clearInterval(interval); + interval = 0; + }) + ]; + } + + private _removeSashListener() { + if (this._sashDisposables) { + dispose(this._sashDisposables); + this._sashDisposables = undefined; + } } layout(width: number, height: number): void { @@ -210,7 +251,7 @@ export class TerminalTabbedView extends Disposable { } private _refreshHasTextClass() { - this._tabTreeContainer.classList.toggle('has-text', this._tabTreeContainer.clientWidth >= DEFAULT_TABS_WIDGET_WIDTH); + this._tabTreeContainer.classList.toggle('has-text', this._tabTreeContainer.clientWidth >= MIDPOINT_WIDGET_WIDTH); } private _createButton(): void { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts b/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts index 9ad174a8f77..58473ca7626 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalTabsWidget.ts @@ -15,19 +15,28 @@ import { ITerminalInstance, ITerminalService } from 'vs/workbench/contrib/termin import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { MenuItemAction } from 'vs/platform/actions/common/actions'; import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { TERMINAL_COMMAND_ID } from 'vs/workbench/contrib/terminal/common/terminal'; +import { TERMINAL_COMMAND_ID, TERMINAL_DECORATIONS_SCHEME } from 'vs/workbench/contrib/terminal/common/terminal'; import { Codicon } from 'vs/base/common/codicons'; import { Action } from 'vs/base/common/actions'; -import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { TerminalDecorationsProvider } from 'vs/workbench/contrib/terminal/browser/terminalDecorationsProvider'; +import { DEFAULT_LABELS_CONTAINER, IResourceLabel, IResourceLabelOptions, IResourceLabelProps, ResourceLabels } from 'vs/workbench/browser/labels'; +import { IDecorationsService } from 'vs/workbench/services/decorations/browser/decorations'; +import { IHoverService } from 'vs/workbench/services/hover/browser/hover'; +import { URI } from 'vs/base/common/uri'; +import Severity from 'vs/base/common/severity'; const $ = DOM.$; +export const MIN_TABS_WIDGET_WIDTH = 46; +export const DEFAULT_TABS_WIDGET_WIDTH = 80; +export const MIDPOINT_WIDGET_WIDTH = (MIN_TABS_WIDGET_WIDTH + DEFAULT_TABS_WIDGET_WIDTH) / 2; export class TerminalTabsWidget extends WorkbenchObjectTree { + private _decorationsProvider: TerminalDecorationsProvider | undefined; + constructor( container: HTMLElement, @IContextKeyService contextKeyService: IContextKeyService, @@ -37,14 +46,15 @@ export class TerminalTabsWidget extends WorkbenchObjectTree @IKeybindingService keybindingService: IKeybindingService, @IAccessibilityService accessibilityService: IAccessibilityService, @ITerminalService private readonly _terminalService: ITerminalService, - @IInstantiationService instantiationService: IInstantiationService + @IInstantiationService instantiationService: IInstantiationService, + @IDecorationsService _decorationsService: IDecorationsService ) { super('TerminalTabsTree', container, { getHeight: () => 22, getTemplateId: () => 'terminal.tabs' }, - [instantiationService.createInstance(TerminalTabsRenderer, container)], + [instantiationService.createInstance(TerminalTabsRenderer, container, instantiationService.createInstance(ResourceLabels, DEFAULT_LABELS_CONTAINER))], { horizontalScrolling: false, supportDynamicHeights: false, @@ -88,6 +98,11 @@ export class TerminalTabsWidget extends WorkbenchObjectTree await instance.focusWhenReady(); } }); + if (!this._decorationsProvider) { + this._decorationsProvider = instantiationService.createInstance(TerminalDecorationsProvider); + _decorationsService.registerDecorationsProvider(this._decorationsProvider); + } + this._terminalService.onInstancePrimaryStatusChanged(() => this._render()); } private _render(): void { @@ -106,6 +121,7 @@ class TerminalTabsRenderer implements ITreeRenderer this._hoverService.showHover(e) } }); + const actionsContainer = DOM.append(label.element, $('.actions')); const actionBar = new ActionBar(actionsContainer, { @@ -140,10 +157,12 @@ class TerminalTabsRenderer implements ITreeRenderer, index: number, template: ITerminalTabEntryTemplate): void { + let instance = node.element; const tab = this._terminalService.getTabForInstance(instance); @@ -176,10 +195,17 @@ class TerminalTabsRenderer implements ITreeRenderer= Severity.Warning) { + label = `${prefix}$(${primaryStatus.icon?.id || instance.icon.id})`; + } else { + label = `${prefix}$(${instance.icon.id})`; + } } else { this.fillActionBar(instance, template); - label = `${prefix}$(${instance.icon.id}) ${instance.title}`; + // Remove "Task - " from only tabs to give more horizontal space as it's obvious from + // the tab icon + label = `${prefix}$(${instance.icon.id}) ${instance.title.replace(/^Task - /, '')}`; } template.label.setLabel(label, undefined, { @@ -188,18 +214,24 @@ class TerminalTabsRenderer implements ITreeRenderer instance.rename()); + const configure = new Action(TERMINAL_COMMAND_ID.CONFIGURE_ACTIVE, localize('terminal.configure', "Configure"), ThemeIcon.asClassName(Codicon.pencil), true, () => instance.configure()); const split = new Action(TERMINAL_COMMAND_ID.SPLIT, localize('terminal.split', "Split"), ThemeIcon.asClassName(Codicon.splitHorizontal), true, async () => this._terminalService.splitInstance(instance)); const kill = new Action(TERMINAL_COMMAND_ID.KILL, localize('terminal.kill', "Kill"), ThemeIcon.asClassName(Codicon.trashcan), true, async () => instance.dispose(true)); // TODO: Cache these in a way that will use the correct instance template.actionBar.clear(); - template.actionBar.push(rename, { icon: true, label: false }); + template.actionBar.push(configure, { icon: true, label: false }); template.actionBar.push(split, { icon: true, label: false }); template.actionBar.push(kill, { icon: true, label: false }); } @@ -207,6 +239,6 @@ class TerminalTabsRenderer implements ITreeRenderer { - userValue: T | undefined; - value: T | undefined; - defaultValue: T | undefined; -} - export interface ICompleteTerminalConfiguration { - 'terminal.integrated.automationShell.windows': ISingleTerminalConfiguration; - 'terminal.integrated.automationShell.osx': ISingleTerminalConfiguration; - 'terminal.integrated.automationShell.linux': ISingleTerminalConfiguration; - 'terminal.integrated.shell.windows': ISingleTerminalConfiguration; - 'terminal.integrated.shell.osx': ISingleTerminalConfiguration; - 'terminal.integrated.shell.linux': ISingleTerminalConfiguration; - 'terminal.integrated.shellArgs.windows': ISingleTerminalConfiguration; - 'terminal.integrated.shellArgs.osx': ISingleTerminalConfiguration; - 'terminal.integrated.shellArgs.linux': ISingleTerminalConfiguration; - 'terminal.integrated.env.windows': ISingleTerminalConfiguration; - 'terminal.integrated.env.osx': ISingleTerminalConfiguration; - 'terminal.integrated.env.linux': ISingleTerminalConfiguration; + 'terminal.integrated.automationShell.windows': string; + 'terminal.integrated.automationShell.osx': string; + 'terminal.integrated.automationShell.linux': string; + 'terminal.integrated.shell.windows': string; + 'terminal.integrated.shell.osx': string; + 'terminal.integrated.shell.linux': string; + 'terminal.integrated.shellArgs.windows': string | string[]; + 'terminal.integrated.shellArgs.osx': string | string[]; + 'terminal.integrated.shellArgs.linux': string | string[]; + 'terminal.integrated.env.windows': ITerminalEnvironment; + 'terminal.integrated.env.osx': ITerminalEnvironment; + 'terminal.integrated.env.linux': ITerminalEnvironment; 'terminal.integrated.inheritEnv': boolean; 'terminal.integrated.cwd': string; 'terminal.integrated.detectLocale': 'auto' | 'off' | 'on'; @@ -69,7 +64,6 @@ export interface ICreateTerminalProcessArguments { shouldPersistTerminal: boolean; cols: number; rows: number; - isWorkspaceShellAllowed: boolean; resolverEnv: { [key: string]: string | null; } | undefined } @@ -139,37 +133,28 @@ export class RemoteTerminalChannelClient { @ILabelService private readonly _labelService: ILabelService, ) { } - private _readSingleTerminalConfiguration(key: string): ISingleTerminalConfiguration { - const result = this._configurationService.inspect(key); - return { - userValue: result.userValue, - value: result.value, - defaultValue: result.defaultValue, - }; - } - restartPtyHost(): Promise { return this._channel.call('$restartPtyHost', []); } - public async createProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise { + public async createProcess(shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: URI | undefined, shouldPersistTerminal: boolean, cols: number, rows: number): Promise { // Be sure to first wait for the remote configuration await this._configurationService.whenRemoteConfigurationLoaded(); const terminalConfig = this._configurationService.getValue(TERMINAL_CONFIG_SECTION); const configuration: ICompleteTerminalConfiguration = { - 'terminal.integrated.automationShell.windows': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.windows'), - 'terminal.integrated.automationShell.osx': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.osx'), - 'terminal.integrated.automationShell.linux': this._readSingleTerminalConfiguration('terminal.integrated.automationShell.linux'), - 'terminal.integrated.shell.windows': this._readSingleTerminalConfiguration('terminal.integrated.shell.windows'), - 'terminal.integrated.shell.osx': this._readSingleTerminalConfiguration('terminal.integrated.shell.osx'), - 'terminal.integrated.shell.linux': this._readSingleTerminalConfiguration('terminal.integrated.shell.linux'), - 'terminal.integrated.shellArgs.windows': this._readSingleTerminalConfiguration('terminal.integrated.shellArgs.windows'), - 'terminal.integrated.shellArgs.osx': this._readSingleTerminalConfiguration('terminal.integrated.shellArgs.osx'), - 'terminal.integrated.shellArgs.linux': this._readSingleTerminalConfiguration('terminal.integrated.shellArgs.linux'), - 'terminal.integrated.env.windows': this._readSingleTerminalConfiguration('terminal.integrated.env.windows'), - 'terminal.integrated.env.osx': this._readSingleTerminalConfiguration('terminal.integrated.env.osx'), - 'terminal.integrated.env.linux': this._readSingleTerminalConfiguration('terminal.integrated.env.linux'), + 'terminal.integrated.automationShell.windows': this._configurationService.getValue('terminal.integrated.automationShell.windows'), + 'terminal.integrated.automationShell.osx': this._configurationService.getValue('terminal.integrated.automationShell.osx'), + 'terminal.integrated.automationShell.linux': this._configurationService.getValue('terminal.integrated.automationShell.linux'), + 'terminal.integrated.shell.windows': this._configurationService.getValue('terminal.integrated.shell.windows'), + 'terminal.integrated.shell.osx': this._configurationService.getValue('terminal.integrated.shell.osx'), + 'terminal.integrated.shell.linux': this._configurationService.getValue('terminal.integrated.shell.linux'), + 'terminal.integrated.shellArgs.windows': this._configurationService.getValue('terminal.integrated.shellArgs.windows'), + 'terminal.integrated.shellArgs.osx': this._configurationService.getValue('terminal.integrated.shellArgs.osx'), + 'terminal.integrated.shellArgs.linux': this._configurationService.getValue('terminal.integrated.shellArgs.linux'), + 'terminal.integrated.env.windows': this._configurationService.getValue('terminal.integrated.env.windows'), + 'terminal.integrated.env.osx': this._configurationService.getValue('terminal.integrated.env.osx'), + 'terminal.integrated.env.linux': this._configurationService.getValue('terminal.integrated.env.linux'), 'terminal.integrated.inheritEnv': terminalConfig.inheritEnv, 'terminal.integrated.cwd': terminalConfig.cwd, 'terminal.integrated.detectLocale': terminalConfig.detectLocale @@ -226,7 +211,6 @@ export class RemoteTerminalChannelClient { shouldPersistTerminal, cols, rows, - isWorkspaceShellAllowed, resolverEnv }; return await this._channel.call('$createProcess', args); @@ -269,7 +253,15 @@ export class RemoteTerminalChannelClient { return this._channel.call('$orphanQuestionReply', [id]); } public sendCommandResult(reqId: number, isError: boolean, payload: any): Promise { - return this._channel.call('$sendCommandResult', [reqId, isError, payload]); + return this._channel.call('$sendCommandResult', [reqId, isError, payload]); + } + + public getDefaultSystemShell(osOverride?: OperatingSystem): Promise { + return this._channel.call('$getDefaultSystemShell', [osOverride]); + } + + public getShellEnvironment(): Promise { + return this._channel.call('$getShellEnvironment'); } public setTerminalLayoutInfo(layout: ITerminalsLayoutInfoById): Promise { diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index d6f66e64da7..0cad4ab6435 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -7,10 +7,11 @@ import * as nls from 'vs/nls'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { OperatingSystem } from 'vs/base/common/platform'; +import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { IExtensionPointDescriptor } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { IProcessDataEvent, IShellLaunchConfig, ITerminalDimensions, ITerminalDimensionsOverride, ITerminalEnvironment, ITerminalLaunchError, TerminalShellType } from 'vs/platform/terminal/common/terminal'; import { IEnvironmentVariableInfo } from 'vs/workbench/contrib/terminal/common/environmentVariable'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const TERMINAL_VIEW_ID = 'terminal'; @@ -50,7 +51,6 @@ export const KEYBINDING_CONTEXT_TERMINAL_FIND_INPUT_NOT_FOCUSED = KEYBINDING_CON export const KEYBINDING_CONTEXT_TERMINAL_PROCESS_SUPPORTED = new RawContextKey('terminalProcessSupported', false, nls.localize('terminalProcessSupportedContextKey', "Whether terminal processes can be launched")); -export const IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY = 'terminal.integrated.isWorkspaceShellAllowed'; export const NEVER_MEASURE_RENDER_TIME_STORAGE_KEY = 'terminal.integrated.neverMeasureRenderTime'; export const TERMINAL_CREATION_COMMANDS = ['workbench.action.terminal.toggleTerminal', 'workbench.action.terminal.new', 'workbench.action.togglePanel', 'workbench.action.terminal.focus']; @@ -77,6 +77,28 @@ export const DEFAULT_FONT_WEIGHT = 'normal'; export const DEFAULT_BOLD_FONT_WEIGHT = 'bold'; export const SUGGESTIONS_FONT_WEIGHT = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']; +export const ITerminalProfileResolverService = createDecorator('terminalProfileResolverService'); +export interface ITerminalProfileResolverService { + readonly _serviceBrand: undefined; + /** + * Resolves the icon of a shell launch config if this will use the default profile + */ + resolveIcon(shellLaunchConfig: IShellLaunchConfig, os: OperatingSystem): void; + resolveShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig, options: IShellLaunchConfigResolveOptions): Promise; + getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise; + getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise; + getDefaultShellArgs(options: IShellLaunchConfigResolveOptions): Promise; + getShellEnvironment(remoteAuthority: string | undefined): Promise; +} + +export interface IShellLaunchConfigResolveOptions { + remoteAuthority: string | undefined; + os: OperatingSystem; + allowAutomationShell?: boolean; +} + +export const TERMINAL_DECORATIONS_SCHEME = 'vscode-terminal'; + export type FontWeight = 'normal' | 'bold' | number; export interface ITerminalProfiles { @@ -102,6 +124,11 @@ export interface ITerminalConfiguration { windows: string[]; }; profiles: ITerminalProfiles; + defaultProfile: { + linux: string | null; + osx: string | null; + windows: string | null; + }; useWslProfiles: boolean; showTabs: boolean; tabsLocation: 'left' | 'right'; @@ -160,19 +187,8 @@ export const DEFAULT_LOCAL_ECHO_EXCLUDE: ReadonlyArray = ['vim', 'vi', ' export interface ITerminalConfigHelper { config: ITerminalConfiguration; - onWorkspacePermissionsChanged: Event; - configFontIsMonospace(): boolean; getFont(): ITerminalFont; - /** Sets whether a workspace shell configuration is allowed or not */ - setWorkspaceShellAllowed(isAllowed: boolean): void; - /** - * Checks and returns whether it's safe to launch the process. If the user has not yet been - * asked, ask for future calls and return false. - * @param osOverride Use a custom OS (eg. remote). - * @param profile The profile if this is launching with a profile. - */ - checkIsProcessLaunchSafe(osOverride?: OperatingSystem, profile?: ITerminalProfile): boolean; showRecommendations(shellLaunchConfig: IShellLaunchConfig): void; } @@ -238,7 +254,6 @@ export interface ITerminalProfile { profileName: string; path: string; isAutoDetected?: boolean; - isWorkspaceProfile?: boolean; args?: string | string[] | undefined; env?: ITerminalEnvironment; overrideName?: boolean; @@ -438,7 +453,8 @@ export const enum TERMINAL_COMMAND_ID { SCROLL_TO_TOP = 'workbench.action.terminal.scrollToTop', CLEAR = 'workbench.action.terminal.clear', CLEAR_SELECTION = 'workbench.action.terminal.clearSelection', - MANAGE_WORKSPACE_SHELL_PERMISSIONS = 'workbench.action.terminal.manageWorkspaceShellPermissions', + CONFIGURE_ACTIVE = 'workbench.action.terminal.configureActive', + CHANGE_ICON = 'workbench.action.terminal.changeIcon', RENAME = 'workbench.action.terminal.rename', RENAME_WITH_ARG = 'workbench.action.terminal.renameWithArg', FIND_FOCUS = 'workbench.action.terminal.focusFind', diff --git a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts index c95b6aa611a..06b42133e33 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalConfiguration.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; +import { ConfigurationScope, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; import { EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { DEFAULT_LETTER_SPACING, DEFAULT_LINE_HEIGHT, TerminalCursorStyle, DEFAULT_COMMANDS_TO_SKIP_SHELL, SUGGESTIONS_FONT_WEIGHT, MINIMUM_FONT_WEIGHT, MAXIMUM_FONT_WEIGHT, DEFAULT_LOCAL_ECHO_EXCLUDE } from 'vs/workbench/contrib/terminal/common/terminal'; -import { isMacintosh, isWindows, Platform } from 'vs/base/common/platform'; +import { isMacintosh, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; const terminalProfileSchema: IJSONSchema = { @@ -59,6 +59,9 @@ export const terminalConfiguration: IConfigurationNode = { default: false }, 'terminal.integrated.automationShell.linux': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize({ key: 'terminal.integrated.automationShell.linux', comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] @@ -67,6 +70,9 @@ export const terminalConfiguration: IConfigurationNode = { default: null }, 'terminal.integrated.automationShell.osx': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize({ key: 'terminal.integrated.automationShell.osx', comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] @@ -75,6 +81,9 @@ export const terminalConfiguration: IConfigurationNode = { default: null }, 'terminal.integrated.automationShell.windows': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize({ key: 'terminal.integrated.automationShell.windows', comment: ['{0} and {1} are the `shell` and `shellArgs` settings keys'] @@ -83,6 +92,9 @@ export const terminalConfiguration: IConfigurationNode = { default: null }, 'terminal.integrated.shellArgs.linux': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize('terminal.integrated.shellArgs.linux', "The command line arguments to use when on the Linux terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), type: 'array', items: { @@ -91,6 +103,9 @@ export const terminalConfiguration: IConfigurationNode = { default: [] }, 'terminal.integrated.shellArgs.osx': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize('terminal.integrated.shellArgs.osx', "The command line arguments to use when on the macOS terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), type: 'array', items: { @@ -102,6 +117,9 @@ export const terminalConfiguration: IConfigurationNode = { default: ['-l'] }, 'terminal.integrated.shellArgs.windows': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize('terminal.integrated.shellArgs.windows', "The command line arguments to use when on the Windows terminal. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration)."), 'anyOf': [ { @@ -119,6 +137,9 @@ export const terminalConfiguration: IConfigurationNode = { default: [] }, 'terminal.integrated.profiles.windows': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize( { key: 'terminal.integrated.profiles.windows', @@ -178,6 +199,9 @@ export const terminalConfiguration: IConfigurationNode = { } }, 'terminal.integrated.profiles.osx': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize( { key: 'terminal.integrated.profile.osx', @@ -213,6 +237,9 @@ export const terminalConfiguration: IConfigurationNode = { } }, 'terminal.integrated.profiles.linux': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize( { key: 'terminal.integrated.profile.linux', @@ -247,21 +274,39 @@ export const terminalConfiguration: IConfigurationNode = { ] } }, + 'terminal.integrated.defaultProfile.linux': { + description: localize('terminal.integrated.defaultProfile.linux', 'The default profile used on Linux. When set to a valid profile name, this will override the values of `terminal.integrated.shell.osx` and `terminal.integrated.shellArgs.osx`.'), + type: ['string', 'null'], + default: null, + scope: ConfigurationScope.APPLICATION // Disallow setting the default in workspace settings + }, + 'terminal.integrated.defaultProfile.osx': { + description: localize('terminal.integrated.defaultProfile.osx', 'The default profile used on macOS. When set to a valid profile name, this will override the values of `terminal.integrated.shell.osx` and `terminal.integrated.shellArgs.osx`.'), + type: ['string', 'null'], + default: null, + scope: ConfigurationScope.APPLICATION // Disallow setting the default in workspace settings + }, + 'terminal.integrated.defaultProfile.windows': { + description: localize('terminal.integrated.defaultProfile.windows', 'The default profile used on Windows. When set to a valid profile name, this will override the values of `terminal.integrated.shell.osx` and `terminal.integrated.shellArgs.osx`.'), + type: ['string', 'null'], + default: null, + scope: ConfigurationScope.APPLICATION // Disallow setting the default in workspace settings + }, 'terminal.integrated.useWslProfiles': { description: localize('terminal.integrated.useWslProfiles', 'Controls whether or not WSL distros are shown in the terminal dropdown'), type: 'boolean', default: true }, 'terminal.integrated.showTabs': { - description: localize('terminal.integrated.showTabs', 'Controls whether or not the terminal tabs widget is shown'), + description: localize('terminal.integrated.showTabs', 'Controls whether terminal tabs display as a list to the side of the terminal. When this is disabled a dropdown will display instead.'), type: 'boolean', default: true }, 'terminal.integrated.tabsLocation': { 'type': 'string', 'enum': ['left', 'right'], - 'default': 'left', - 'description': localize('terminal.integrated.tabsLocation', "Controls the location of the terminal tabs, either left or right of the terminal container.") + 'default': 'right', + 'description': localize('terminal.integrated.tabsLocation', "Controls the location of the terminal tabs, either to the left or right of the actual terminal(s).") }, 'terminal.integrated.macOptionIsMeta': { description: localize('terminal.integrated.macOptionIsMeta', "Controls whether to treat the option key as the meta key in the terminal on macOS."), @@ -459,6 +504,9 @@ export const terminalConfiguration: IConfigurationNode = { default: true }, 'terminal.integrated.env.osx': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize('terminal.integrated.env.osx', "Object with environment variables that will be added to the VS Code process to be used by the terminal on macOS. Set to `null` to delete the environment variable."), type: 'object', additionalProperties: { @@ -467,6 +515,9 @@ export const terminalConfiguration: IConfigurationNode = { default: {} }, 'terminal.integrated.env.linux': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize('terminal.integrated.env.linux', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Linux. Set to `null` to delete the environment variable."), type: 'object', additionalProperties: { @@ -475,6 +526,9 @@ export const terminalConfiguration: IConfigurationNode = { default: {} }, 'terminal.integrated.env.windows': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: localize('terminal.integrated.env.windows', "Object with environment variables that will be added to the VS Code process to be used by the terminal on Windows. Set to `null` to delete the environment variable."), type: 'object', additionalProperties: { @@ -596,16 +650,25 @@ function getTerminalShellConfigurationStub(linux: string, osx: string, windows: type: 'object', properties: { 'terminal.integrated.shell.linux': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: linux, type: ['string', 'null'], default: null }, 'terminal.integrated.shell.osx': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: osx, type: ['string', 'null'], default: null }, 'terminal.integrated.shell.windows': { + requireTrust: true, + // TODO: Remove when workspace trust is enabled by default + scope: ConfigurationScope.APPLICATION, markdownDescription: windows, type: ['string', 'null'], default: null @@ -621,9 +684,9 @@ export function getNoDefaultTerminalShellConfiguration(): IConfigurationNode { localize('terminal.integrated.shell.windows.noDefault', "The path of the shell that the terminal uses on Windows. [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).")); } -export async function getTerminalShellConfiguration(getSystemShell: (p: Platform) => Promise): Promise { +export async function getTerminalShellConfiguration(getSystemShell: (os: OperatingSystem) => Promise): Promise { return getTerminalShellConfigurationStub( - localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(Platform.Linux)), - localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(Platform.Mac)), - localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(Platform.Windows))); + localize('terminal.integrated.shell.linux', "The path of the shell that the terminal uses on Linux (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(OperatingSystem.Linux)), + localize('terminal.integrated.shell.osx', "The path of the shell that the terminal uses on macOS (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(OperatingSystem.Macintosh)), + localize('terminal.integrated.shell.windows', "The path of the shell that the terminal uses on Windows (default: {0}). [Read more about configuring the shell](https://code.visualstudio.com/docs/editor/integrated-terminal#_configuration).", await getSystemShell(OperatingSystem.Windows))); } diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index aaa5e8efd3a..b99e05c00a1 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -4,26 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'vs/base/common/path'; -import * as platform from 'vs/base/common/platform'; import { URI as Uri } from 'vs/base/common/uri'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; import { sanitizeProcessEnvironment } from 'vs/base/common/processes'; import { ILogService } from 'vs/platform/log/common/log'; import { IShellLaunchConfig, ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; +import { IProcessEnvironment, isWindows, locale, OperatingSystem, OS, platform, Platform } from 'vs/base/common/platform'; /** * This module contains utility functions related to the environment, cwd and paths. */ -export function mergeEnvironments(parent: platform.IProcessEnvironment, other: ITerminalEnvironment | undefined): void { +export function mergeEnvironments(parent: IProcessEnvironment, other: ITerminalEnvironment | undefined): void { if (!other) { return; } // On Windows apply the new values ignoring case, while still retaining // the case of the original key. - if (platform.isWindows) { + if (isWindows) { for (const configKey in other) { let actualKey = configKey; for (const envKey in parent) { @@ -55,7 +55,7 @@ function _mergeEnvironmentValue(env: ITerminalEnvironment, key: string, value: s } } -export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, version: string | undefined, locale: string | undefined, detectLocale: 'auto' | 'off' | 'on'): void { +export function addTerminalEnvironmentKeys(env: IProcessEnvironment, version: string | undefined, locale: string | undefined, detectLocale: 'auto' | 'off' | 'on'): void { env['TERM_PROGRAM'] = 'vscode'; if (version) { env['TERM_PROGRAM_VERSION'] = version; @@ -66,7 +66,7 @@ export function addTerminalEnvironmentKeys(env: platform.IProcessEnvironment, ve env['COLORTERM'] = 'truecolor'; } -function mergeNonNullKeys(env: platform.IProcessEnvironment, other: ITerminalEnvironment | undefined) { +function mergeNonNullKeys(env: IProcessEnvironment, other: ITerminalEnvironment | undefined) { if (!other) { return; } @@ -92,7 +92,7 @@ function resolveConfigurationVariables(variableResolver: VariableResolver, env: return env; } -export function shouldSetLangEnvVariable(env: platform.IProcessEnvironment, detectLocale: 'auto' | 'off' | 'on'): boolean { +export function shouldSetLangEnvVariable(env: IProcessEnvironment, detectLocale: 'auto' | 'off' | 'on'): boolean { if (detectLocale === 'on') { return true; } @@ -230,7 +230,7 @@ function _resolveCwd(cwd: string, variableResolver: VariableResolver | undefined function _sanitizeCwd(cwd: string): string { // Make the drive letter uppercase on Windows (see #9448) - if (platform.platform === platform.Platform.Windows && cwd && cwd[1] === ':') { + if (OS === OperatingSystem.Windows && cwd && cwd[1] === ':') { return cwd[0].toUpperCase() + cwd.substr(1); } return cwd; @@ -270,31 +270,33 @@ export function createVariableResolver(lastActiveWorkspace: IWorkspaceFolder | u return (str) => configurationResolverService.resolve(lastActiveWorkspace, str); } +/** + * @deprecated Use ITerminalProfileResolverService + */ export function getDefaultShell( - fetchSetting: (key: TerminalShellSetting) => { userValue?: string | string[], value?: string | string[], defaultValue?: string | string[] }, - isWorkspaceShellAllowed: boolean, + fetchSetting: (key: TerminalShellSetting) => string | undefined, defaultShell: string, isWoW64: boolean, windir: string | undefined, variableResolver: VariableResolver | undefined, logService: ILogService, useAutomationShell: boolean, - platformOverride: platform.Platform = platform.platform + platformOverride: Platform = platform ): string { - let maybeExecutable: string | null = null; + let maybeExecutable: string | undefined; if (useAutomationShell) { // If automationShell is specified, this should override the normal setting - maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'automationShell', platformOverride); + maybeExecutable = getShellSetting(fetchSetting, 'automationShell', platformOverride) as string | undefined; } if (!maybeExecutable) { - maybeExecutable = getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'shell', platformOverride); + maybeExecutable = getShellSetting(fetchSetting, 'shell', platformOverride) as string | undefined; } let executable: string = maybeExecutable || defaultShell; // Change Sysnative to System32 if the OS is Windows but NOT WoW64. It's // safe to assume that this was used by accident as Sysnative does not // exist and will break the terminal in non-WoW64 environments. - if ((platformOverride === platform.Platform.Windows) && !isWoW64 && windir) { + if ((platformOverride === Platform.Windows) && !isWoW64 && windir) { const sysnativePath = path.join(windir, 'Sysnative').replace(/\//g, '\\').toLowerCase(); if (executable && executable.toLowerCase().indexOf(sysnativePath) === 0) { executable = path.join(windir, 'System32', executable.substr(sysnativePath.length + 1)); @@ -302,7 +304,7 @@ export function getDefaultShell( } // Convert / to \ on Windows for convenience - if (executable && platformOverride === platform.Platform.Windows) { + if (executable && platformOverride === Platform.Windows) { executable = executable.replace(/\//g, '\\'); } @@ -317,27 +319,28 @@ export function getDefaultShell( return executable; } +/** + * @deprecated Use ITerminalProfileResolverService + */ export function getDefaultShellArgs( - fetchSetting: (key: TerminalShellSetting | TerminalShellArgsSetting) => { userValue?: string | string[], value?: string | string[], defaultValue?: string | string[] }, - isWorkspaceShellAllowed: boolean, + fetchSetting: (key: TerminalShellSetting | TerminalShellArgsSetting) => string | string[] | undefined, useAutomationShell: boolean, variableResolver: VariableResolver | undefined, logService: ILogService, - platformOverride: platform.Platform = platform.platform, + platformOverride: Platform = platform, ): string | string[] { if (useAutomationShell) { - if (!!getShellSetting(fetchSetting, isWorkspaceShellAllowed, 'automationShell', platformOverride)) { + if (!!getShellSetting(fetchSetting, 'automationShell', platformOverride)) { return []; } } - const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux'; - const shellArgsConfigValue = fetchSetting(`terminal.integrated.shellArgs.${platformKey}`); - let args = ((isWorkspaceShellAllowed ? shellArgsConfigValue.value : shellArgsConfigValue.userValue) || shellArgsConfigValue.defaultValue); + const platformKey = platformOverride === Platform.Windows ? 'windows' : platformOverride === Platform.Mac ? 'osx' : 'linux'; + let args = fetchSetting(`terminal.integrated.shellArgs.${platformKey}`); if (!args) { return []; } - if (typeof args === 'string' && platformOverride === platform.Platform.Windows) { + if (typeof args === 'string' && platformOverride === Platform.Windows) { return variableResolver ? variableResolver(args) : args; } if (variableResolver) { @@ -356,28 +359,24 @@ export function getDefaultShellArgs( } function getShellSetting( - fetchSetting: (key: TerminalShellSetting) => { userValue?: string | string[], value?: string | string[], defaultValue?: string | string[] }, - isWorkspaceShellAllowed: boolean, + fetchSetting: (key: TerminalShellSetting) => string | string[] | undefined, type: 'automationShell' | 'shell', - platformOverride: platform.Platform = platform.platform, -): string | null { - const platformKey = platformOverride === platform.Platform.Windows ? 'windows' : platformOverride === platform.Platform.Mac ? 'osx' : 'linux'; - const shellConfigValue = fetchSetting(`terminal.integrated.${type}.${platformKey}`); - const executable = (isWorkspaceShellAllowed ? shellConfigValue.value : shellConfigValue.userValue) || (shellConfigValue.defaultValue); - return executable; + platformOverride: Platform = platform, +): string | string[] | undefined { + const platformKey = platformOverride === Platform.Windows ? 'windows' : platformOverride === Platform.Mac ? 'osx' : 'linux'; + return fetchSetting(`terminal.integrated.${type}.${platformKey}`); } export function createTerminalEnvironment( shellLaunchConfig: IShellLaunchConfig, - envFromConfig: { userValue?: ITerminalEnvironment, value?: ITerminalEnvironment, defaultValue?: ITerminalEnvironment }, + envFromConfig: ITerminalEnvironment | undefined, variableResolver: VariableResolver | undefined, - isWorkspaceShellAllowed: boolean, version: string | undefined, detectLocale: 'auto' | 'off' | 'on', - baseEnv: platform.IProcessEnvironment -): platform.IProcessEnvironment { + baseEnv: IProcessEnvironment +): IProcessEnvironment { // Create a terminal environment based on settings, launch config and permissions - let env: platform.IProcessEnvironment = {}; + let env: IProcessEnvironment = {}; if (shellLaunchConfig.strictEnv) { // strictEnv is true, only use the requested env (ignoring null entries) mergeNonNullKeys(env, shellLaunchConfig.env); @@ -385,9 +384,7 @@ export function createTerminalEnvironment( // Merge process env with the env from config and from shellLaunchConfig mergeNonNullKeys(env, baseEnv); - // const platformKey = platform.isWindows ? 'windows' : (platform.isMacintosh ? 'osx' : 'linux'); - // const envFromConfigValue = this._workspaceConfigurationService.inspect(`terminal.integrated.env.${platformKey}`); - const allowedEnvFromConfig = { ...(isWorkspaceShellAllowed ? envFromConfig.value : envFromConfig.userValue) }; + const allowedEnvFromConfig = { ...envFromConfig }; // Resolve env vars from config and shell if (variableResolver) { @@ -408,7 +405,7 @@ export function createTerminalEnvironment( mergeEnvironments(env, shellLaunchConfig.env); // Adding other env keys necessary to create the process - addTerminalEnvironmentKeys(env, version, platform.locale, detectLocale); + addTerminalEnvironmentKeys(env, version, locale, detectLocale); } return env; } diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts index 943630caba9..6f6943b6e23 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminal.contribution.ts @@ -14,12 +14,15 @@ import { getTerminalShellConfiguration } from 'vs/workbench/contrib/terminal/com import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { getSystemShell } from 'vs/base/node/shell'; import { process } from 'vs/base/parts/sandbox/electron-sandbox/globals'; -import { Platform } from 'vs/base/common/platform'; +import { OperatingSystem } from 'vs/base/common/platform'; +import { ElectronTerminalProfileResolverService } from 'vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService'; +import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; // This file contains additional desktop-only contributions on top of those in browser/ // Register services registerSingleton(ITerminalInstanceService, TerminalInstanceService, true); +registerSingleton(ITerminalProfileResolverService, ElectronTerminalProfileResolverService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(TerminalNativeContribution, LifecyclePhase.Ready); @@ -27,5 +30,5 @@ workbenchRegistry.registerWorkbenchContribution(TerminalNativeContribution, Life // Register configurations const configurationRegistry = Registry.as(Extensions.Configuration); -const systemShell = async (p: Platform) => getSystemShell(p, await process.shellEnv()); +const systemShell = async (os: OperatingSystem) => getSystemShell(os, await process.shellEnv()); getTerminalShellConfiguration(systemShell).then(config => configurationRegistry.registerConfiguration(config)); diff --git a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts index facc10fc01b..1e0007a6fca 100644 --- a/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts +++ b/src/vs/workbench/contrib/terminal/electron-browser/terminalInstanceService.ts @@ -3,23 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; -import { IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY } from 'vs/workbench/contrib/terminal/common/terminal'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IProcessEnvironment, platform, Platform } from 'vs/base/common/platform'; -import { getSystemShell } from 'vs/base/node/shell'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ILogService } from 'vs/platform/log/common/log'; -import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IProcessEnvironment } from 'vs/base/common/platform'; import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment'; -import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; -import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; import type { Terminal as XTermTerminal } from 'xterm'; import type { SearchAddon as XTermSearchAddon } from 'xterm-addon-search'; import type { Unicode11Addon as XTermUnicode11Addon } from 'xterm-addon-unicode11'; import type { WebglAddon as XTermWebglAddon } from 'xterm-addon-webgl'; -import { createVariableResolver, getDefaultShell, getDefaultShellArgs } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; let Terminal: typeof XTermTerminal; let SearchAddon: typeof XTermSearchAddon; @@ -30,13 +20,6 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst public _serviceBrand: undefined; constructor( - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IStorageService private readonly _storageService: IStorageService, - @IConfigurationResolverService private readonly _configurationResolverService: IConfigurationResolverService, - @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, - @IHistoryService private readonly _historyService: IHistoryService, - @ILogService private readonly _logService: ILogService, - @IShellEnvironmentService private readonly _shellEnvironmentService: IShellEnvironmentService ) { super(); } @@ -69,40 +52,6 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst return WebglAddon; } - private _isWorkspaceShellAllowed(): boolean { - return this._storageService.getBoolean(IS_WORKSPACE_SHELL_ALLOWED_STORAGE_KEY, StorageScope.WORKSPACE, false); - } - - public async getDefaultShellAndArgs(useAutomationShell: boolean, platformOverride: Platform = platform): Promise<{ shell: string, args: string | string[] }> { - const isWorkspaceShellAllowed = this._isWorkspaceShellAllowed(); - const activeWorkspaceRootUri = this._historyService.getLastActiveWorkspaceRoot(); - let lastActiveWorkspace = activeWorkspaceRootUri ? this._workspaceContextService.getWorkspaceFolder(activeWorkspaceRootUri) : undefined; - lastActiveWorkspace = lastActiveWorkspace === null ? undefined : lastActiveWorkspace; - - const shell = getDefaultShell( - (key) => this._configurationService.inspect(key), - isWorkspaceShellAllowed, - await getSystemShell(platformOverride, await this._shellEnvironmentService.getShellEnv()), - process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'), - process.env.windir, - createVariableResolver(lastActiveWorkspace, this._configurationResolverService), - this._logService, - useAutomationShell, - platformOverride - ); - - const args = getDefaultShellArgs( - (key) => this._configurationService.inspect(key), - isWorkspaceShellAllowed, - useAutomationShell, - createVariableResolver(lastActiveWorkspace, this._configurationResolverService), - this._logService, - platformOverride - ); - - return Promise.resolve({ shell, args }); - } - public getMainProcessParentEnv(): Promise { return getMainProcessParentEnv(); } diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts index a343883e565..42c740c4772 100644 --- a/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/localTerminalService.ts @@ -5,7 +5,7 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; -import { IProcessEnvironment } from 'vs/base/common/platform'; +import { IProcessEnvironment, OperatingSystem } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -126,6 +126,14 @@ export class LocalTerminalService extends Disposable implements ILocalTerminalSe this._localPtyService.reduceConnectionGraceTime(); } + public async getDefaultSystemShell(osOverride?: OperatingSystem): Promise { + return this._localPtyService.getDefaultSystemShell(osOverride); + } + + public async getShellEnvironment(): Promise { + return this._localPtyService.getShellEnvironment(); + } + public async setTerminalLayoutInfo(layoutInfo?: ITerminalsLayoutInfoById): Promise { const args: ISetTerminalLayoutInfoArgs = { workspaceId: this._getWorkspaceId(), diff --git a/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts new file mode 100644 index 00000000000..d22e66d5594 --- /dev/null +++ b/src/vs/workbench/contrib/terminal/electron-sandbox/terminalProfileResolverService.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ILocalTerminalService } from 'vs/platform/terminal/common/terminal'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IRemoteTerminalService, ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; +import { BaseTerminalProfileResolverService } from 'vs/workbench/contrib/terminal/browser/terminalProfileResolverService'; +import { IConfigurationResolverService } from 'vs/workbench/services/configurationResolver/common/configurationResolver'; +import { IShellEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; + +export class ElectronTerminalProfileResolverService extends BaseTerminalProfileResolverService { + + constructor( + @IConfigurationResolverService configurationResolverService: IConfigurationResolverService, + @IConfigurationService configurationService: IConfigurationService, + @IHistoryService historyService: IHistoryService, + @ILogService logService: ILogService, + @IShellEnvironmentService shellEnvironmentService: IShellEnvironmentService, + @ITerminalService terminalService: ITerminalService, + @ILocalTerminalService localTerminalService: ILocalTerminalService, + @IRemoteTerminalService remoteTerminalService: IRemoteTerminalService, + @IWorkspaceContextService workspaceContextService: IWorkspaceContextService + ) { + super( + { + getDefaultSystemShell: async (remoteAuthority, platform) => { + const service = remoteAuthority ? remoteTerminalService : localTerminalService; + return service.getDefaultSystemShell(platform); + }, + getShellEnvironment: (remoteAuthority) => { + if (remoteAuthority) { + remoteTerminalService.getShellEnvironment(); + } + return shellEnvironmentService.getShellEnv(); + } + }, + configurationService, + configurationResolverService, + historyService, + logService, + terminalService, + workspaceContextService + ); + } +} diff --git a/src/vs/workbench/contrib/terminal/node/terminal.ts b/src/vs/workbench/contrib/terminal/node/terminal.ts index 51a10b87cf7..adb21019594 100644 --- a/src/vs/workbench/contrib/terminal/node/terminal.ts +++ b/src/vs/workbench/contrib/terminal/node/terminal.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as platform from 'vs/base/common/platform'; +import { isLinux } from 'vs/base/common/platform'; import { SymlinkSupport } from 'vs/base/node/pfs'; import { LinuxDistro } from 'vs/workbench/contrib/terminal/common/terminal'; let detectedDistro = LinuxDistro.Unknown; -if (platform.isLinux) { +if (isLinux) { const file = '/etc/os-release'; SymlinkSupport.existsFile(file).then(async exists => { if (!exists) { diff --git a/src/vs/workbench/contrib/terminal/node/terminalProfiles.ts b/src/vs/workbench/contrib/terminal/node/terminalProfiles.ts index 32618e20922..a1fe90183ab 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProfiles.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProfiles.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as fs from 'fs'; -import * as platform from 'vs/base/common/platform'; import { normalize, basename, delimiter } from 'vs/base/common/path'; import { enumeratePowerShellInstallations } from 'vs/base/node/powershell'; import { findExecutable, getWindowsBuildNumber } from 'vs/platform/terminal/node/terminalEnvironment'; @@ -16,6 +15,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import * as pfs from 'vs/base/node/pfs'; import { ITerminalEnvironment } from 'vs/platform/terminal/common/terminal'; import { Codicon } from 'vs/base/common/codicons'; +import { isMacintosh, isWindows } from 'vs/base/common/platform'; let profileSources: Map | undefined; @@ -24,10 +24,10 @@ export function detectAvailableProfiles(configuredProfilesOnly: boolean, fsProvi existsFile: pfs.SymlinkSupport.existsFile, readFile: fs.promises.readFile }; - if (platform.isWindows) { + if (isWindows) { return detectAvailableWindowsProfiles(configuredProfilesOnly, fsProvider, logService, config?.useWslProfiles, config?.profiles.windows, variableResolver, workspaceFolder); } - return detectAvailableUnixProfiles(fsProvider, logService, configuredProfilesOnly, platform.isMacintosh ? config?.profiles.osx : config?.profiles.linux, testPaths, variableResolver, workspaceFolder); + return detectAvailableUnixProfiles(fsProvider, logService, configuredProfilesOnly, isMacintosh ? config?.profiles.osx : config?.profiles.linux, testPaths, variableResolver, workspaceFolder); } async function detectAvailableWindowsProfiles(configuredProfilesOnly: boolean, fsProvider: IFsProvider, logService?: ILogService, useWslProfiles?: boolean, configProfiles?: { [key: string]: ITerminalProfileObject }, variableResolver?: ExtHostVariableResolverService, workspaceFolder?: IWorkspaceFolder): Promise { @@ -115,7 +115,7 @@ async function transformToTerminalProfiles(entries: IterableIterator<[string, IT icon = profile.icon || source.icon; } else { originalPaths = Array.isArray(profile.path) ? profile.path : [profile.path]; - args = platform.isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined; + args = isWindows ? profile.args : Array.isArray(profile.args) ? profile.args : undefined; icon = profile.icon; } diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts index 2a717e05cee..1f1a4da2775 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalConfigHelper.test.ts @@ -20,7 +20,7 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); await configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: 'bar' } }); - const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, 'bar', 'terminal.integrated.fontFamily should be selected over editor.fontFamily'); }); @@ -29,7 +29,7 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); await configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.setLinuxDistro(LinuxDistro.Fedora); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, '\'DejaVu Sans Mono\', monospace', 'Fedora should have its font overridden when terminal.integrated.fontFamily not set'); @@ -39,7 +39,7 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); await configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, '\'Ubuntu Mono\', monospace', 'Ubuntu should have its font overridden when terminal.integrated.fontFamily not set'); @@ -49,7 +49,7 @@ suite('Workbench - TerminalConfigHelper', () => { const configurationService = new TestConfigurationService(); await configurationService.setUserConfiguration('editor', { fontFamily: 'foo' }); await configurationService.setUserConfiguration('terminal', { integrated: { fontFamily: null } }); - const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + const configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontFamily, 'foo', 'editor.fontFamily should be the fallback when terminal.integrated.fontFamily not set'); }); @@ -67,7 +67,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 10 } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 10, 'terminal.integrated.fontSize should be selected over editor.fontSize'); @@ -80,12 +80,12 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 0 } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 8, 'The minimum terminal font size (with adjustment) should be used when terminal.integrated.fontSize less than it'); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 6, 'The minimum terminal font size should be used when terminal.integrated.fontSize less than it'); @@ -98,7 +98,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: 1500 } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, 25, 'The maximum terminal font size should be used when terminal.integrated.fontSize more than it'); @@ -111,12 +111,12 @@ suite('Workbench - TerminalConfigHelper', () => { fontSize: null } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.setLinuxDistro(LinuxDistro.Ubuntu); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize + 2, 'The default editor font size (with adjustment) should be used when terminal.integrated.fontSize is not set'); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().fontSize, EDITOR_FONT_DEFAULTS.fontSize, 'The default editor font size should be used when terminal.integrated.fontSize is not set'); }); @@ -134,7 +134,7 @@ suite('Workbench - TerminalConfigHelper', () => { lineHeight: 2 } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().lineHeight, 2, 'terminal.integrated.lineHeight should be selected over editor.lineHeight'); @@ -148,7 +148,7 @@ suite('Workbench - TerminalConfigHelper', () => { lineHeight: 0 } }); - configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.getFont().lineHeight, 1, 'editor.lineHeight should be 1 when terminal.integrated.lineHeight not set'); }); @@ -161,7 +161,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -173,7 +173,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontFamily: 'sans-serif' } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -185,7 +185,7 @@ suite('Workbench - TerminalConfigHelper', () => { fontFamily: 'serif' } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); @@ -201,7 +201,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), true, 'monospace is monospaced'); }); @@ -217,7 +217,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'sans-serif is not monospaced'); }); @@ -233,7 +233,7 @@ suite('Workbench - TerminalConfigHelper', () => { } }); - let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!, null!); + let configHelper = new TerminalConfigHelper(configurationService, null!, null!, null!, null!, null!); configHelper.panelContainer = fixture; assert.strictEqual(configHelper.configFontIsMonospace(), false, 'serif is not monospaced'); }); diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts index 90b0b89f15f..6baaae4e476 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalProcessManager.test.ts @@ -8,12 +8,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper'; import { TerminalProcessManager } from 'vs/workbench/contrib/terminal/browser/terminalProcessManager'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { ITestInstantiationService, TestProductService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { ITestInstantiationService, TestProductService, TestTerminalProfileResolverService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IProductService } from 'vs/platform/product/common/productService'; import { IEnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariable'; import { EnvironmentVariableService } from 'vs/workbench/contrib/terminal/common/environmentVariableService'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; +import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; suite('Workbench - TerminalProcessManager', () => { let instantiationService: ITestInstantiationService; @@ -32,6 +33,7 @@ suite('Workbench - TerminalProcessManager', () => { instantiationService.stub(IConfigurationService, configurationService); instantiationService.stub(IProductService, TestProductService); instantiationService.stub(IEnvironmentVariableService, instantiationService.createInstance(EnvironmentVariableService)); + instantiationService.stub(ITerminalProfileResolverService, TestTerminalProfileResolverService); const configHelper = instantiationService.createInstance(TerminalConfigHelper); manager = instantiationService.createInstance(TerminalProcessManager, 1, configHelper); diff --git a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts index bdf2e2eca2a..d1b90d0c3ed 100644 --- a/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/common/terminalEnvironment.test.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as platform from 'vs/base/common/platform'; import { URI as Uri } from 'vs/base/common/uri'; import { IStringDictionary } from 'vs/base/common/collections'; import { addTerminalEnvironmentKeys, mergeEnvironments, getCwd, getDefaultShell, getLangEnvVariable, shouldSetLangEnvVariable } from 'vs/workbench/contrib/terminal/common/terminalEnvironment'; +import { isWindows, Platform } from 'vs/base/common/platform'; suite('Workbench - TerminalEnvironment', () => { suite('addTerminalEnvironmentKeys', () => { @@ -132,7 +132,7 @@ suite('Workbench - TerminalEnvironment', () => { }); }); - (!platform.isWindows ? test.skip : test)('should add keys ignoring case on Windows', () => { + (!isWindows ? test.skip : test)('should add keys ignoring case on Windows', () => { const parent = { a: 'b' }; @@ -159,7 +159,7 @@ suite('Workbench - TerminalEnvironment', () => { }); }); - (!platform.isWindows ? test.skip : test)('null values should delete keys from the parent env ignoring case on Windows', () => { + (!isWindows ? test.skip : test)('null values should delete keys from the parent env ignoring case on Windows', () => { const parent = { a: 'b', c: 'd' @@ -212,43 +212,39 @@ suite('Workbench - TerminalEnvironment', () => { suite('getDefaultShell', () => { test('should change Sysnative to System32 in non-WoW64 systems', () => { const shell = getDefaultShell(key => { - return ({ - 'terminal.integrated.shell.windows': { userValue: 'C:\\Windows\\Sysnative\\cmd.exe', value: undefined, defaultValue: undefined } - } as any)[key]; - }, false, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, platform.Platform.Windows); + return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; + }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); assert.strictEqual(shell, 'C:\\Windows\\System32\\cmd.exe'); }); test('should not change Sysnative to System32 in WoW64 systems', () => { const shell = getDefaultShell(key => { - return ({ - 'terminal.integrated.shell.windows': { userValue: 'C:\\Windows\\Sysnative\\cmd.exe', value: undefined, defaultValue: undefined } - } as any)[key]; - }, false, 'DEFAULT', true, 'C:\\Windows', undefined, {} as any, false, platform.Platform.Windows); + return ({ 'terminal.integrated.shell.windows': 'C:\\Windows\\Sysnative\\cmd.exe' } as any)[key]; + }, 'DEFAULT', true, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); assert.strictEqual(shell, 'C:\\Windows\\Sysnative\\cmd.exe'); }); test('should use automationShell when specified', () => { const shell1 = getDefaultShell(key => { return ({ - 'terminal.integrated.shell.windows': { userValue: 'shell', value: undefined, defaultValue: undefined }, - 'terminal.integrated.automationShell.windows': { userValue: undefined, value: undefined, defaultValue: undefined } + 'terminal.integrated.shell.windows': 'shell', + 'terminal.integrated.automationShell.windows': undefined } as any)[key]; - }, false, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, platform.Platform.Windows); + }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, false, Platform.Windows); assert.strictEqual(shell1, 'shell', 'automationShell was false'); const shell2 = getDefaultShell(key => { return ({ - 'terminal.integrated.shell.windows': { userValue: 'shell', value: undefined, defaultValue: undefined }, - 'terminal.integrated.automationShell.windows': { userValue: undefined, value: undefined, defaultValue: undefined } + 'terminal.integrated.shell.windows': 'shell', + 'terminal.integrated.automationShell.windows': undefined } as any)[key]; - }, false, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, platform.Platform.Windows); + }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, Platform.Windows); assert.strictEqual(shell2, 'shell', 'automationShell was true'); const shell3 = getDefaultShell(key => { return ({ - 'terminal.integrated.shell.windows': { userValue: 'shell', value: undefined, defaultValue: undefined }, - 'terminal.integrated.automationShell.windows': { userValue: 'automationShell', value: undefined, defaultValue: undefined } + 'terminal.integrated.shell.windows': 'shell', + 'terminal.integrated.automationShell.windows': 'automationShell' } as any)[key]; - }, false, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, platform.Platform.Windows); + }, 'DEFAULT', false, 'C:\\Windows', undefined, {} as any, true, Platform.Windows); assert.strictEqual(shell3, 'automationShell', 'automationShell was true and specified in settings'); }); }); diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts index 29bc46b38e6..4ad344d672e 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalProfiles.test.ts @@ -21,7 +21,6 @@ function profilesEqual(actualProfiles: ITerminalProfile[], expectedProfiles: ITe strictEqual(actual.path, expected.path); deepStrictEqual(actual.args, expected.args); strictEqual(actual.isAutoDetected, expected.isAutoDetected); - strictEqual(actual.isWorkspaceProfile, expected.isWorkspaceProfile); strictEqual(actual.overrideName, expected.overrideName); } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts index 5db66fff1c6..ebc4021f651 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByLocation.ts @@ -68,7 +68,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes for (const inTree of [...this.items.values()].sort((a, b) => b.depth - a.depth)) { const lookup = this.results.getStateById(inTree.test.item.extId)?.[1]; - inTree.ownState = lookup?.state.state ?? TestResultState.Unset; const computed = lookup?.computedState ?? TestResultState.Unset; if (computed !== inTree.state) { inTree.state = computed; @@ -84,7 +83,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes this._register(results.onTestChanged(({ item: result }) => { const item = this.items.get(result.item.extId); if (item) { - item.ownState = result.state.state; item.retired = result.retired; refreshComputedState(computedStateAccessor, item, this.addUpdated, result.computedState); this.addUpdated(item); @@ -270,7 +268,6 @@ export class HierarchicalByLocationProjection extends Disposable implements ITes const prevState = this.results.getStateById(treeElement.test.item.extId)?.[1]; if (prevState) { - treeElement.ownState = prevState.state.state; treeElement.retired = prevState.retired; refreshComputedState(computedStateAccessor, treeElement, this.addUpdated, prevState.computedState); } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts index 41a5824a3a5..8d62b8ecc89 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalByName.ts @@ -140,7 +140,7 @@ export class HierarchicalByNameProjection extends HierarchicalByLocationProjecti const parent = this.getOrCreateFolderElement(folder); const actualParent = item.parent ? this.items.get(item.parent) as HierarchicalByNameElement : undefined; for (const testRoot of parent.children) { - if (testRoot.test.src.provider === item.src.provider) { + if (testRoot.test.src.controller === item.src.controller) { return new HierarchicalByNameElement(item, testRoot, r => this.changes.addedOrRemoved(r), actualParent); } } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts index 323b6424b5d..324e28dff1f 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/hierarchalNodes.ts @@ -55,7 +55,6 @@ export class HierarchicalElement implements ITestTreeElement { public state = TestResultState.Unset; public retired = false; - public ownState = TestResultState.Unset; constructor(public readonly test: InternalTestItem, public readonly parentItem: HierarchicalFolder | HierarchicalElement) { this.test = { ...test, item: { ...test.item } }; // clone since we Object.assign updatese @@ -97,7 +96,6 @@ export class HierarchicalFolder implements ITestTreeElement { public retired = false; public state = TestResultState.Unset; - public ownState = TestResultState.Unset; constructor(public readonly folder: IWorkspaceFolder) { } diff --git a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts index a456d777b8b..e076f828940 100644 --- a/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts +++ b/src/vs/workbench/contrib/testing/browser/explorerProjections/index.ts @@ -112,7 +112,6 @@ export interface ITestTreeElement { */ readonly retired: boolean; - readonly ownState: TestResultState; readonly label: string; readonly parentItem: ITestTreeElement | null; } diff --git a/src/vs/workbench/contrib/testing/browser/media/testing.css b/src/vs/workbench/contrib/testing/browser/media/testing.css index 02669c967ec..f9d69a0298a 100644 --- a/src/vs/workbench/contrib/testing/browser/media/testing.css +++ b/src/vs/workbench/contrib/testing/browser/media/testing.css @@ -72,10 +72,8 @@ .monaco-action-bar .action-item > .action-label { - width: 16px; - height: 100%; - line-height: 22px; - margin-right: 8px; + padding: 2px; + margin-right: 2px; } .monaco-workbench .part > .title > .title-actions .action-label.codicon-testing-autorun::after { diff --git a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts index 3e60eb44ced..d208ba4b30b 100644 --- a/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts +++ b/src/vs/workbench/contrib/testing/browser/testExplorerActions.ts @@ -440,8 +440,9 @@ export class ShowMostRecentOutputAction extends Action2 { constructor() { super({ id: 'testing.showMostRecentOutput', - title: localize('testing.showMostRecentOutput', "Show Most Recent Output"), - f1: false, + title: localize('testing.showMostRecentOutput', "Show Output"), + f1: true, + category, icon: Codicon.terminal, menu: { id: MenuId.ViewTitle, @@ -878,7 +879,7 @@ abstract class RunOrDebugFailedTests extends RunOrDebugExtsById { const resultSet = results[i]; for (const test of resultSet.tests) { const path = this.getPathForTest(test, resultSet).join(sep); - if (isFailedState(test.state.state)) { + if (isFailedState(test.ownComputedState)) { paths.add(path); } else { paths.delete(path); diff --git a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts index fae2288cb23..c7305a1a8b0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingDecorations.ts +++ b/src/vs/workbench/contrib/testing/browser/testingDecorations.ts @@ -185,17 +185,21 @@ export class TestingDecorations extends Disposable implements IEditorContributio continue; // do not show decorations for outdated tests } - for (let i = 0; i < stateItem.state.messages.length; i++) { - const m = stateItem.state.messages[i]; - if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) { - const uri = buildTestUri({ - type: TestUriType.ResultActualOutput, - messageIndex: i, - resultId: result.id, - testExtId: stateItem.item.extId, - }); + for (let taskId = 0; taskId < stateItem.tasks.length; taskId++) { + const state = stateItem.tasks[taskId]; + for (let i = 0; i < state.messages.length; i++) { + const m = state.messages[i]; + if (!this.invalidatedMessages.has(m) && hasValidLocation(uri, m)) { + const uri = buildTestUri({ + type: TestUriType.ResultActualOutput, + messageIndex: i, + taskIndex: taskId, + resultId: result.id, + testExtId: stateItem.item.extId, + }); - newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); + newDecorations.push(this.instantiationService.createInstance(TestMessageDecoration, m, uri, m.location, this.editor)); + } } } } @@ -275,7 +279,7 @@ class RunTestDecoration extends Disposable implements ITestDecoration { : test.children.size > 0 ? testingRunAllIcon : testingRunIcon; const hoverMessage = new MarkdownString('', true).appendText(localize('failedHoverMessage', '{0} has failed. ', test.item.label)); - if (stateItem?.state.messages.length) { + if (stateItem?.tasks.some(s => s.messages.length > 0)) { const args = encodeURIComponent(JSON.stringify([test.item.extId])); hoverMessage.appendMarkdown(`[${localize('failedPeekAction', 'Peek Error')}](command:vscode.peekTestError?${args})`); } diff --git a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts index 542763578e0..803ee309d85 100644 --- a/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts +++ b/src/vs/workbench/contrib/testing/browser/testingExplorerView.ts @@ -475,7 +475,7 @@ export class TestingExplorerViewModel extends Disposable { */ private async tryPeekError(item: ITestTreeElement) { const lookup = item.test && this.testResults.getStateById(item.test.item.extId); - return lookup && isFailedState(lookup[1].state.state) + return lookup && lookup[1].tasks.some(s => isFailedState(s.state)) ? this.peekOpener.tryPeekFirstError(lookup[0], lookup[1], { preserveFocus: true }) : false; } @@ -638,9 +638,9 @@ class TestsFilter implements ITreeFilter { case TestExplorerStateFilter.All: return FilterResult.Include; case TestExplorerStateFilter.OnlyExecuted: - return element.ownState !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit; + return element.state !== TestResultState.Unset ? FilterResult.Include : FilterResult.Inherit; case TestExplorerStateFilter.OnlyFailed: - return isFailedState(element.ownState) ? FilterResult.Include : FilterResult.Inherit; + return isFailedState(element.state) ? FilterResult.Include : FilterResult.Inherit; } } diff --git a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts index 53a647c5267..e6b54d9e2c0 100644 --- a/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts +++ b/src/vs/workbench/contrib/testing/browser/testingOutputPeek.ts @@ -31,7 +31,7 @@ import { EditorModel } from 'vs/workbench/common/editor'; import { testingPeekBorder } from 'vs/workbench/contrib/testing/browser/theme'; import { AutoOpenPeekViewWhen, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; import { Testing } from 'vs/workbench/contrib/testing/common/constants'; -import { ITestItem, ITestMessage, ITestState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestItem, ITestMessage, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { isFailedState } from 'vs/workbench/contrib/testing/common/testingStates'; import { buildTestUri, parseTestUri, TestUriType } from 'vs/workbench/contrib/testing/common/testingUri'; @@ -42,7 +42,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic interface ITestDto { test: ITestItem, messageIndex: number; - state: ITestState; + messages: ITestMessage[]; expectedUri: URI; actualUri: URI; messageUri: URI; @@ -78,12 +78,12 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener * @returns a boolean if a peek was opened */ public async tryPeekFirstError(result: ITestResult, test: TestResultItem, options?: Partial) { - const index = test.state.messages.findIndex(m => !!m.location); - if (index === -1) { + const candidate = this.getCandidateMessage(test); + if (!candidate) { return false; } - const message = test.state.messages[index]; + const message = candidate.message; const pane = await this.editorService.openEditor({ resource: message.location!.uri, options: { selection: message.location!.range, revealIfOpened: true, ...options } @@ -96,7 +96,8 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener TestingOutputPeekController.get(control).show(buildTestUri({ type: TestUriType.ResultMessage, - messageIndex: index, + taskIndex: candidate.taskId, + messageIndex: candidate.index, resultId: result.id, testExtId: test.item.extId, })); @@ -112,7 +113,8 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener return; } - if (!isFailedState(evt.item.state.state) || !evt.item.state.messages.length) { + const candidate = this.getCandidateMessage(evt.item); + if (!candidate) { return; } @@ -137,6 +139,24 @@ export class TestingPeekOpener extends Disposable implements ITestingPeekOpener this.tryPeekFirstError(evt.result, evt.item); } + + private getCandidateMessage(test: TestResultItem) { + for (let taskId = 0; taskId < test.tasks.length; taskId++) { + const { messages, state } = test.tasks[taskId]; + if (!isFailedState(state)) { + continue; + } + + const index = messages.findIndex(m => !!m.location); + if (index === -1) { + continue; + } + + return { taskId, index, message: messages[index] }; + } + + return undefined; + } } /** @@ -205,7 +225,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo return; } - const message = dto.state.messages[dto.messageIndex]; + const message = dto.messages[dto.messageIndex]; if (!message?.location) { return; } @@ -253,7 +273,7 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo * else, then clear the peek. */ private closePeekOnTestChange(evt: TestResultItemChange) { - if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous === evt.item.state.state) { + if (evt.reason !== TestResultItemChangeReason.OwnStateChange || evt.previous === evt.item.ownComputedState) { return; } @@ -273,9 +293,13 @@ export class TestingOutputPeekController extends Disposable implements IEditorCo } const test = this.testResults.getResult(parts.resultId)?.getStateById(parts.testExtId); + if (!test || !test.tasks[parts.taskIndex]) { + return; + } + return test && { test: test.item, - state: test.state, + messages: test.tasks[parts.taskIndex].messages, messageIndex: parts.messageIndex, expectedUri: buildTestUri({ ...parts, type: TestUriType.ResultExpectedOutput }), actualUri: buildTestUri({ ...parts, type: TestUriType.ResultActualOutput }), @@ -382,8 +406,8 @@ class TestingDiffOutputPeek extends TestingOutputPeek { /** * @override */ - public async setModel({ test, state, messageIndex, expectedUri, actualUri }: ITestDto) { - const message = state.messages[messageIndex]; + public async setModel({ test, messages, messageIndex, expectedUri, actualUri }: ITestDto) { + const message = messages[messageIndex]; if (!message?.location) { return; } @@ -440,8 +464,8 @@ class TestingMessageOutputPeek extends TestingOutputPeek { /** * @override */ - public async setModel({ state, test, messageIndex, messageUri }: ITestDto) { - const message = state.messages[messageIndex]; + public async setModel({ messages, test, messageIndex, messageUri }: ITestDto) { + const message = messages[messageIndex]; if (!message?.location) { return; } diff --git a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts index 3d96e10cdbc..0e5cd72e71b 100644 --- a/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts +++ b/src/vs/workbench/contrib/testing/common/ownedTestCollection.ts @@ -7,8 +7,10 @@ import { mapFind } from 'vs/base/common/arrays'; import { DeferredPromise, isThenable, RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { assertNever } from 'vs/base/common/types'; +import { ExtHostTestItemEvent, ExtHostTestItemEventType, getPrivateApiFor } from 'vs/workbench/api/common/extHostTestingPrivateApi'; import * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; -import { TestItem as TestItemImpl, TestItemHookProperty } from 'vs/workbench/api/common/extHostTypes'; +import { TestItemImpl, TestItemStatus } from 'vs/workbench/api/common/extHostTypes'; import { applyTestItemUpdate, InternalTestItem, TestDiffOpType, TestItemExpandState, TestsDiff, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testCollection'; type TestItemRaw = Convert.TestItem.Raw; @@ -209,8 +211,8 @@ export class SingleUseTestCollection implements IDisposable { /** * Adds a new root node to the collection. */ - public addRoot(item: TestItemRaw, providerId: string) { - this.addItem(item, providerId, null); + public addRoot(item: TestItemRaw, controllerId: string) { + this.addItem(item, controllerId, null); } /** @@ -290,7 +292,7 @@ export class SingleUseTestCollection implements IDisposable { public dispose() { for (const item of this.testItemToInternal.values()) { item.discoverCts?.dispose(true); - (item.actual as TestItemImpl)[TestItemHookProperty] = undefined; + getPrivateApiFor(item.actual).bus.dispose(); } this.diff = []; @@ -298,7 +300,42 @@ export class SingleUseTestCollection implements IDisposable { this.debounceSendDiff.dispose(); } - private addItem(actual: TestItemRaw, providerId: string, parent: OwnedCollectionTestItem | null) { + private onTestItemEvent(internal: OwnedCollectionTestItem, evt: ExtHostTestItemEvent) { + const extId = internal?.actual.id; + + switch (evt[0]) { + case ExtHostTestItemEventType.Invalidated: + this.pushDiff([TestDiffOpType.Retire, extId]); + break; + + case ExtHostTestItemEventType.Disposed: + this.removeItem(internal); + break; + + case ExtHostTestItemEventType.NewChild: + this.addItem(evt[1], internal.src.controller, internal); + break; + + case ExtHostTestItemEventType.SetProp: + const [_, key, value] = evt; + switch (key) { + case 'status': + this.updateExpandability(internal); + break; + case 'range': + this.pushDiff([TestDiffOpType.Update, { extId, item: { range: Convert.Range.from(value) }, }]); + break; + default: + this.pushDiff([TestDiffOpType.Update, { extId, item: { [key]: value } }]); + break; + } + break; + default: + assertNever(evt[0]); + } + } + + private addItem(actual: TestItemRaw, controllerId: string, parent: OwnedCollectionTestItem | null) { if (!(actual instanceof TestItemImpl)) { throw new Error(`TestItems provided to the VS Code API must extend \`vscode.TestItem\`, but ${actual.id} did not`); } @@ -312,15 +349,15 @@ export class SingleUseTestCollection implements IDisposable { } const parentId = parent ? parent.item.extId : null; - const expand = actual.expandable ? TestItemExpandState.Expandable : TestItemExpandState.NotExpandable; + const expand = actual.resolveHandler ? TestItemExpandState.Expandable : TestItemExpandState.NotExpandable; const pExpandLvls = parent?.expandLevels; - const src = { provider: providerId, tree: this.testIdToInternal.object.id }; + const src = { controller: controllerId, tree: this.testIdToInternal.object.id }; const internal: OwnedCollectionTestItem = { actual, parent: parentId, item: Convert.TestItem.from(actual), - expandLevels: pExpandLvls && expand === TestItemExpandState.Expandable ? pExpandLvls - 1 : undefined, - expand, + expandLevels: pExpandLvls /* intentionally undefined or 0 */ ? pExpandLvls - 1 : undefined, + expand: TestItemExpandState.NotExpandable, // updated by `updateExpandability` down below src, }; @@ -328,19 +365,49 @@ export class SingleUseTestCollection implements IDisposable { this.testItemToInternal.set(actual, internal); this.pushDiff([TestDiffOpType.Add, { parent: parentId, src, expand, item: internal.item }]); - actual[TestItemHookProperty] = { - created: item => this.addItem(item, providerId, internal!), - delete: id => this.removeItembyId(id), - invalidate: item => this.pushDiff([TestDiffOpType.Retire, item]), - setProp: (key, value) => this.pushDiff([TestDiffOpType.Update, { - extId: actual.id, - item: { [key]: key === 'range' ? Convert.Range.from(value as any) : value }, - }]) - }; + const api = getPrivateApiFor(actual); + api.parent = parent?.actual; + api.bus.event(this.onTestItemEvent.bind(this, internal)); + + // important that this comes after binding the event bus otherwise we + // might miss a synchronous discovery completion + this.updateExpandability(internal); // Discover any existing children that might have already been added - for (const child of actual.children) { - this.addItem(child, providerId, internal); + for (const child of api.children.values()) { + if (!this.testItemToInternal.has(child)) { + this.addItem(child, controllerId, internal); + } + } + } + + /** + * Updates the `expand` state of the item. Should be called whenever the + * resolved state of the item changes. Can automatically expand the item + * if requested by a consumer. + */ + private updateExpandability(internal: OwnedCollectionTestItem) { + let newState: TestItemExpandState; + if (!internal.actual.resolveHandler) { + newState = TestItemExpandState.NotExpandable; + } else if (internal.actual.status === TestItemStatus.Pending) { + newState = internal.discoverCts + ? TestItemExpandState.BusyExpanding + : TestItemExpandState.Expandable; + } else { + internal.initialExpand?.complete(); + newState = TestItemExpandState.Expanded; + } + + if (newState === internal.expand) { + return; + } + + internal.expand = newState; + this.pushDiff([TestDiffOpType.Update, { extId: internal.actual.id, expand: newState }]); + + if (newState === TestItemExpandState.Expandable && internal.expandLevels !== undefined) { + this.refreshChildren(internal); } } @@ -354,8 +421,8 @@ export class SingleUseTestCollection implements IDisposable { return; } - const asyncChildren = [...internal.actual.children] - .map(c => this.expand(c.id, levels - 1)) + const asyncChildren = [...internal.actual.children.values()] + .map(c => this.expand(c.id, levels)) .filter(isThenable); if (asyncChildren.length) { @@ -371,37 +438,30 @@ export class SingleUseTestCollection implements IDisposable { internal.discoverCts.dispose(true); } + if (!internal.actual.resolveHandler) { + const p = new DeferredPromise(); + p.complete(); + return p; + } + internal.expand = TestItemExpandState.BusyExpanding; internal.discoverCts = new CancellationTokenSource(); this.pushExpandStateUpdate(internal); - const updateComplete = new DeferredPromise(); - internal.initialExpand = updateComplete; + internal.initialExpand = new DeferredPromise(); + internal.actual.resolveHandler(internal.discoverCts.token); - internal.actual.discoverChildren({ - report: event => { - if (!event.busy) { - internal.expand = TestItemExpandState.Expanded; - if (!updateComplete.isSettled) { updateComplete.complete(); } - this.pushExpandStateUpdate(internal); - } else { - internal.expand = TestItemExpandState.BusyExpanding; - this.pushExpandStateUpdate(internal); - } - } - }, internal.discoverCts.token); - - return updateComplete; + return internal.initialExpand; } private pushExpandStateUpdate(internal: OwnedCollectionTestItem) { this.pushDiff([TestDiffOpType.Update, { extId: internal.actual.id, expand: internal.expand }]); } - private removeItembyId(id: string) { - this.pushDiff([TestDiffOpType.Remove, id]); + private removeItem(internal: OwnedCollectionTestItem) { + this.pushDiff([TestDiffOpType.Remove, internal.actual.id]); - const queue = [this.testIdToInternal.object.get(id)]; + const queue: (OwnedCollectionTestItem | undefined)[] = [internal]; while (queue.length) { const item = queue.pop(); if (!item) { @@ -411,11 +471,12 @@ export class SingleUseTestCollection implements IDisposable { item.discoverCts?.dispose(true); this.testIdToInternal.object.delete(item.item.extId); this.testItemToInternal.delete(item.actual); - for (const child of item.actual.children) { + for (const child of item.actual.children.values()) { queue.push(this.testIdToInternal.object.get(child.id)); } } } + public flushDiff() { const diff = this.collectDiff(); if (diff.length) { diff --git a/src/vs/workbench/contrib/testing/common/testCollection.ts b/src/vs/workbench/contrib/testing/common/testCollection.ts index 7923e668bd4..eaf56c459fb 100644 --- a/src/vs/workbench/contrib/testing/common/testCollection.ts +++ b/src/vs/workbench/contrib/testing/common/testCollection.ts @@ -9,9 +9,11 @@ import { IRange, Range } from 'vs/editor/common/core/range'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { TestMessageSeverity, TestResultState } from 'vs/workbench/api/common/extHostTypes'; -export interface TestIdWithSrc { +export type TestIdWithSrc = Required; + +export interface TestIdWithMaybeSrc { testId: string; - src: { provider: string; tree: number }; + src?: { controller: string; tree: number }; } /** @@ -24,14 +26,25 @@ export type TestIdPath = string[]; * Request to the main thread to run a set of tests. */ export interface RunTestsRequest { - tests: TestIdWithSrc[]; + tests: TestIdWithMaybeSrc[]; exclude?: string[]; debug: boolean; isAutoRun?: boolean; } /** - * Request from the main thread to run tests for a single provider. + * Request to the main thread to run a set of tests. + */ +export interface ExtensionRunTestsRequest { + id: string; + tests: string[]; + exclude: string[]; + debug: boolean; + persist: boolean; +} + +/** + * Request from the main thread to run tests for a single controller. */ export interface RunTestForProviderRequest { runId: string; @@ -56,17 +69,23 @@ export interface ITestMessage { location: IRichLocation | undefined; } -export interface ITestState { +export interface ITestTaskState { state: TestResultState; duration: number | undefined; messages: ITestMessage[]; } +export interface ITestRunTask { + id: string; + name: string | undefined; + running: boolean; +} + /** * The TestItem from .d.ts, as a plain object without children. */ export interface ITestItem { - /** ID of the test given by the test provider */ + /** ID of the test given by the test controller */ extId: string; label: string; children?: never; @@ -75,7 +94,6 @@ export interface ITestItem { description: string | undefined; runnable: boolean; debuggable: boolean; - expandable: boolean; } export const enum TestItemExpandState { @@ -89,7 +107,7 @@ export const enum TestItemExpandState { * TestItem-like shape, butm with an ID and children as strings. */ export interface InternalTestItem { - src: { provider: string; tree: number }; + src: { controller: string; tree: number }; expand: TestItemExpandState; parent: string | null; item: ITestItem; @@ -116,9 +134,15 @@ export const applyTestItemUpdate = (internal: InternalTestItem | ITestItemUpdate /** * Test result item used in the main thread. */ -export interface TestResultItem extends IncrementalTestCollectionItem { - /** Current state of this test */ - state: ITestState; +export interface TestResultItem { + /** Parent ID, if any */ + parent: string | null; + /** Raw test item properties */ + item: ITestItem; + /** State of this test in various tasks */ + tasks: ITestTaskState[]; + /** State of this test as a computation of its tasks */ + ownComputedState: TestResultState; /** Computed state based on children */ computedState: TestResultState; /** True if the test is outdated */ @@ -142,6 +166,8 @@ export interface ISerializedTestResults { output?: string; /** Subset of test result items */ items: SerializedTestResultItem[]; + /** Tasks involved in the run. */ + tasks: ITestRunTask[]; } export const enum TestDiffOpType { @@ -151,7 +177,7 @@ export const enum TestDiffOpType { Update, /** Removes a test (and all its children) */ Remove, - /** Changes the number of providers who are yet to publish their collection roots. */ + /** Changes the number of controllers who are yet to publish their collection roots. */ DeltaRootsComplete, /** Retires a test/result */ Retire, @@ -227,9 +253,9 @@ export abstract class AbstractIncrementalTestCollection(); /** - * Number of 'busy' providers. + * Number of 'busy' controllers. */ - protected busyProviderCount = 0; + protected busyControllerCount = 0; /** * Number of pending roots. @@ -260,7 +286,7 @@ export abstract class AbstractIncrementalTestCollection; + /** + * List of this result's subtasks. + */ + tasks: ReadonlyArray; + /** * Gets the state of the test by its extension-assigned ID. */ @@ -166,77 +170,20 @@ export class LiveOutputController { } } +interface TestResultItemWithChildren extends TestResultItem { + /** Children in the run */ + children: TestResultItemWithChildren[]; +} -const itemToNode = ( - item: IncrementalTestCollectionItem, - byExtId: Map, -): TestResultItem => { - const n: TestResultItem = { - ...item, - // shallow-clone the test to take a 'snapshot' of it at the point in time where tests run - item: { ...item.item }, - children: new Set(item.children), - state: { - duration: undefined, - messages: [], - state: TestResultState.Unset - }, - computedState: TestResultState.Unset, - retired: false, - }; - - byExtId.set(n.item.extId, n); - - return n; -}; - -const makeParents = ( - collection: IMainThreadTestCollection, - child: IncrementalTestCollectionItem, - byExtId: Map, -) => { - const parent = child.parent && collection.getNodeById(child.parent); - if (!parent) { - return; - } - - let parentResultItem = byExtId.get(parent.item.extId); - if (parentResultItem) { - parentResultItem.children.add(child.item.extId); - return; // no need to recurse, all parents already in result - } - - parentResultItem = itemToNode(parent, byExtId); - parentResultItem.children = new Set([child.item.extId]); - makeParents(collection, parent, byExtId); -}; - -const makeNodeAndChildren = ( - collection: IMainThreadTestCollection, - test: IncrementalTestCollectionItem, - excluded: ReadonlySet, - byExtId: Map, - isExecutedDirectly = true, -): TestResultItem => { - const existing = byExtId.get(test.item.extId); - if (existing) { - return existing; - } - - const mapped = itemToNode(test, byExtId); - if (isExecutedDirectly) { - mapped.direct = true; - } - - for (const childId of test.children) { - const child = collection.getNodeById(childId); - if (child && !excluded.has(childId)) { - makeNodeAndChildren(collection, child, excluded, byExtId, false); - } - } - - return mapped; -}; +const itemToNode = (item: ITestItem, parent: string | null): TestResultItemWithChildren => ({ + parent, + item: { ...item }, + children: [], + tasks: [], + ownComputedState: TestResultState.Unset, + computedState: TestResultState.Unset, + retired: false, +}); export const enum TestResultItemChangeReason { Retired, @@ -255,39 +202,29 @@ export type TestResultItemChange = { item: TestResultItem; result: ITestResult } * and marked as "complete" when the run finishes. */ export class LiveTestResult implements ITestResult { - /** - * Creates a new TestResult, pulling tests from the associated list - * of collections. - */ - public static from( - resultId: string, - collections: ReadonlyArray, - output: LiveOutputController, - req: RunTestsRequest, - ) { - const testByExtId = new Map(); - const excludeSet = new Set(req.exclude); - for (const test of req.tests) { - for (const collection of collections) { - const node = collection.getNodeById(test.testId); - if (!node) { - continue; - } - - makeNodeAndChildren(collection, node, excludeSet, testByExtId); - makeParents(collection, node, testByExtId); - } - } - - return new LiveTestResult(resultId, collections, testByExtId, excludeSet, output, !!req.isAutoRun); - } - private readonly completeEmitter = new Emitter(); private readonly changeEmitter = new Emitter(); + private readonly testById = new Map(); private _completedAt?: number; public readonly onChange = this.changeEmitter.event; public readonly onComplete = this.completeEmitter.event; + public readonly tasks: ITestRunTask[] = []; + + /** + * Test IDs directly included in this run. + */ + public readonly includedIds: ReadonlySet; + + /** + * Test IDs excluded from this run. + */ + public readonly excludedIds: ReadonlySet; + + /** + * Gets whether this test is from an auto-run. + */ + public readonly isAutoRun: boolean; /** * @inheritdoc @@ -308,21 +245,11 @@ export class LiveTestResult implements ITestResult { return this.testById.values(); } - private readonly computedStateAccessor: IComputedStateAccessor = { - getOwnState: i => i.state.state, + private readonly computedStateAccessor: IComputedStateAccessor = { + getOwnState: i => i.ownComputedState, getCurrentComputedState: i => i.computedState, setComputedState: (i, s) => i.computedState = s, - getChildren: i => { - const { testById: testByExtId } = this; - return (function* () { - for (const childId of i.children) { - const child = testByExtId.get(childId); - if (child) { - yield child; - } - } - })(); - }, + getChildren: i => i.children[Symbol.iterator](), getParents: i => { const { testById: testByExtId } = this; return (function* () { @@ -341,13 +268,12 @@ export class LiveTestResult implements ITestResult { constructor( public readonly id: string, - private readonly collections: ReadonlyArray, - private readonly testById: Map, - private readonly excluded: ReadonlySet, public readonly output: LiveOutputController, - public readonly isAutoRun: boolean, + private readonly req: ExtensionRunTestsRequest | RunTestsRequest, ) { - this.counts[TestResultState.Unset] = testById.size; + this.isAutoRun = 'isAutoRun' in this.req && !!this.req.isAutoRun; + this.includedIds = new Set(req.tests.map(t => typeof t === 'string' ? t : t.testId)); + this.excludedIds = new Set(req.exclude); } /** @@ -358,47 +284,71 @@ export class LiveTestResult implements ITestResult { } /** - * Updates all tests in the collection to the given state. + * Adds a new run task to the results. */ - public setAllToState(state: TestResultState, when: (_t: TestResultItem) => boolean) { - for (const test of this.testById.values()) { - if (when(test)) { - this.fireUpdateAndRefresh(test, state); - } + public addTask(task: ITestRunTask) { + const index = this.tasks.length; + this.tasks.push(task); + + for (const test of this.tests) { + test.tasks.push({ duration: undefined, messages: [], state: TestResultState.Unset }); + this.fireUpdateAndRefresh(test, index, TestResultState.Queued); } } + /** + * Add the chain of tests to the run. The first test in the chain should + * be either a test root, or a previously-known test. + */ + public addTestChainToRun(chain: ReadonlyArray) { + let parent = this.testById.get(chain[0].extId); + if (!parent) { // must be a test root + parent = this.addTestToRun(chain[0], null); + } + + for (let i = 1; i < chain.length; i++) { + parent = this.addTestToRun(chain[i], parent.item.extId); + } + + for (let i = 0; i < this.tasks.length; i++) { + this.fireUpdateAndRefresh(parent, i, TestResultState.Queued); + } + + return undefined; + } + /** * Updates the state of the test by its internal ID. */ - public updateState(testId: string, state: TestResultState, duration?: number) { - const entry = this.testById.get(testId) ?? this.addTestToRun(testId); + public updateState(testId: string, taskId: string, state: TestResultState, duration?: number) { + const entry = this.testById.get(testId); if (!entry) { return; } + const index = this.mustGetTaskIndex(taskId); if (duration !== undefined) { - entry.state.duration = duration; + entry.tasks[index].duration = duration; } - this.fireUpdateAndRefresh(entry, state); + this.fireUpdateAndRefresh(entry, index, state); } /** * Appends a message for the test in the run. */ - public appendMessage(testId: string, message: ITestMessage) { - const entry = this.testById.get(testId) ?? this.addTestToRun(testId); + public appendMessage(testId: string, taskId: string, message: ITestMessage) { + const entry = this.testById.get(testId); if (!entry) { return; } - entry.state.messages.push(message); + entry.tasks[this.mustGetTaskIndex(taskId)].messages.push(message); this.changeEmitter.fire({ item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, - previous: entry.state.state, + previous: entry.ownComputedState, }); } @@ -409,24 +359,6 @@ export class LiveTestResult implements ITestResult { return this.output.read(); } - private fireUpdateAndRefresh(entry: TestResultItem, newState: TestResultState) { - const previous = entry.state.state; - if (newState === previous) { - return; - } - - entry.state.state = newState; - this.counts[previous]--; - this.counts[newState]++; - refreshComputedState(this.computedStateAccessor, entry, t => - this.changeEmitter.fire( - t === entry - ? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous } - : { item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange } - ), - ); - } - /** * Marks a test as retired. This can trigger it to be rerun in live mode. */ @@ -436,11 +368,10 @@ export class LiveTestResult implements ITestResult { return; } - const queue: Iterable[] = [[root.item.extId]]; + const queue = [[root]]; while (queue.length) { - for (const id of queue.pop()!) { - const entry = this.testById.get(id); - if (entry && !entry.retired) { + for (const entry of queue.pop()!) { + if (!entry.retired) { entry.retired = true; queue.push(entry.children); this.changeEmitter.fire({ @@ -456,23 +387,15 @@ export class LiveTestResult implements ITestResult { } /** - * Adds a test, by its ID, to the test run. This can end up being called - * if tests were started while discovery was still happening, so initially - * we didn't serialize/capture the test. + * Marks the task in the test run complete. */ - private addTestToRun(testId: string) { - for (const collection of this.collections) { - let test = collection.getNodeById(testId); - if (test) { - const originalSize = this.testById.size; - makeParents(collection, test, this.testById); - const node = makeNodeAndChildren(collection, test, this.excluded, this.testById, false); - this.counts[TestResultState.Unset] += this.testById.size - originalSize; - return node; - } - } - - return undefined; + public markTaskComplete(taskId: string) { + this.tasks[this.mustGetTaskIndex(taskId)].running = false; + this.setAllToState( + TestResultState.Unset, + taskId, + t => t.state === TestResultState.Queued || t.state === TestResultState.Running, + ); } /** @@ -483,11 +406,11 @@ export class LiveTestResult implements ITestResult { throw new Error('cannot complete a test result multiple times'); } - // un-queue any tests that weren't explicitly updated - this.setAllToState( - TestResultState.Unset, - t => t.state.state === TestResultState.Queued || t.state.state === TestResultState.Running, - ); + for (const task of this.tasks) { + if (task.running) { + this.markTaskComplete(task.id); + } + } this._completedAt = Date.now(); this.completeEmitter.fire(); @@ -497,16 +420,80 @@ export class LiveTestResult implements ITestResult { * @inheritdoc */ public toJSON(): ISerializedTestResults | undefined { - return this.completedAt ? this.doSerialize.getValue() : undefined; + return this.completedAt && !('persist' in this.req && this.req.persist === false) + ? this.doSerialize.getValue() + : undefined; + } + + /** + * Updates all tests in the collection to the given state. + */ + protected setAllToState(state: TestResultState, taskId: string, when: (task: ITestTaskState, item: TestResultItem) => boolean) { + const index = this.mustGetTaskIndex(taskId); + for (const test of this.testById.values()) { + if (when(test.tasks[index], test)) { + this.fireUpdateAndRefresh(test, index, state); + } + } + } + + private fireUpdateAndRefresh(entry: TestResultItem, taskIndex: number, newState: TestResultState) { + const previousOwnComputed = entry.ownComputedState; + entry.tasks[taskIndex].state = newState; + const newOwnComputed = maxPriority(...entry.tasks.map(t => t.state)); + if (newOwnComputed === previousOwnComputed) { + return; + } + + entry.ownComputedState = newOwnComputed; + this.counts[previousOwnComputed]--; + this.counts[newOwnComputed]++; + refreshComputedState(this.computedStateAccessor, entry, t => + this.changeEmitter.fire( + t === entry + ? { item: entry, result: this, reason: TestResultItemChangeReason.OwnStateChange, previous: previousOwnComputed } + : { item: t, result: this, reason: TestResultItemChangeReason.ComputedStateChange } + ), + ); + } + + private addTestToRun(item: ITestItem, parent: string | null) { + const node = itemToNode(item, parent); + node.direct = this.includedIds.has(item.extId); + this.testById.set(item.extId, node); + this.counts[TestResultState.Unset]++; + + if (parent) { + this.testById.get(parent)?.children.push(node); + } + + if (this.tasks.length) { + for (let i = 0; i < this.tasks.length; i++) { + node.tasks.push({ duration: undefined, messages: [], state: TestResultState.Queued }); + } + } + + return node; + } + + private mustGetTaskIndex(taskId: string) { + const index = this.tasks.findIndex(t => t.id === taskId); + if (index === -1) { + throw new Error(`Unknown task ${taskId} in updateState`); + } + + return index; } private readonly doSerialize = new Lazy((): ISerializedTestResults => ({ id: this.id, completedAt: this.completedAt!, + tasks: this.tasks, items: [...this.testById.values()].map(entry => ({ ...entry, retired: undefined, - children: [...entry.children], + src: undefined, + children: [...entry.children.map(c => c.item.extId)], })), })); } @@ -530,6 +517,11 @@ export class HydratedTestResult implements ITestResult { */ public readonly completedAt: number; + /** + * @inheritdoc + */ + public readonly tasks: ITestRunTask[]; + /** * @inheritdoc */ @@ -546,19 +538,22 @@ export class HydratedTestResult implements ITestResult { ) { this.id = serialized.id; this.completedAt = serialized.completedAt; + this.tasks = serialized.tasks; for (const item of serialized.items) { - const cast: TestResultItem = { ...item, retired: true, children: new Set(item.children) }; + const cast: TestResultItem = { ...item, retired: true }; cast.item.uri = URI.revive(cast.item.uri); - for (const message of cast.state.messages) { - if (message.location) { - message.location.uri = URI.revive(message.location.uri); - message.location.range = Range.lift(message.location.range); + for (const task of cast.tasks) { + for (const message of task.messages) { + if (message.location) { + message.location.uri = URI.revive(message.location.uri); + message.location.range = Range.lift(message.location.range); + } } } - this.counts[item.state.state]++; + this.counts[item.ownComputedState]++; this.testById.set(item.item.extId, cast); } } diff --git a/src/vs/workbench/contrib/testing/common/testResultService.ts b/src/vs/workbench/contrib/testing/common/testResultService.ts index 8db013579e6..2657bbed62e 100644 --- a/src/vs/workbench/contrib/testing/common/testResultService.ts +++ b/src/vs/workbench/contrib/testing/common/testResultService.ts @@ -7,17 +7,14 @@ import { findFirstInSorted } from 'vs/base/common/arrays'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { once } from 'vs/base/common/functional'; -import { Iterable } from 'vs/base/common/iterator'; -import { equals } from 'vs/base/common/objects'; import { generateUuid } from 'vs/base/common/uuid'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TestResultState } from 'vs/workbench/api/common/extHostTypes'; -import { RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ExtensionRunTestsRequest, RunTestsRequest, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { ITestResult, LiveTestResult, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { IMainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testService'; export type ResultChangeEvent = | { completed: LiveTestResult } @@ -50,7 +47,7 @@ export interface ITestResultService { /** * Creates a new, live test result. */ - createLiveResult(collections: ReadonlyArray, req: RunTestsRequest): LiveTestResult; + createLiveResult(req: RunTestsRequest | ExtensionRunTestsRequest): LiveTestResult; /** * Adds a new test result to the collection. @@ -70,14 +67,6 @@ export interface ITestResultService { export const ITestResultService = createDecorator('testResultService'); -/** - * Returns if the tests in the results are exactly equal. Check the counts - * first as a cheap check before starting to iterate. - */ -const resultsEqual = (a: ITestResult, b: ITestResult) => - a.completedAt === b.completedAt && equals(a.counts, b.counts) && Iterable.equals(a.tests, b.tests, - (at, bt) => equals(at.state, bt.state) && equals(at.item, bt.item)); - export class TestResultService implements ITestResultService { declare _serviceBrand: undefined; private changeResultEmitter = new Emitter(); @@ -135,9 +124,13 @@ export class TestResultService implements ITestResultService { /** * @inheritdoc */ - public createLiveResult(collections: ReadonlyArray, req: RunTestsRequest) { - const id = generateUuid(); - return this.push(LiveTestResult.from(id, collections, this.storage.getOutputController(id), req)); + public createLiveResult(req: RunTestsRequest | ExtensionRunTestsRequest) { + if ('id' in req) { + return this.push(new LiveTestResult(req.id, this.storage.getOutputController(req.id), req)); + } else { + const id = generateUuid(); + return this.push(new LiveTestResult(id, this.storage.getOutputController(id), req)); + } } /** @@ -148,11 +141,6 @@ export class TestResultService implements ITestResultService { this.results.unshift(result); } else { const index = findFirstInSorted(this.results, r => r.completedAt !== undefined && r.completedAt <= result.completedAt!); - const prev = this.results[index]; - if (prev && resultsEqual(result, prev)) { - return result; - } - this.results.splice(index, 0, result); this.persistScheduler.schedule(); } @@ -166,7 +154,6 @@ export class TestResultService implements ITestResultService { result.onChange(this.testChangeEmitter.fire, this.testChangeEmitter); this.isRunning.set(true); this.changeResultEmitter.fire({ started: result }); - result.setAllToState(TestResultState.Queued, () => true); } else { this.changeResultEmitter.fire({ inserted: result }); // If this is not a new result, go through each of its tests. For each diff --git a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts index a4f609925b5..0cd4a5515df 100644 --- a/src/vs/workbench/contrib/testing/common/testServiceImpl.ts +++ b/src/vs/workbench/contrib/testing/common/testServiceImpl.ts @@ -3,16 +3,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { groupBy } from 'vs/base/common/arrays'; +import { groupBy, mapFind } from 'vs/base/common/arrays'; import { disposableTimeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable, IReference, toDisposable } from 'vs/base/common/lifecycle'; +import { isDefined } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; +import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { ExtHostTestingResource } from 'vs/workbench/api/common/extHost.protocol'; import { MutableObservableValue } from 'vs/workbench/contrib/testing/common/observableValue'; import { StoredValue } from 'vs/workbench/contrib/testing/common/storedValue'; @@ -63,6 +65,7 @@ export class TestService extends Disposable implements ITestService { @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @ITestResultService private readonly testResults: ITestResultService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, ) { super(); this.providerCount = TestingContextKeys.providerCount.bindTo(contextKeyService); @@ -74,7 +77,7 @@ export class TestService extends Disposable implements ITestService { * @inheritdoc */ public async expandTest(test: TestIdWithSrc, levels: number) { - await this.testControllers.get(test.src.provider)?.expandTest(test, levels); + await this.testControllers.get(test.src.controller)?.expandTest(test, levels); } /** @@ -159,7 +162,7 @@ export class TestService extends Disposable implements ITestService { } } - return this.testControllers.get(test.src.provider)?.lookupTest(test); + return this.testControllers.get(test.src.controller)?.lookupTest(test); } /** @@ -193,18 +196,36 @@ export class TestService extends Disposable implements ITestService { req.exclude = [...this.excludeTests.value]; } - const subscriptions = [...this.testSubscriptions.values()] - .filter(v => req.tests.some(t => v.collection.getNodeById(t.testId))) - .map(s => this.subscribeToDiffs(s.ident.resource, s.ident.uri)); - const result = this.testResults.createLiveResult(subscriptions.map(s => s.object), req); + const result = this.testResults.createLiveResult(req); + const trust = await this.workspaceTrustRequestService.requestWorkspaceTrust({ + modal: true, + message: localize('testTrust', "Running tests may execute code in your workspace."), + }); + + if (!trust) { + return result; + } + + const testsWithIds = req.tests.map(test => { + if (test.src) { + return test as TestIdWithSrc; + } + + const subscribed = mapFind(this.testSubscriptions.values(), s => s.collection.getNodeById(test.testId)); + if (!subscribed) { + return undefined; + } + + return { testId: test.testId, src: subscribed.src }; + }).filter(isDefined); try { - const tests = groupBy(req.tests, (a, b) => a.src.provider === b.src.provider ? 0 : 1); + const tests = groupBy(testsWithIds, (a, b) => a.src.controller === b.src.controller ? 0 : 1); const cancelSource = new CancellationTokenSource(token); this.runningTests.set(req, cancelSource); const requests = tests.map( - group => this.testControllers.get(group[0].src.provider)?.runTests( + group => this.testControllers.get(group[0].src.controller)?.runTests( { runId: result.id, debug: req.debug, @@ -221,7 +242,6 @@ export class TestService extends Disposable implements ITestService { return result; } finally { this.runningTests.delete(req); - subscriptions.forEach(s => s.dispose()); result.markComplete(); } } @@ -372,7 +392,7 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< * @inheritdoc */ public get busyProviders() { - return this.busyProviderCount; + return this.busyControllerCount; } /** @@ -457,12 +477,12 @@ export class MainThreadTestCollection extends AbstractIncrementalTestCollection< * Applies the diff to the collection. */ public override apply(diff: TestsDiff) { - let prevBusy = this.busyProviderCount; + let prevBusy = this.busyControllerCount; let prevPendingRoots = this.pendingRootCount; super.apply(diff); - if (prevBusy !== this.busyProviderCount) { - this.busyProvidersChangeEmitter.fire(this.busyProviderCount); + if (prevBusy !== this.busyControllerCount) { + this.busyProvidersChangeEmitter.fire(this.busyControllerCount); } if (prevPendingRoots !== this.pendingRootCount) { this.pendingRootChangeEmitter.fire(this.pendingRootCount); diff --git a/src/vs/workbench/contrib/testing/common/testStubs.ts b/src/vs/workbench/contrib/testing/common/testStubs.ts index 5c0e5241df6..d159f66d391 100644 --- a/src/vs/workbench/contrib/testing/common/testStubs.ts +++ b/src/vs/workbench/contrib/testing/common/testStubs.ts @@ -3,28 +3,45 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; import { URI } from 'vs/base/common/uri'; -import { IProgress } from 'vs/platform/progress/common/progress'; -import { TestItem, TestResultState } from 'vs/workbench/api/common/extHostTypes'; +import { TestItemImpl, TestItemStatus, TestResultState } from 'vs/workbench/api/common/extHostTypes'; -export class StubTestItem extends TestItem { - parent: StubTestItem | undefined; +export { TestItemImpl, TestResultState } from 'vs/workbench/api/common/extHostTypes'; +export * as Convert from 'vs/workbench/api/common/extHostTypeConverters'; - constructor(id: string, label: string, private readonly pendingChildren: StubTestItem[]) { - super(id, label, URI.file('/'), pendingChildren.length > 0); +export const stubTest = (label: string, idPrefix = 'id-', children: TestItemImpl[] = []): TestItemImpl => { + const item = new TestItemImpl(idPrefix + label, label, URI.file('/'), undefined); + if (children.length) { + item.status = TestItemStatus.Pending; + item.resolveHandler = () => { + for (const child of children) { + item.addChild(child); + } + + item.status = TestItemStatus.Resolved; + }; } - public override discoverChildren(progress: IProgress<{ busy: boolean }>) { - for (const child of this.pendingChildren) { - this.children.add(child); + return item; +}; + +export const testStubsChain = (stub: TestItemImpl, path: string[], slice = 0) => { + const tests = [stub]; + for (const segment of path) { + if (stub.status !== TestItemStatus.Resolved) { + stub.resolveHandler!(CancellationToken.None); } - progress.report({ busy: false }); - } -} + stub = stub.children.get(segment)!; + if (!stub) { + throw new Error(`missing child ${segment}`); + } -export const stubTest = (label: string, idPrefix = 'id-', children: StubTestItem[] = []): StubTestItem => { - return new StubTestItem(idPrefix + label, label, children); + tests.push(stub); + } + + return tests.slice(slice); }; export const testStubs = { diff --git a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts index 36a375b2a11..1e44ba1ba3f 100644 --- a/src/vs/workbench/contrib/testing/common/testingAutoRun.ts +++ b/src/vs/workbench/contrib/testing/common/testingAutoRun.ts @@ -10,7 +10,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { AutoRunMode, getTestingConfiguration, TestingConfigKeys } from 'vs/workbench/contrib/testing/common/configuration'; -import { InternalTestItem, TestDiffOpType, TestIdWithSrc } from 'vs/workbench/contrib/testing/common/testCollection'; +import { TestDiffOpType, TestIdWithMaybeSrc } from 'vs/workbench/contrib/testing/common/testCollection'; import { TestingContextKeys } from 'vs/workbench/contrib/testing/common/testingContextKeys'; import { TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; @@ -67,7 +67,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { */ private makeRunner() { let isRunning = false; - const rerunIds = new Map(); + const rerunIds = new Map(); const store = new DisposableStore(); const cts = new CancellationTokenSource(); store.add(toDisposable(() => cts.dispose(true))); @@ -91,8 +91,8 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { } }, delay)); - const addToRerun = (test: InternalTestItem) => { - rerunIds.set(`${test.item.extId}/${test.src.provider}`, ({ testId: test.item.extId, src: test.src })); + const addToRerun = (test: TestIdWithMaybeSrc) => { + rerunIds.set(`${test.testId}/${test.src?.controller}`, test); if (!isRunning) { scheduler.schedule(delay); } @@ -100,7 +100,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { store.add(this.results.onTestChanged(evt => { if (evt.reason === TestResultItemChangeReason.Retired) { - addToRerun(evt.item); + addToRerun({ testId: evt.item.item.extId }); } })); @@ -113,7 +113,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { for (const [, collection] of sub.workspaceFolderCollections) { for (const rootId of collection.rootIds) { const root = collection.getNodeById(rootId); - if (root) { addToRerun(root); } + if (root) { addToRerun({ testId: root.item.extId, src: root.src }); } } } } @@ -122,7 +122,7 @@ export class TestingAutoRun extends Disposable implements ITestingAutoRun { store.add(sub.onDiff(([, diff]) => { for (const entry of diff) { if (entry[0] === TestDiffOpType.Add) { - addToRerun(entry[1]); + addToRerun({ testId: entry[1].item.extId, src: entry[1].src }); } } })); diff --git a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts index 49a8b21e2ec..38930d4c833 100644 --- a/src/vs/workbench/contrib/testing/common/testingContentProvider.ts +++ b/src/vs/workbench/contrib/testing/common/testingContentProvider.ts @@ -47,13 +47,13 @@ export class TestingContentProvider implements IWorkbenchContribution, ITextMode let text: string | undefined; switch (parsed.type) { case TestUriType.ResultActualOutput: - text = test.state.messages[parsed.messageIndex]?.actualOutput; + text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.actualOutput; break; case TestUriType.ResultExpectedOutput: - text = test.state.messages[parsed.messageIndex]?.expectedOutput; + text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.expectedOutput; break; case TestUriType.ResultMessage: - text = test.state.messages[parsed.messageIndex]?.message.toString(); + text = test.tasks[parsed.taskIndex].messages[parsed.messageIndex]?.message.toString(); break; } diff --git a/src/vs/workbench/contrib/testing/common/testingStates.ts b/src/vs/workbench/contrib/testing/common/testingStates.ts index 2d7f9bd0db8..977b6867261 100644 --- a/src/vs/workbench/contrib/testing/common/testingStates.ts +++ b/src/vs/workbench/contrib/testing/common/testingStates.ts @@ -34,7 +34,25 @@ export const stateNodes = Object.entries(statePriority).reduce( export const cmpPriority = (a: TestResultState, b: TestResultState) => statePriority[b] - statePriority[a]; -export const maxPriority = (a: TestResultState, b: TestResultState) => statePriority[a] > statePriority[b] ? a : b; +export const maxPriority = (...states: TestResultState[]) => { + switch (states.length) { + case 0: + return TestResultState.Unset; + case 1: + return states[0]; + case 2: + return statePriority[states[0]] > statePriority[states[1]] ? states[0] : states[1]; + default: + let max = states[0]; + for (let i = 1; i < states.length; i++) { + if (statePriority[max] < statePriority[states[i]]) { + max = states[i]; + } + } + + return max; + } +}; export const statesInOrder = Object.keys(statePriority).map(s => Number(s) as TestResultState).sort(cmpPriority); diff --git a/src/vs/workbench/contrib/testing/common/testingUri.ts b/src/vs/workbench/contrib/testing/common/testingUri.ts index 9503c99e152..7f19b9db3ec 100644 --- a/src/vs/workbench/contrib/testing/common/testingUri.ts +++ b/src/vs/workbench/contrib/testing/common/testingUri.ts @@ -15,6 +15,7 @@ export const enum TestUriType { interface IResultTestUri { resultId: string; + taskIndex: number; testExtId: string; } @@ -46,17 +47,18 @@ export const parseTestUri = (uri: URI): ParsedTestUri | undefined => { const [locationId, ...request] = uri.path.slice(1).split('/'); if (request[0] === TestUriParts.Messages) { - const index = Number(request[1]); - const part = request[2]; + const taskIndex = Number(request[1]); + const index = Number(request[2]); + const part = request[3]; const testExtId = uri.query; if (type === TestUriParts.Results) { switch (part) { case TestUriParts.Text: - return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultMessage }; + return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultMessage }; case TestUriParts.ActualOutput: - return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput }; + return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultActualOutput }; case TestUriParts.ExpectedOutput: - return { resultId: locationId, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; + return { resultId: locationId, taskIndex, testExtId, messageIndex: index, type: TestUriType.ResultExpectedOutput }; } } } @@ -69,20 +71,20 @@ export const buildTestUri = (parsed: ParsedTestUri): URI => { scheme: TEST_DATA_SCHEME, authority: TestUriParts.Results }; - const msgRef = (locationId: string, index: number, ...remaining: string[]) => + const msgRef = (locationId: string, ...remaining: (string | number)[]) => URI.from({ ...uriParts, query: parsed.testExtId, - path: ['', locationId, TestUriParts.Messages, index, ...remaining].join('/'), + path: ['', locationId, TestUriParts.Messages, ...remaining].join('/'), }); switch (parsed.type) { case TestUriType.ResultActualOutput: - return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ActualOutput); + return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.ActualOutput); case TestUriType.ResultExpectedOutput: - return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.ExpectedOutput); + return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.ExpectedOutput); case TestUriType.ResultMessage: - return msgRef(parsed.resultId, parsed.messageIndex, TestUriParts.Text); + return msgRef(parsed.resultId, parsed.taskIndex, parsed.messageIndex, TestUriParts.Text); default: throw new Error('Invalid test uri'); } diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts index 7548ab2d7db..64f8c3471df 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByLocation.test.ts @@ -96,7 +96,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { { e: 'b' } ]); - tests.children.get('id-a')!.children.add(testStubs.test('ac')); + tests.children.get('id-a')!.addChild(testStubs.test('ac')); assert.deepStrictEqual(harness.flush(folder1), [ { e: 'a', children: [{ e: 'aa' }, { e: 'ab' }, { e: 'ac' }] }, @@ -110,7 +110,7 @@ suite('Workbench - Testing Explorer Hierarchal by Location Projection', () => { harness.flush(folder1); harness.tree.expand(harness.projection.getElementByTestId('id-a')!); - tests.children.get('id-a')!.children.delete('id-ab'); + tests.children.get('id-a')!.children.get('id-ab')!.dispose(); assert.deepStrictEqual(harness.flush(folder1), [ { e: 'a', children: [{ e: 'aa' }] }, diff --git a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts index c1292616584..afc857561e6 100644 --- a/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts +++ b/src/vs/workbench/contrib/testing/test/browser/explorerProjections/hierarchalByName.test.ts @@ -68,7 +68,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.c.addRoot(tests, 'a'); harness.flush(folder1); - tests.children.get('id-a')!.children.add(testStubs.test('ac')); + tests.children.get('id-a')!.addChild(testStubs.test('ac')); assert.deepStrictEqual(harness.flush(folder1), [ { e: 'aa' }, @@ -83,7 +83,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.c.addRoot(tests, 'a'); harness.flush(folder1); - tests.children.get('id-a')!.children.delete('id-ab'); + tests.children.get('id-a')!.children.get('id-ab')!.dispose(); assert.deepStrictEqual(harness.flush(folder1), [ { e: 'aa' }, @@ -96,7 +96,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.c.addRoot(tests, 'a'); harness.flush(folder1); - tests.children.get('id-b')!.children.add(testStubs.test('ba')); + tests.children.get('id-b')!.addChild(testStubs.test('ba')); assert.deepStrictEqual(harness.flush(folder1), [ { e: 'aa' }, @@ -111,7 +111,7 @@ suite('Workbench - Testing Explorer Hierarchal by Name Projection', () => { harness.flush(folder1); const child = testStubs.test('ba'); - tests.children.get('id-b')!.children.add(child); + tests.children.get('id-b')!.addChild(child); harness.flush(folder1); child.runnable = false; diff --git a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts index 63bad953408..04925971647 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultService.test.ts @@ -9,11 +9,11 @@ import { bufferToStream, newWriteableBufferStream, VSBuffer } from 'vs/base/comm import { Lazy } from 'vs/base/common/lazy'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; -import { InternalTestItem } from 'vs/workbench/contrib/testing/common/testCollection'; +import { ITestTaskState, TestResultItem } from 'vs/workbench/contrib/testing/common/testCollection'; import { HydratedTestResult, LiveOutputController, LiveTestResult, makeEmptyCounts, TestResultItemChange, TestResultItemChangeReason } from 'vs/workbench/contrib/testing/common/testResult'; import { TestResultService } from 'vs/workbench/contrib/testing/common/testResultService'; import { InMemoryResultStorage, ITestResultStorage } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { ReExportedTestRunState as TestRunState } from 'vs/workbench/contrib/testing/common/testStubs'; +import { Convert, ReExportedTestRunState as TestRunState, TestItemImpl, TestResultState, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs'; import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -23,34 +23,60 @@ export const emptyOutputController = () => new LiveOutputController( ); suite('Workbench - Test Results Service', () => { - const getLabelsIn = (it: Iterable) => [...it].map(t => t.item.label).sort(); + const getLabelsIn = (it: Iterable) => [...it].map(t => t.item.label).sort(); const getChangeSummary = () => [...changed] .map(c => ({ reason: c.reason, label: c.item.item.label })) .sort((a, b) => a.label.localeCompare(b.label)); - let r: LiveTestResult; + let r: TestLiveTestResult; let changed = new Set(); + let tests: TestItemImpl; + + const defaultOpts = { + exclude: [], + debug: false, + id: 'x', + persist: true, + }; + + class TestLiveTestResult extends LiveTestResult { + public override setAllToState(state: TestResultState, taskId: string, when: (task: ITestTaskState, item: TestResultItem) => boolean) { + super.setAllToState(state, taskId, when); + } + } setup(async () => { changed = new Set(); - r = LiveTestResult.from( + r = new TestLiveTestResult( 'foo', - [await getInitializedMainTestCollection()], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false }, + { ...defaultOpts, tests: ['id-a'] }, ); r.onChange(e => changed.add(e)); + r.addTask({ id: 't', name: undefined, running: true }); + + tests = testStubs.nested(); + r.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from)); + r.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-ab'], 1).map(Convert.TestItem.from)); }); suite('LiveTestResult', () => { - test('is empty if no tests are requesteed', async () => { - const r = LiveTestResult.from('', [await getInitializedMainTestCollection()], emptyOutputController(), { tests: [], debug: false }); - assert.deepStrictEqual(getLabelsIn(r.tests), []); + test('is empty if no tests are yet present', async () => { + assert.deepStrictEqual(getLabelsIn(new TestLiveTestResult( + 'foo', + emptyOutputController(), + { ...defaultOpts, tests: ['id-a'] }, + ).tests), []); }); - test('does not change or retire initially', () => { - assert.deepStrictEqual(0, changed.size); + test('initially queues with update', () => { + assert.deepStrictEqual(getChangeSummary(), [ + { label: 'a', reason: TestResultItemChangeReason.ComputedStateChange }, + { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'ab', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'root', reason: TestResultItemChangeReason.ComputedStateChange }, + ]); }); test('initializes with the subtree of requested tests', () => { @@ -60,19 +86,29 @@ suite('Workbench - Test Results Service', () => { test('initializes with valid counts', () => { assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), - [TestRunState.Unset]: 4 + [TestRunState.Queued]: 2, + [TestRunState.Unset]: 2, }); }); test('setAllToState', () => { - r.setAllToState(TestRunState.Queued, t => t.item.label !== 'root'); + changed.clear(); + r.setAllToState(TestRunState.Queued, 't', (_, t) => t.item.label !== 'root'); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), [TestRunState.Unset]: 1, [TestRunState.Queued]: 3, }); - assert.deepStrictEqual(r.getStateById('id-a')?.state.state, TestRunState.Queued); + r.setAllToState(TestRunState.Passed, 't', (_, t) => t.item.label !== 'root'); + assert.deepStrictEqual(r.counts, { + ...makeEmptyCounts(), + [TestRunState.Unset]: 1, + [TestRunState.Passed]: 3, + }); + + assert.deepStrictEqual(r.getStateById('id-a')?.ownComputedState, TestRunState.Passed); + assert.deepStrictEqual(r.getStateById('id-a')?.tasks[0].state, TestRunState.Passed); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, @@ -82,22 +118,26 @@ suite('Workbench - Test Results Service', () => { }); test('updateState', () => { - r.updateState('id-a', TestRunState.Running); + changed.clear(); + r.updateState('id-aa', 't', TestRunState.Running); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), + [TestRunState.Unset]: 2, [TestRunState.Running]: 1, - [TestRunState.Unset]: 3, + [TestRunState.Queued]: 1, }); - assert.deepStrictEqual(r.getStateById('id-a')?.state.state, TestRunState.Running); + assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Running); // update computed state: assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running); assert.deepStrictEqual(getChangeSummary(), [ - { label: 'a', reason: TestResultItemChangeReason.OwnStateChange }, + { label: 'a', reason: TestResultItemChangeReason.ComputedStateChange }, + { label: 'aa', reason: TestResultItemChangeReason.OwnStateChange }, { label: 'root', reason: TestResultItemChangeReason.ComputedStateChange }, ]); }); test('retire', () => { + changed.clear(); r.retire('id-a'); assert.deepStrictEqual(getChangeSummary(), [ { label: 'a', reason: TestResultItemChangeReason.Retired }, @@ -110,21 +150,20 @@ suite('Workbench - Test Results Service', () => { assert.strictEqual(changed.size, 0); }); - test('addTestToRun', () => { - r.updateState('id-b', TestRunState.Running); + test('ignores outside run', () => { + changed.clear(); + r.updateState('id-b', 't', TestRunState.Running); assert.deepStrictEqual(r.counts, { ...makeEmptyCounts(), - [TestRunState.Running]: 1, - [TestRunState.Unset]: 4, + [TestRunState.Queued]: 2, + [TestRunState.Unset]: 2, }); - assert.deepStrictEqual(r.getStateById('id-b')?.state.state, TestRunState.Running); - // update computed state: - assert.deepStrictEqual(r.getStateById('id-root')?.computedState, TestRunState.Running); + assert.deepStrictEqual(r.getStateById('id-b'), undefined); }); test('markComplete', () => { - r.setAllToState(TestRunState.Queued, () => true); - r.updateState('id-aa', TestRunState.Passed); + r.setAllToState(TestRunState.Queued, 't', () => true); + r.updateState('id-aa', 't', TestRunState.Passed); changed.clear(); r.markComplete(); @@ -135,8 +174,8 @@ suite('Workbench - Test Results Service', () => { [TestRunState.Unset]: 3, }); - assert.deepStrictEqual(r.getStateById('id-root')?.state.state, TestRunState.Unset); - assert.deepStrictEqual(r.getStateById('id-aa')?.state.state, TestRunState.Passed); + assert.deepStrictEqual(r.getStateById('id-root')?.ownComputedState, TestRunState.Unset); + assert.deepStrictEqual(r.getStateById('id-aa')?.ownComputedState, TestRunState.Passed); }); }); @@ -160,7 +199,7 @@ suite('Workbench - Test Results Service', () => { test('serializes and re-hydrates', async () => { results.push(r); - r.updateState('id-aa', TestRunState.Passed); + r.updateState('id-aa', 't', TestRunState.Passed); r.markComplete(); await timeout(0); // allow persistImmediately async to happen @@ -175,12 +214,12 @@ suite('Workbench - Test Results Service', () => { const [rehydrated, actual] = results.getStateById('id-root')!; const expected: any = { ...r.getStateById('id-root')! }; - delete expected.state.duration; // delete undefined props that don't survive serialization + delete expected.tasks[0].duration; // delete undefined props that don't survive serialization delete expected.item.range; delete expected.item.description; expected.item.uri = actual.item.uri; - assert.deepStrictEqual(actual, { ...expected, retired: true }); + assert.deepStrictEqual(actual, { ...expected, src: undefined, retired: true, children: ['id-a'] }); assert.deepStrictEqual(rehydrated.counts, r.counts); assert.strictEqual(typeof rehydrated.completedAt, 'number'); }); @@ -189,11 +228,10 @@ suite('Workbench - Test Results Service', () => { results.push(r); r.markComplete(); - const r2 = results.push(LiveTestResult.from( + const r2 = results.push(new LiveTestResult( '', - [await getInitializedMainTestCollection()], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false } + { ...defaultOpts, tests: [] } )); results.clear(); @@ -202,11 +240,10 @@ suite('Workbench - Test Results Service', () => { test('keeps ongoing tests on top', async () => { results.push(r); - const r2 = results.push(LiveTestResult.from( + const r2 = results.push(new LiveTestResult( '', - [await getInitializedMainTestCollection()], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: '1' }], debug: false } + { ...defaultOpts, tests: [] } )); assert.deepStrictEqual(results.results, [r2, r]); @@ -219,10 +256,12 @@ suite('Workbench - Test Results Service', () => { const makeHydrated = async (completedAt = 42, state = TestRunState.Passed) => new HydratedTestResult({ completedAt, id: 'some-id', + tasks: [{ id: 't', running: false, name: undefined }], items: [{ ...(await getInitializedMainTestCollection()).getNodeById('id-a')!, - state: { state, duration: 0, messages: [] }, + tasks: [{ state, duration: 0, messages: [] }], computedState: state, + ownComputedState: state, retired: undefined, children: [], }] @@ -235,16 +274,14 @@ suite('Workbench - Test Results Service', () => { assert.deepStrictEqual(results.results, [r, hydrated]); }); - test('deduplicates identical results', async () => { + test('inserts in correct order', async () => { results.push(r); const hydrated1 = await makeHydrated(); results.push(hydrated1); - const hydrated2 = await makeHydrated(); - results.push(hydrated2); assert.deepStrictEqual(results.results, [r, hydrated1]); }); - test('does not deduplicate if different completedAt', async () => { + test('inserts in correct order 2', async () => { results.push(r); const hydrated1 = await makeHydrated(); results.push(hydrated1); @@ -252,14 +289,5 @@ suite('Workbench - Test Results Service', () => { results.push(hydrated2); assert.deepStrictEqual(results.results, [r, hydrated1, hydrated2]); }); - - test('does not deduplicate if different tests', async () => { - results.push(r); - const hydrated1 = await makeHydrated(); - results.push(hydrated1); - const hydrated2 = await makeHydrated(undefined, TestRunState.Failed); - results.push(hydrated2); - assert.deepStrictEqual(results.results, [r, hydrated2, hydrated1]); - }); }); }); diff --git a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts index 33237417545..1d2d205854d 100644 --- a/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testResultStorage.test.ts @@ -8,24 +8,32 @@ import { range } from 'vs/base/common/arrays'; import { NullLogService } from 'vs/platform/log/common/log'; import { ITestResult, LiveTestResult } from 'vs/workbench/contrib/testing/common/testResult'; import { InMemoryResultStorage, RETAIN_MAX_RESULTS } from 'vs/workbench/contrib/testing/common/testResultStorage'; -import { MainThreadTestCollection } from 'vs/workbench/contrib/testing/common/testServiceImpl'; -import { getInitializedMainTestCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; +import { Convert, testStubs, testStubsChain } from 'vs/workbench/contrib/testing/common/testStubs'; import { emptyOutputController } from 'vs/workbench/contrib/testing/test/common/testResultService.test'; import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices'; suite('Workbench - Test Result Storage', () => { let storage: InMemoryResultStorage; - let collection: MainThreadTestCollection; const makeResult = (addMessage?: string) => { - const t = LiveTestResult.from( + const t = new LiveTestResult( '', - [collection], emptyOutputController(), - { tests: [{ src: { provider: 'provider', tree: 0 }, testId: 'id-a' }], debug: false } + { + tests: [], + exclude: [], + debug: false, + id: 'x', + persist: true, + } ); + + t.addTask({ id: 't', name: undefined, running: true }); + const tests = testStubs.nested(); + t.addTestChainToRun(testStubsChain(tests, ['id-a', 'id-aa']).map(Convert.TestItem.from)); + if (addMessage) { - t.appendMessage('id-a', { + t.appendMessage('id-a', 't', { message: addMessage, actualOutput: undefined, expectedOutput: undefined, @@ -41,7 +49,6 @@ suite('Workbench - Test Result Storage', () => { assert.deepStrictEqual((await storage.read()).map(r => r.id), stored.map(s => s.id)); setup(async () => { - collection = await getInitializedMainTestCollection(); storage = new InMemoryResultStorage(new TestStorageService(), new NullLogService()); }); @@ -68,7 +75,7 @@ suite('Workbench - Test Result Storage', () => { test('limits stored result by budget', async () => { const r = range(100).map(() => makeResult('a'.repeat(2048))); await storage.persist(r); - await assertStored(r.slice(0, 41)); + await assertStored(r.slice(0, 46)); }); test('always stores the min number of results', async () => { diff --git a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts index e9c1d60412e..b2e1bdf7112 100644 --- a/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts +++ b/src/vs/workbench/contrib/testing/test/common/testingUri.test.ts @@ -9,9 +9,9 @@ import { buildTestUri, ParsedTestUri, parseTestUri, TestUriType } from 'vs/workb suite('Workbench - Testing URIs', () => { test('round trip', () => { const uris: ParsedTestUri[] = [ - { type: TestUriType.ResultActualOutput, messageIndex: 42, resultId: 'r', testExtId: 't' }, - { type: TestUriType.ResultExpectedOutput, messageIndex: 42, resultId: 'r', testExtId: 't' }, - { type: TestUriType.ResultMessage, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultActualOutput, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultExpectedOutput, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' }, + { type: TestUriType.ResultMessage, taskIndex: 1, messageIndex: 42, resultId: 'r', testExtId: 't' }, ]; for (const uri of uris) { diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index f9303c76d1d..e1e9339f79e 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -212,7 +212,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu @IActivityService private readonly activityService: IActivityService, @IContextKeyService private readonly contextKeyService: IContextKeyService, @IProductService private readonly productService: IProductService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IHostService private readonly hostService: IHostService ) { super(); this.state = updateService.state; @@ -241,14 +241,14 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu this.registerGlobalActivityActions(); } - private onUpdateStateChange(state: UpdateState): void { + private async onUpdateStateChange(state: UpdateState): Promise { this.updateStateContextKey.set(state.type); switch (state.type) { case StateType.Idle: if (state.error) { this.onError(state.error); - } else if (this.state.type === StateType.CheckingForUpdates && this.state.context === this.environmentService.sessionId) { + } else if (this.state.type === StateType.CheckingForUpdates && this.state.explicit && await this.hostService.hadLastFocus()) { this.onUpdateNotAvailable(); } break; @@ -437,7 +437,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu } private registerGlobalActivityActions(): void { - CommandsRegistry.registerCommand('update.check', () => this.updateService.checkForUpdates(this.environmentService.sessionId)); + CommandsRegistry.registerCommand('update.check', () => this.updateService.checkForUpdates(true)); MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '7_update', command: { @@ -630,13 +630,12 @@ export class CheckForVSCodeUpdateAction extends Action { constructor( id: string, label: string, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IUpdateService private readonly updateService: IUpdateService, ) { super(id, label, undefined, true); } override run(): Promise { - return this.updateService.checkForUpdates(this.environmentService.sessionId); + return this.updateService.checkForUpdates(true); } } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index a6e2cfb2931..bb1ec9cf358 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -235,8 +235,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme const findOptions: FindInPageOptions = { forward: options.forward, findNext: true, - matchCase: options.matchCase, - medialCapitalAsWordStart: options.medialCapitalAsWordStart + matchCase: options.matchCase }; this._findStarted = true; diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts index fb36f2ba324..0ac8b668955 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.contribution.ts @@ -15,7 +15,7 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { KeyCode } from 'vs/base/common/keyCodes'; import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { GettingStartedService, IGettingStartedService } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService'; +import { IGettingStartedService } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService'; import { GettingStartedInput } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedInput'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -167,9 +167,10 @@ registerAction2(class extends Action2 { class WorkbenchConfigurationContribution { constructor( - @IInstantiationService instantiationService: IInstantiationService, + @IInstantiationService _instantiationService: IInstantiationService, + @IGettingStartedService _gettingStartedService: IGettingStartedService, ) { - instantiationService.createInstance(GettingStartedService); + // Init the getting started service via DI. } } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css index f2c1db5f242..36b7e0b7351 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.css @@ -407,17 +407,19 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent { height: 100%; max-width: 1200px; + max-height: 1000px; margin: 0 auto; padding: 0 12px; display: grid; grid-template-columns: minmax(auto, 400px) 1fr; - grid-template-rows: minmax(40px, 20%) 100px 1fr; + grid-template-rows: minmax(40px, 20%) 100px max-content 1fr; grid-column-gap: 20px; grid-template-areas: - "back ." - "title ." + "back img" + "title img" "tasks img" + ". img" ; } @@ -454,7 +456,7 @@ .monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media { grid-area: img; } -.monaco-workbench .part.editor > .content .gettingStartedContainer.width-semi-constrained .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media { +.monaco-workbench .part.editor > .content .gettingStartedContainer .gettingStartedSlideDetails .gettingStartedDetailsContent > .getting-started-media { align-self: center; } diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts index e40e2569bae..e1998de5ece 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStarted.ts @@ -158,7 +158,9 @@ export class GettingStartedPage extends EditorPane { ourTask.title = task.title; ourTask.description = task.description; ourTask.media.path = task.media.path; - // TODO: JacksonKearl, update the actual rendering if not much time has passed + + this.container.querySelectorAll(`[x-task-title-for="${task.id}"]`).forEach(item => (item as HTMLDivElement).innerText = task.title); + this.container.querySelectorAll(`[x-task-description-for="${task.id}"]`).forEach(item => this.buildTaskMarkdownDescription((item), task.description)); })); this._register(this.gettingStartedService.onDidChangeCategory(category => { @@ -167,7 +169,9 @@ export class GettingStartedPage extends EditorPane { ourCategory.title = category.title; ourCategory.description = category.description; - // TODO: JacksonKearl, update the actual rendering if not much time has passed + + this.container.querySelectorAll(`[x-category-title-for="${category.id}"]`).forEach(item => (item as HTMLDivElement).innerText = ourCategory.title); + this.container.querySelectorAll(`[x-category-description-for="${category.id}"]`).forEach(item => (item as HTMLDivElement).innerText = ourCategory.description); })); this._register(this.gettingStartedService.onDidProgressTask(task => { @@ -618,7 +622,7 @@ export class GettingStartedPage extends EditorPane { 'x-dispatch': 'hideCategory:' + category.id, 'title': localize('close', "Hide"), }), - $('h3.category-title', {}, category.title), + $('h3.category-title', { 'x-category-title-for': category.id }, category.title), $('.category-progress', { 'x-data-category-id': category.id, }, $('.progress-bar-outer', { 'role': 'progressbar' }, $('.progress-bar-inner')))); @@ -703,85 +707,87 @@ export class GettingStartedPage extends EditorPane { return category.icon.type === 'icon' ? $(ThemeIcon.asCSSSelector(category.icon.icon)) : $('img.category-icon', { src: category.icon.path }); } + private buildTaskMarkdownDescription(container: HTMLElement, text: LinkedText[]) { + while (container.firstChild) { container.removeChild(container.firstChild); } + + for (const linkedText of text) { + if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { + const node = linkedText.nodes[0]; + const buttonContainer = append(container, $('.button-container')); + const button = new Button(buttonContainer, { title: node.title, supportIcons: true }); + + const isCommand = node.href.startsWith('command:'); + const toSide = node.href.startsWith('command:toSide:'); + const command = node.href.replace(/command:(toSide:)?/, 'command:'); + + button.label = node.label; + button.onDidClick(async e => { + e.stopPropagation(); + e.preventDefault(); + + this.telemetryService.publicLog2('gettingStarted.ActionExecuted', { command: 'runTaskAction', argument: node.href }); + + const fullSize = this.groupsService.contentDimension; + + if (toSide && fullSize.width > 700) { + if (this.groupsService.count === 1) { + this.groupsService.addGroup(this.groupsService.groups[0], GroupDirection.LEFT, { activate: true }); + + let gettingStartedSize: number; + if (fullSize.width > 1600) { + gettingStartedSize = 800; + } else if (fullSize.width > 800) { + gettingStartedSize = 400; + } else { + gettingStartedSize = 350; + } + + const gettingStartedGroup = this.groupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(group => (group.activeEditor instanceof GettingStartedInput)); + this.groupsService.setSize(assertIsDefined(gettingStartedGroup), { width: gettingStartedSize, height: fullSize.height }); + } + + const nonGettingStartedGroup = this.groupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(group => !(group.activeEditor instanceof GettingStartedInput)); + if (nonGettingStartedGroup) { + this.groupsService.activateGroup(nonGettingStartedGroup); + nonGettingStartedGroup.focus(); + } + } + this.openerService.open(command, { allowCommands: true }); + + }, null, this.detailsPageDisposables); + + if (isCommand) { + const keybindingLabel = this.getKeybindingLabel(command); + if (keybindingLabel) { + container.appendChild($('span.shortcut-message', {}, 'Tip: Use keyboard shortcut ', $('span.keybinding', {}, keybindingLabel))); + } + } + + this.detailsPageDisposables.add(button); + this.detailsPageDisposables.add(attachButtonStyler(button, this.themeService)); + } else { + const p = append(container, $('p')); + for (const node of linkedText.nodes) { + if (typeof node === 'string') { + append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true })); + } else { + const link = this.instantiationService.createInstance(Link, node); + + append(p, link.el); + this.detailsPageDisposables.add(link); + this.detailsPageDisposables.add(attachLinkStyler(link, this.themeService)); + } + } + } + } + return container; + } + private buildCategorySlide(categoryID: string, selectedItem?: string) { if (this.detailsScrollbar) { this.detailsScrollbar.dispose(); } this.detailsPageDisposables.clear(); - const renderMarkdownDescription = (text: LinkedText[]): HTMLElement => { - const container = $('.task-description-container'); - for (const linkedText of text) { - if (linkedText.nodes.length === 1 && typeof linkedText.nodes[0] !== 'string') { - const node = linkedText.nodes[0]; - const buttonContainer = append(container, $('.button-container')); - const button = new Button(buttonContainer, { title: node.title, supportIcons: true }); - - const isCommand = node.href.startsWith('command:'); - const toSide = node.href.startsWith('command:toSide:'); - const command = node.href.replace(/command:(toSide:)?/, 'command:'); - - button.label = node.label; - button.onDidClick(async e => { - e.stopPropagation(); - e.preventDefault(); - - this.telemetryService.publicLog2('gettingStarted.ActionExecuted', { command: 'runTaskAction', argument: node.href }); - - const fullSize = this.groupsService.contentDimension; - - if (toSide && fullSize.width > 700) { - if (this.groupsService.count === 1) { - this.groupsService.addGroup(this.groupsService.groups[0], GroupDirection.LEFT, { activate: true }); - - let gettingStartedSize: number; - if (fullSize.width > 1600) { - gettingStartedSize = 800; - } else if (fullSize.width > 800) { - gettingStartedSize = 400; - } else { - gettingStartedSize = 350; - } - - const gettingStartedGroup = this.groupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(group => (group.activeEditor instanceof GettingStartedInput)); - this.groupsService.setSize(assertIsDefined(gettingStartedGroup), { width: gettingStartedSize, height: fullSize.height }); - } - - const nonGettingStartedGroup = this.groupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE).find(group => !(group.activeEditor instanceof GettingStartedInput)); - if (nonGettingStartedGroup) { - this.groupsService.activateGroup(nonGettingStartedGroup); - } - } - this.openerService.open(command, { allowCommands: true }); - - }, null, this.detailsPageDisposables); - - if (isCommand) { - const keybindingLabel = this.getKeybindingLabel(command); - if (keybindingLabel) { - container.appendChild($('span.shortcut-message', {}, 'Tip: Use keyboard shortcut ', $('span.keybinding', {}, keybindingLabel))); - } - } - - this.detailsPageDisposables.add(button); - this.detailsPageDisposables.add(attachButtonStyler(button, this.themeService)); - } else { - const p = append(container, $('p')); - for (const node of linkedText.nodes) { - if (typeof node === 'string') { - append(p, renderFormattedText(node, { inline: true, renderCodeSegements: true })); - } else { - const link = this.instantiationService.createInstance(Link, node); - - append(p, link.el); - this.detailsPageDisposables.add(link); - this.detailsPageDisposables.add(attachLinkStyler(link, this.themeService)); - } - } - } - } - return container; - }; - const category = this.gettingStartedCategories.find(category => category.id === categoryID); if (!category) { throw Error('could not find category with ID ' + categoryID); } @@ -792,8 +798,8 @@ export class GettingStartedPage extends EditorPane { {}, this.iconWidgetFor(category), $('.category-description-container', {}, - $('h2.category-title', {}, category.title), - $('.category-description.description', {}, category.description))); + $('h2.category-title', { 'x-category-title-for': category.id }, category.title), + $('.category-description.description', { 'x-category-description-for': category.id }, category.description))); const categoryElements = category.content.items.map( (task, i, arr) => { @@ -803,9 +809,12 @@ export class GettingStartedPage extends EditorPane { 'x-dispatch': 'toggleTaskCompletion:' + task.id, }); + const container = $('.task-description-container', { 'x-task-description-for': task.id }); + this.buildTaskMarkdownDescription(container, task.description); + const taskDescription = $('.task-container', {}, - $('h3.task-title', {}, task.title), - renderMarkdownDescription(task.description), + $('h3.task-title', { 'x-task-title-for': task.id }, task.title), + container, $('.image-description', { 'aria-label': localize('imageShowing', "Image showing {0}", task.media.altText) }), ); diff --git a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts index b12940e08e9..80aa852c385 100644 --- a/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts +++ b/src/vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService.ts @@ -113,7 +113,6 @@ export interface IGettingStartedCategoryWithProgress extends Omit readonly onDidAddCategory: Event readonly onDidRemoveCategory: Event readonly onDidChangeTask: Event @@ -131,8 +130,6 @@ export interface IGettingStartedService { export class GettingStartedService extends Disposable implements IGettingStartedService { declare readonly _serviceBrand: undefined; - private readonly _onDidAddTask = new Emitter(); - onDidAddTask: Event = this._onDidAddTask.event; private readonly _onDidAddCategory = new Emitter(); onDidAddCategory: Event = this._onDidAddCategory.event; @@ -503,21 +500,6 @@ export class GettingStartedService extends Disposable implements IGettingStarted listening.forEach(id => this.progressTask(id)); } - public registerTask(task: IGettingStartedTask): IGettingStartedTask { - const category = this.gettingStartedContributions.get(task.category); - if (!category) { throw Error('Registering getting started task to category that does not exist (' + task.category + ')'); } - if (category.content.type !== 'items') { throw Error('Registering getting started task to category that is not of `items` type (' + task.category + ')'); } - if (this.tasks.has(task.id)) { throw Error('Attempting to register task with id ' + task.id + ' twice. Second is dropped.'); } - this.tasks.set(task.id, task); - let insertIndex: number | undefined = category.content.items.findIndex(item => item.order > task.order); - if (insertIndex === -1) { insertIndex = undefined; } - insertIndex = insertIndex ?? category.content.items.length; - category.content.items.splice(insertIndex, 0, task); - this.registerDoneListeners(task); - this._onDidAddTask.fire(this.getTaskProgress(task)); - return task; - } - private registerStartEntry(categoryDescriptor: IGettingStartedStartEntryDescriptor): void { const oldCategory = this.gettingStartedContributions.get(categoryDescriptor.id); if (oldCategory) { @@ -539,8 +521,12 @@ export class GettingStartedService extends Disposable implements IGettingStarted } const category: IGettingStartedCategory = { ...categoryDescriptor, content: { type: 'items', items } }; - this.gettingStartedContributions.set(categoryDescriptor.id, category); + items.forEach(task => { + if (this.tasks.has(task.id)) { throw Error('Attempting to register task with id ' + task.id + ' twice. Second is dropped.'); } + this.tasks.set(task.id, task); + this.registerDoneListeners(task); + }); this._onDidAddCategory.fire(this.getCategoryProgress(category)); } diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index ed17f97bf4b..8471fa8ea01 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -20,7 +20,7 @@ import { localize } from 'vs/nls'; import { Action, WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { FileAccess, Schemas } from 'vs/base/common/network'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { getInstalledExtensions, IExtensionStatus, onExtensionChanged, isKeymapExtension } from 'vs/workbench/contrib/extensions/common/extensionsUtils'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -53,7 +53,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationNode, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IGettingStartedService } from 'vs/workbench/contrib/welcome/gettingStarted/browser/gettingStartedService'; export const DEFAULT_STARTUP_EDITOR_CONFIG: IConfigurationNode = { @@ -110,7 +109,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { @IInstantiationService private readonly instantiationService: IInstantiationService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, - @IBackupFileService private readonly backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @IFileService private readonly fileService: IFileService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -118,7 +117,6 @@ export class WelcomePageContribution implements IWorkbenchContribution { @ICommandService private readonly commandService: ICommandService, @ITelemetryService private readonly telemetryService: ITelemetryService, @ILogService private readonly logService: ILogService, - @IGettingStartedService _gettingStartedService: IGettingStartedService, // initializes event listeners @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, ) { this.tasExperimentService = tasExperimentService; @@ -176,7 +174,7 @@ export class WelcomePageContribution implements IWorkbenchContribution { private async run() { const enabled = isWelcomePageEnabled(this.configurationService, this.contextService); if (enabled && this.lifecycleService.startupKind !== StartupKind.ReloadedWindow) { - const hasBackups = await this.backupFileService.hasBackups(); + const hasBackups = await this.workingCopyBackupService.hasBackups(); if (hasBackups) { return; } // Open the welcome even if we opened a set of default editors diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts index 09edb81bf4b..be87363368f 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel, ITextEditorModel } from 'vs/workbench/common/editor'; +import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { DisposableStore, IReference } from 'vs/base/common/lifecycle'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import * as marked from 'vs/base/common/marked/marked'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 7c822b4fed8..7b1af890696 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -313,10 +313,13 @@ export class WalkThroughPart extends EditorPane { model.snippets.forEach((snippet, i) => { const model = snippet.textEditorModel; + if (!model) { + return; + } const id = `snippet-${model.uri.fragment}`; const div = innerContent.querySelector(`#${id.replace(/[\\.]/g, '\\$&')}`) as HTMLElement; - const options = this.getEditorOptions(snippet.textEditorModel.getModeId()); + const options = this.getEditorOptions(model.getModeId()); const telemetryData = { target: this.input instanceof WalkThroughInput ? this.input.getTelemetryFrom() : undefined, snippet: i diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 33c501c8a16..60ffd579341 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -13,7 +13,7 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { Severity } from 'vs/platform/notification/common/notification'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustStorageService, WorkspaceTrustRequestOptions, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, WorkspaceTrustRequestOptions, workspaceTrustToString } from 'vs/platform/workspace/common/workspaceTrust'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IActivityService, IconBadge } from 'vs/workbench/services/activity/common/activity'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; @@ -55,7 +55,6 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben @IActivityService private readonly activityService: IActivityService, @ICommandService private readonly commandService: ICommandService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IWorkspaceTrustStorageService private readonly workspaceTrustStorageService: IWorkspaceTrustStorageService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, @@ -63,9 +62,10 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben ) { super(); - this.registerListeners(); - - this.showIntroductionModal(); + if (isWorkspaceTrustEnabled(configurationService)) { + this.registerListeners(); + this.showIntroductionModal(); + } } private toggleRequestBadge(visible: boolean): void { @@ -81,7 +81,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben private showIntroductionModal(): void { const workspaceTrustIntroDialogDoNotShowAgainKey = 'workspace.trust.introduction.doNotShowAgain'; - const doNotShowAgain = this.storageService.getBoolean(workspaceTrustIntroDialogDoNotShowAgainKey, StorageScope.GLOBAL, true); + const doNotShowAgain = this.storageService.getBoolean(workspaceTrustIntroDialogDoNotShowAgainKey, StorageScope.GLOBAL, false); if (!doNotShowAgain && this.shouldShowIntroduction) { // Show welcome dialog (async () => { @@ -102,11 +102,11 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben markdown: new MarkdownString(localize('workspaceTrustDescription', "{0} provides many powerful features that rely on the files that are open in the current workspace. This can mean unintended code execution from the workspace and should only happen if you trust the source of the files you have open.", 'VS Code' || product.nameShort)), }, { - markdown: new MarkdownString(`![${localize('altTextTrustedBadge', "Shield Badge on Activity Bar")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/trusted-badge.png', require).toString(true)})\n*${localize('workspaceTrustBadgeDescription', "When features are disabled in an untrusted workspace, you will see this shield icon in the Activity Bar.")}*`), + markdown: new MarkdownString(`![${localize('altTextTrustedBadge', "Shield Badge on Activity Bar")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/trusted-badge.png', require).toString(false)})\n*${localize('workspaceTrustBadgeDescription', "When features are disabled in an untrusted workspace, you will see this shield icon in the Activity Bar.")}*`), classes: ['workspace-trust-dialog-image-row', 'badge-row'] }, { - markdown: new MarkdownString(`![${localize('altTextUntrustedStatus', "Workspace Trust Status Bar Entry")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/untrusted-status.png', require).toString(true)})\n*${localize('workspaceTrustUntrustedDescription', "When the workspace is untrusted, you will see this status bar entry. It is hidden when the workspace is trusted.")}*`), + markdown: new MarkdownString(`![${localize('altTextUntrustedStatus', "Workspace Trust Status Bar Entry")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/untrusted-status.png', require).toString(false)})\n*${localize('workspaceTrustUntrustedDescription', "When the workspace is untrusted, you will see this status bar entry. It is hidden when the workspace is trusted.")}*`), classes: ['workspace-trust-dialog-image-row', 'status-bar'] }, { @@ -215,8 +215,8 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben return e.join(new Promise(async resolve => { // Workspace is trusted and there are added/changed folders if (trusted && (e.changes.added.length || e.changes.changed.length)) { - const addedFoldersTrustStateInfo = e.changes.added.map(folder => this.workspaceTrustStorageService.getFolderTrustStateInfo(folder.uri)); - if (!addedFoldersTrustStateInfo.map(i => i.trusted).every(trusted => trusted)) { + const addedFoldersTrustInfo = e.changes.added.map(folder => this.workspaceTrustManagementService.getFolderTrustInfo(folder.uri)); + if (!addedFoldersTrustInfo.map(i => i.trusted).every(trusted => trusted)) { const result = await this.dialogService.show( Severity.Info, localize('addWorkspaceFolderMessage', "Do you trust the files in this folder?"), @@ -229,7 +229,7 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben ); // Mark added/changed folders as trusted - this.workspaceTrustStorageService.setFoldersTrust(addedFoldersTrustStateInfo.map(i => i.uri), result.choice === 0); + this.workspaceTrustManagementService.setFoldersTrust(addedFoldersTrustInfo.map(i => i.uri), result.choice === 0); resolve(); } @@ -441,8 +441,7 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben @ITelemetryService private readonly telemetryService: ITelemetryService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IWorkspaceTrustStorageService private readonly workspaceTrustStorageService: IWorkspaceTrustStorageService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService ) { super(); @@ -459,18 +458,14 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben type WorkspaceTrustInfoEventClassification = { trustedFoldersCount: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; - untrustedFoldersCount: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; type WorkspaceTrustInfoEvent = { trustedFoldersCount: number, - untrustedFoldersCount: number }; - const trustStateInfo = this.workspaceTrustStorageService.getTrustStateInfo(); this.telemetryService.publicLog2('workspaceTrustFolderCounts', { - trustedFoldersCount: trustStateInfo.uriTrustInfo.filter(item => item.trusted).length, - untrustedFoldersCount: trustStateInfo.uriTrustInfo.filter(item => !item.trusted).length + trustedFoldersCount: this.workspaceTrustManagementService.getTrustedFolders().length, }); } @@ -520,7 +515,7 @@ class WorkspaceTrustTelemetryContribution extends Disposable implements IWorkben }; for (const folder of this.workspaceContextService.getWorkspace().folders) { - const { trusted, uri } = this.workspaceTrustStorageService.getFolderTrustStateInfo(folder.uri); + const { trusted, uri } = this.workspaceTrustManagementService.getFolderTrustInfo(folder.uri); if (!trusted) { continue; } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css index 0fda6179da4..7cd581b7ea0 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.css @@ -81,19 +81,13 @@ justify-content: space-evenly; } -.workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { - border: 2px solid rgba(0, 0, 0, 0); - margin: 0px 4px; - display: flex; - flex-direction: column; - padding: 0px 40px; -} - .workspace-trust-editor.trusted .workspace-trust-features .workspace-trust-limitations.trusted { + border-width: 2px; border-color: var(--workspace-trust-state-trusted-color) !important; } .workspace-trust-editor.untrusted .workspace-trust-features .workspace-trust-limitations.untrusted { + border-width: 2px; border-color: var(--workspace-trust-state-untrusted-color) !important; } @@ -156,6 +150,7 @@ .workspace-trust-editor .workspace-trust-features .workspace-trust-untrusted-description { font-style: italic; + padding-bottom: 10px; } /** Buttons Container */ diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 4a241eec767..db57abb7e2b 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -9,6 +9,7 @@ import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableEle import { Action } from 'vs/base/common/actions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Codicon, registerCodicon } from 'vs/base/common/codicons'; +import { Color, RGBA } from 'vs/base/common/color'; import { debounce } from 'vs/base/common/decorators'; import { Iterable } from 'vs/base/common/iterator'; import { splitName } from 'vs/base/common/labels'; @@ -27,10 +28,11 @@ import { IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common import { Link } from 'vs/platform/opener/browser/link'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { foreground } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachLinkStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; -import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IColorTheme, ICssStyleCollector, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustStorageService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; @@ -40,7 +42,7 @@ import { getInstalledExtensions, IExtensionStatus } from 'vs/workbench/contrib/e import { trustedForegroundColor, untrustedForegroundColor } from 'vs/workbench/contrib/workspace/browser/workspaceTrustColors'; import { IWorkspaceTrustSettingChangeEvent, WorkspaceTrustSettingArrayRenderer, WorkspaceTrustTree, WorkspaceTrustTreeModel } from 'vs/workbench/contrib/workspace/browser/workspaceTrustTree'; import { filterSettingsRequireWorkspaceTrust, IWorkbenchConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { WorkspaceTrustEditorInput } from 'vs/workbench/services/workspaces/browser/workspaceTrustEditorInput'; const untrustedIcon = registerCodicon('workspace-untrusted-icon', Codicon.workspaceUntrusted); @@ -77,12 +79,11 @@ export class WorkspaceTrustEditor extends EditorPane { @IStorageService storageService: IStorageService, @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, @IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService, - @IExtensionWorkspaceTrustRequestService private readonly extensionWorkspaceTrustRequestService: IExtensionWorkspaceTrustRequestService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IContextMenuService private readonly contextMenuService: IContextMenuService, @IDialogService private readonly dialogService: IDialogService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IWorkspaceTrustStorageService private readonly workspaceTrustStorageService: IWorkspaceTrustStorageService, @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService, ) { super(WorkspaceTrustEditor.ID, telemetryService, themeService, storageService); } @@ -106,6 +107,14 @@ export class WorkspaceTrustEditor extends EditorPane { this.rootElement.style.setProperty('--workspace-trust-state-trusted-color', colors.trustedForegroundColor?.toString() || ''); this.rootElement.style.setProperty('--workspace-trust-state-untrusted-color', colors.untrustedForegroundColor?.toString() || ''); })); + + this._register(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { + const foregroundColor = theme.getColor(foreground); + if (foregroundColor) { + const fgWithOpacity = new Color(new RGBA(foregroundColor.rgba.r, foregroundColor.rgba.g, foregroundColor.rgba.b, 0.3)); + collector.addRule(`.workspace-trust-editor .workspace-trust-features .workspace-trust-limitations { border: 1px solid ${fgWithOpacity}; margin: 0px 4px; display: flex; flex-direction: column; padding: 10px 40px;}`); + } + })); } async override setInput(input: WorkspaceTrustEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { @@ -118,10 +127,10 @@ export class WorkspaceTrustEditor extends EditorPane { } private registerListeners(): void { - this._register(this.workspaceTrustStorageService.onDidStorageChange(() => this.render())); - this._register(this.workspaceTrustManagementService.onDidChangeTrust(() => this.render())); this._register(this.extensionWorkbenchService.onChange(() => this.render())); this._register(this.configurationService.onDidChangeUntrustdSettings(() => this.render())); + this._register(this.workspaceTrustManagementService.onDidChangeTrust(() => this.render())); + this._register(this.workspaceTrustManagementService.onDidChangeTrustedFolders(() => this.render())); } private getHeaderContainerClass(trusted: boolean): string { @@ -203,15 +212,16 @@ export class WorkspaceTrustEditor extends EditorPane { this.renderAffectedFeatures(settingsRequiringTrustedWorkspaceCount, onDemandExtensionCount + onStartExtensionCount); // Configuration Tree - this.workspaceTrustSettingsTreeModel.update(this.workspaceTrustStorageService.getTrustStateInfo()); + this.workspaceTrustSettingsTreeModel.update(this.workspaceTrustManagementService.getTrustedFolders()); this.trustSettingsTree.setChildren(null, Iterable.map(this.workspaceTrustSettingsTreeModel.settings, s => { return { element: s }; })); + this.bodyScrollBar.getDomNode().style.height = `calc(100% - ${this.headerContainer.clientHeight}px)`; this.bodyScrollBar.scanDomNode(); this.rendering = false; } private getExtensionCountByTrustRequestType(extensions: IExtensionStatus[], trustRequestType: ExtensionWorkspaceTrustRequestType): number { - const filtered = extensions.filter(ext => this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(ext.local.manifest) === trustRequestType); + const filtered = extensions.filter(ext => this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(ext.local.manifest) === trustRequestType); const set = new Set(); for (const ext of filtered) { set.add(ext.identifier.id); @@ -275,10 +285,7 @@ export class WorkspaceTrustEditor extends EditorPane { } private addTrustButtonToElement(parent: HTMLElement): void { - const workspaceFolders = this.workspaceService.getWorkspace().folders; - - - if (workspaceFolders.length) { + if (this.workspaceTrustManagementService.canSetWorkspaceTrust()) { const buttonRow = append(parent, $('.workspace-trust-buttons-row')); const buttonContainer = append(buttonRow, $('.workspace-trust-buttons')); const buttonBar = this.rerenderDisposables.add(new ButtonBar(buttonContainer)); @@ -308,8 +315,11 @@ export class WorkspaceTrustEditor extends EditorPane { }; const trustUris = async (uris?: URI[]) => { - const folderURIs = uris || this.workspaceService.getWorkspace().folders.map(folder => folder.uri); - this.workspaceTrustStorageService.setFoldersTrust(folderURIs, true); + if (!uris) { + this.workspaceTrustManagementService.setWorkspaceTrust(true); + } else { + this.workspaceTrustManagementService.setFoldersTrust(uris, true); + } }; const trustChoiceWithMenu: IPromptChoiceWithMenu = { @@ -342,8 +352,9 @@ export class WorkspaceTrustEditor extends EditorPane { private addUntrustedTextToElement(parent: HTMLElement): void { const isWorkspaceTrusted = this.workspaceTrustManagementService.isWorkpaceTrusted(); + const canSetWorkspaceTrust = this.workspaceTrustManagementService.canSetWorkspaceTrust(); - if (isWorkspaceTrusted) { + if (canSetWorkspaceTrust && isWorkspaceTrusted) { const textElement = append(parent, $('.workspace-trust-untrusted-description')); textElement.innerText = localize('untrustedFolder', "This workspace is trusted via one or more of the trusted folders below."); } @@ -403,7 +414,7 @@ export class WorkspaceTrustEditor extends EditorPane { if (isArray(change.value)) { if (change.key === 'trustedFolders') { - applyChangesWithPrompt(false, () => this.workspaceTrustStorageService.setTrustedFolders(change.value!)); + applyChangesWithPrompt(false, () => this.workspaceTrustManagementService.setTrustedFolders(change.value!)); } } } @@ -420,8 +431,6 @@ export class WorkspaceTrustEditor extends EditorPane { participant.layout(); }); - this.bodyScrollBar.getDomNode().style.height = `calc(100% - ${this.headerContainer.clientHeight}px)`; - this.bodyScrollBar.scanDomNode(); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts index 5138c594a06..74c16ab436a 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustTree.ts @@ -28,7 +28,6 @@ import { NonCollapsibleObjectTreeModel } from 'vs/workbench/contrib/preferences/ import { AbstractListSettingWidget, focusedRowBackground, focusedRowBorder, ISettingListChangeEvent, rowHoverBackground, settingsHeaderForeground, settingsSelectBackground, settingsTextInputBorder, settingsTextInputForeground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { attachButtonStyler, attachInputBoxStyler, attachStyler } from 'vs/platform/theme/common/styler'; import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { IWorkspaceTrustStateInfo } from 'vs/platform/workspace/common/workspaceTrust'; import { IAction } from 'vs/base/common/actions'; import { settingsEditIcon, settingsRemoveIcon } from 'vs/workbench/contrib/preferences/browser/preferencesIcons'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -573,17 +572,13 @@ export class WorkspaceTrustTreeModel { settings: WorkspaceTrustSettingsTreeEntry[] = []; - update(trustInfo: IWorkspaceTrustStateInfo): void { + update(trustedFolders: URI[]): void { this.settings = []; - if (trustInfo.uriTrustInfo) { - const trustedFolders = trustInfo.uriTrustInfo.filter(folder => folder.trusted).map(folder => folder.uri); - - this.settings.push(new WorkspaceTrustSettingsTreeEntry( - 'trustedFolders', - localize('trustedFolders', "Trusted Folders"), - localize('trustedFoldersDescription', "All workspaces under the following folders will be trusted."), - trustedFolders)); - } + this.settings.push(new WorkspaceTrustSettingsTreeEntry( + 'trustedFolders', + localize('trustedFolders', "Trusted Folders"), + localize('trustedFoldersDescription', "All workspaces under the following folders will be trusted."), + trustedFolders)); } } diff --git a/src/vs/workbench/electron-sandbox/shared.desktop.main.ts b/src/vs/workbench/electron-sandbox/shared.desktop.main.ts index ffacd0b0f8a..18bb665ea99 100644 --- a/src/vs/workbench/electron-sandbox/shared.desktop.main.ts +++ b/src/vs/workbench/electron-sandbox/shared.desktop.main.ts @@ -45,8 +45,8 @@ import { ProxyChannel } from 'vs/base/parts/ipc/common/ipc'; import product from 'vs/platform/product/common/product'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { NativeLogService } from 'vs/workbench/services/log/electron-sandbox/logService'; -import { WorkspaceTrustManagementService, WorkspaceTrustStorageService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustStorageService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustManagementService } from 'vs/workbench/services/workspaces/common/workspaceTrust'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; export abstract class SharedDesktopMain extends Disposable { @@ -263,10 +263,7 @@ export abstract class SharedDesktopMain extends Disposable { ]); // Workspace Trust Service - // TODO @lszomoru: Following two services shall be merged into single service - const workspaceTrustStorageService = new WorkspaceTrustStorageService(storageService, uriIdentityService); - serviceCollection.set(IWorkspaceTrustStorageService, workspaceTrustStorageService); - const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, configurationService, workspaceTrustStorageService); + const workspaceTrustManagementService = new WorkspaceTrustManagementService(configurationService, storageService, uriIdentityService, configurationService, environmentService); serviceCollection.set(IWorkspaceTrustManagementService, workspaceTrustManagementService); configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkpaceTrusted()); this._register(workspaceTrustManagementService.onDidChangeTrust(() => configurationService.updateWorkspaceTrust(workspaceTrustManagementService.isWorkpaceTrusted()))); diff --git a/src/vs/workbench/electron-sandbox/window.ts b/src/vs/workbench/electron-sandbox/window.ts index cafcccccb7b..4168a2a7c68 100644 --- a/src/vs/workbench/electron-sandbox/window.ts +++ b/src/vs/workbench/electron-sandbox/window.ts @@ -48,7 +48,8 @@ import { posix, dirname } from 'vs/base/common/path'; import { getBaseLabel } from 'vs/base/common/labels'; import { ITunnelService, extractLocalHostUriMetaDataForPortMapping } from 'vs/platform/remote/common/tunnel'; import { IWorkbenchLayoutService, Parts, positionFromString, Position } from 'vs/workbench/services/layout/browser/layoutService'; -import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { Event } from 'vs/base/common/event'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; @@ -57,6 +58,8 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { AuthInfo } from 'vs/base/parts/sandbox/electron-sandbox/electronTypes'; import { ILogService } from 'vs/platform/log/common/log'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { whenTextEditorClosed } from 'vs/workbench/browser/editor'; export class NativeWindow extends Disposable { @@ -105,7 +108,8 @@ export class NativeWindow extends Disposable { @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, @IDialogService private readonly dialogService: IDialogService, @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService + @ILogService private readonly logService: ILogService, + @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); @@ -664,8 +668,8 @@ export class NativeWindow extends Disposable { private async trackClosedWaitFiles(waitMarkerFile: URI, resourcesToWaitFor: URI[]): Promise { - // Wait for the resources to be closed in the editor... - await this.editorService.whenClosed(resourcesToWaitFor.map(resource => ({ resource })), { waitForSaved: true }); + // Wait for the resources to be closed in the text editor... + await this.instantiationService.invokeFunction(accessor => whenTextEditorClosed(accessor, resourcesToWaitFor)); // ...before deleting the wait marker file await this.fileService.del(waitMarkerFile); diff --git a/src/vs/workbench/services/backup/common/backup.ts b/src/vs/workbench/services/backup/common/backup.ts deleted file mode 100644 index dfd54a9ee75..00000000000 --- a/src/vs/workbench/services/backup/common/backup.ts +++ /dev/null @@ -1,78 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from 'vs/base/common/uri'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model'; -import { CancellationToken } from 'vs/base/common/cancellation'; - -export const IBackupFileService = createDecorator('backupFileService'); - -export interface IResolvedBackup { - readonly value: ITextBufferFactory; - readonly meta?: T; -} - -/** - * A service that handles any I/O and state associated with the backup system. - */ -export interface IBackupFileService { - - readonly _serviceBrand: undefined; - - /** - * Finds out if there are any backups stored. - */ - hasBackups(): Promise; - - /** - * Finds out if the provided resource with the given version is backed up. - * - * Note: if the backup service has not been initialized yet, this may return - * the wrong result. Always use `resolve()` if you can do a long running - * operation. - */ - hasBackupSync(resource: URI, versionId?: number): boolean; - - /** - * Gets a list of file backups for the current workspace. - * - * @return The list of backups. - */ - getBackups(): Promise; - - /** - * Resolves the backup for the given resource if that exists. - * - * @param resource The resource to get the backup for. - * @return The backup file's backed up content and metadata if available or undefined - * if not backup exists. - */ - resolve(resource: URI): Promise | undefined>; - - /** - * Backs up a resource. - * - * @param resource The resource to back up. - * @param content The optional content of the resource as snapshot. - * @param versionId The optionsl version id of the resource to backup. - * @param meta The optional meta data of the resource to backup. This information - * can be restored later when loading the backup again. - * @param token The optional cancellation token if the operation can be cancelled. - */ - backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise; - - /** - * Discards the backup associated with a resource if it exists. - * - * @param resource The resource whose backup is being discarded discard to back up. - */ - discardBackup(resource: URI): Promise; - - /** - * Discards all backups. - */ - discardBackups(): Promise; -} diff --git a/src/vs/workbench/services/backup/common/backupFileService.ts b/src/vs/workbench/services/backup/common/backupFileService.ts deleted file mode 100644 index bfe8124b9c1..00000000000 --- a/src/vs/workbench/services/backup/common/backupFileService.ts +++ /dev/null @@ -1,518 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { join } from 'vs/base/common/path'; -import { basename, isEqual, joinPath } from 'vs/base/common/resources'; -import { URI } from 'vs/base/common/uri'; -import { coalesce } from 'vs/base/common/arrays'; -import { equals, deepClone } from 'vs/base/common/objects'; -import { ResourceQueue } from 'vs/base/common/async'; -import { IResolvedBackup, IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; -import { ITextSnapshot } from 'vs/editor/common/model'; -import { createTextBufferFactoryFromStream, createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { ResourceMap } from 'vs/base/common/map'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { TextSnapshotReadable, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { ILogService } from 'vs/platform/log/common/log'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Schemas } from 'vs/base/common/network'; -import { hash } from 'vs/base/common/hash'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; -import { BackupRestorer } from 'vs/workbench/services/backup/common/backupRestorer'; -import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; - -export interface IBackupFilesModel { - resolve(backupRoot: URI): Promise; - - get(): URI[]; - has(resource: URI, versionId?: number, meta?: object): boolean; - - add(resource: URI, versionId?: number, meta?: object): void; - remove(resource: URI): void; - move(source: URI, target: URI): void; - - count(): number; - - clear(): void; -} - -interface IBackupCacheEntry { - versionId?: number; - meta?: object; -} - -export class BackupFilesModel implements IBackupFilesModel { - - private readonly cache = new ResourceMap(); - - constructor(private fileService: IFileService) { } - - async resolve(backupRoot: URI): Promise { - try { - const backupRootStat = await this.fileService.resolve(backupRoot); - if (backupRootStat.children) { - await Promise.all(backupRootStat.children - .filter(child => child.isDirectory) - .map(async backupSchema => { - - // Read backup directory for backups - const backupSchemaStat = await this.fileService.resolve(backupSchema.resource); - - // Remember known backups in our caches - if (backupSchemaStat.children) { - backupSchemaStat.children.forEach(backupHash => this.add(backupHash.resource)); - } - })); - } - } catch (error) { - // ignore any errors - } - } - - add(resource: URI, versionId = 0, meta?: object): void { - this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache... - } - - count(): number { - return this.cache.size; - } - - has(resource: URI, versionId?: number, meta?: object): boolean { - const entry = this.cache.get(resource); - if (!entry) { - return false; // unknown resource - } - - if (typeof versionId === 'number' && versionId !== entry.versionId) { - return false; // different versionId - } - - if (meta && !equals(meta, entry.meta)) { - return false; // different metadata - } - - return true; - } - - get(): URI[] { - return [...this.cache.keys()]; - } - - remove(resource: URI): void { - this.cache.delete(resource); - } - - move(source: URI, target: URI): void { - const entry = this.cache.get(source); - if (entry) { - this.cache.delete(source); - this.cache.set(target, entry); - } - } - - clear(): void { - this.cache.clear(); - } -} - -export abstract class BackupFileService implements IBackupFileService { - - declare readonly _serviceBrand: undefined; - - private impl: BackupFileServiceImpl | InMemoryBackupFileService; - - constructor( - backupWorkspaceHome: URI | undefined, - @IFileService protected fileService: IFileService, - @ILogService private readonly logService: ILogService - ) { - this.impl = this.initialize(backupWorkspaceHome); - } - - private hashPath(resource: URI): string { - return hashPath(resource); - } - - private initialize(backupWorkspaceHome: URI | undefined): BackupFileServiceImpl | InMemoryBackupFileService { - if (backupWorkspaceHome) { - return new BackupFileServiceImpl(backupWorkspaceHome, this.hashPath, this.fileService, this.logService); - } - - return new InMemoryBackupFileService(this.hashPath); - } - - reinitialize(backupWorkspaceHome: URI | undefined): void { - - // Re-init implementation (unless we are running in-memory) - if (this.impl instanceof BackupFileServiceImpl) { - if (backupWorkspaceHome) { - this.impl.initialize(backupWorkspaceHome); - } else { - this.impl = new InMemoryBackupFileService(this.hashPath); - } - } - } - - hasBackups(): Promise { - return this.impl.hasBackups(); - } - - hasBackupSync(resource: URI, versionId?: number): boolean { - return this.impl.hasBackupSync(resource, versionId); - } - - backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise { - return this.impl.backup(resource, content, versionId, meta, token); - } - - discardBackup(resource: URI): Promise { - return this.impl.discardBackup(resource); - } - - discardBackups(): Promise { - return this.impl.discardBackups(); - } - - getBackups(): Promise { - return this.impl.getBackups(); - } - - resolve(resource: URI): Promise | undefined> { - return this.impl.resolve(resource); - } - - toBackupResource(resource: URI): URI { - return this.impl.toBackupResource(resource); - } -} - -class BackupFileServiceImpl extends Disposable implements IBackupFileService { - - private static readonly PREAMBLE_END_MARKER = '\n'; - private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator - private static readonly PREAMBLE_MAX_LENGTH = 10000; - - declare readonly _serviceBrand: undefined; - - private backupWorkspacePath!: URI; - - private readonly ioOperationQueues = this._register(new ResourceQueue()); // queue IO operations to ensure write/delete file order - - private ready!: Promise; - private model!: IBackupFilesModel; - - constructor( - backupWorkspaceResource: URI, - private readonly hashPath: (resource: URI) => string, - @IFileService private readonly fileService: IFileService, - @ILogService private readonly logService: ILogService - ) { - super(); - - this.initialize(backupWorkspaceResource); - } - - initialize(backupWorkspaceResource: URI): void { - this.backupWorkspacePath = backupWorkspaceResource; - - this.ready = this.doInitialize(); - } - - private async doInitialize(): Promise { - this.model = new BackupFilesModel(this.fileService); - - // Resolve backup model - await this.model.resolve(this.backupWorkspacePath); - - // Migrate hashes as needed. We used to hash with a MD5 - // sum of the path but switched to our own simpler hash - // to avoid a node.js dependency. We still want to - // support the older hash so we: - // - iterate over all backups - // - detect if the file name length is 32 (MD5 length) - // - read the backup's target file path - // - rename the backup to the new hash - // - update the backup in our model - // - // TODO@bpasero remove me eventually - for (const backupResource of this.model.get()) { - if (basename(backupResource).length !== 32) { - continue; // not a MD5 hash, already uses new hash function - } - - try { - const resource = await this.readUri(backupResource); - if (!resource) { - this.logService.warn(`Backup: Unable to read target URI of backup ${backupResource} for migration to new hash.`); - continue; - } - - const expectedBackupResource = this.toBackupResource(resource); - if (!isEqual(expectedBackupResource, backupResource)) { - await this.fileService.move(backupResource, expectedBackupResource, true); - this.model.move(backupResource, expectedBackupResource); - } - } catch (error) { - this.logService.error(`Backup: Unable to migrate backup ${backupResource} to new hash.`); - } - } - - return this.model; - } - - async hasBackups(): Promise { - const model = await this.ready; - - return model.count() > 0; - } - - hasBackupSync(resource: URI, versionId?: number): boolean { - const backupResource = this.toBackupResource(resource); - - return this.model.has(backupResource, versionId); - } - - async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise { - const model = await this.ready; - if (token?.isCancellationRequested) { - return; - } - - const backupResource = this.toBackupResource(resource); - if (model.has(backupResource, versionId, meta)) { - return; // return early if backup version id matches requested one - } - - return this.ioOperationQueues.queueFor(backupResource).queue(async () => { - if (token?.isCancellationRequested) { - return; - } - - let preamble: string | undefined = undefined; - - // With Metadata: URI + META-START + Meta + END - if (meta) { - const preambleWithMeta = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify(meta)}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`; - if (preambleWithMeta.length < BackupFileServiceImpl.PREAMBLE_MAX_LENGTH) { - preamble = preambleWithMeta; - } - } - - // Without Metadata: URI + END - if (!preamble) { - preamble = `${resource.toString()}${BackupFileServiceImpl.PREAMBLE_END_MARKER}`; - } - - // Update content with value - await this.fileService.writeFile(backupResource, new TextSnapshotReadable(content || stringToSnapshot(''), preamble)); - - // Update model - model.add(backupResource, versionId, meta); - }); - } - - async discardBackups(): Promise { - const model = await this.ready; - - await this.deleteIgnoreFileNotFound(this.backupWorkspacePath); - - model.clear(); - } - - discardBackup(resource: URI): Promise { - const backupResource = this.toBackupResource(resource); - - return this.doDiscardBackup(backupResource); - } - - private async doDiscardBackup(backupResource: URI): Promise { - const model = await this.ready; - - return this.ioOperationQueues.queueFor(backupResource).queue(async () => { - await this.deleteIgnoreFileNotFound(backupResource); - - model.remove(backupResource); - }); - } - - private async deleteIgnoreFileNotFound(resource: URI): Promise { - try { - await this.fileService.del(resource, { recursive: true }); - } catch (error) { - if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { - throw error; // re-throw any other error than file not found which is OK - } - } - } - - async getBackups(): Promise { - const model = await this.ready; - - const backups = await Promise.all(model.get().map(backupResource => this.readUri(backupResource))); - - return coalesce(backups); - } - - private async readUri(backupResource: URI): Promise { - const backupPreamble = await this.readToMatchingString(backupResource, BackupFileServiceImpl.PREAMBLE_END_MARKER, BackupFileServiceImpl.PREAMBLE_MAX_LENGTH); - if (!backupPreamble) { - return undefined; - } - - // Preamble with metadata: URI + META-START + Meta + END - const metaStartIndex = backupPreamble.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR); - if (metaStartIndex > 0) { - return URI.parse(backupPreamble.substring(0, metaStartIndex)); - } - - // Preamble without metadata: URI + END - else { - return URI.parse(backupPreamble); - } - } - - private async readToMatchingString(backupResource: URI, matchingString: string, maximumBytesToRead: number): Promise { - const contents = (await this.fileService.readFile(backupResource, { length: maximumBytesToRead })).value.toString(); - - const matchingStringIndex = contents.indexOf(matchingString); - if (matchingStringIndex >= 0) { - return contents.substr(0, matchingStringIndex); - } - - // Unable to find matching string in file - return undefined; - } - - async resolve(resource: URI): Promise | undefined> { - const backupResource = this.toBackupResource(resource); - - const model = await this.ready; - if (!model.has(backupResource)) { - return undefined; // require backup to be present - } - - // Metadata extraction - let metaRaw = ''; - let metaEndFound = false; - - // Add a filter method to filter out everything until the meta end marker - const metaPreambleFilter = (chunk: VSBuffer) => { - const chunkString = chunk.toString(); - - if (!metaEndFound) { - const metaEndIndex = chunkString.indexOf(BackupFileServiceImpl.PREAMBLE_END_MARKER); - if (metaEndIndex === -1) { - metaRaw += chunkString; - - return VSBuffer.fromString(''); // meta not yet found, return empty string - } - - metaEndFound = true; - metaRaw += chunkString.substring(0, metaEndIndex); // ensure to get last chunk from metadata - - return VSBuffer.fromString(chunkString.substr(metaEndIndex + 1)); // meta found, return everything after - } - - return chunk; - }; - - // Read backup into factory - const content = await this.fileService.readFileStream(backupResource); - const factory = await createTextBufferFactoryFromStream(content.value, metaPreambleFilter); - - // Extract meta data (if any) - let meta: T | undefined; - const metaStartIndex = metaRaw.indexOf(BackupFileServiceImpl.PREAMBLE_META_SEPARATOR); - if (metaStartIndex !== -1) { - try { - meta = JSON.parse(metaRaw.substr(metaStartIndex + 1)); - } catch (error) { - // ignore JSON parse errors - } - } - - // We have seen reports (e.g. https://github.com/microsoft/vscode/issues/78500) where - // if VSCode goes down while writing the backup file, the file can turn empty because - // it always first gets truncated and then written to. In this case, we will not find - // the meta-end marker ('\n') and as such the backup can only be invalid. We bail out - // here if that is the case. - if (!metaEndFound) { - this.logService.trace(`Backup: Could not find meta end marker in ${backupResource}. The file is probably corrupt (filesize: ${content.size}).`); - - return undefined; - } - - return { value: factory, meta }; - } - - toBackupResource(resource: URI): URI { - return joinPath(this.backupWorkspacePath, resource.scheme, this.hashPath(resource)); - } -} - -export class InMemoryBackupFileService implements IBackupFileService { - - declare readonly _serviceBrand: undefined; - - private backups = new Map(); - - constructor(private readonly hashPath: (resource: URI) => string) { } - - async hasBackups(): Promise { - return this.backups.size > 0; - } - - hasBackupSync(resource: URI, versionId?: number): boolean { - const backupResource = this.toBackupResource(resource); - - return this.backups.has(backupResource.toString()); - } - - async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T, token?: CancellationToken): Promise { - const backupResource = this.toBackupResource(resource); - this.backups.set(backupResource.toString(), { content: content || stringToSnapshot(''), meta }); - } - - async resolve(resource: URI): Promise | undefined> { - const backupResource = this.toBackupResource(resource); - const backup = this.backups.get(backupResource.toString()); - if (backup) { - return { value: createTextBufferFactoryFromSnapshot(backup.content), meta: backup.meta as T | undefined }; - } - - return undefined; - } - - async getBackups(): Promise { - return Array.from(this.backups.keys()).map(key => URI.parse(key)); - } - - async discardBackup(resource: URI): Promise { - this.backups.delete(this.toBackupResource(resource).toString()); - } - - async discardBackups(): Promise { - this.backups.clear(); - } - - toBackupResource(resource: URI): URI { - return URI.file(join(resource.scheme, this.hashPath(resource))); - } -} - -/* - * Exported only for testing - */ -export function hashPath(resource: URI): string { - const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); - - return hash(str).toString(16); -} - -// Register Backup Restorer -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BackupRestorer, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts deleted file mode 100644 index 0e2ddeb0a02..00000000000 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ /dev/null @@ -1,722 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { isWindows } from 'vs/base/common/platform'; -import { tmpdir } from 'os'; -import { promises, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; -import { dirname, join } from 'vs/base/common/path'; -import { readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; -import { URI } from 'vs/base/common/uri'; -import { BackupFilesModel, hashPath } from 'vs/workbench/services/backup/common/backupFileService'; -import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { DefaultEndOfLine, ITextSnapshot } from 'vs/editor/common/model'; -import { Schemas } from 'vs/base/common/network'; -import { FileService } from 'vs/platform/files/common/fileService'; -import { NullLogService } from 'vs/platform/log/common/log'; -import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; -import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; -import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles'; -import { IFileService } from 'vs/platform/files/common/files'; -import { NativeBackupFileService } from 'vs/workbench/services/backup/electron-sandbox/backupFileService'; -import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; -import { VSBuffer } from 'vs/base/common/buffer'; -import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; -import { TestProductService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { insert } from 'vs/base/common/arrays'; -import { hash } from 'vs/base/common/hash'; -import { isEqual } from 'vs/base/common/resources'; - -class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService { - - constructor(testDir: string, backupPath: string) { - super({ ...TestWorkbenchConfiguration, backupPath, 'user-data-dir': testDir }, TestProductService); - } -} - -export class NodeTestBackupFileService extends NativeBackupFileService { - - override readonly fileService: IFileService; - - private backupResourceJoiners: Function[]; - private discardBackupJoiners: Function[]; - discardedBackups: URI[]; - private pendingBackupsArr: Promise[]; - - constructor(testDir: string, workspaceBackupPath: string) { - const environmentService = new TestWorkbenchEnvironmentService(testDir, workspaceBackupPath); - const logService = new NullLogService(); - const fileService = new FileService(logService); - const diskFileSystemProvider = new DiskFileSystemProvider(logService); - fileService.registerProvider(Schemas.file, diskFileSystemProvider); - fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService)); - - super(environmentService, fileService, logService); - - this.fileService = fileService; - this.backupResourceJoiners = []; - this.discardBackupJoiners = []; - this.discardedBackups = []; - this.pendingBackupsArr = []; - } - - async waitForAllBackups(): Promise { - await Promise.all(this.pendingBackupsArr); - } - - joinBackupResource(): Promise { - return new Promise(resolve => this.backupResourceJoiners.push(resolve)); - } - - async override backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: any, token?: CancellationToken): Promise { - const p = super.backup(resource, content, versionId, meta, token); - const removeFromPendingBackups = insert(this.pendingBackupsArr, p.then(undefined, undefined)); - - try { - await p; - } finally { - removeFromPendingBackups(); - } - - while (this.backupResourceJoiners.length) { - this.backupResourceJoiners.pop()!(); - } - } - - joinDiscardBackup(): Promise { - return new Promise(resolve => this.discardBackupJoiners.push(resolve)); - } - - async override discardBackup(resource: URI): Promise { - await super.discardBackup(resource); - this.discardedBackups.push(resource); - - while (this.discardBackupJoiners.length) { - this.discardBackupJoiners.pop()!(); - } - } - - async getBackupContents(resource: URI): Promise { - const backupResource = this.toBackupResource(resource); - - const fileContents = await this.fileService.readFile(backupResource); - - return fileContents.value.toString(); - } -} - -suite('BackupFileService', () => { - - let testDir: string; - let backupHome: string; - let workspacesJsonPath: string; - let workspaceBackupPath: string; - let fooBackupPath: string; - let barBackupPath: string; - let untitledBackupPath: string; - let customFileBackupPath: string; - - let service: NodeTestBackupFileService; - - let workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); - let fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); - let customFile = URI.parse('customScheme://some/path'); - let customFileWithFragment = URI.parse('customScheme2://some/path#fragment'); - let barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); - let fooBarFile = URI.file(isWindows ? 'c:\\Foo Bar' : '/Foo Bar'); - let untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); - - setup(async () => { - testDir = getRandomTestPath(tmpdir(), 'vsctests', 'backupfileservice'); - backupHome = join(testDir, 'Backups'); - workspacesJsonPath = join(backupHome, 'workspaces.json'); - workspaceBackupPath = join(backupHome, hashPath(workspaceResource)); - fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashPath(fooFile)); - barBackupPath = join(workspaceBackupPath, barFile.scheme, hashPath(barFile)); - untitledBackupPath = join(workspaceBackupPath, untitledFile.scheme, hashPath(untitledFile)); - customFileBackupPath = join(workspaceBackupPath, customFile.scheme, hashPath(customFile)); - - service = new NodeTestBackupFileService(testDir, workspaceBackupPath); - - await promises.mkdir(backupHome, { recursive: true }); - - return writeFile(workspacesJsonPath, ''); - }); - - teardown(() => { - return rimraf(testDir); - }); - - suite('hashPath', () => { - test('should correctly hash the path for untitled scheme URIs', () => { - const uri = URI.from({ - scheme: 'untitled', - path: 'Untitled-1' - }); - const actual = hashPath(uri); - // If these hashes change people will lose their backed up files! - assert.strictEqual(actual, '-7f9c1a2e'); - assert.strictEqual(actual, hash(uri.fsPath).toString(16)); - }); - - test('should correctly hash the path for file scheme URIs', () => { - const uri = URI.file('/foo'); - const actual = hashPath(uri); - // If these hashes change people will lose their backed up files! - if (isWindows) { - assert.strictEqual(actual, '20ffaa13'); - } else { - assert.strictEqual(actual, '20eb3560'); - } - assert.strictEqual(actual, hash(uri.fsPath).toString(16)); - }); - - test('should correctly hash the path for custom scheme URIs', () => { - const uri = URI.from({ - scheme: 'vscode-custom', - path: 'somePath' - }); - const actual = hashPath(uri); - // If these hashes change people will lose their backed up files! - assert.strictEqual(actual, '-44972d98'); - assert.strictEqual(actual, hash(uri.toString()).toString(16)); - }); - }); - - suite('getBackupResource', () => { - test('should get the correct backup path for text files', () => { - // Format should be: /// - const backupResource = fooFile; - const workspaceHash = hashPath(workspaceResource); - const filePathHash = hashPath(backupResource); - const expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString(); - assert.strictEqual(service.toBackupResource(backupResource).toString(), expectedPath); - }); - - test('should get the correct backup path for untitled files', () => { - // Format should be: /// - const backupResource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); - const workspaceHash = hashPath(workspaceResource); - const filePathHash = hashPath(backupResource); - const expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString(); - assert.strictEqual(service.toBackupResource(backupResource).toString(), expectedPath); - }); - }); - - suite('backup', () => { - test('no text', async () => { - await service.backup(fooFile); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\n`); - assert.ok(service.hasBackupSync(fooFile)); - }); - - test('text file', async () => { - await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\ntest`); - assert.ok(service.hasBackupSync(fooFile)); - }); - - test('text file (with version)', async () => { - await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false), 666); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\ntest`); - assert.ok(!service.hasBackupSync(fooFile, 555)); - assert.ok(service.hasBackupSync(fooFile, 666)); - }); - - test('text file (with meta)', async () => { - await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false), undefined, { etag: '678', orphaned: true }); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()} {"etag":"678","orphaned":true}\ntest`); - assert.ok(service.hasBackupSync(fooFile)); - }); - - test('untitled file', async () => { - await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); - assert.strictEqual(existsSync(untitledBackupPath), true); - assert.strictEqual(readFileSync(untitledBackupPath).toString(), `${untitledFile.toString()}\ntest`); - assert.ok(service.hasBackupSync(untitledFile)); - }); - - test('text file (ITextSnapshot)', async () => { - const model = createTextModel('test'); - - await service.backup(fooFile, model.createSnapshot()); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\ntest`); - assert.ok(service.hasBackupSync(fooFile)); - - model.dispose(); - }); - - test('untitled file (ITextSnapshot)', async () => { - const model = createTextModel('test'); - - await service.backup(untitledFile, model.createSnapshot()); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); - assert.strictEqual(existsSync(untitledBackupPath), true); - assert.strictEqual(readFileSync(untitledBackupPath).toString(), `${untitledFile.toString()}\ntest`); - - model.dispose(); - }); - - test('text file (large file, ITextSnapshot)', async () => { - const largeString = (new Array(10 * 1024)).join('Large String\n'); - const model = createTextModel(largeString); - - await service.backup(fooFile, model.createSnapshot()); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\n${largeString}`); - assert.ok(service.hasBackupSync(fooFile)); - - model.dispose(); - }); - - test('untitled file (large file, ITextSnapshot)', async () => { - const largeString = (new Array(10 * 1024)).join('Large String\n'); - const model = createTextModel(largeString); - - await service.backup(untitledFile, model.createSnapshot()); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); - assert.strictEqual(existsSync(untitledBackupPath), true); - assert.strictEqual(readFileSync(untitledBackupPath).toString(), `${untitledFile.toString()}\n${largeString}`); - assert.ok(service.hasBackupSync(untitledFile)); - - model.dispose(); - }); - - test('cancellation', async () => { - const cts = new CancellationTokenSource(); - const promise = service.backup(fooFile, undefined, undefined, undefined, cts.token); - cts.cancel(); - await promise; - - assert.strictEqual(existsSync(fooBackupPath), false); - assert.ok(!service.hasBackupSync(fooFile)); - }); - }); - - suite('discardBackup', () => { - test('text file', async () => { - await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - assert.ok(service.hasBackupSync(fooFile)); - - await service.discardBackup(fooFile); - assert.strictEqual(existsSync(fooBackupPath), false); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 0); - assert.ok(!service.hasBackupSync(fooFile)); - }); - - test('untitled file', async () => { - await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); - await service.discardBackup(untitledFile); - assert.strictEqual(existsSync(untitledBackupPath), false); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 0); - }); - }); - - suite('discardBackups', () => { - test('text file', async () => { - await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); - await service.backup(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 2); - await service.discardBackups(); - assert.strictEqual(existsSync(fooBackupPath), false); - assert.strictEqual(existsSync(barBackupPath), false); - assert.strictEqual(existsSync(join(workspaceBackupPath, 'file')), false); - }); - - test('untitled file', async () => { - await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); - await service.discardBackups(); - assert.strictEqual(existsSync(untitledBackupPath), false); - assert.strictEqual(existsSync(join(workspaceBackupPath, 'untitled')), false); - }); - - test('can backup after discarding all', async () => { - await service.discardBackups(); - await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - assert.strictEqual(existsSync(workspaceBackupPath), true); - }); - }); - - suite('getBackups', () => { - test('("file") - text file', async () => { - await service.backup(fooFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - const textFiles = await service.getBackups(); - assert.deepStrictEqual(textFiles.map(f => f.fsPath), [fooFile.fsPath]); - await service.backup(barFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - const textFiles_1 = await service.getBackups(); - assert.deepStrictEqual(textFiles_1.map(f => f.fsPath), [fooFile.fsPath, barFile.fsPath]); - }); - - test('("file") - untitled file', async () => { - await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - const textFiles = await service.getBackups(); - assert.deepStrictEqual(textFiles.map(f => f.fsPath), [untitledFile.fsPath]); - }); - - test('("untitled") - untitled file', async () => { - await service.backup(untitledFile, createTextBufferFactory('test').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - const textFiles = await service.getBackups(); - assert.deepStrictEqual(textFiles.map(f => f.fsPath), ['Untitled-1']); - }); - }); - - suite('resolve', () => { - - interface IBackupTestMetaData { - mtime?: number; - size?: number; - etag?: string; - orphaned?: boolean; - } - - test('should restore the original contents (untitled file)', async () => { - const contents = 'test\nand more stuff'; - - await testResolveBackup(untitledFile, contents); - }); - - test('should restore the original contents (untitled file with metadata)', async () => { - const contents = 'test\nand more stuff'; - - const meta = { - etag: 'the Etag', - size: 666, - mtime: Date.now(), - orphaned: true - }; - - await testResolveBackup(untitledFile, contents, meta); - }); - - test('should restore the original contents (text file)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'consectetur ', - 'adipiscing ßß elit' - ].join(''); - - await testResolveBackup(fooFile, contents); - }); - - test('should restore the original contents (text file - custom scheme)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'consectetur ', - 'adipiscing ßß elit' - ].join(''); - - await testResolveBackup(customFile, contents); - }); - - test('should restore the original contents (text file with metadata)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'adipiscing ßß elit', - 'consectetur ' - ].join(''); - - const meta = { - etag: 'theEtag', - size: 888, - mtime: Date.now(), - orphaned: false - }; - - await testResolveBackup(fooFile, contents, meta); - }); - - test('should restore the original contents (text file with metadata changed once)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'adipiscing ßß elit', - 'consectetur ' - ].join(''); - - const meta = { - etag: 'theEtag', - size: 888, - mtime: Date.now(), - orphaned: false - }; - - await testResolveBackup(fooFile, contents, meta); - - // Change meta and test again - meta.size = 999; - await testResolveBackup(fooFile, contents, meta); - }); - - test('should restore the original contents (text file with broken metadata)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'adipiscing ßß elit', - 'consectetur ' - ].join(''); - - const meta = { - etag: 'theEtag', - size: 888, - mtime: Date.now(), - orphaned: false - }; - - await service.backup(fooFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false), 1, meta); - - const fileContents = readFileSync(fooBackupPath).toString(); - assert.strictEqual(fileContents.indexOf(fooFile.toString()), 0); - - const metaIndex = fileContents.indexOf('{'); - const newFileContents = fileContents.substring(0, metaIndex) + '{{' + fileContents.substr(metaIndex); - writeFileSync(fooBackupPath, newFileContents); - - const backup = await service.resolve(fooFile); - assert.ok(backup); - assert.strictEqual(contents, snapshotToString(backup!.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer.createSnapshot(true))); - assert.ok(!backup!.meta); - }); - - test('should restore the original contents (text file with metadata and fragment URI)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'adipiscing ßß elit', - 'consectetur ' - ].join(''); - - const meta = { - etag: 'theEtag', - size: 888, - mtime: Date.now(), - orphaned: false - }; - - await testResolveBackup(customFileWithFragment, contents, meta); - }); - - test('should restore the original contents (text file with space in name with metadata)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'adipiscing ßß elit', - 'consectetur ' - ].join(''); - - const meta = { - etag: 'theEtag', - size: 888, - mtime: Date.now(), - orphaned: false - }; - - await testResolveBackup(fooBarFile, contents, meta); - }); - - test('should restore the original contents (text file with too large metadata to persist)', async () => { - const contents = [ - 'Lorem ipsum ', - 'dolor öäü sit amet ', - 'adipiscing ßß elit', - 'consectetur ' - ].join(''); - - const meta = { - etag: (new Array(100 * 1024)).join('Large String'), - size: 888, - mtime: Date.now(), - orphaned: false - }; - - await testResolveBackup(fooBarFile, contents, meta, null); - }); - - test('should ignore invalid backups', async () => { - const contents = 'test\nand more stuff'; - - await service.backup(fooBarFile, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false), 1); - - const backup = await service.resolve(fooBarFile); - if (!backup) { - throw new Error('Unexpected missing backup'); - } - - await service.fileService.writeFile(service.toBackupResource(fooBarFile), VSBuffer.fromString('')); - - let err: Error | undefined = undefined; - try { - await service.resolve(fooBarFile); - } catch (error) { - err = error; - } - - assert.ok(!err); - }); - - async function testResolveBackup(resource: URI, contents: string, meta?: IBackupTestMetaData, expectedMeta?: IBackupTestMetaData | null) { - if (typeof expectedMeta === 'undefined') { - expectedMeta = meta; - } - - await service.backup(resource, createTextBufferFactory(contents).create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false), 1, meta); - - const backup = await service.resolve(resource); - assert.ok(backup); - assert.strictEqual(contents, snapshotToString(backup!.value.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer.createSnapshot(true))); - - if (expectedMeta) { - assert.strictEqual(backup!.meta!.etag, expectedMeta.etag); - assert.strictEqual(backup!.meta!.size, expectedMeta.size); - assert.strictEqual(backup!.meta!.mtime, expectedMeta.mtime); - assert.strictEqual(backup!.meta!.orphaned, expectedMeta.orphaned); - } else { - assert.ok(!backup!.meta); - } - } - }); - - suite('BackupFilesModel', () => { - - test('simple', () => { - const model = new BackupFilesModel(service.fileService); - - const resource1 = URI.file('test.html'); - - assert.strictEqual(model.has(resource1), false); - - model.add(resource1); - - assert.strictEqual(model.has(resource1), true); - assert.strictEqual(model.has(resource1, 0), true); - assert.strictEqual(model.has(resource1, 1), false); - assert.strictEqual(model.has(resource1, 1, { foo: 'bar' }), false); - - model.remove(resource1); - - assert.strictEqual(model.has(resource1), false); - - model.add(resource1); - - assert.strictEqual(model.has(resource1), true); - assert.strictEqual(model.has(resource1, 0), true); - assert.strictEqual(model.has(resource1, 1), false); - - model.clear(); - - assert.strictEqual(model.has(resource1), false); - - model.add(resource1, 1); - - assert.strictEqual(model.has(resource1), true); - assert.strictEqual(model.has(resource1, 0), false); - assert.strictEqual(model.has(resource1, 1), true); - - const resource2 = URI.file('test1.html'); - const resource3 = URI.file('test2.html'); - const resource4 = URI.file('test3.html'); - - model.add(resource2); - model.add(resource3); - model.add(resource4, undefined, { foo: 'bar' }); - - assert.strictEqual(model.has(resource1), true); - assert.strictEqual(model.has(resource2), true); - assert.strictEqual(model.has(resource3), true); - - assert.strictEqual(model.has(resource4), true); - assert.strictEqual(model.has(resource4, undefined, { foo: 'bar' }), true); - assert.strictEqual(model.has(resource4, undefined, { bar: 'foo' }), false); - - const resource5 = URI.file('test4.html'); - model.move(resource4, resource5); - assert.strictEqual(model.has(resource4), false); - assert.strictEqual(model.has(resource5), true); - }); - - test('resolve', async () => { - await promises.mkdir(dirname(fooBackupPath), { recursive: true }); - writeFileSync(fooBackupPath, 'foo'); - const model = new BackupFilesModel(service.fileService); - - await model.resolve(URI.file(workspaceBackupPath)); - assert.strictEqual(model.has(URI.file(fooBackupPath)), true); - }); - - test('get', () => { - const model = new BackupFilesModel(service.fileService); - - assert.deepStrictEqual(model.get(), []); - - const file1 = URI.file('/root/file/foo.html'); - const file2 = URI.file('/root/file/bar.html'); - const untitled = URI.file('/root/untitled/bar.html'); - - model.add(file1); - model.add(file2); - model.add(untitled); - - assert.deepStrictEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]); - }); - }); - - suite('Hash migration', () => { - - test('works', async () => { - - // Prepare backups of the old MD5 hash format - mkdirSync(join(workspaceBackupPath, fooFile.scheme), { recursive: true }); - mkdirSync(join(workspaceBackupPath, untitledFile.scheme), { recursive: true }); - mkdirSync(join(workspaceBackupPath, customFile.scheme), { recursive: true }); - writeFileSync(join(workspaceBackupPath, fooFile.scheme, '8a8589a2f1c9444b89add38166f50229'), `${fooFile.toString()}\ntest file`); - writeFileSync(join(workspaceBackupPath, untitledFile.scheme, '13264068d108c6901b3592ea654fcd57'), `${untitledFile.toString()}\ntest untitled`); - writeFileSync(join(workspaceBackupPath, customFile.scheme, 'bf018572af7b38746b502893bd0adf6c'), `${customFile.toString()}\ntest custom`); - - service.reinitialize(URI.file(workspaceBackupPath)); - - const backups = await service.getBackups(); - assert.strictEqual(backups.length, 3); - assert.ok(backups.some(backup => isEqual(backup, fooFile))); - assert.ok(backups.some(backup => isEqual(backup, untitledFile))); - assert.ok(backups.some(backup => isEqual(backup, customFile))); - - assert.strictEqual(readdirSync(join(workspaceBackupPath, fooFile.scheme)).length, 1); - assert.strictEqual(existsSync(fooBackupPath), true); - assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\ntest file`); - assert.ok(service.hasBackupSync(fooFile)); - - assert.strictEqual(readdirSync(join(workspaceBackupPath, untitledFile.scheme)).length, 1); - assert.strictEqual(existsSync(untitledBackupPath), true); - assert.strictEqual(readFileSync(untitledBackupPath).toString(), `${untitledFile.toString()}\ntest untitled`); - assert.ok(service.hasBackupSync(untitledFile)); - - assert.strictEqual(readdirSync(join(workspaceBackupPath, customFile.scheme)).length, 1); - assert.strictEqual(existsSync(customFileBackupPath), true); - assert.strictEqual(readFileSync(customFileBackupPath).toString(), `${customFile.toString()}\ntest custom`); - assert.ok(service.hasBackupSync(customFile)); - }); - }); -}); diff --git a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts index a6eb25f5abf..9d81b17e5c9 100644 --- a/src/vs/workbench/services/dialogs/browser/fileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/fileDialogService.ts @@ -8,9 +8,17 @@ import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { AbstractFileDialogService } from 'vs/workbench/services/dialogs/browser/abstractFileDialogService'; import { Schemas } from 'vs/base/common/network'; +import { memoize } from 'vs/base/common/decorators'; +import { HTMLFileSystemProvider } from 'vs/platform/files/browser/htmlFileSystemProvider'; +import { generateUuid } from 'vs/base/common/uuid'; export class FileDialogService extends AbstractFileDialogService implements IFileDialogService { + @memoize + private get fileSystemProvider(): HTMLFileSystemProvider { + return this.fileService.getProvider(Schemas.file) as HTMLFileSystemProvider; + } + async pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise { const schema = this.getFileSystemSchema(options); @@ -18,7 +26,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultFilePath(schema); } - return this.pickFileFolderAndOpenSimplified(schema, options, false); + if (this.shouldUseSimplified(schema)) { + return this.pickFileFolderAndOpenSimplified(schema, options, false); + } + + throw new Error('Method not implemented.'); } async pickFileAndOpen(options: IPickAndOpenOptions): Promise { @@ -28,7 +40,17 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultFilePath(schema); } - return this.pickFileAndOpenSimplified(schema, options, false); + if (this.shouldUseSimplified(schema)) { + return this.pickFileAndOpenSimplified(schema, options, false); + } + + const [handle] = await window.showOpenFilePicker({ multiple: false }); + const uuid = generateUuid(); + const uri = URI.from({ scheme: Schemas.file, authority: uuid, path: `/${handle.name}` }); + + this.fileSystemProvider.registerFileHandle(uuid, handle); + + await this.openerService.open(uri, { fromUserGesture: true, editorOptions: { pinned: true } }); } async pickFolderAndOpen(options: IPickAndOpenOptions): Promise { @@ -38,7 +60,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultFolderPath(schema); } - return this.pickFolderAndOpenSimplified(schema, options); + if (this.shouldUseSimplified(schema)) { + return this.pickFolderAndOpenSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } async pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise { @@ -48,27 +74,50 @@ export class FileDialogService extends AbstractFileDialogService implements IFil options.defaultUri = await this.defaultWorkspacePath(schema); } - return this.pickWorkspaceAndOpenSimplified(schema, options); + if (this.shouldUseSimplified(schema)) { + return this.pickWorkspaceAndOpenSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } async pickFileToSave(defaultUri: URI, availableFileSystems?: string[]): Promise { const schema = this.getFileSystemSchema({ defaultUri, availableFileSystems }); - return this.pickFileToSaveSimplified(schema, this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems)); + + if (this.shouldUseSimplified(schema)) { + return this.pickFileToSaveSimplified(schema, this.getPickFileToSaveDialogOptions(defaultUri, availableFileSystems)); + } + + throw new Error('Method not implemented.'); } async showSaveDialog(options: ISaveDialogOptions): Promise { const schema = this.getFileSystemSchema(options); - return this.showSaveDialogSimplified(schema, options); + + if (this.shouldUseSimplified(schema)) { + return this.showSaveDialogSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } async showOpenDialog(options: IOpenDialogOptions): Promise { const schema = this.getFileSystemSchema(options); - return this.showOpenDialogSimplified(schema, options); + + if (this.shouldUseSimplified(schema)) { + return this.showOpenDialogSimplified(schema, options); + } + + throw new Error('Method not implemented.'); } protected addFileSchemaIfNeeded(schema: string): string[] { return schema === Schemas.untitled ? [Schemas.file] : [schema]; } + + private shouldUseSimplified(schema: string): boolean { + return schema !== Schemas.file; + } } registerSingleton(IFileDialogService, FileDialogService, true); diff --git a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts index 2db31b9aa70..515fc319e6e 100644 --- a/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/simpleFileDialog.ts @@ -800,13 +800,25 @@ export class SimpleFileDialog { private async updateItems(newFolder: URI, force: boolean = false, trailing?: string) { this.busy = true; - this.userEnteredPathSegment = trailing ? trailing : ''; this.autoCompletePathSegment = ''; - const newValue = trailing ? this.pathAppend(newFolder, trailing) : this.pathFromUri(newFolder, true); - this.currentFolder = resources.addTrailingPathSeparator(newFolder, this.separator); const updatingPromise = createCancelablePromise(async token => { - return this.createItems(this.currentFolder, token).then(items => { + let folderStat: IFileStat | undefined; + try { + folderStat = await this.fileService.resolve(newFolder); + if (!folderStat.isDirectory) { + trailing = resources.basename(newFolder); + newFolder = resources.dirname(newFolder); + folderStat = undefined; + } + } catch (e) { + // The file/directory doesn't exist + } + const newValue = trailing ? this.pathAppend(newFolder, trailing) : this.pathFromUri(newFolder, true); + this.currentFolder = resources.addTrailingPathSeparator(newFolder, this.separator); + this.userEnteredPathSegment = trailing ? trailing : ''; + + return this.createItems(folderStat, this.currentFolder, token).then(items => { if (token.isCancellationRequested) { this.busy = false; return; @@ -814,10 +826,9 @@ export class SimpleFileDialog { this.filePickBox.items = items; this.filePickBox.activeItems = [this.filePickBox.items[0]]; - if (this.allowFolderSelection) { - this.filePickBox.activeItems = []; - } - // the user might have continued typing while we were updating. Only update the input box if it doesn't matche directory. + this.filePickBox.activeItems = []; + + // the user might have continued typing while we were updating. Only update the input box if it doesn't match the directory. if (!equalsIgnoreCase(this.filePickBox.value, newValue) && force) { this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; this.insertText(newValue, newValue); @@ -893,12 +904,14 @@ export class SimpleFileDialog { return null; } - private async createItems(currentFolder: URI, token: CancellationToken): Promise { + private async createItems(folder: IFileStat | undefined, currentFolder: URI, token: CancellationToken): Promise { const result: FileQuickPickItem[] = []; const backDir = this.createBackItem(currentFolder); try { - const folder = await this.fileService.resolve(currentFolder); + if (!folder) { + folder = await this.fileService.resolve(currentFolder); + } const items = folder.children ? await Promise.all(folder.children.map(child => this.createItem(child, currentFolder, token))) : []; for (let item of items) { if (item) { diff --git a/src/vs/workbench/services/editor/browser/editorOverrideService.ts b/src/vs/workbench/services/editor/browser/editorOverrideService.ts index 8863063fc00..8a3d503fcea 100644 --- a/src/vs/workbench/services/editor/browser/editorOverrideService.ts +++ b/src/vs/workbench/services/editor/browser/editorOverrideService.ts @@ -250,6 +250,7 @@ export class EditorOverrideService extends Disposable implements IEditorOverride if (selectedContribution.editorInfo.instanceOf(existing)) { existing.dispose(); } + return; } } return group.openEditor(input, options); diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index c2a893e1803..b2c546b0860 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -34,7 +34,6 @@ import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/u import { Promises, timeout } from 'vs/base/common/async'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { indexOfPath } from 'vs/base/common/extpath'; -import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ILogService } from 'vs/platform/log/common/log'; @@ -76,7 +75,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { @IFileService private readonly fileService: IFileService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @ILogService private readonly logService: ILogService, @IQuickInputService private readonly quickInputService: IQuickInputService @@ -1353,67 +1351,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#endregion - - //#region Editor Tracking - - whenClosed(editors: IResourceEditorInput[], options?: { waitForSaved: boolean }): Promise { - let remainingEditors = [...editors]; - - return new Promise(resolve => { - const listener = this.onDidCloseEditor(async event => { - const primaryResource = EditorResourceAccessor.getOriginalUri(event.editor, { supportSideBySide: SideBySideEditor.PRIMARY }); - const secondaryResource = EditorResourceAccessor.getOriginalUri(event.editor, { supportSideBySide: SideBySideEditor.SECONDARY }); - - // Remove from resources to wait for being closed based on the - // resources from editors that got closed - remainingEditors = remainingEditors.filter(({ resource }) => { - if (this.uriIdentityService.extUri.isEqual(resource, primaryResource) || this.uriIdentityService.extUri.isEqual(resource, secondaryResource)) { - return false; // remove - the closing editor matches this resource - } - - return true; // keep - not yet closed - }); - - // All resources to wait for being closed are closed - if (remainingEditors.length === 0) { - if (options?.waitForSaved) { - // If auto save is configured with the default delay (1s) it is possible - // to close the editor while the save still continues in the background. As such - // we have to also check if the editors to track for are dirty and if so wait - // for them to get saved. - const dirtyResources = editors.filter(({ resource }) => this.workingCopyService.isDirty(resource)).map(({ resource }) => resource); - if (dirtyResources.length > 0) { - await Promises.settled(dirtyResources.map(async resource => await this.whenSaved(resource))); - } - } - - listener.dispose(); - - resolve(); - } - }); - }); - } - - private whenSaved(resource: URI): Promise { - return new Promise(resolve => { - if (!this.workingCopyService.isDirty(resource)) { - return resolve(); // return early if resource is not dirty - } - - // Otherwise resolve promise when resource is saved - const listener = this.workingCopyService.onDidChangeDirty(workingCopy => { - if (!workingCopy.isDirty() && this.uriIdentityService.extUri.isEqual(resource, workingCopy.resource)) { - listener.dispose(); - - resolve(); - } - }); - }); - } - - //#endregion - override dispose(): void { super.dispose(); @@ -1505,8 +1442,6 @@ export class DelegatingEditorService implements IEditorService { revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { return this.editorService.revert(editors, options); } revertAll(options?: IRevertAllEditorsOptions): Promise { return this.editorService.revertAll(options); } - whenClosed(editors: IResourceEditorInput[]): Promise { return this.editorService.whenClosed(editors); } - //#endregion } diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 07dc69e6055..22f5e2b117c 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -223,7 +223,10 @@ export interface IEditorService { isOpen(editor: IEditorInput): boolean; /** - * Find the existing editors for a given resource. + * Find the existing editors for a given resource. It is possible + * that multiple editors are returned in case the same resource + * is opened in different editors. To find the specific editor, + * either check on the `typeId` or do an `instanceof` check. */ findEditors(resource: URI, group: IEditorGroup | GroupIdentifier): IEditorInput[]; @@ -271,13 +274,4 @@ export interface IEditorService { * @returns `true` if all editors reverted and `false` otherwise. */ revertAll(options?: IRevertAllEditorsOptions): Promise; - - /** - * Track the provided editors until all have been closed. - * - * @param options use `waitForSaved: true` to wait for the resources - * being saved. If auto-save is enabled, it may be possible to close - * an editor while the save continues in the background. - */ - whenClosed(editors: IResourceEditorInput[], options?: { waitForSaved: boolean }): Promise; } diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index 71d654b4459..d56a6d50fba 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -1029,22 +1029,6 @@ suite('EditorService', () => { handler.dispose(); }); - test('whenClosed', async function () { - const [, service] = await createEditorService(); - - const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID); - const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID); - - const editor = await service.openEditor(input1, { pinned: true }); - await service.openEditor(input2, { pinned: true }); - - const whenClosed = service.whenClosed([{ resource: input1.resource }, { resource: input2.resource }]); - - editor?.group?.closeAllEditors(); - - await whenClosed; - }); - test('findEditors', async () => { const [part, service] = await createEditorService(); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index a661d279ed5..cc55d7b5288 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -108,9 +108,6 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment @memoize get remoteAuthority(): string | undefined { return this.options.remoteAuthority; } - @memoize - get sessionId(): string { return this.configuration.sessionId; } - @memoize get isBuilt(): boolean { return !!this.productService.commit; } diff --git a/src/vs/workbench/services/environment/common/environmentService.ts b/src/vs/workbench/services/environment/common/environmentService.ts index 094d022f64d..6604c9823ae 100644 --- a/src/vs/workbench/services/environment/common/environmentService.ts +++ b/src/vs/workbench/services/environment/common/environmentService.ts @@ -29,8 +29,6 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService { readonly remoteAuthority?: string; - readonly sessionId: string; - readonly logFile: URI; readonly extHostLogsPath: URI; diff --git a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts index 889902b99b1..9fb4fb45d75 100644 --- a/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-sandbox/environmentService.ts @@ -50,9 +50,6 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment @memoize get machineId() { return this.configuration.machineId; } - @memoize - get sessionId() { return this.configuration.sessionId; } - @memoize get remoteAuthority() { return this.configuration.remoteAuthority; } diff --git a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts index 60742700d05..5aad5397e18 100644 --- a/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts +++ b/src/vs/workbench/services/extensionManagement/browser/extensionEnablementService.ts @@ -14,9 +14,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IExtension, isAuthenticaionProviderExtension, isLanguagePackExtension } from 'vs/platform/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IProductService } from 'vs/platform/product/common/productService'; import { StorageManager } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { webWorkerExtHostConfig } from 'vs/workbench/services/extensions/common/extensions'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; @@ -27,7 +25,8 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { Promises } from 'vs/base/common/async'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { getVirtualWorkspaceScheme } from 'vs/platform/remote/common/remoteHosts'; const SOURCE = 'IWorkbenchExtensionEnablementService'; @@ -39,7 +38,6 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench public readonly onEnablementChanged: Event = this._onEnablementChanged.event; private readonly storageManger: StorageManager; - private readonly extensionKindController: ExtensionKindController; constructor( @IStorageService storageService: IStorageService, @@ -47,10 +45,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IExtensionWorkspaceTrustRequestService private readonly extensionWorkspaceTrustRequestService: IExtensionWorkspaceTrustRequestService, @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, - @IProductService productService: IProductService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncAccountService private readonly userDataSyncAccountService: IUserDataSyncAccountService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -58,7 +54,8 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench @IHostService readonly hostService: IHostService, @IExtensionBisectService private readonly extensionBisectService: IExtensionBisectService, @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(); this.storageManger = this._register(new StorageManager(storageService)); @@ -86,8 +83,6 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench }]); }); } - - this.extensionKindController = new ExtensionKindController(productService, configurationService); } private get hasWorkspace(): boolean { @@ -105,6 +100,9 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench if (this._isDisabledInEnv(extension)) { return EnablementState.DisabledByEnvironment; } + if (this._isDisabledByVirtualWorkspace(extension)) { + return EnablementState.DisabledByVirtualWorkspace; + } if (this._isDisabledByExtensionKind(extension)) { return EnablementState.DisabledByExtensionKind; } @@ -121,7 +119,10 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } const enablementState = this.getEnablementState(extension); - if (enablementState === EnablementState.DisabledByEnvironment || enablementState === EnablementState.DisabledByExtensionKind) { + if (enablementState === EnablementState.DisabledByEnvironment + || enablementState === EnablementState.DisabledByVirtualWorkspace + || enablementState === EnablementState.DisabledByTrustRequirement + || enablementState === EnablementState.DisabledByExtensionKind) { return false; } return true; @@ -243,10 +244,17 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } + private _isDisabledByVirtualWorkspace(extension: IExtension): boolean { + if (getVirtualWorkspaceScheme(this.contextService.getWorkspace()) !== undefined) { + return !this.extensionManifestPropertiesService.canSupportVirtualWorkspace(extension.manifest); + } + return false; + } + private _isDisabledByExtensionKind(extension: IExtension): boolean { if (this.extensionManagementServerService.remoteExtensionManagementServer || this.extensionManagementServerService.webExtensionManagementServer) { const server = this.extensionManagementServerService.getExtensionManagementServer(extension); - for (const extensionKind of this.extensionKindController.getExtensionKind(extension.manifest)) { + for (const extensionKind of this.extensionManifestPropertiesService.getExtensionKind(extension.manifest)) { if (extensionKind === 'ui') { if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === server) { return false; @@ -281,7 +289,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench return false; } - return this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(extension.manifest) === 'onStart'; + return this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(extension.manifest) === 'onStart'; } private _getEnablementState(identifier: IExtensionIdentifier): EnablementState { @@ -434,7 +442,7 @@ export class ExtensionEnablementService extends Disposable implements IWorkbench private async _getExtensionsByWorkspaceTrustRequirement(): Promise { const extensions = await this.extensionManagementService.getInstalled(); - return extensions.filter(e => this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(e.manifest) === 'onStart'); + return extensions.filter(e => this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(e.manifest) === 'onStart'); } public async updateEnablementByWorkspaceTrustRequirement(): Promise { diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 3f20f4b55bc..edb80d1af33 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -36,6 +36,7 @@ export const enum EnablementState { DisabledByTrustRequirement, DisabledByExtensionKind, DisabledByEnvironment, + DisabledByVirtualWorkspace, DisabledGlobally, DisabledWorkspace, EnabledGlobally, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts index bd730af5360..d824f860b7d 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts @@ -18,6 +18,7 @@ import { WebRemoteExtensionManagementService } from 'vs/workbench/services/exten import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IProductService } from 'vs/platform/product/common/productService'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -34,10 +35,11 @@ export class ExtensionManagementServerService implements IExtensionManagementSer @IProductService productService: IProductService, @IConfigurationService configurationService: IConfigurationService, @IInstantiationService instantiationService: IInstantiationService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new WebRemoteExtensionManagementService(remoteAgentConnection.getChannel('extensions'), galleryService, configurationService, productService); + const extensionManagementService = new WebRemoteExtensionManagementService(remoteAgentConnection.getChannel('extensions'), galleryService, configurationService, productService, extensionManifestPropertiesService); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts index cc66e2789d5..2bbab15fe30 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -15,7 +15,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { localize } from 'vs/nls'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IProductService } from 'vs/platform/product/common/productService'; import { Schemas } from 'vs/base/common/network'; import { IDownloadService } from 'vs/platform/download/common/download'; @@ -26,7 +25,7 @@ import { canceled } from 'vs/base/common/errors'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; import { Promises } from 'vs/base/common/async'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export class ExtensionManagementService extends Disposable implements IWorkbenchExtensionManagementService { @@ -39,19 +38,17 @@ export class ExtensionManagementService extends Disposable implements IWorkbench protected readonly servers: IExtensionManagementServer[] = []; - protected readonly extensionKindController: ExtensionKindController; - constructor( @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, - @IExtensionWorkspaceTrustRequestService private readonly extensionWorkspaceTrustRequestService: IExtensionWorkspaceTrustRequestService, @IConfigurationService protected readonly configurationService: IConfigurationService, @IProductService protected readonly productService: IProductService, @IDownloadService protected readonly downloadService: IDownloadService, @IUserDataAutoSyncEnablementService private readonly userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IDialogService private readonly dialogService: IDialogService, - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IExtensionManifestPropertiesService private readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(); if (this.extensionManagementServerService.localExtensionManagementServer) { @@ -68,8 +65,6 @@ export class ExtensionManagementService extends Disposable implements IWorkbench this.onDidInstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidInstallExtension); return emitter; }, new EventMultiplexer())).event; this.onUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onUninstallExtension); return emitter; }, new EventMultiplexer())).event; this.onDidUninstallExtension = this._register(this.servers.reduce((emitter: EventMultiplexer, server) => { emitter.add(server.extensionManagementService.onDidUninstallExtension); return emitter; }, new EventMultiplexer())).event; - - this.extensionKindController = new ExtensionKindController(productService, configurationService); } async getInstalled(type?: ExtensionType): Promise { @@ -113,7 +108,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench private async uninstallInServer(extension: ILocalExtension, server: IExtensionManagementServer, options?: UninstallOptions): Promise { if (server === this.extensionManagementServerService.localExtensionManagementServer) { const installedExtensions = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getInstalled(ExtensionType.User); - const dependentNonUIExtensions = installedExtensions.filter(i => !this.extensionKindController.prefersExecuteOnUI(i.manifest) + const dependentNonUIExtensions = installedExtensions.filter(i => !this.extensionManifestPropertiesService.prefersExecuteOnUI(i.manifest) && i.manifest.extensionDependencies && i.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier))); if (dependentNonUIExtensions.length) { return Promise.reject(new Error(this.getDependentsErrorMessage(extension, dependentNonUIExtensions))); @@ -184,7 +179,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench const [local] = await Promises.settled([this.extensionManagementServerService.localExtensionManagementServer, this.extensionManagementServerService.remoteExtensionManagementServer].map(server => this.installVSIX(vsix, server))); return local; } - if (this.extensionKindController.prefersExecuteOnUI(manifest)) { + if (this.extensionManifestPropertiesService.prefersExecuteOnUI(manifest)) { // Install only on local server return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer); } @@ -299,7 +294,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench return this.extensionManagementServerService.localExtensionManagementServer; } - const extensionKind = this.extensionKindController.getExtensionKind(manifest); + const extensionKind = this.extensionManifestPropertiesService.getExtensionKind(manifest); for (const kind of extensionKind) { if (kind === 'ui' && this.extensionManagementServerService.localExtensionManagementServer) { return this.extensionManagementServerService.localExtensionManagementServer; @@ -363,7 +358,7 @@ export class ExtensionManagementService extends Disposable implements IWorkbench } protected async checkForWorkspaceTrust(manifest: IExtensionManifest): Promise { - if (this.extensionWorkspaceTrustRequestService.getExtensionWorkspaceTrustRequestType(manifest) === 'onStart') { + if (this.extensionManifestPropertiesService.getExtensionWorkspaceTrustRequestType(manifest) === 'onStart') { const trustState = await this.workspaceTrustRequestService.requestWorkspaceTrust({ modal: true, message: localize('extensionInstallWorkspaceTrustMessage', "Enabling this extension requires a trusted workspace."), diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts index 588bccce94e..1438be0c5f7 100644 --- a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -5,30 +5,27 @@ import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { IExtensionManagementService, IGalleryExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IProductService } from 'vs/platform/product/common/productService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export class WebRemoteExtensionManagementService extends ExtensionManagementChannelClient implements IExtensionManagementService { - protected readonly extensionKindController: ExtensionKindController; - constructor( channel: IChannel, @IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService, @IConfigurationService protected readonly configurationService: IConfigurationService, - @IProductService protected readonly productService: IProductService + @IProductService protected readonly productService: IProductService, + @IExtensionManifestPropertiesService protected readonly extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(channel); - - this.extensionKindController = new ExtensionKindController(productService, configurationService); } async override canInstall(extension: IGalleryExtension): Promise { const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); - return !!manifest && this.extensionKindController.canExecuteOnWorkspace(manifest); + return !!manifest && this.extensionManifestPropertiesService.canExecuteOnWorkspace(manifest); } } diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts index f272cca128d..f9fb1ef1c31 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/extensionManagementService.ts @@ -18,7 +18,7 @@ import { joinPath } from 'vs/base/common/resources'; import { IUserDataAutoSyncEnablementService, IUserDataSyncResourceEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export class ExtensionManagementService extends BaseExtensionManagementService { @@ -26,16 +26,16 @@ export class ExtensionManagementService extends BaseExtensionManagementService { @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IExtensionGalleryService extensionGalleryService: IExtensionGalleryService, - @IExtensionWorkspaceTrustRequestService extensionWorkspaceTrustRequestService: IExtensionWorkspaceTrustRequestService, @IConfigurationService configurationService: IConfigurationService, @IProductService productService: IProductService, @IDownloadService downloadService: IDownloadService, @IUserDataAutoSyncEnablementService userDataAutoSyncEnablementService: IUserDataAutoSyncEnablementService, @IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService, @IDialogService dialogService: IDialogService, - @IWorkspaceTrustRequestService workspaceTrustRequestService: IWorkspaceTrustRequestService + @IWorkspaceTrustRequestService workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(extensionManagementServerService, extensionGalleryService, extensionWorkspaceTrustRequestService, configurationService, productService, downloadService, userDataAutoSyncEnablementService, userDataSyncResourceEnablementService, dialogService, workspaceTrustRequestService); + super(extensionManagementServerService, extensionGalleryService, configurationService, productService, downloadService, userDataAutoSyncEnablementService, userDataSyncResourceEnablementService, dialogService, workspaceTrustRequestService, extensionManifestPropertiesService); } protected async override installVSIX(vsix: URI, server: IExtensionManagementServer): Promise { diff --git a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts index 2457ecfb80b..746049b0f8a 100644 --- a/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-sandbox/remoteExtensionManagementService.ts @@ -21,6 +21,7 @@ import { WebRemoteExtensionManagementService } from 'vs/workbench/services/exten import { IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { Promises } from 'vs/base/common/async'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export class NativeRemoteExtensionManagementService extends WebRemoteExtensionManagementService implements IExtensionManagementService { @@ -33,9 +34,10 @@ export class NativeRemoteExtensionManagementService extends WebRemoteExtensionMa @IExtensionGalleryService galleryService: IExtensionGalleryService, @IConfigurationService configurationService: IConfigurationService, @IProductService productService: IProductService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { - super(channel, galleryService, configurationService, productService); + super(channel, galleryService, configurationService, productService, extensionManifestPropertiesService); this.localExtensionManagementService = localExtensionManagementServer.extensionManagementService; } @@ -125,7 +127,7 @@ export class NativeRemoteExtensionManagementService extends WebRemoteExtensionMa for (let idx = 0; idx < extensions.length; idx++) { const extension = extensions[idx]; const manifest = manifests[idx]; - if (manifest && this.extensionKindController.prefersExecuteOnUI(manifest) === uiExtension) { + if (manifest && this.extensionManifestPropertiesService.prefersExecuteOnUI(manifest) === uiExtension) { result.set(extension.identifier.id.toLowerCase(), extension); extensionsManifests.push(manifest); } diff --git a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts index c5e2c7fcf88..1bd62ad1b2f 100644 --- a/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts +++ b/src/vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test.ts @@ -9,21 +9,20 @@ import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManage import { ExtensionEnablementService } from 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { Emitter } from 'vs/base/common/event'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionContributions, ExtensionType, IExtension, IExtensionManifest, ExtensionWorkspaceTrustRequestType } from 'vs/platform/extensions/common/extensions'; +import { IExtensionContributions, ExtensionType, IExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { productService, TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestLifecycleService } from 'vs/workbench/test/browser/workbenchTestServices'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { IUserDataSyncAccountService, UserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { IUserDataAutoSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; -// import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; @@ -32,7 +31,9 @@ import { mock } from 'vs/base/test/common/mock'; import { IExtensionBisectService } from 'vs/workbench/services/extensionManagement/browser/extensionBisect'; import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { TestWorkspaceTrustManagementService, TestWorkspaceTrustRequestService } from 'vs/workbench/services/workspaces/test/common/testWorkspaceTrustService'; -import { IExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { ExtensionManifestPropertiesService, IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; +import { TestContextService, TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; +import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; function createStorageService(instantiationService: TestInstantiationService): IStorageService { let service = instantiationService.get(IStorageService); @@ -41,6 +42,7 @@ function createStorageService(instantiationService: TestInstantiationService): I if (!workspaceContextService) { workspaceContextService = instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER, + getWorkspace: () => TestWorkspace as IWorkspace }); } service = instantiationService.stub(IStorageService, new InMemoryStorageService()); @@ -56,13 +58,11 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { super( storageService, new GlobalExtensionEnablementService(storageService), - instantiationService.get(IWorkspaceContextService), + instantiationService.get(IWorkspaceContextService) || new TestContextService(), instantiationService.get(IWorkbenchEnvironmentService) || instantiationService.stub(IWorkbenchEnvironmentService, { configuration: Object.create(null) } as IWorkbenchEnvironmentService), extensionManagementService, - new class extends mock() { override getExtensionWorkspaceTrustRequestType(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType { return 'never'; } }, instantiationService.get(IConfigurationService), extensionManagementServerService, - productService, instantiationService.get(IUserDataAutoSyncEnablementService) || instantiationService.stub(IUserDataAutoSyncEnablementService, >{ isEnabled() { return false; } }), instantiationService.get(IUserDataSyncAccountService) || instantiationService.stub(IUserDataSyncAccountService, UserDataSyncAccountService), instantiationService.get(ILifecycleService) || instantiationService.stub(ILifecycleService, new TestLifecycleService()), @@ -70,7 +70,8 @@ export class TestExtensionEnablementService extends ExtensionEnablementService { instantiationService.get(IHostService), new class extends mock() { override isDisabledByBisect() { return false; } }, instantiationService.get(IWorkspaceTrustManagementService) || instantiationService.stub(IWorkspaceTrustManagementService, new TestWorkspaceTrustManagementService()), - instantiationService.get(IWorkspaceTrustRequestService) || instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService()) + instantiationService.get(IWorkspaceTrustRequestService) || instantiationService.stub(IWorkspaceTrustRequestService, new TestWorkspaceTrustRequestService()), + instantiationService.get(IExtensionManifestPropertiesService) || instantiationService.stub(IExtensionManifestPropertiesService, new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService())) ); } @@ -476,6 +477,45 @@ suite('ExtensionEnablementService Test', () => { assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByEnvironment); }); + test('test extension does not support vitrual workspace is not enabled in virtual workspace', async () => { + const extension = aLocalExtension2('pub.a', { supportsVirtualWorkspace: false }); + instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(!testObject.isEnabled(extension)); + assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.DisabledByVirtualWorkspace); + }); + + test('test canChangeEnablement return false when extension is disabled in virtual workspace', () => { + const extension = aLocalExtension2('pub.a', { supportsVirtualWorkspace: false }); + instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(!testObject.canChangeEnablement(extension)); + }); + + test('test extension does not support vitrual workspace is enabled in virtual workspace', async () => { + const extension = aLocalExtension2('pub.a', { supportsVirtualWorkspace: false }); + instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA') }] }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(testObject.isEnabled(extension)); + assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); + }); + + test('test extension supports virtual workspace is enabled in virtual workspace', async () => { + const extension = aLocalExtension2('pub.a', { supportsVirtualWorkspace: true }); + instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(testObject.isEnabled(extension)); + assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); + }); + + test('test extension without any value for virtual worksapce is enabled in virtual workspace', async () => { + const extension = aLocalExtension2('pub.a'); + instantiationService.stub(IWorkspaceContextService, 'getWorkspace', { folders: [{ uri: URI.file('worskapceA').with(({ scheme: 'virtual' })) }] }); + testObject = new TestExtensionEnablementService(instantiationService); + assert.ok(testObject.isEnabled(extension)); + assert.deepStrictEqual(testObject.getEnablementState(extension), EnablementState.EnabledGlobally); + }); + test('test local workspace extension is disabled by kind', async () => { instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService)); const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) }); @@ -642,7 +682,7 @@ function aLocalExtension(id: string, contributes?: IExtensionContributions, type return aLocalExtension2(id, contributes ? { contributes } : {}, isUndefinedOrNull(type) ? {} : { type }); } -function aLocalExtension2(id: string, manifest: any = {}, properties: any = {}): ILocalExtension { +function aLocalExtension2(id: string, manifest: Partial = {}, properties: any = {}): ILocalExtension { const [publisher, name] = id.split('.'); manifest = { name, publisher, ...manifest }; properties = { diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index d957e9f96b5..a50a1aba3b4 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -26,6 +26,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -47,6 +48,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IWebExtensionsScannerService private readonly _webExtensionsScannerService: IWebExtensionsScannerService, @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super( new ExtensionRunningLocationClassifier( @@ -63,6 +65,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionManagementService, contextService, configurationService, + extensionManifestPropertiesService ); this._runningLocation = new Map(); diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index f7c0ec9f9fe..aa7bc62b662 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -29,10 +29,10 @@ import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtens import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionActivationHost as IWorkspaceContainsActivationHost, checkGlobFileExists, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExtensionKindController } from 'vs/workbench/services/extensions/common/extensionsUtil'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; const hasOwnProperty = Object.hasOwnProperty; const NO_OP_VOID_PROMISE = Promise.resolve(undefined); @@ -108,8 +108,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx private _extensionHostActivationTimes: Map; private _extensionHostExtensionRuntimeErrors: Map; - private readonly _extensionKindController: ExtensionKindController; - constructor( protected readonly _runningLocationClassifier: ExtensionRunningLocationClassifier, @IInstantiationService protected readonly _instantiationService: IInstantiationService, @@ -122,6 +120,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx @IExtensionManagementService protected readonly _extensionManagementService: IExtensionManagementService, @IWorkspaceContextService private readonly _contextService: IWorkspaceContextService, @IConfigurationService protected readonly _configurationService: IConfigurationService, + @IExtensionManifestPropertiesService protected readonly _extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super(); @@ -150,8 +149,6 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._runningLocation = new Map(); - this._extensionKindController = new ExtensionKindController(this._productService, this._configurationService); - this._register(this._extensionEnablementService.onEnablementChanged((extensions) => { let toAdd: IExtension[] = []; let toRemove: string[] = []; @@ -189,7 +186,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return this._environmentService.extensionDevelopmentKind; } - return this._extensionKindController.getExtensionKind(extensionDescription); + return this._extensionManifestPropertiesService.getExtensionKind(extensionDescription); } protected _getExtensionHostManager(kind: ExtensionHostKind): ExtensionHostManager | null { diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 5d158e414bf..b363ff4c1ff 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -71,7 +71,7 @@ export class ExtensionHostManager extends Disposable { return { value: this._createExtensionHostCustomers(protocol) }; }, (err) => { - console.error('Error received from starting extension host'); + console.error(`Error received from starting extension host (kind: ${this.kind})`); console.error(err); return null; } diff --git a/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts new file mode 100644 index 00000000000..cacab97b21b --- /dev/null +++ b/src/vs/workbench/services/extensions/common/extensionManifestPropertiesService.ts @@ -0,0 +1,312 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IExtensionManifest, ExtensionKind, ExtensionIdentifier, ExtensionWorkspaceTrustRequestType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry'; +import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { ExtensionWorkspaceTrustRequest } from 'vs/base/common/product'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EXTENSION_REQUEST } from 'vs/workbench/services/workspaces/common/workspaceTrust'; + +export const IExtensionManifestPropertiesService = createDecorator('extensionManifestPropertiesService'); + +export interface IExtensionManifestPropertiesService { + readonly _serviceBrand: undefined; + + prefersExecuteOnUI(manifest: IExtensionManifest): boolean; + prefersExecuteOnWorkspace(manifest: IExtensionManifest): boolean; + prefersExecuteOnWeb(manifest: IExtensionManifest): boolean; + + canExecuteOnUI(manifest: IExtensionManifest): boolean; + canExecuteOnWorkspace(manifest: IExtensionManifest): boolean; + canExecuteOnWeb(manifest: IExtensionManifest): boolean; + + getExtensionKind(manifest: IExtensionManifest): ExtensionKind[]; + getExtensionWorkspaceTrustRequestType(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType; + canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean; +} + +export class ExtensionManifestPropertiesService extends Disposable implements IExtensionManifestPropertiesService { + + readonly _serviceBrand: undefined; + + private _uiExtensionPoints: Set | null = null; + private _productExtensionKindsMap: Map | null = null; + private _configuredExtensionKindsMap: Map | null = null; + + private _productVirtualWorkspaceSupportMap: Map | null = null; + private _configuredVirtualWorkspaceSupportMap: Map | null = null; + + private readonly _configuredExtensionWorkspaceTrustRequestMap: Map; + private readonly _productExtensionWorkspaceTrustRequestMap: Map; + + constructor( + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + + // Workspace trust request type (settings.json) + this._configuredExtensionWorkspaceTrustRequestMap = new Map(); + const configuredExtensionWorkspaceTrustRequests = configurationService.inspect<{ [key: string]: { request: ExtensionWorkspaceTrustRequestType, version?: string } }>(WORKSPACE_TRUST_EXTENSION_REQUEST).userValue || {}; + for (const id of Object.keys(configuredExtensionWorkspaceTrustRequests)) { + this._configuredExtensionWorkspaceTrustRequestMap.set(ExtensionIdentifier.toKey(id), configuredExtensionWorkspaceTrustRequests[id]); + } + + // Workpace trust request type (products.json) + this._productExtensionWorkspaceTrustRequestMap = new Map(); + if (productService.extensionWorkspaceTrustRequest) { + for (const id of Object.keys(productService.extensionWorkspaceTrustRequest)) { + this._productExtensionWorkspaceTrustRequestMap.set(ExtensionIdentifier.toKey(id), productService.extensionWorkspaceTrustRequest[id]); + } + } + } + + prefersExecuteOnUI(manifest: IExtensionManifest): boolean { + const extensionKind = this.getExtensionKind(manifest); + return (extensionKind.length > 0 && extensionKind[0] === 'ui'); + } + + prefersExecuteOnWorkspace(manifest: IExtensionManifest): boolean { + const extensionKind = this.getExtensionKind(manifest); + return (extensionKind.length > 0 && extensionKind[0] === 'workspace'); + } + + prefersExecuteOnWeb(manifest: IExtensionManifest): boolean { + const extensionKind = this.getExtensionKind(manifest); + return (extensionKind.length > 0 && extensionKind[0] === 'web'); + } + + canExecuteOnUI(manifest: IExtensionManifest): boolean { + const extensionKind = this.getExtensionKind(manifest); + return extensionKind.some(kind => kind === 'ui'); + } + + canExecuteOnWorkspace(manifest: IExtensionManifest): boolean { + const extensionKind = this.getExtensionKind(manifest); + return extensionKind.some(kind => kind === 'workspace'); + } + + canExecuteOnWeb(manifest: IExtensionManifest): boolean { + const extensionKind = this.getExtensionKind(manifest); + return extensionKind.some(kind => kind === 'web'); + } + + getExtensionKind(manifest: IExtensionManifest): ExtensionKind[] { + // check in config + let result = this.getConfiguredExtensionKind(manifest); + if (typeof result !== 'undefined') { + return this.toArray(result); + } + + // check product.json + result = this.getProductExtensionKind(manifest); + if (typeof result !== 'undefined') { + return result; + } + + // check the manifest itself + result = manifest.extensionKind; + if (typeof result !== 'undefined') { + return this.toArray(result); + } + + return this.deduceExtensionKind(manifest); + } + + getExtensionWorkspaceTrustRequestType(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType { + // Workspace trust feature is disabled, or extension has no entry point + if (!isWorkspaceTrustEnabled(this.configurationService) || !manifest.main) { + return 'never'; + } + + // Get extension workspace trust requirements from settings.json + const configuredWorkspaceTrustRequest = this.getConfiguredExtensionWorkspaceTrustRequest(manifest); + + // Get extension workspace trust requirements from product.json + const productWorkspaceTrustRequest = this.getProductExtensionWorkspaceTrustRequest(manifest); + + // Use settings.json override value if it exists + if (configuredWorkspaceTrustRequest) { + return configuredWorkspaceTrustRequest; + } + + // Use product.json override value if it exists + if (productWorkspaceTrustRequest?.override) { + return productWorkspaceTrustRequest.override; + } + + // Use extension manifest value if it exists + if (manifest.workspaceTrust?.request !== undefined) { + return manifest.workspaceTrust.request; + } + + // Use product.json default value if it exists + if (productWorkspaceTrustRequest?.default) { + return productWorkspaceTrustRequest.default; + } + + return 'onStart'; + } + + canSupportVirtualWorkspace(manifest: IExtensionManifest): boolean { + // check user configured + const userConfiguredVirtualWorkspaceSupport = this.getConfiguredVirtualWorkspaceSupport(manifest); + if (userConfiguredVirtualWorkspaceSupport !== undefined) { + return userConfiguredVirtualWorkspaceSupport; + } + + const productConfiguredWorkspaceSchemes = this.getProductWorkspaceSchemes(manifest); + + // check override from product + if (productConfiguredWorkspaceSchemes?.override !== undefined) { + return productConfiguredWorkspaceSchemes.override; + } + + // check the manifest + if (manifest.supportsVirtualWorkspace !== undefined) { + return manifest.supportsVirtualWorkspace; + } + + // check default from product + if (productConfiguredWorkspaceSchemes?.default !== undefined) { + return productConfiguredWorkspaceSchemes.default; + } + + // Default - supports virtual workspace + return true; + } + + deduceExtensionKind(manifest: IExtensionManifest): ExtensionKind[] { + // Not an UI extension if it has main + if (manifest.main) { + if (manifest.browser) { + return ['workspace', 'web']; + } + return ['workspace']; + } + + if (manifest.browser) { + return ['web']; + } + + // Not an UI nor web extension if it has dependencies or an extension pack + if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) { + return ['workspace']; + } + + if (manifest.contributes) { + // Not an UI nor web extension if it has no ui contributions + for (const contribution of Object.keys(manifest.contributes)) { + if (!this.isUIExtensionPoint(contribution)) { + return ['workspace']; + } + } + } + + return ['ui', 'workspace', 'web']; + } + + private isUIExtensionPoint(extensionPoint: string): boolean { + if (this._uiExtensionPoints === null) { + const uiExtensionPoints = new Set(); + ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').forEach(e => { + uiExtensionPoints.add(e.name); + }); + this._uiExtensionPoints = uiExtensionPoints; + } + return this._uiExtensionPoints.has(extensionPoint); + } + + private getProductExtensionKind(manifest: IExtensionManifest): ExtensionKind[] | undefined { + if (this._productExtensionKindsMap === null) { + const productExtensionKindsMap = new Map(); + if (this.productService.extensionKind) { + for (const id of Object.keys(this.productService.extensionKind)) { + productExtensionKindsMap.set(ExtensionIdentifier.toKey(id), this.productService.extensionKind[id]); + } + } + this._productExtensionKindsMap = productExtensionKindsMap; + } + + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._productExtensionKindsMap.get(ExtensionIdentifier.toKey(extensionId)); + } + + private getConfiguredExtensionKind(manifest: IExtensionManifest): ExtensionKind | ExtensionKind[] | undefined { + if (this._configuredExtensionKindsMap === null) { + const configuredExtensionKindsMap = new Map(); + const configuredExtensionKinds = this.configurationService.getValue<{ [key: string]: ExtensionKind | ExtensionKind[] }>('remote.extensionKind') || {}; + for (const id of Object.keys(configuredExtensionKinds)) { + configuredExtensionKindsMap.set(ExtensionIdentifier.toKey(id), configuredExtensionKinds[id]); + } + this._configuredExtensionKindsMap = configuredExtensionKindsMap; + } + + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._configuredExtensionKindsMap.get(ExtensionIdentifier.toKey(extensionId)); + } + + private getProductWorkspaceSchemes(manifest: IExtensionManifest): { default?: boolean, override?: boolean } | undefined { + if (this._productVirtualWorkspaceSupportMap === null) { + const productWorkspaceSchemesMap = new Map(); + if (this.productService.extensionSupportsVirtualWorkspace) { + for (const id of Object.keys(this.productService.extensionSupportsVirtualWorkspace)) { + productWorkspaceSchemesMap.set(ExtensionIdentifier.toKey(id), this.productService.extensionSupportsVirtualWorkspace[id]); + } + } + this._productVirtualWorkspaceSupportMap = productWorkspaceSchemesMap; + } + + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._productVirtualWorkspaceSupportMap.get(ExtensionIdentifier.toKey(extensionId)); + } + + private getConfiguredVirtualWorkspaceSupport(manifest: IExtensionManifest): boolean | undefined { + if (this._configuredVirtualWorkspaceSupportMap === null) { + const configuredWorkspaceSchemesMap = new Map(); + const configuredWorkspaceSchemes = this.configurationService.getValue<{ [key: string]: boolean }>('extensions.supportsVirtualWorkspace') || {}; + for (const id of Object.keys(configuredWorkspaceSchemes)) { + if (configuredWorkspaceSchemes[id] !== undefined) { + configuredWorkspaceSchemesMap.set(ExtensionIdentifier.toKey(id), configuredWorkspaceSchemes[id]); + } + } + this._configuredVirtualWorkspaceSupportMap = configuredWorkspaceSchemesMap; + } + + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._configuredVirtualWorkspaceSupportMap.get(ExtensionIdentifier.toKey(extensionId)); + } + + private getConfiguredExtensionWorkspaceTrustRequest(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType | undefined { + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + const extensionWorkspaceTrustRequest = this._configuredExtensionWorkspaceTrustRequestMap.get(ExtensionIdentifier.toKey(extensionId)); + + if (extensionWorkspaceTrustRequest && (extensionWorkspaceTrustRequest.version === undefined || extensionWorkspaceTrustRequest.version === manifest.version)) { + return extensionWorkspaceTrustRequest.request; + } + + return undefined; + } + + private getProductExtensionWorkspaceTrustRequest(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequest | undefined { + const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); + return this._productExtensionWorkspaceTrustRequestMap.get(ExtensionIdentifier.toKey(extensionId)); + } + + private toArray(extensionKind: ExtensionKind | ExtensionKind[]): ExtensionKind[] { + if (Array.isArray(extensionKind)) { + return extensionKind; + } + return extensionKind === 'ui' ? ['ui', 'workspace'] : [extensionKind]; + } +} + +registerSingleton(IExtensionManifestPropertiesService, ExtensionManifestPropertiesService); diff --git a/src/vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest.ts b/src/vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest.ts deleted file mode 100644 index 4d39c185c8c..00000000000 --- a/src/vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest.ts +++ /dev/null @@ -1,104 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable } from 'vs/base/common/lifecycle'; -import { ExtensionWorkspaceTrustRequest } from 'vs/base/common/product'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionIdentifier, ExtensionWorkspaceTrustRequestType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { isWorkspaceTrustEnabled, WORKSPACE_TRUST_EXTENSION_REQUEST } from 'vs/workbench/services/workspaces/common/workspaceTrust'; - -export const IExtensionWorkspaceTrustRequestService = createDecorator('extensionWorkspaceTrustRequestService'); - -export interface IExtensionWorkspaceTrustRequestService { - readonly _serviceBrand: undefined; - - getExtensionWorkspaceTrustRequestType(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType; -} - -export class ExtensionWorkspaceTrustRequestService extends Disposable implements IExtensionWorkspaceTrustRequestService { - _serviceBrand: undefined; - - private readonly _configuredExtensionWorkspaceTrustRequestMap: Map; - private readonly _productExtensionWorkspaceTrustRequestMap: Map; - - constructor( - @IProductService productService: IProductService, - @IConfigurationService private readonly configurationService: IConfigurationService - ) { - super(); - - // Settings.json - this._configuredExtensionWorkspaceTrustRequestMap = new Map(); - const configuredExtensionWorkspaceTrustRequests = configurationService.inspect<{ [key: string]: { request: ExtensionWorkspaceTrustRequestType, version?: string } }>(WORKSPACE_TRUST_EXTENSION_REQUEST).userValue || {}; - for (const id of Object.keys(configuredExtensionWorkspaceTrustRequests)) { - this._configuredExtensionWorkspaceTrustRequestMap.set(ExtensionIdentifier.toKey(id), configuredExtensionWorkspaceTrustRequests[id]); - } - - // Products.json - this._productExtensionWorkspaceTrustRequestMap = new Map(); - if (productService.extensionWorkspaceTrustRequest) { - for (const id of Object.keys(productService.extensionWorkspaceTrustRequest)) { - this._productExtensionWorkspaceTrustRequestMap.set(ExtensionIdentifier.toKey(id), productService.extensionWorkspaceTrustRequest[id]); - } - } - } - - private getConfiguredExtensionWorkspaceTrustRequest(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType | undefined { - const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); - const extensionWorkspaceTrustRequest = this._configuredExtensionWorkspaceTrustRequestMap.get(ExtensionIdentifier.toKey(extensionId)); - - if (extensionWorkspaceTrustRequest && (extensionWorkspaceTrustRequest.version === undefined || extensionWorkspaceTrustRequest.version === manifest.version)) { - return extensionWorkspaceTrustRequest.request; - } - - return undefined; - } - - private getProductExtensionWorkspaceTrustRequest(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequest | undefined { - const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name); - return this._productExtensionWorkspaceTrustRequestMap.get(ExtensionIdentifier.toKey(extensionId)); - } - - getExtensionWorkspaceTrustRequestType(manifest: IExtensionManifest): ExtensionWorkspaceTrustRequestType { - // Workspace trust feature is disabled, or extension has no entry point - if (!isWorkspaceTrustEnabled(this.configurationService) || !manifest.main) { - return 'never'; - } - - // Get extension workspace trust requirements from settings.json - const configuredWorkspaceTrustRequest = this.getConfiguredExtensionWorkspaceTrustRequest(manifest); - - // Get extension workspace trust requirements from product.json - const productWorkspaceTrustRequest = this.getProductExtensionWorkspaceTrustRequest(manifest); - - // Use settings.json override value if it exists - if (configuredWorkspaceTrustRequest) { - return configuredWorkspaceTrustRequest; - } - - // Use product.json override value if it exists - if (productWorkspaceTrustRequest?.override) { - return productWorkspaceTrustRequest.override; - } - - // Use extension manifest value if it exists - if (manifest.workspaceTrust?.request !== undefined) { - return manifest.workspaceTrust.request; - } - - // Use product.json default value if it exists - if (productWorkspaceTrustRequest?.default) { - return productWorkspaceTrustRequest.default; - } - - return 'onStart'; - } -} - -registerSingleton(IExtensionWorkspaceTrustRequestService, ExtensionWorkspaceTrustRequestService); diff --git a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts index 36193d26362..94f8a35a6cd 100644 --- a/src/vs/workbench/services/extensions/common/extensionsRegistry.ts +++ b/src/vs/workbench/services/extensions/common/extensionsRegistry.ts @@ -415,6 +415,11 @@ export const schema: IJSONSchema = { } ] }, + supportsVirtualWorkspace: { + description: nls.localize('supportsVirtualWorkspace', "When disabled the extension is not enabled in the workspace that has all folders using custom scheme. Default is `true`."), + type: 'boolean', + default: true + }, scripts: { type: 'object', properties: { diff --git a/src/vs/workbench/services/extensions/common/extensionsUtil.ts b/src/vs/workbench/services/extensions/common/extensionsUtil.ts index ce539f48eea..93a114150af 100644 --- a/src/vs/workbench/services/extensions/common/extensionsUtil.ts +++ b/src/vs/workbench/services/extensions/common/extensionsUtil.ts @@ -10,8 +10,7 @@ import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/ex import { isNonEmptyArray } from 'vs/base/common/arrays'; import { IProductService } from 'vs/platform/product/common/productService'; - -export class ExtensionKindController { +export class ExtensionKindController2 { constructor( @IProductService private readonly productService: IProductService, @IConfigurationService private readonly configurationService: IConfigurationService, diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 380d121f77e..f573b93115e 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -15,7 +15,7 @@ import { IWorkbenchExtensionEnablementService, EnablementState, IWebExtensionsSc import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IRemoteExtensionHostDataProvider, RemoteExtensionHost, IRemoteExtensionHostInitData } from 'vs/workbench/services/extensions/common/remoteExtensionHost'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, RemoteTrustOption, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleService, LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -42,6 +42,12 @@ import { Schemas } from 'vs/base/common/network'; import { ExtensionHostExitCode } from 'vs/workbench/services/extensions/common/extensionHostProtocol'; import { updateProxyConfigurationsScope } from 'vs/platform/request/common/request'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; + +const MACHINE_PROMPT = false; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -69,6 +75,9 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteExplorerService private readonly _remoteExplorerService: IRemoteExplorerService, @IExtensionGalleryService private readonly _extensionGalleryService: IExtensionGalleryService, @ILogService private readonly _logService: ILogService, + @IDialogService private readonly _dialogService: IDialogService, + @IWorkspaceTrustManagementService private readonly _workspaceTrustManagementService: IWorkspaceTrustManagementService, + @IExtensionManifestPropertiesService extensionManifestPropertiesService: IExtensionManifestPropertiesService, ) { super( new ExtensionRunningLocationClassifier( @@ -85,6 +94,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten extensionManagementService, contextService, configurationService, + extensionManifestPropertiesService ); this._enableLocalWebWorker = this._isLocalWebWorkerEnabled(); @@ -358,6 +368,38 @@ export class ExtensionService extends AbstractExtensionService implements IExten return; } + let promptForMachineTrust = MACHINE_PROMPT; + + if (resolverResult.options?.trust === RemoteTrustOption.DisableTrust) { + promptForMachineTrust = false; + this._workspaceTrustManagementService.setWorkspaceTrust(true); + } else if (resolverResult.options?.trust === RemoteTrustOption.MachineTrusted) { + promptForMachineTrust = false; + } + + if (promptForMachineTrust) { + const dialogResult = await this._dialogService.show( + Severity.Info, + nls.localize('machineTrustQuestion', "Do you trust the machine you're connecting to?"), + [nls.localize('yes', "Yes, connect."), nls.localize('no', "No, do not connect.")], + { + cancelId: 1, + custom: { + icon: Codicon.remoteExplorer + }, + // checkbox: { label: nls.localize('remember', "Remember my choice"), checked: true } + } + ); + + if (dialogResult.choice !== 0) { + // Did not confirm trust + this._notificationService.notify({ severity: Severity.Warning, message: nls.localize('trustFailure', "Refused to connect to untrusted machine.") }); + // Proceed with the local extension host + await this._startLocalExtensionHost(localExtensions); + return; + } + } + // set the resolved authority this._remoteAuthorityResolverService._setResolvedAuthority(resolverResult.authority, resolverResult.options); this._remoteExplorerService.setTunnelInformation(resolverResult.tunnelInformation); diff --git a/src/vs/workbench/services/extensions/test/common/extensionWorkspaceTrustRequest.test.ts b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts similarity index 72% rename from src/vs/workbench/services/extensions/test/common/extensionWorkspaceTrustRequest.test.ts rename to src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts index ef33c006a77..941d25e0557 100644 --- a/src/vs/workbench/services/extensions/test/common/extensionWorkspaceTrustRequest.test.ts +++ b/src/vs/workbench/services/extensions/test/common/extensionManifestPropertiesService.test.ts @@ -4,16 +4,52 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { ExtensionWorkspaceTrustRequestService } from 'vs/workbench/services/extensions/common/extensionWorkspaceTrustRequest'; +import { IExtensionManifest, ExtensionKind, ExtensionWorkspaceTrustRequestType } from 'vs/platform/extensions/common/extensions'; +import { ExtensionManifestPropertiesService } from 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; +import { TestProductService } from 'vs/workbench/test/common/workbenchTestServices'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; -import { ExtensionWorkspaceTrustRequestType, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +suite('ExtensionManifestPropertiesService - ExtensionKind', () => { -suite('ExtensionWorkspaceTrustRequestService', () => { - let testObject: ExtensionWorkspaceTrustRequestService; + function check(manifest: Partial, expected: ExtensionKind[]): void { + const extensionManifestPropertiesService = new ExtensionManifestPropertiesService(TestProductService, new TestConfigurationService()); + assert.deepStrictEqual(extensionManifestPropertiesService.deduceExtensionKind(manifest), expected); + } + + test('declarative with extension dependencies => workspace', () => { + check({ extensionDependencies: ['ext1'] }, ['workspace']); + }); + + test('declarative extension pack => workspace', () => { + check({ extensionPack: ['ext1', 'ext2'] }, ['workspace']); + }); + + test('declarative with unknown contribution point => workspace', () => { + check({ contributes: { 'unknownPoint': { something: true } } }, ['workspace']); + }); + + test('simple declarative => ui, workspace, web', () => { + check({}, ['ui', 'workspace', 'web']); + }); + + test('only browser => web', () => { + check({ browser: 'main.browser.js' }, ['web']); + }); + + test('only main => workspace', () => { + check({ main: 'main.js' }, ['workspace']); + }); + + test('main and browser => workspace, web', () => { + check({ main: 'main.js', browser: 'main.browser.js' }, ['workspace', 'web']); + }); +}); + +suite('ExtensionManifestPropertiesService - ExtensionWorkspaceTrustType', () => { + let testObject: ExtensionManifestPropertiesService; let instantiationService: TestInstantiationService; let testConfigurationService: TestConfigurationService; @@ -28,12 +64,16 @@ suite('ExtensionWorkspaceTrustRequestService', () => { teardown(() => testObject.dispose()); function assertWorkspaceTrustRequest(extensionMaifest: IExtensionManifest, expected: ExtensionWorkspaceTrustRequestType): void { - testObject = instantiationService.createInstance(ExtensionWorkspaceTrustRequestService); + testObject = instantiationService.createInstance(ExtensionManifestPropertiesService); const workspaceTrustRequest = testObject.getExtensionWorkspaceTrustRequestType(extensionMaifest); assert.strictEqual(workspaceTrustRequest, expected); } + function getExtensionManifest(properties: any = {}): IExtensionManifest { + return Object.create({ name: 'a', publisher: 'pub', version: '1.0.0', ...properties }) as IExtensionManifest; + } + test('test extension workspace trust request when main entry point is missing', () => { instantiationService.stub(IProductService, >{}); @@ -108,12 +148,3 @@ suite('ExtensionWorkspaceTrustRequestService', () => { assertWorkspaceTrustRequest(extensionMaifest, 'onStart'); }); }); - -function getExtensionManifest(properties: any = {}): IExtensionManifest { - return Object.create({ - name: 'a', - publisher: 'pub', - version: '1.0.0', - ...properties - }) as IExtensionManifest; -} diff --git a/src/vs/workbench/services/extensions/test/common/extensionsUtil.test.ts b/src/vs/workbench/services/extensions/test/common/extensionsUtil.test.ts deleted file mode 100644 index 4ca1afc4eeb..00000000000 --- a/src/vs/workbench/services/extensions/test/common/extensionsUtil.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as assert from 'assert'; -import { deduceExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IExtensionManifest, ExtensionKind } from 'vs/platform/extensions/common/extensions'; - -suite('ExtensionKind', () => { - - function check(manifest: Partial, expected: ExtensionKind[]): void { - assert.deepStrictEqual(deduceExtensionKind(manifest), expected); - } - - test('declarative with extension dependencies => workspace', () => { - check({ extensionDependencies: ['ext1'] }, ['workspace']); - }); - - test('declarative extension pack => workspace', () => { - check({ extensionPack: ['ext1', 'ext2'] }, ['workspace']); - }); - - test('declarative with unknown contribution point => workspace', () => { - check({ contributes: { 'unknownPoint': { something: true } } }, ['workspace']); - }); - - test('simple declarative => ui, workspace, web', () => { - check({}, ['ui', 'workspace', 'web']); - }); - - test('only browser => web', () => { - check({ browser: 'main.browser.js' }, ['web']); - }); - - test('only main => workspace', () => { - check({ main: 'main.js' }, ['workspace']); - }); - - test('main and browser => workspace, web', () => { - check({ main: 'main.js', browser: 'main.browser.js' }, ['workspace', 'web']); - }); -}); diff --git a/src/vs/workbench/services/host/browser/browserHostService.ts b/src/vs/workbench/services/host/browser/browserHostService.ts index b3ca09ae0d2..b00b6f0f813 100644 --- a/src/vs/workbench/services/host/browser/browserHostService.ts +++ b/src/vs/workbench/services/host/browser/browserHostService.ts @@ -11,6 +11,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen } from 'vs/platform/windows/common/windows'; import { pathsToEditors } from 'vs/workbench/common/editor'; +import { whenTextEditorClosed } from 'vs/workbench/browser/editor'; import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IModifierKeyStatus, ModifierKeyEmitter, trackFocus } from 'vs/base/browser/dom'; @@ -61,8 +62,10 @@ export interface IWorkspaceProvider { * - `payload`: arbitrary payload that should be made available * to the opening window via the `IWorkspaceProvider.payload` property. * @param payload optional payload to send to the workspace to open. + * + * @returns true if successfully opened, false otherwise. */ - open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise; + open(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise; } enum HostShutdownReason { @@ -109,7 +112,7 @@ export class BrowserHostService extends Disposable implements IHostService { this.workspaceProvider = new class implements IWorkspaceProvider { readonly workspace = undefined; readonly trusted = undefined; - async open() { } + async open() { return true; } }; } @@ -307,8 +310,8 @@ export class BrowserHostService extends Disposable implements IHostService { if (waitMarkerFileURI) { (async () => { - // Wait for the resources to be closed in the editor... - await editorService.whenClosed(fileOpenables.map(openable => ({ resource: openable.fileUri })), { waitForSaved: true }); + // Wait for the resources to be closed in the text editor... + await this.instantiationService.invokeFunction(accessor => whenTextEditorClosed(accessor, fileOpenables.map(fileOpenable => fileOpenable.fileUri))); // ...before deleting the wait marker file await this.fileService.del(waitMarkerFileURI); @@ -371,7 +374,7 @@ export class BrowserHostService extends Disposable implements IHostService { return this.doOpen(undefined, { reuse: options?.forceReuseWindow }); } - private doOpen(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { + private async doOpen(workspace: IWorkspace, options?: { reuse?: boolean, payload?: object }): Promise { // We know that `workspaceProvider.open` will trigger a shutdown // with `options.reuse` so we update `shutdownReason` to reflect that @@ -379,7 +382,7 @@ export class BrowserHostService extends Disposable implements IHostService { this.shutdownReason = HostShutdownReason.Api; } - return this.workspaceProvider.open(workspace, options); + await this.workspaceProvider.open(workspace, options); } async toggleFullScreen(): Promise { diff --git a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts index 0edff07b875..a656a7db041 100644 --- a/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts +++ b/src/vs/workbench/services/keybinding/test/browser/keybindingEditing.test.ts @@ -27,13 +27,13 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; -import { TestBackupFileService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestPathService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { TestWorkingCopyBackupService, TestEditorGroupsService, TestEditorService, TestEnvironmentService, TestLifecycleService, TestPathService, TestTextFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; @@ -115,7 +115,7 @@ suite('KeybindingsEditing', () => { instantiationService.stub(IWorkingCopyFileService, disposables.add(instantiationService.createInstance(WorkingCopyFileService))); instantiationService.stub(ITextFileService, disposables.add(instantiationService.createInstance(TestTextFileService))); instantiationService.stub(ITextModelService, disposables.add(instantiationService.createInstance(TextModelResolverService))); - instantiationService.stub(IBackupFileService, new TestBackupFileService()); + instantiationService.stub(IWorkingCopyBackupService, new TestWorkingCopyBackupService()); testObject = disposables.add(instantiationService.createInstance(KeybindingsEditingService)); diff --git a/src/vs/workbench/services/path/browser/pathService.ts b/src/vs/workbench/services/path/browser/pathService.ts index dd43ac3fdc6..b1d5cac951d 100644 --- a/src/vs/workbench/services/path/browser/pathService.ts +++ b/src/vs/workbench/services/path/browser/pathService.ts @@ -39,7 +39,7 @@ function defaultUriScheme(environmentService: IWorkbenchEnvironmentService, cont return configuration.scheme; } - throw new Error('Empty workspace is not supported in browser when there is no remote connection.'); + return Schemas.file; } registerSingleton(IPathService, BrowserPathService, true); diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 71b7f1b2101..e1cc1d116cb 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -295,7 +295,7 @@ export class FileWalker { /** * Public for testing. */ - readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error | null, stdout?: string) => void): void { + readStdout(cmd: childProcess.ChildProcess, encoding: BufferEncoding, cb: (err: Error | null, stdout?: string) => void): void { let all = ''; this.collectStdout(cmd, encoding, () => { }, (err: Error | null, stdout?: string, last?: boolean) => { if (err) { @@ -310,7 +310,7 @@ export class FileWalker { }); } - private collectStdout(cmd: childProcess.ChildProcess, encoding: string, onMessage: (message: IProgressMessage) => void, cb: (err: Error | null, stdout?: string, last?: boolean) => void): void { + private collectStdout(cmd: childProcess.ChildProcess, encoding: BufferEncoding, onMessage: (message: IProgressMessage) => void, cb: (err: Error | null, stdout?: string, last?: boolean) => void): void { let onData = (err: Error | null, stdout?: string, last?: boolean) => { if (err || last) { onData = () => { }; @@ -357,7 +357,7 @@ export class FileWalker { }); } - private forwardData(stream: Readable, encoding: string, cb: (err: Error | null, stdout?: string) => void): StringDecoder { + private forwardData(stream: Readable, encoding: BufferEncoding, cb: (err: Error | null, stdout?: string) => void): StringDecoder { const decoder = new StringDecoder(encoding); stream.on('data', (data: Buffer) => { cb(null, decoder.write(data)); @@ -373,7 +373,7 @@ export class FileWalker { return buffers; } - private decodeData(buffers: Buffer[], encoding: string): string { + private decodeData(buffers: Buffer[], encoding: BufferEncoding): string { const decoder = new StringDecoder(encoding); return buffers.map(buffer => decoder.write(buffer)).join(''); } diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts index 736e0b9df78..15e0573c013 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/telemetryService.ts @@ -39,7 +39,7 @@ export class TelemetryService extends Disposable implements ITelemetryService { const channel = sharedProcessService.getChannel('telemetryAppender'); const config: ITelemetryServiceConfig = { appender: new TelemetryAppenderClient(channel), - commonProperties: resolveWorkbenchCommonProperties(storageService, fileService, environmentService.os.release, productService.commit, productService.version, environmentService.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.remoteAuthority), + commonProperties: resolveWorkbenchCommonProperties(storageService, fileService, environmentService.os.release, environmentService.os.hostname, productService.commit, productService.version, environmentService.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.remoteAuthority), piiPaths: [environmentService.appRoot, environmentService.extensionsPath], sendErrorTelemetry: true }; diff --git a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts index 59e9af6b584..d81230c34f2 100644 --- a/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts +++ b/src/vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties.ts @@ -14,6 +14,7 @@ export async function resolveWorkbenchCommonProperties( storageService: IStorageService, fileService: IFileService, release: string, + hostname: string, commit: string | undefined, version: string | undefined, machineId: string, @@ -21,7 +22,7 @@ export async function resolveWorkbenchCommonProperties( installSourcePath: string, remoteAuthority?: string ): Promise<{ [name: string]: string | boolean | undefined }> { - const result = await resolveCommonProperties(fileService, release, process.arch, commit, version, machineId, msftInternalDomains, installSourcePath); + const result = await resolveCommonProperties(fileService, release, hostname, process.arch, commit, version, machineId, msftInternalDomains, installSourcePath); const instanceId = storageService.get(instanceStorageKey, StorageScope.GLOBAL)!; const firstSessionDate = storageService.get(firstSessionDateStorageKey, StorageScope.GLOBAL)!; const lastSessionDate = storageService.get(lastSessionDateStorageKey, StorageScope.GLOBAL)!; diff --git a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts index 2e602cbe552..b311ad724f6 100644 --- a/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts +++ b/src/vs/workbench/services/telemetry/test/electron-browser/commonProperties.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import * as fs from 'fs'; import { join } from 'vs/base/common/path'; -import { release, tmpdir } from 'os'; +import { release, tmpdir, hostname } from 'os'; import { resolveWorkbenchCommonProperties } from 'vs/workbench/services/telemetry/electron-sandbox/workbenchCommonProperties'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { IStorageService, StorageScope, InMemoryStorageService, StorageTarget } from 'vs/platform/storage/common/storage'; @@ -46,7 +46,7 @@ suite('Telemetry - common properties', function () { test('default', async function () { await fs.promises.mkdir(parentDir, { recursive: true }); fs.writeFileSync(installSource, 'my.install.source'); - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok('commitHash' in props); assert.ok('sessionID' in props); assert.ok('timestamp' in props); @@ -67,7 +67,7 @@ suite('Telemetry - common properties', function () { assert.ok('common.instanceId' in props, 'instanceId'); assert.ok('common.machineId' in props, 'machineId'); fs.unlinkSync(installSource); - const props_1 = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); + const props_1 = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok(!('common.source' in props_1)); }); @@ -75,14 +75,14 @@ suite('Telemetry - common properties', function () { testStorageService.store('telemetry.lastSessionDate', new Date().toUTCString(), StorageScope.GLOBAL, StorageTarget.MACHINE); - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); assert.ok('common.lastSessionDate' in props); // conditional, see below assert.ok('common.isNewSession' in props); assert.strictEqual(props['common.isNewSession'], '0'); }); test('values chance on ask', async function () { - const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), commit, version, 'someMachineId', undefined, installSource); + const props = await resolveWorkbenchCommonProperties(testStorageService, testFileService, release(), hostname(), commit, version, 'someMachineId', undefined, installSource); let value1 = props['common.sequence']; let value2 = props['common.sequence']; assert.ok(value1 !== value2, 'seq'); diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 2b0e9b8c947..61e54c6e7db 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -5,8 +5,8 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, IResourceEncoding, stringToSnapshot, ITextFileSaveAsOptions } from 'vs/workbench/services/textfile/common/textfiles'; -import { IRevertOptions, IEncodingSupport } from 'vs/workbench/common/editor'; +import { IEncodingSupport, ITextFileService, ITextFileStreamContent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, ITextFileSaveOptions, ITextFileEditorModelManager, IResourceEncoding, stringToSnapshot, ITextFileSaveAsOptions, IReadTextFileEncodingOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { IRevertOptions } from 'vs/workbench/common/editor'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IFileService, FileOperationError, FileOperationResult, IFileStatWithMetadata, ICreateFileOptions, IFileStreamContent } from 'vs/platform/files/common/files'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -20,7 +20,7 @@ import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream import { IModelService } from 'vs/editor/common/services/modelService'; import { joinPath, dirname, basename, toLocalResource, extname, isEqual } from 'vs/base/common/resources'; import { IDialogService, IFileDialogService, IConfirmation } from 'vs/platform/dialogs/common/dialogs'; -import { VSBuffer, VSBufferReadable, bufferToStream } from 'vs/base/common/buffer'; +import { VSBuffer, VSBufferReadable, bufferToStream, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ITextSnapshot, ITextModel } from 'vs/editor/common/model'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; @@ -35,7 +35,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces'; import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, toEncodeReadable, toDecodeStream, IDecodeStreamResult } from 'vs/workbench/services/textfile/common/encoding'; -import { consumeStream } from 'vs/base/common/stream'; +import { consumeStream, ReadableStream } from 'vs/base/common/stream'; import { IModeService } from 'vs/editor/common/services/modeService'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -136,10 +136,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } // read through encoding library - const decoder = await toDecodeStream(bufferStream.value, { - guessEncoding: options?.autoGuessEncoding || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), - overwriteEncoding: detectedEncoding => this.encoding.getReadEncoding(resource, options, detectedEncoding) - }); + const decoder = await this.doGetDecodedStream(resource, bufferStream.value, options); // validate binary if (options?.acceptTextOnly && decoder.detected.seemsBinary) { @@ -150,12 +147,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex } async create(operations: { resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions }[], undoInfo?: IFileOperationUndoRedoInfo): Promise { - const operationsWithContents: ICreateFileOperation[] = await Promise.all(operations.map(async o => { - const contents = await this.getEncodedReadable(o.resource, o.value); + const operationsWithContents: ICreateFileOperation[] = await Promise.all(operations.map(async operation => { + const contents = await this.getEncodedReadable(operation.resource, operation.value); return { - resource: o.resource, + resource: operation.resource, contents, - overwrite: o.options?.overwrite + overwrite: operation.options?.overwrite }; })); @@ -168,6 +165,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return this.fileService.writeFile(resource, readable, options); } + async getEncodedReadable(resource: URI, value: ITextSnapshot): Promise; + async getEncodedReadable(resource: URI, value: string): Promise; + async getEncodedReadable(resource: URI, value?: ITextSnapshot): Promise; + async getEncodedReadable(resource: URI, value?: string): Promise; async getEncodedReadable(resource: URI, value?: string | ITextSnapshot): Promise; async getEncodedReadable(resource: URI, value: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; async getEncodedReadable(resource: URI, value?: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise { @@ -188,6 +189,19 @@ export abstract class AbstractTextFileService extends Disposable implements ITex return toEncodeReadable(snapshot, encoding, { addBOM }); } + async getDecodedStream(resource: URI, value: VSBufferReadableStream, options?: IReadTextFileEncodingOptions): Promise> { + return (await this.doGetDecodedStream(resource, value, options)).stream; + } + + private doGetDecodedStream(resource: URI, stream: VSBufferReadableStream, options?: IReadTextFileEncodingOptions): Promise { + + // read through encoding library + return toDecodeStream(stream, { + guessEncoding: options?.autoGuessEncoding || this.textResourceConfigurationService.getValue(resource, 'files.autoGuessEncoding'), + overwriteEncoding: detectedEncoding => this.encoding.getReadEncoding(resource, options, detectedEncoding) + }); + } + //#endregion @@ -600,7 +614,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings { }; } - getReadEncoding(resource: URI, options: IReadTextFileOptions | undefined, detectedEncoding: string | null): Promise { + getReadEncoding(resource: URI, options: IReadTextFileEncodingOptions | undefined, detectedEncoding: string | null): Promise { let preferredEncoding: string | undefined; // Encoding passed in as option diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fc2a3c78134..0d4ac96b799 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -7,10 +7,10 @@ import { localize } from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { assertIsDefined, withNullAsUndefined } from 'vs/base/common/types'; -import { ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileResolveReason } from 'vs/workbench/services/textfile/common/textfiles'; -import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEditorModel, ITextFileStreamContent, ITextFileResolveOptions, IResolvedTextFileEditorModel, ITextFileSaveOptions, TextFileResolveReason } from 'vs/workbench/services/textfile/common/textfiles'; +import { IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; -import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService, IWorkingCopyBackupMeta, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -19,13 +19,15 @@ import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ILogService } from 'vs/platform/log/common/log'; import { basename } from 'vs/base/common/path'; -import { IWorkingCopyService, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyBackup, WorkingCopyCapabilities, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { UTF8 } from 'vs/workbench/services/textfile/common/encoding'; +import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; -interface IBackupMetaData { +interface IBackupMetaData extends IWorkingCopyBackupMeta { mtime: number; ctime: number; size: number; @@ -66,6 +68,8 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion + readonly typeId = NO_TYPE_ID; // IMPORTANT: never change this to not break existing assumptions (e.g. backups) + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.labelService.getUriLabel(this.resource)); @@ -97,7 +101,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil @IModelService modelService: IModelService, @IFileService private readonly fileService: IFileService, @ITextFileService private readonly textFileService: ITextFileService, - @IBackupFileService private readonly backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILogService private readonly logService: ILogService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, @@ -201,7 +205,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil }; } - return { meta, content: withNullAsUndefined(this.createSnapshot()) }; + // Fill in content the same way we would do when + // saving the file via the text file service + // encoding support (hardcode UTF-8) + const content = await this.textFileService.getEncodedReadable(this.resource, withNullAsUndefined(this.createSnapshot()), { encoding: UTF8 }); + + return { meta, content }; } //#endregion @@ -337,7 +346,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil private async resolveFromBackup(options?: ITextFileResolveOptions): Promise { // Resolve backup if any - const backup = await this.backupFileService.resolve(this.resource); + const backup = await this.workingCopyBackupService.resolve(this); // Resolve preferred encoding if we need it let encoding = UTF8; @@ -355,7 +364,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Try to resolve from backup if we have any if (backup) { - this.doResolveFromBackup(backup, encoding, options); + await this.doResolveFromBackup(backup, encoding, options); return true; } @@ -364,7 +373,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return false; } - private doResolveFromBackup(backup: IResolvedBackup, encoding: string, options?: ITextFileResolveOptions): void { + private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup, encoding: string, options?: ITextFileResolveOptions): Promise { this.logService.trace('[text file model] doResolveFromBackup()', this.resource.toString(true)); // Resolve with backup @@ -375,12 +384,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil ctime: backup.meta ? backup.meta.ctime : Date.now(), size: backup.meta ? backup.meta.size : 0, etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown! - value: backup.value, + value: await createTextBufferFactoryFromStream(await this.textFileService.getDecodedStream(this.resource, backup.value, { encoding: UTF8 })), encoding }, true /* dirty (resolved from backup) */, options); // Restore orphaned flag based on state - if (backup.meta && backup.meta.orphaned) { + if (backup.meta?.orphaned) { this.setOrphaned(true); } } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index ff99cedd1ea..6609bd7da94 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -6,14 +6,15 @@ import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IEncodingSupport, IModeSupport, ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { ReadableStream } from 'vs/base/common/stream'; import { IBaseStatWithMetadata, IFileStatWithMetadata, IWriteFileOptions, FileOperationError, FileOperationResult, IReadFileStreamOptions } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; -import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; +import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { areFunctions, isUndefinedOrNull } from 'vs/base/common/types'; -import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IUntitledTextEditorModelManager } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; @@ -98,29 +99,44 @@ export interface ITextFileService extends IDisposable { create(operations: { resource: URI, value?: string | ITextSnapshot, options?: { overwrite?: boolean } }[], undoInfo?: IFileOperationUndoRedoInfo): Promise; /** - * Returns the readable that uses the appropriate encoding. + * Returns the readable that uses the appropriate encoding. This method should + * be used whenever a `string` or `ITextSnapshot` is being persisted to the + * file system. */ + getEncodedReadable(resource: URI, value: ITextSnapshot, options?: IWriteTextFileOptions): Promise; + getEncodedReadable(resource: URI, value: string, options?: IWriteTextFileOptions): Promise; + getEncodedReadable(resource: URI, value?: ITextSnapshot, options?: IWriteTextFileOptions): Promise; + getEncodedReadable(resource: URI, value?: string, options?: IWriteTextFileOptions): Promise; getEncodedReadable(resource: URI, value?: string | ITextSnapshot, options?: IWriteTextFileOptions): Promise; -} - -export interface IReadTextFileOptions extends IReadFileStreamOptions { /** - * The optional acceptTextOnly parameter allows to fail this request early if the file - * contents are not textual. + * Returns a stream of strings that uses the appropriate encoding. This method should + * be used whenever a `VSBufferReadableStream` is being loaded from the file system. */ - acceptTextOnly?: boolean; + getDecodedStream(resource: URI, value: VSBufferReadableStream, options?: IReadTextFileEncodingOptions): Promise>; +} + +export interface IReadTextFileEncodingOptions { /** * The optional encoding parameter allows to specify the desired encoding when resolving * the contents of the file. */ - encoding?: string; + readonly encoding?: string; /** * The optional guessEncoding parameter allows to guess encoding from content of the file. */ - autoGuessEncoding?: boolean; + readonly autoGuessEncoding?: boolean; +} + +export interface IReadTextFileOptions extends IReadTextFileEncodingOptions, IReadFileStreamOptions { + + /** + * The optional acceptTextOnly parameter allows to fail this request early if the file + * contents are not textual. + */ + readonly acceptTextOnly?: boolean; } export interface IWriteTextFileOptions extends IWriteFileOptions { @@ -128,13 +144,13 @@ export interface IWriteTextFileOptions extends IWriteFileOptions { /** * The encoding to use when updating a file. */ - encoding?: string; + readonly encoding?: string; /** * Whether to write to the file as elevated (admin) user. When setting this option a prompt will * ask the user to authenticate as super user. */ - writeElevated?: boolean; + readonly writeElevated?: boolean; } export const enum TextFileOperationResult { @@ -165,8 +181,8 @@ export interface IResourceEncodings { } export interface IResourceEncoding { - encoding: string; - hasBOM: boolean; + readonly encoding: string; + readonly hasBOM: boolean; } /** @@ -229,7 +245,7 @@ interface IBaseTextFileContent extends IBaseStatWithMetadata { /** * The encoding of the content if known. */ - encoding: string; + readonly encoding: string; } export interface ITextFileContent extends IBaseTextFileContent { @@ -237,7 +253,7 @@ export interface ITextFileContent extends IBaseTextFileContent { /** * The content of a text file. */ - value: string; + readonly value: string; } export interface ITextFileStreamContent extends IBaseTextFileContent { @@ -245,7 +261,7 @@ export interface ITextFileStreamContent extends IBaseTextFileContent { /** * The line grouped content of a text file. */ - value: ITextBufferFactory; + readonly value: ITextBufferFactory; } export interface ITextFileEditorModelResolveOrCreateOptions { @@ -253,24 +269,24 @@ export interface ITextFileEditorModelResolveOrCreateOptions { /** * Context why the model is being resolved or created. */ - reason?: TextFileResolveReason; + readonly reason?: TextFileResolveReason; /** * The language mode to use for the model text content. */ - mode?: string; + readonly mode?: string; /** * The encoding to use when resolving the model text content. */ - encoding?: string; + readonly encoding?: string; /** * The contents to use for the model if known. If not * provided, the contents will be retrieved from the * underlying resource or backup if present. */ - contents?: ITextBufferFactory; + readonly contents?: ITextBufferFactory; /** * If the model was already resolved before, allows to trigger @@ -280,24 +296,24 @@ export interface ITextFileEditorModelResolveOrCreateOptions { * - sync: resolve() will only return resolved when the * model has finished reloading. */ - reload?: { - async: boolean + readonly reload?: { + readonly async: boolean }; /** * Allow to resolve a model even if we think it is a binary file. */ - allowBinary?: boolean; + readonly allowBinary?: boolean; } export interface ITextFileSaveEvent { - model: ITextFileEditorModel; - reason: SaveReason; + readonly model: ITextFileEditorModel; + readonly reason: SaveReason; } export interface ITextFileResolveEvent { - model: ITextFileEditorModel; - reason: TextFileResolveReason; + readonly model: ITextFileEditorModel; + readonly reason: TextFileResolveReason; } export interface ITextFileSaveParticipant { @@ -368,24 +384,24 @@ export interface ITextFileSaveOptions extends ISaveOptions { /** * Save the file with an attempt to unlock it. */ - writeUnlock?: boolean; + readonly writeUnlock?: boolean; /** * Save the file with elevated privileges. * * Note: This may not be supported in all environments. */ - writeElevated?: boolean; + readonly writeElevated?: boolean; /** * Allows to write to a file even if it has been modified on disk. */ - ignoreModifiedSince?: boolean; + readonly ignoreModifiedSince?: boolean; /** * If set, will bubble up the error to the caller instead of handling it. */ - ignoreErrorHandler?: boolean; + readonly ignoreErrorHandler?: boolean; } export interface ITextFileSaveAsOptions extends ITextFileSaveOptions { @@ -393,7 +409,7 @@ export interface ITextFileSaveAsOptions extends ITextFileSaveOptions { /** * Optional URI to use as suggested file path to save as. */ - suggestedTarget?: URI; + readonly suggestedTarget?: URI; } export interface ITextFileResolveOptions { @@ -403,22 +419,56 @@ export interface ITextFileResolveOptions { * provided, the contents will be retrieved from the * underlying resource or backup if present. */ - contents?: ITextBufferFactory; + readonly contents?: ITextBufferFactory; /** * Go to file bypassing any cache of the model if any. */ - forceReadFromFile?: boolean; + readonly forceReadFromFile?: boolean; /** * Allow to resolve a model even if we think it is a binary file. */ - allowBinary?: boolean; + readonly allowBinary?: boolean; /** * Context why the model is being resolved. */ - reason?: TextFileResolveReason; + readonly reason?: TextFileResolveReason; +} + +export const enum EncodingMode { + + /** + * Instructs the encoding support to encode the object with the provided encoding + */ + Encode, + + /** + * Instructs the encoding support to decode the object with the provided encoding + */ + Decode +} + +export interface IEncodingSupport { + + /** + * Gets the encoding of the object if known. + */ + getEncoding(): string | undefined; + + /** + * Sets the encoding for the object for saving. + */ + setEncoding(encoding: string, mode: EncodingMode): void; +} + +export interface IModeSupport { + + /** + * Sets the language mode of the object. + */ + setMode(mode: string): void; } export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport, IWorkingCopy { @@ -485,35 +535,6 @@ export function stringToSnapshot(value: string): ITextSnapshot { }; } -export class TextSnapshotReadable implements VSBufferReadable { - private preambleHandled = false; - - constructor(private snapshot: ITextSnapshot, private preamble?: string) { } - - read(): VSBuffer | null { - let value = this.snapshot.read(); - - // Handle preamble if provided - if (!this.preambleHandled) { - this.preambleHandled = true; - - if (typeof this.preamble === 'string') { - if (typeof value === 'string') { - value = this.preamble + value; - } else { - value = this.preamble; - } - } - } - - if (typeof value === 'string') { - return VSBuffer.fromString(value); - } - - return null; - } -} - export function toBufferOrReadable(value: string): VSBuffer; export function toBufferOrReadable(value: ITextSnapshot): VSBufferReadable; export function toBufferOrReadable(value: string | ITextSnapshot): VSBuffer | VSBufferReadable; @@ -527,5 +548,14 @@ export function toBufferOrReadable(value: string | ITextSnapshot | undefined): V return VSBuffer.fromString(value); } - return new TextSnapshotReadable(value); + return { + read: () => { + const chunk = value.read(); + if (typeof chunk === 'string') { + return VSBuffer.fromString(chunk); + } + + return null; + } + }; } diff --git a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts index 9ad4e3ce61f..9da62b19580 100644 --- a/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts +++ b/src/vs/workbench/services/textfile/test/browser/textFileEditorModel.test.ts @@ -5,9 +5,8 @@ import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EncodingMode } from 'vs/workbench/common/editor'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; -import { TextFileEditorModelState, snapshotToString, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; +import { EncodingMode, TextFileEditorModelState, snapshotToString, isTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; import { createFileEditorInput, workbenchInstantiationService, TestServiceAccessor, TestReadonlyTextFileEditorModel, getLastResolvedFileStat } from 'vs/workbench/test/browser/workbenchTestServices'; import { toResource } from 'vs/base/test/common/utils'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; @@ -15,7 +14,10 @@ import { FileOperationResult, FileOperationError } from 'vs/platform/files/commo import { timeout } from 'vs/base/common/async'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { assertIsDefined } from 'vs/base/common/types'; -import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { URI } from 'vs/base/common/uri'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; suite('Files - TextFileEditorModel', () => { @@ -99,7 +101,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.hasState(TextFileEditorModelState.DIRTY)); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); let workingCopyEvent = false; accessor.workingCopyService.onDidChangeDirty(e => { @@ -119,7 +121,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(workingCopyEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), false); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); savedEvent = false; @@ -174,7 +176,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(saveErrorEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); } finally { accessor.fileService.writeShouldThrowError = undefined; } @@ -210,7 +212,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(saveErrorEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); model.dispose(); } finally { @@ -240,7 +242,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(saveErrorEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); model.dispose(); } finally { @@ -370,7 +372,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.isDirty()); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); await model.revert(); assert.strictEqual(model.isDirty(), false); @@ -379,7 +381,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(workingCopyEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), false); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); model.dispose(); }); @@ -403,7 +405,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.isDirty()); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); await model.revert({ soft: true }); assert.strictEqual(model.isDirty(), false); @@ -412,7 +414,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(workingCopyEvent); assert.strictEqual(accessor.workingCopyService.dirtyCount, 0); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), false); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), false); model.dispose(); }); @@ -437,7 +439,7 @@ suite('Files - TextFileEditorModel', () => { assert.ok(model.isDirty()); assert.strictEqual(accessor.workingCopyService.dirtyCount, 1); - assert.strictEqual(accessor.workingCopyService.isDirty(model.resource), true); + assert.strictEqual(accessor.workingCopyService.isDirty(model.resource, model.typeId), true); }); test('Update Dirty', async function () { @@ -752,4 +754,35 @@ suite('Files - TextFileEditorModel', () => { participant.dispose(); } + + test('backup and restore (simple)', async function () { + return testBackupAndRestore(toResource.call(this, '/path/index_async.txt'), toResource.call(this, '/path/index_async2.txt'), 'Some very small file text content.'); + }); + + test('backup and restore (large, #121347)', async function () { + const largeContent = '국어한\n'.repeat(100000); + return testBackupAndRestore(toResource.call(this, '/path/index_async.txt'), toResource.call(this, '/path/index_async2.txt'), largeContent); + }); + + async function testBackupAndRestore(resourceA: URI, resourceB: URI, contents: string): Promise { + const originalModel: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, resourceA, 'utf8', undefined); + await originalModel.resolve({ + contents: await createTextBufferFactoryFromStream(await accessor.textFileService.getDecodedStream(resourceA, bufferToStream(VSBuffer.fromString(contents)))) + }); + + assert.strictEqual(originalModel.textEditorModel?.getValue(), contents); + + const backup = await originalModel.backup(CancellationToken.None); + const modelRestoredIdentifier = { typeId: originalModel.typeId, resource: resourceB }; + await accessor.workingCopyBackupService.backup(modelRestoredIdentifier, backup.content); + + const modelRestored: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, modelRestoredIdentifier.resource, 'utf8', undefined); + await modelRestored.resolve(); + + assert.strictEqual(modelRestored.textEditorModel?.getValue(), contents); + assert.strictEqual(modelRestored.isDirty(), true); + + originalModel.dispose(); + modelRestored.dispose(); + } }); diff --git a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts index dab4b4e6225..f60ecdefbc3 100644 --- a/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/common/textFileService.io.test.ts @@ -8,10 +8,11 @@ import { ITextFileService, snapshotToString, TextFileOperationError, TextFileOpe import { URI } from 'vs/base/common/uri'; import { join, basename } from 'vs/base/common/path'; import { UTF16le, UTF8_with_bom, UTF16be, UTF8, UTF16le_BOM, UTF16be_BOM, UTF8_BOM } from 'vs/workbench/services/textfile/common/encoding'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { ITextSnapshot, DefaultEndOfLine } from 'vs/editor/common/model'; import { isWindows } from 'vs/base/common/platform'; +import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; export interface Params { setup(): Promise<{ @@ -527,10 +528,27 @@ export default function createSuite(params: Params) { async function testLargeEncoding(encoding: string, needle: string): Promise { const resource = URI.file(join(testDir, `lorem_${encoding}.txt`)); + // Verify via `ITextFileService.readStream` const result = await service.readStream(resource, { encoding }); assert.strictEqual(result.encoding, encoding); - const contents = snapshotToString(result.value.create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + let contents = snapshotToString(result.value.create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + + assert.strictEqual(contents.indexOf(needle), 0); + assert.ok(contents.indexOf(needle, 10) > 0); + + // Verify via `ITextFileService.getDecodedTextFactory` + const rawFile = await params.readFile(resource.fsPath); + let rawFileVSBuffer: VSBuffer; + if (rawFile instanceof VSBuffer) { + rawFileVSBuffer = rawFile; + } else { + rawFileVSBuffer = VSBuffer.wrap(rawFile); + } + + const factory = await createTextBufferFactoryFromStream(await service.getDecodedStream(resource, bufferToStream(rawFileVSBuffer), { encoding })); + + contents = snapshotToString(factory.create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); assert.strictEqual(contents.indexOf(needle), 0); assert.ok(contents.indexOf(needle, 10) > 0); diff --git a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts index bcefbded729..57a724eeab3 100644 --- a/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts +++ b/src/vs/workbench/services/textfile/test/electron-browser/nativeTextFileService.io.test.ts @@ -30,8 +30,8 @@ flakySuite('Files - NativeTextFileService i/o', function () { let testDir: string; function readFile(path: string): Promise; - function readFile(path: string, encoding: string): Promise; - function readFile(path: string, encoding?: string): Promise { + function readFile(path: string, encoding: BufferEncoding): Promise; + function readFile(path: string, encoding?: BufferEncoding): Promise { return promises.readFile(path, encoding); } diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts index 983fa41147b..f43cf5b010e 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorInput.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor'; +import { Verbosity } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { EncodingMode, IEncodingSupport, IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { ILabelService } from 'vs/platform/label/common/label'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; diff --git a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts index c1f446a2f7b..14cf1552a05 100644 --- a/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts +++ b/src/vs/workbench/services/untitled/common/untitledTextEditorModel.ts @@ -3,19 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEncodingSupport, ISaveOptions, IModeSupport } from 'vs/workbench/common/editor'; +import { ISaveOptions } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { Event, Emitter } from 'vs/base/common/event'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService'; -import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model'; -import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { ITextModel } from 'vs/editor/common/model'; +import { createTextBufferFactory, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel'; import { ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, WorkingCopyCapabilities, IWorkingCopyBackup, NO_TYPE_ID } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { IEncodingSupport, IModeSupport, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; import { withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -23,6 +24,8 @@ import { ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { getCharContainingOffset } from 'vs/base/common/strings'; +import { UTF8 } from 'vs/workbench/services/textfile/common/encoding'; +import { bufferToStream, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport, IEncodingSupport, IWorkingCopy { @@ -73,6 +76,8 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private static readonly FIRST_LINE_NAME_MAX_LENGTH = 40; private static readonly FIRST_LINE_NAME_CANDIDATE_MAX_LENGTH = UntitledTextEditorModel.FIRST_LINE_NAME_MAX_LENGTH * 10; + //#region Events + private readonly _onDidChangeContent = this._register(new Emitter()); readonly onDidChangeContent = this._onDidChangeContent.event; @@ -88,6 +93,10 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private readonly _onDidRevert = this._register(new Emitter()); readonly onDidRevert = this._onDidRevert.event; + //#endregion + + readonly typeId = NO_TYPE_ID; // IMPORTANT: never change this to not break existing assumptions (e.g. backups) + readonly capabilities = WorkingCopyCapabilities.Untitled; private cachedModelFirstLineWords: string | undefined = undefined; @@ -119,7 +128,7 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt private preferredEncoding: string | undefined, @IModeService modeService: IModeService, @IModelService modelService: IModelService, - @IBackupFileService private readonly backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ITextResourceConfigurationService private readonly textResourceConfigurationService: ITextResourceConfigurationService, @IWorkingCopyService private readonly workingCopyService: IWorkingCopyService, @ITextFileService private readonly textFileService: ITextFileService, @@ -237,6 +246,9 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt return false; } + + //#region Dirty + isDirty(): boolean { return this.dirty; } @@ -250,6 +262,11 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this._onDidChangeDirty.fire(); } + //#endregion + + + //#region Save / Revert / Backup + async save(options?: ISaveOptions): Promise { const target = await this.textFileService.save(this.resource, options); @@ -269,25 +286,44 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt } async backup(token: CancellationToken): Promise { - return { content: withNullAsUndefined(this.createSnapshot()) }; + + // Fill in content the same way we would do when + // saving the file via the text file service + // encoding support (hardcode UTF-8) + const content = await this.textFileService.getEncodedReadable(this.resource, withNullAsUndefined(this.createSnapshot()), { encoding: UTF8 }); + + return { content }; } + //#endregion + + + //#region Resolve + async override resolve(): Promise { - // Check for backups - const backup = await this.backupFileService.resolve(this.resource); - - let untitledContents: ITextBufferFactory; - if (backup) { - untitledContents = backup.value; - } else { - untitledContents = createTextBufferFactory(this.initialValue || ''); - } - // Create text editor model if not yet done let createdUntitledModel = false; + let hasBackup = false; if (!this.textEditorModel) { - this.createTextEditorModel(untitledContents, this.resource, this.preferredMode); + let untitledContents: VSBufferReadableStream; + + // Check for backups or use initial value or empty + const backup = await this.workingCopyBackupService.resolve(this); + if (backup) { + untitledContents = backup.value; + hasBackup = true; + } else { + untitledContents = bufferToStream(VSBuffer.fromString(this.initialValue || '')); + } + + // Determine untitled contents based on backup + // or initial value. We must use text file service + // to create the text factory to respect encodings + // accordingly. + const untitledContentsFactory = await createTextBufferFactoryFromStream(await this.textFileService.getDecodedStream(this.resource, untitledContents, { encoding: UTF8 })); + + this.createTextEditorModel(untitledContentsFactory, this.resource, this.preferredMode); createdUntitledModel = true; } @@ -308,16 +344,16 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt if (createdUntitledModel) { // Name - if (backup || this.initialValue) { + if (hasBackup || this.initialValue) { this.updateNameFromFirstLine(textEditorModel); } // Untitled associated to file path are dirty right away as well as untitled with content - this.setDirty(this.hasAssociatedFilePath || !!backup || !!this.initialValue); + this.setDirty(this.hasAssociatedFilePath || !!hasBackup || !!this.initialValue); // If we have initial contents, make sure to emit this // as the appropiate events to the outside. - if (backup || this.initialValue) { + if (hasBackup || this.initialValue) { this._onDidChangeContent.fire(); } } @@ -383,4 +419,6 @@ export class UntitledTextEditorModel extends BaseTextEditorModel implements IUnt this._onDidChangeName.fire(); } } + + //#endregion } diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index b80bebbb2ad..713f3336ca9 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -15,6 +15,7 @@ import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel'; +import { CancellationToken } from 'vs/base/common/cancellation'; suite('Untitled text editors', () => { @@ -149,10 +150,10 @@ suite('Untitled text editors', () => { const model = await input.resolve(); model.textEditorModel?.setValue('foo bar'); assert.ok(model.isDirty()); - assert.ok(workingCopyService.isDirty(model.resource)); + assert.ok(workingCopyService.isDirty(model.resource, model.typeId)); model.textEditorModel?.setValue(''); assert.ok(!model.isDirty()); - assert.ok(!workingCopyService.isDirty(model.resource)); + assert.ok(!workingCopyService.isDirty(model.resource, model.typeId)); input.dispose(); model.dispose(); }); @@ -543,4 +544,36 @@ suite('Untitled text editors', () => { input.dispose(); model.dispose(); }); + + test('backup and restore (simple)', async function () { + return testBackupAndRestore('Some very small file text content.'); + }); + + test('backup and restore (large, #121347)', async function () { + const largeContent = '국어한\n'.repeat(100000); + return testBackupAndRestore(largeContent); + }); + + async function testBackupAndRestore(content: string) { + const service = accessor.untitledTextEditorService; + const originalInput = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + const restoredInput = instantiationService.createInstance(UntitledTextEditorInput, service.create()); + + const originalModel = await originalInput.resolve(); + originalModel.textEditorModel?.setValue(content); + + const backup = await originalModel.backup(CancellationToken.None); + const modelRestoredIdentifier = { typeId: originalModel.typeId, resource: restoredInput.resource }; + await accessor.workingCopyBackupService.backup(modelRestoredIdentifier, backup.content); + + const restoredModel = await restoredInput.resolve(); + + assert.strictEqual(restoredModel.textEditorModel?.getValue(), content); + assert.strictEqual(restoredModel.isDirty(), true); + + originalInput.dispose(); + originalModel.dispose(); + restoredInput.dispose(); + restoredModel.dispose(); + } }); diff --git a/src/vs/workbench/services/update/browser/updateService.ts b/src/vs/workbench/services/update/browser/updateService.ts index 83064efac07..6e47235d31b 100644 --- a/src/vs/workbench/services/update/browser/updateService.ts +++ b/src/vs/workbench/services/update/browser/updateService.ts @@ -44,25 +44,25 @@ export class BrowserUpdateService extends Disposable implements IUpdateService { ) { super(); - this.checkForUpdates(); + this.checkForUpdates(false); } async isLatestVersion(): Promise { - const update = await this.doCheckForUpdates(); + const update = await this.doCheckForUpdates(false); return !!update; } - async checkForUpdates(): Promise { - await this.doCheckForUpdates(); + async checkForUpdates(explicit: boolean): Promise { + await this.doCheckForUpdates(explicit); } - private async doCheckForUpdates(): Promise { + private async doCheckForUpdates(explicit: boolean): Promise { if (this.environmentService.options && this.environmentService.options.updateProvider) { const updateProvider = this.environmentService.options.updateProvider; // State -> Checking for Updates - this.state = State.CheckingForUpdates(null); + this.state = State.CheckingForUpdates(explicit); const update = await updateProvider.checkForUpdate(); if (update) { diff --git a/src/vs/workbench/services/backup/browser/backupFileService.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupService.ts similarity index 72% rename from src/vs/workbench/services/backup/browser/backupFileService.ts rename to src/vs/workbench/services/workingCopy/browser/workingCopyBackupService.ts index a1618bd6a13..e4d32112712 100644 --- a/src/vs/workbench/services/backup/browser/backupFileService.ts +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupService.ts @@ -6,17 +6,17 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ILogService } from 'vs/platform/log/common/log'; -import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; +import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { joinPath } from 'vs/base/common/resources'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { BrowserBackupTracker } from 'vs/workbench/services/backup/browser/backupTracker'; +import { BrowserWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/browser/workingCopyBackupTracker'; -export class BrowserBackupFileService extends BackupFileService { +export class BrowserWorkingCopyBackupService extends WorkingCopyBackupService { constructor( @IWorkspaceContextService contextService: IWorkspaceContextService, @@ -29,7 +29,7 @@ export class BrowserBackupFileService extends BackupFileService { } // Register Service -registerSingleton(IBackupFileService, BrowserBackupFileService); +registerSingleton(IWorkingCopyBackupService, BrowserWorkingCopyBackupService); // Register Backup Tracker -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BrowserBackupTracker, LifecyclePhase.Starting); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BrowserWorkingCopyBackupTracker, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/backup/browser/backupTracker.ts b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts similarity index 74% rename from src/vs/workbench/services/backup/browser/backupTracker.ts rename to src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts index ea1931c4a2a..841497d1ac4 100644 --- a/src/vs/workbench/services/backup/browser/backupTracker.ts +++ b/src/vs/workbench/services/workingCopy/browser/workingCopyBackupTracker.ts @@ -3,24 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ILogService } from 'vs/platform/log/common/log'; -import { BackupTracker } from 'vs/workbench/services/backup/common/backupTracker'; +import { WorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/common/workingCopyBackupTracker'; -export class BrowserBackupTracker extends BackupTracker implements IWorkbenchContribution { +export class BrowserWorkingCopyBackupTracker extends WorkingCopyBackupTracker implements IWorkbenchContribution { constructor( - @IBackupFileService backupFileService: IBackupFileService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, @ILogService logService: ILogService ) { - super(backupFileService, workingCopyService, logService, lifecycleService, filesConfigurationService); + super(workingCopyBackupService, workingCopyService, logService, lifecycleService, filesConfigurationService); } protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { @@ -40,7 +40,7 @@ export class BrowserBackupTracker extends BackupTracker implements IWorkbenchCon } for (const dirtyWorkingCopy of dirtyWorkingCopies) { - if (!this.backupFileService.hasBackupSync(dirtyWorkingCopy.resource, this.getContentVersion(dirtyWorkingCopy))) { + if (!this.workingCopyBackupService.hasBackupSync(dirtyWorkingCopy, this.getContentVersion(dirtyWorkingCopy))) { this.logService.warn('Unload veto: pending backups'); return true; // dirty without backup: veto diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts index 90f4d274479..64b9aea3484 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopy.ts @@ -9,16 +9,15 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { ETAG_DISABLED, FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; -import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { raceCancellation, TaskSequentializer, timeout } from 'vs/base/common/async'; import { ILogService } from 'vs/platform/log/common/log'; -import { DefaultEndOfLine, ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model'; import { assertIsDefined } from 'vs/base/common/types'; -import { ITextFileEditorModel, ITextFileService, snapshotToString, stringToSnapshot } from 'vs/workbench/services/textfile/common/textfiles'; -import { newWriteableBufferStream, streamToBuffer, VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { ITextFileEditorModel, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { VSBufferReadableStream } from 'vs/base/common/buffer'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; -import { isWindows } from 'vs/base/common/platform'; +import { IWorkingCopyBackupService, IWorkingCopyBackupMeta, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; export interface IFileWorkingCopyModelFactory { @@ -309,7 +308,7 @@ export interface IFileWorkingCopyResolveOptions { /** * Metadata associated with a file working copy backup. */ -interface IFileWorkingCopyBackupMetaData { +interface IFileWorkingCopyBackupMetaData extends IWorkingCopyBackupMeta { mtime: number; ctime: number; size: number; @@ -353,6 +352,7 @@ export class FileWorkingCopy extends Disposable //#endregion constructor( + readonly typeId: string, readonly resource: URI, readonly name: string, private readonly modelFactory: IFileWorkingCopyModelFactory, @@ -360,7 +360,7 @@ export class FileWorkingCopy extends Disposable @ILogService private readonly logService: ILogService, @ITextFileService private readonly textFileService: ITextFileService, @IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService, - @IBackupFileService private readonly backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @IWorkingCopyService workingCopyService: IWorkingCopyService ) { super(); @@ -506,11 +506,11 @@ export class FileWorkingCopy extends Disposable private lastResolvedFileStat: IFileStatWithMetadata | undefined; async resolve(options?: IFileWorkingCopyResolveOptions): Promise { - this.logService.trace('[file working copy] resolve() - enter', this.resource.toString(true)); + this.trace('[file working copy] resolve() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.logService.trace('[file working copy] resolve() - exit - without resolving because file working copy is disposed', this.resource.toString(true)); + this.trace('[file working copy] resolve() - exit - without resolving because file working copy is disposed'); return; } @@ -519,7 +519,7 @@ export class FileWorkingCopy extends Disposable // resolve a working copy that is dirty or is in the process of saving to prevent // data loss. if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { - this.logService.trace('[file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved', this.resource.toString(true)); + this.trace('[file working copy] resolve() - exit - without resolving because file working copy is dirty or being saved'); return; } @@ -548,7 +548,7 @@ export class FileWorkingCopy extends Disposable } private async resolveFromBuffer(buffer: VSBufferReadableStream): Promise { - this.logService.trace('[file working copy] resolveFromBuffer()', this.resource.toString(true)); + this.trace('[file working copy] resolveFromBuffer()'); // Try to resolve metdata from disk let mtime: number; @@ -591,12 +591,12 @@ export class FileWorkingCopy extends Disposable private async resolveFromBackup(): Promise { // Resolve backup if any - const backup = await this.backupFileService.resolve(this.resource); + const backup = await this.workingCopyBackupService.resolve(this); // Abort if someone else managed to resolve the working copy by now let isNew = !this.isResolved(); if (!isNew) { - this.logService.trace('[file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile', this.resource.toString(true)); + this.trace('[file working copy] resolveFromBackup() - exit - withoutresolving because previously new file working copy got created meanwhile'); return true; // imply that resolving has happened in another operation } @@ -612,8 +612,8 @@ export class FileWorkingCopy extends Disposable return false; } - private async doResolveFromBackup(backup: IResolvedBackup): Promise { - this.logService.trace('[file working copy] doResolveFromBackup()', this.resource.toString(true)); + private async doResolveFromBackup(backup: IResolvedWorkingCopyBackup): Promise { + this.trace('[file working copy] doResolveFromBackup()'); // Resolve with backup await this.resolveFromContent({ @@ -623,7 +623,7 @@ export class FileWorkingCopy extends Disposable ctime: backup.meta ? backup.meta.ctime : Date.now(), size: backup.meta ? backup.meta.size : 0, etag: backup.meta ? backup.meta.etag : ETAG_DISABLED, // etag disabled if unknown! - value: this.textBufferFactoryToStream(backup.value) + value: backup.value }, true /* dirty (resolved from backup) */); // Restore orphaned flag based on state @@ -633,7 +633,7 @@ export class FileWorkingCopy extends Disposable } private async resolveFromFile(options?: IFileWorkingCopyResolveOptions): Promise { - this.logService.trace('[file working copy] resolveFromFile()', this.resource.toString(true)); + this.trace('[file working copy] resolveFromFile()'); const forceReadFromFile = options?.forceReadFromFile; @@ -660,7 +660,7 @@ export class FileWorkingCopy extends Disposable // Return early if the working copy content has changed // meanwhile to prevent loosing any changes if (currentVersionId !== this.versionId) { - this.logService.trace('[file working copy] resolveFromFile() - exit - without resolving because file working copy content changed', this.resource.toString(true)); + this.trace('[file working copy] resolveFromFile() - exit - without resolving because file working copy content changed'); return; } @@ -692,11 +692,11 @@ export class FileWorkingCopy extends Disposable } private async resolveFromContent(content: IFileStreamContent, dirty: boolean): Promise { - this.logService.trace('[file working copy] resolveFromContent() - enter', this.resource.toString(true)); + this.trace('[file working copy] resolveFromContent() - enter'); // Return early if we are disposed if (this.isDisposed()) { - this.logService.trace('[file working copy] resolveFromContent() - exit - because working copy is disposed', this.resource.toString(true)); + this.trace('[file working copy] resolveFromContent() - exit - because working copy is disposed'); return; } @@ -736,7 +736,7 @@ export class FileWorkingCopy extends Disposable } private async doCreateModel(contents: VSBufferReadableStream): Promise { - this.logService.trace('[file working copy] doCreateModel()', this.resource.toString(true)); + this.trace('[file working copy] doCreateModel()'); // Create model and dispose it when we get disposed this._model = this._register(await this.modelFactory.createModel(this.resource, contents, CancellationToken.None)); @@ -748,7 +748,7 @@ export class FileWorkingCopy extends Disposable private ignoreDirtyOnModelContentChange = false; private async doUpdateModel(contents: VSBufferReadableStream): Promise { - this.logService.trace('[file working copy] doUpdateModel()', this.resource.toString(true)); + this.trace('[file working copy] doUpdateModel()'); // Update model value in a block that ignores content change events for dirty tracking this.ignoreDirtyOnModelContentChange = true; @@ -773,11 +773,11 @@ export class FileWorkingCopy extends Disposable } private onModelContentChanged(model: IFileWorkingCopyModel, isUndoingOrRedoing: boolean): void { - this.logService.trace(`[file working copy] onModelContentChanged() - enter`, this.resource.toString(true)); + this.trace(`[file working copy] onModelContentChanged() - enter`); // In any case increment the version id because it tracks the textual content state of the model at all times this.versionId++; - this.logService.trace(`[file working copy] onModelContentChanged() - new versionId ${this.versionId}`, this.resource.toString(true)); + this.trace(`[file working copy] onModelContentChanged() - new versionId ${this.versionId}`); // Remember when the user changed the model through a undo/redo operation. // We need this information to throttle save participants to fix @@ -794,7 +794,7 @@ export class FileWorkingCopy extends Disposable // The contents changed as a matter of Undo and the version reached matches the saved one // In this case we clear the dirty flag and emit a SAVED event to indicate this state. if (model.versionId === this.savedVersionId) { - this.logService.trace('[file working copy] onModelContentChanged() - model content changed back to last saved version', this.resource.toString(true)); + this.trace('[file working copy] onModelContentChanged() - model content changed back to last saved version'); // Clear flags const wasDirty = this.dirty; @@ -808,7 +808,7 @@ export class FileWorkingCopy extends Disposable // Otherwise the content has changed and we signal this as becoming dirty else { - this.logService.trace('[file working copy] onModelContentChanged() - model content changed and marked as dirty', this.resource.toString(true)); + this.trace('[file working copy] onModelContentChanged() - model content changed and marked as dirty'); // Mark as dirty this.setDirty(true); @@ -837,7 +837,13 @@ export class FileWorkingCopy extends Disposable }; } - return { meta, content: await this.modelTextSnapshot(token) }; + // Fill in content if we are resolved + let content: VSBufferReadableStream | undefined = undefined; + if (this.isResolved()) { + content = await raceCancellation(this.model.snapshot(token), token); + } + + return { meta, content }; } //#endregion @@ -857,7 +863,7 @@ export class FileWorkingCopy extends Disposable } if (this.isReadonly()) { - this.logService.trace('[file working copy] save() - ignoring request for readonly resource', this.resource.toString(true)); + this.trace('[file working copy] save() - ignoring request for readonly resource'); return false; // if working copy is readonly we do not attempt to save at all } @@ -866,15 +872,15 @@ export class FileWorkingCopy extends Disposable (this.hasState(FileWorkingCopyState.CONFLICT) || this.hasState(FileWorkingCopyState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE) ) { - this.logService.trace('[file working copy] save() - ignoring auto save request for file working copy that is in conflict or error', this.resource.toString(true)); + this.trace('[file working copy] save() - ignoring auto save request for file working copy that is in conflict or error'); return false; // if working copy is in save conflict or error, do not save unless save reason is explicit } // Actually do save - this.logService.trace('[file working copy] save() - enter', this.resource.toString(true)); + this.trace('[file working copy] save() - enter'); await this.doSave(options); - this.logService.trace('[file working copy] save() - exit', this.resource.toString(true)); + this.trace('[file working copy] save() - exit'); return true; } @@ -885,7 +891,7 @@ export class FileWorkingCopy extends Disposable } let versionId = this.versionId; - this.logService.trace(`[file working copy] doSave(${versionId}) - enter with versionId ${versionId}`, this.resource.toString(true)); + this.trace(`[file working copy] doSave(${versionId}) - enter with versionId ${versionId}`); // Lookup any running pending save for this versionId and return it if found // @@ -893,7 +899,7 @@ export class FileWorkingCopy extends Disposable // while the save was not yet finished to disk // if (this.saveSequentializer.hasPending(versionId)) { - this.logService.trace(`[file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`, this.resource.toString(true)); + this.trace(`[file working copy] doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); return this.saveSequentializer.pending; } @@ -902,7 +908,7 @@ export class FileWorkingCopy extends Disposable // // Scenario: user invoked save action even though the working copy is not dirty if (!options.force && !this.dirty) { - this.logService.trace(`[file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`, this.resource.toString(true)); + this.trace(`[file working copy] doSave(${versionId}) - exit - because not dirty and/or versionId is different (this.isDirty: ${this.dirty}, this.versionId: ${this.versionId})`); return; } @@ -916,7 +922,7 @@ export class FileWorkingCopy extends Disposable // while the first save has not returned yet. // if (this.saveSequentializer.hasPending()) { - this.logService.trace(`[file working copy] doSave(${versionId}) - exit - because busy saving`, this.resource.toString(true)); + this.trace(`[file working copy] doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to // cancel the pending operation so that ours can run @@ -975,7 +981,7 @@ export class FileWorkingCopy extends Disposable await this.textFileService.files.runSaveParticipants(this.model, { reason: options.reason ?? SaveReason.EXPLICIT }, saveCancellation.token); } } catch (error) { - this.logService.error(`[file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true)); + this.logService.error(`[file working copy] runSaveParticipants(${versionId}) - resulted in an error: ${error.toString()}`, this.resource.toString(true), this.typeId); } } @@ -1009,7 +1015,7 @@ export class FileWorkingCopy extends Disposable // Save to Disk. We mark the save operation as currently pending with // the latest versionId because it might have changed from a save // participant triggering - this.logService.trace(`[file working copy] doSave(${versionId}) - before write()`, this.resource.toString(true)); + this.trace(`[file working copy] doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedFileWorkingCopy = this; return this.saveSequentializer.setPending(versionId, (async () => { @@ -1052,10 +1058,10 @@ export class FileWorkingCopy extends Disposable // Update dirty state unless working copy has changed meanwhile if (versionId === this.versionId) { - this.logService.trace(`[file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`, this.resource.toString(true)); + this.trace(`[file working copy] handleSaveSuccess(${versionId}) - setting dirty to false because versionId did not change`); this.setDirty(false); } else { - this.logService.trace(`[file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`, this.resource.toString(true)); + this.trace(`[file working copy] handleSaveSuccess(${versionId}) - not setting dirty to false because versionId did change meanwhile`); } // Update orphan state given save was successful @@ -1066,7 +1072,7 @@ export class FileWorkingCopy extends Disposable } private handleSaveError(error: Error, versionId: number, options: IFileWorkingCopySaveOptions): void { - this.logService.error(`[file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true)); + this.logService.error(`[file working copy] handleSaveError(${versionId}) - exit - resulted in a save error: ${error.toString()}`, this.resource.toString(true), this.typeId); // Return early if the save() call was made asking to // handle the save error itself. @@ -1200,6 +1206,10 @@ export class FileWorkingCopy extends Disposable return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } + private trace(msg: string): void { + this.logService.trace(msg, this.resource.toString(true), this.typeId); + } + //#endregion //#region Dispose @@ -1211,7 +1221,7 @@ export class FileWorkingCopy extends Disposable } override dispose(): void { - this.logService.trace('[file working copy] dispose()', this.resource.toString(true)); + this.trace('[file working copy] dispose()'); // State this.disposed = true; @@ -1235,32 +1245,5 @@ export class FileWorkingCopy extends Disposable return !!(textFileModel && this.model && (textFileModel as unknown) === (this.model as unknown)); } - // TODO@bpasero backups should account for binary data, - // and be able to deal with VSBuffer directly - - private textBufferFactoryToStream(factory: ITextBufferFactory): VSBufferReadableStream { - const stream = newWriteableBufferStream(); - - const contents = snapshotToString(factory.create(isWindows ? DefaultEndOfLine.CRLF : DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - stream.end(VSBuffer.fromString(contents)); - - return stream; - } - - private async modelTextSnapshot(token: CancellationToken): Promise { - if (!this.isResolved()) { - return undefined; - } - - const snapshot = await raceCancellation(this.model.snapshot(token), token); - if (token.isCancellationRequested) { - return undefined; - } - - const contents = await streamToBuffer(assertIsDefined(snapshot)); - - return stringToSnapshot(contents.toString()); - } - //#endregion } diff --git a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts index 28ebd79377d..beed31e6630 100644 --- a/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/fileWorkingCopyManager.ts @@ -199,6 +199,7 @@ export class FileWorkingCopyManager extends Dis private readonly workingCopyResolveQueue = this._register(new ResourceQueue()); constructor( + private readonly workingCopyTypeId: string, private readonly modelFactory: IFileWorkingCopyModelFactory, @IFileService private readonly fileService: IFileService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @@ -458,7 +459,7 @@ export class FileWorkingCopyManager extends Dis else { didCreateWorkingCopy = true; - const newWorkingCopy = workingCopy = this.instantiationService.createInstance(FileWorkingCopy, resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory) as unknown as IFileWorkingCopy; + const newWorkingCopy = workingCopy = this.instantiationService.createInstance(FileWorkingCopy, this.workingCopyTypeId, resource, this.labelService.getUriBasenameLabel(resource), this.modelFactory) as unknown as IFileWorkingCopy; workingCopyResolve = workingCopy.resolve(options); this.registerWorkingCopy(newWorkingCopy); diff --git a/src/vs/workbench/services/workingCopy/common/workingCopy.ts b/src/vs/workbench/services/workingCopy/common/workingCopy.ts new file mode 100644 index 00000000000..273091c845e --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopy.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event } from 'vs/base/common/event'; +import { URI } from 'vs/base/common/uri'; +import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; + +export const enum WorkingCopyCapabilities { + + /** + * Signals no specific capability for the working copy. + */ + None = 0, + + /** + * Signals that the working copy requires + * additional input when saving, e.g. an + * associated path to save to. + */ + Untitled = 1 << 1 +} + +/** + * Data to be associated with working copy backups. Use + * `IWorkingCopyBackupService.resolve(workingCopy)` to + * retrieve the backup when loading the working copy. + */ +export interface IWorkingCopyBackup { + + /** + * Any serializable metadata to be associated with the backup. + */ + meta?: IWorkingCopyBackupMeta; + + /** + * The actual snapshot of the contents of the working copy at + * the time the backup was made. + */ + content?: VSBufferReadable | VSBufferReadableStream; +} + +/** + * @deprecated it is important to provide a type identifier + * for working copies to enable all capabilities. + */ +export const NO_TYPE_ID = ''; + +/** + * Every working copy has in common that it is identified by + * a resource `URI` and a `typeId`. There can only be one + * working copy registered with the same `URI` and `typeId`. + */ +export interface IWorkingCopyIdentifier { + + /** + * The type identifier of the working copy for grouping + * working copies of the same domain together. + * + * There can only be one working copy for a given resource + * and type identifier. + */ + readonly typeId: string; + + /** + * The resource of the working copy must be unique for + * working copies of the same `typeId`. + */ + readonly resource: URI; +} + +/** + * A working copy is an abstract concept to unify handling of + * data that can be worked on (e.g. edited) in an editor. + * + * + * A working copy resource may be the backing store of the data + * (e.g. a file on disk), but that is not a requirement. If + * your working copy is file based, consider to use the + * `IFileWorkingCopy` instead that simplifies a lot of things + * when working with file based working copies. + */ +export interface IWorkingCopy extends IWorkingCopyIdentifier { + + /** + * Human readable name of the working copy. + */ + readonly name: string; + + /** + * The capabilities of the working copy. + */ + readonly capabilities: WorkingCopyCapabilities; + + + //#region Events + + /** + * Used by the workbench to signal if the working copy + * is dirty or not. Typically a working copy is dirty + * once changed until saved or reverted. + */ + readonly onDidChangeDirty: Event; + + /** + * Used by the workbench e.g. to trigger auto-save + * (unless this working copy is untitled) and backups. + */ + readonly onDidChangeContent: Event; + + //#endregion + + + //#region Dirty Tracking + + isDirty(): boolean; + + //#endregion + + + //#region Save / Backup + + /** + * The workbench may call this method often after it receives + * the `onDidChangeContent` event for the working copy. The motivation + * is to allow to quit VSCode with dirty working copies present. + * + * Providers of working copies should use `IWorkingCopyBackupService.resolve(workingCopy)` + * to retrieve the backup metadata associated when loading the working copy. + * + * @param token support for cancellation + */ + backup(token: CancellationToken): Promise; + + /** + * Asks the working copy to save. If the working copy was dirty, it is + * expected to be non-dirty after this operation has finished. + * + * @returns `true` if the operation was successful and `false` otherwise. + */ + save(options?: ISaveOptions): Promise; + + /** + * Asks the working copy to revert. If the working copy was dirty, it is + * expected to be non-dirty after this operation has finished. + */ + revert(options?: IRevertOptions): Promise; + + //#endregion +} diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts new file mode 100644 index 00000000000..a8eefe2808b --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackup.ts @@ -0,0 +1,97 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +export const IWorkingCopyBackupService = createDecorator('workingCopyBackupService'); + +/** + * Working copy backup metadata that can be associated + * with the backup. + * + * Some properties may be reserved as outlined here and + * cannot be used. + */ +export interface IWorkingCopyBackupMeta { + [key: string]: unknown; + + /** + * `typeId` is a reverved property that cannot be used + * as backup metadata. + */ + typeId?: never; +} + +/** + * A resolved working copy backup carries the backup value + * as well as associated metadata with it. + */ +export interface IResolvedWorkingCopyBackup { + + /** + * The content of the working copy backup. + */ + readonly value: VSBufferReadableStream; + + /** + * Additional metadata that is associated with + * the working copy backup. + */ + readonly meta?: T; +} + +/** + * The working copy backup service is the main service to handle backups + * for working copies. + * Methods allow to persist and resolve working copy backups from the file + * system. + */ +export interface IWorkingCopyBackupService { + + readonly _serviceBrand: undefined; + + /** + * Finds out if there are any working copy backups stored. + */ + hasBackups(): Promise; + + /** + * Finds out if a working copy backup with the given identifier + * and optional version exists. + * + * Note: if the backup service has not been initialized yet, this may return + * the wrong result. Always use `resolve()` if you can do a long running + * operation. + */ + hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number): boolean; + + /** + * Gets a list of working copy backups for the current workspace. + */ + getBackups(): Promise; + + /** + * Resolves the working copy backup for the given identifier if that exists. + */ + resolve(identifier: IWorkingCopyIdentifier): Promise | undefined>; + + /** + * Stores a new working copy backup for the given identifier. + */ + backup(identifier: IWorkingCopyIdentifier, content?: VSBufferReadable | VSBufferReadableStream, versionId?: number, meta?: IWorkingCopyBackupMeta, token?: CancellationToken): Promise; + + /** + * Discards the working copy backup associated with the identifier if it exists. + */ + discardBackup(identifier: IWorkingCopyIdentifier): Promise; + + /** + * Discards all working copy backups. + */ + discardBackups(): Promise; +} diff --git a/src/vs/workbench/services/backup/common/backupRestorer.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupRestorer.ts similarity index 67% rename from src/vs/workbench/services/backup/common/backupRestorer.ts rename to src/vs/workbench/services/workingCopy/common/workingCopyBackupRestorer.ts index 6098e8a45d4..b477ebc04fb 100644 --- a/src/vs/workbench/services/backup/common/backupRestorer.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupRestorer.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI } from 'vs/base/common/uri'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { Schemas } from 'vs/base/common/network'; @@ -18,8 +17,9 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { ILogService } from 'vs/platform/log/common/log'; import { Promises } from 'vs/base/common/async'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; -export class BackupRestorer implements IWorkbenchContribution { +export class WorkingCopyBackupRestorer implements IWorkbenchContribution { private static readonly UNTITLED_REGEX = /Untitled-\d+/; @@ -27,7 +27,7 @@ export class BackupRestorer implements IWorkbenchContribution { constructor( @IEditorService private readonly editorService: IEditorService, - @IBackupFileService private readonly backupFileService: IBackupFileService, + @IWorkingCopyBackupService private readonly workingCopyBackupService: IWorkingCopyBackupService, @ILifecycleService private readonly lifecycleService: ILifecycleService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -44,7 +44,7 @@ export class BackupRestorer implements IWorkbenchContribution { protected async doRestoreBackups(): Promise { // Resolve all backup resources that exist for this window - const backups = await this.backupFileService.getBackups(); + const backups = await this.workingCopyBackupService.getBackups(); // Trigger `resolve` in each opened editor that can be found // for the given resource and keep track of backups that are @@ -64,29 +64,29 @@ export class BackupRestorer implements IWorkbenchContribution { } } - private async resolveOpenedBackupEditors(resources: URI[]): Promise { - const unresolvedBackups: URI[] = []; + private async resolveOpenedBackupEditors(backups: IWorkingCopyIdentifier[]): Promise { + const unresolvedBackups: IWorkingCopyIdentifier[] = []; - await Promises.settled(resources.map(async resource => { - const openedEditor = this.findOpenedEditorByResource(resource); + await Promises.settled(backups.map(async backup => { + const openedEditor = this.findOpenedEditor(backup); if (openedEditor) { try { await openedEditor.resolve(); } catch (error) { - unresolvedBackups.push(resource); // ignore error and remember as unresolved + unresolvedBackups.push(backup); // ignore error and remember as unresolved } } else { - unresolvedBackups.push(resource); + unresolvedBackups.push(backup); } })); return unresolvedBackups; } - private findOpenedEditorByResource(resource: URI): IEditorInput | undefined { + private findOpenedEditor(backup: IWorkingCopyIdentifier): IEditorInput | undefined { for (const editor of this.editorService.editors) { - const customFactory = this.editorInputFactories.getCustomEditorInputFactory(resource.scheme); - if (customFactory?.canResolveBackup(editor, resource) || isEqual(editor.resource, resource)) { + const customFactory = this.editorInputFactories.getCustomEditorInputFactory(backup.resource.scheme); + if (customFactory?.canResolveBackup(editor, backup.resource) || isEqual(editor.resource, backup.resource)) { return editor; } } @@ -94,14 +94,14 @@ export class BackupRestorer implements IWorkbenchContribution { return undefined; } - private async openEditors(resources: URI[]): Promise { + private async openEditors(backups: IWorkingCopyIdentifier[]): Promise { const hasOpenedEditors = this.editorService.visibleEditors.length > 0; - const editors = await Promises.settled(resources.map((resource, index) => this.resolveEditor(resource, index, hasOpenedEditors))); + const editors = await Promises.settled(backups.map((backup, index) => this.resolveEditor(backup, index, hasOpenedEditors))); await this.editorService.openEditors(editors); } - private async resolveEditor(resource: URI, index: number, hasOpenedEditors: boolean): Promise { + private async resolveEditor(backup: IWorkingCopyIdentifier, index: number, hasOpenedEditors: boolean): Promise { // Set editor as `inactive` if we have other editors const options = { pinned: true, preserveFocus: true, inactive: index > 0 || hasOpenedEditors }; @@ -109,20 +109,20 @@ export class BackupRestorer implements IWorkbenchContribution { // This is a (weak) strategy to find out if the untitled input had // an associated file path or not by just looking at the path. and // if so, we must ensure to restore the local resource it had. - if (resource.scheme === Schemas.untitled && !BackupRestorer.UNTITLED_REGEX.test(resource.path)) { - return { resource: toLocalResource(resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme), options, forceUntitled: true }; + if (backup.resource.scheme === Schemas.untitled && !WorkingCopyBackupRestorer.UNTITLED_REGEX.test(backup.resource.path)) { + return { resource: toLocalResource(backup.resource, this.environmentService.remoteAuthority, this.pathService.defaultUriScheme), options, forceUntitled: true }; } // Handle custom editors by asking the custom editor input factory // to create the input. - const customFactory = this.editorInputFactories.getCustomEditorInputFactory(resource.scheme); + const customFactory = this.editorInputFactories.getCustomEditorInputFactory(backup.resource.scheme); if (customFactory) { - const editor = await customFactory.createCustomEditorInput(resource, this.instantiationService); + const editor = await customFactory.createCustomEditorInput(backup.resource, this.instantiationService); return { editor, options }; } // Finally return with a simple resource based input - return { resource, options }; + return { resource: backup.resource, options }; } } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts new file mode 100644 index 00000000000..c0a865fdcac --- /dev/null +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupService.ts @@ -0,0 +1,558 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { basename, isEqual, joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { coalesce } from 'vs/base/common/arrays'; +import { equals, deepClone } from 'vs/base/common/objects'; +import { Promises, ResourceQueue } from 'vs/base/common/async'; +import { IResolvedWorkingCopyBackup, IWorkingCopyBackupService, IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { ResourceMap } from 'vs/base/common/map'; +import { isReadableStream, peekStream } from 'vs/base/common/stream'; +import { bufferToStream, prefixedBufferReadable, prefixedBufferStream, readableToBuffer, streamToBuffer, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Schemas } from 'vs/base/common/network'; +import { hash } from 'vs/base/common/hash'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { WorkingCopyBackupRestorer } from 'vs/workbench/services/workingCopy/common/workingCopyBackupRestorer'; +import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; +import { isEmptyObject } from 'vs/base/common/types'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; + +export class WorkingCopyBackupsModel { + + private readonly cache = new ResourceMap<{ versionId?: number, meta?: IWorkingCopyBackupMeta }>(); + + static async create(backupRoot: URI, fileService: IFileService): Promise { + const model = new WorkingCopyBackupsModel(backupRoot, fileService); + + await model.resolve(); + + return model; + } + + private constructor(private backupRoot: URI, private fileService: IFileService) { } + + private async resolve(): Promise { + try { + const backupRootStat = await this.fileService.resolve(this.backupRoot); + if (backupRootStat.children) { + await Promises.settled(backupRootStat.children + .filter(child => child.isDirectory) + .map(async backupSchemaFolder => { + + // Read backup directory for backups + const backupSchemaFolderStat = await this.fileService.resolve(backupSchemaFolder.resource); + + // Remember known backups in our caches + if (backupSchemaFolderStat.children) { + for (const backupForSchema of backupSchemaFolderStat.children) { + if (!backupForSchema.isDirectory) { + this.add(backupForSchema.resource); + } + } + } + })); + } + } catch (error) { + // ignore any errors + } + } + + add(resource: URI, versionId = 0, meta?: IWorkingCopyBackupMeta): void { + this.cache.set(resource, { versionId, meta: deepClone(meta) }); // make sure to not store original meta in our cache... + } + + count(): number { + return this.cache.size; + } + + has(resource: URI, versionId?: number, meta?: IWorkingCopyBackupMeta): boolean { + const entry = this.cache.get(resource); + if (!entry) { + return false; // unknown resource + } + + if (typeof versionId === 'number' && versionId !== entry.versionId) { + return false; // different versionId + } + + if (meta && !equals(meta, entry.meta)) { + return false; // different metadata + } + + return true; + } + + get(): URI[] { + return Array.from(this.cache.keys()); + } + + remove(resource: URI): void { + this.cache.delete(resource); + } + + move(source: URI, target: URI): void { + const entry = this.cache.get(source); + if (entry) { + this.cache.delete(source); + this.cache.set(target, entry); + } + } + + clear(): void { + this.cache.clear(); + } +} + +export abstract class WorkingCopyBackupService implements IWorkingCopyBackupService { + + declare readonly _serviceBrand: undefined; + + private impl: NativeWorkingCopyBackupServiceImpl | InMemoryWorkingCopyBackupService; + + constructor( + backupWorkspaceHome: URI | undefined, + @IFileService protected fileService: IFileService, + @ILogService private readonly logService: ILogService + ) { + this.impl = this.initialize(backupWorkspaceHome); + } + + private initialize(backupWorkspaceHome: URI | undefined): NativeWorkingCopyBackupServiceImpl | InMemoryWorkingCopyBackupService { + if (backupWorkspaceHome) { + return new NativeWorkingCopyBackupServiceImpl(backupWorkspaceHome, this.fileService, this.logService); + } + + return new InMemoryWorkingCopyBackupService(); + } + + reinitialize(backupWorkspaceHome: URI | undefined): void { + + // Re-init implementation (unless we are running in-memory) + if (this.impl instanceof NativeWorkingCopyBackupServiceImpl) { + if (backupWorkspaceHome) { + this.impl.initialize(backupWorkspaceHome); + } else { + this.impl = new InMemoryWorkingCopyBackupService(); + } + } + } + + hasBackups(): Promise { + return this.impl.hasBackups(); + } + + hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number): boolean { + return this.impl.hasBackupSync(identifier, versionId); + } + + backup(identifier: IWorkingCopyIdentifier, content?: VSBufferReadableStream | VSBufferReadable, versionId?: number, meta?: IWorkingCopyBackupMeta, token?: CancellationToken): Promise { + return this.impl.backup(identifier, content, versionId, meta, token); + } + + discardBackup(identifier: IWorkingCopyIdentifier): Promise { + return this.impl.discardBackup(identifier); + } + + discardBackups(): Promise { + return this.impl.discardBackups(); + } + + getBackups(): Promise { + return this.impl.getBackups(); + } + + resolve(identifier: IWorkingCopyIdentifier): Promise | undefined> { + return this.impl.resolve(identifier); + } + + toBackupResource(identifier: IWorkingCopyIdentifier): URI { + return this.impl.toBackupResource(identifier); + } +} + +class NativeWorkingCopyBackupServiceImpl extends Disposable implements IWorkingCopyBackupService { + + private static readonly PREAMBLE_END_MARKER = '\n'; + private static readonly PREAMBLE_END_MARKER_CHARCODE = '\n'.charCodeAt(0); + private static readonly PREAMBLE_META_SEPARATOR = ' '; // using a character that is know to be escaped in a URI as separator + private static readonly PREAMBLE_MAX_LENGTH = 10000; + + declare readonly _serviceBrand: undefined; + + private readonly ioOperationQueues = this._register(new ResourceQueue()); // queue IO operations to ensure write/delete file order + + private ready!: Promise; + private model!: WorkingCopyBackupsModel; + + constructor( + private backupWorkspaceHome: URI, + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService + ) { + super(); + + this.initialize(backupWorkspaceHome); + } + + initialize(backupWorkspaceResource: URI): void { + this.backupWorkspaceHome = backupWorkspaceResource; + + this.ready = this.doInitialize(); + } + + private async doInitialize(): Promise { + + // Create backup model + this.model = await WorkingCopyBackupsModel.create(this.backupWorkspaceHome, this.fileService); + + // Migrate hashes as needed. We used to hash with a MD5 + // sum of the path but switched to our own simpler hash + // to avoid a node.js dependency. We still want to + // support the older hash so we: + // - iterate over all backups + // - detect if the file name length is 32 (MD5 length) + // - read the backup's target file path + // - rename the backup to the new hash + // - update the backup in our model + // + // TODO@bpasero remove me eventually + for (const backupResource of this.model.get()) { + if (basename(backupResource).length !== 32) { + continue; // not a MD5 hash, already uses new hash function + } + + try { + const identifier = await this.resolveIdentifier(backupResource); + if (!identifier) { + this.logService.warn(`Backup: Unable to read target URI of backup ${backupResource} for migration to new hash.`); + continue; + } + + const expectedBackupResource = this.toBackupResource(identifier); + if (!isEqual(expectedBackupResource, backupResource)) { + await this.fileService.move(backupResource, expectedBackupResource, true); + this.model.move(backupResource, expectedBackupResource); + } + } catch (error) { + this.logService.error(`Backup: Unable to migrate backup ${backupResource} to new hash.`); + } + } + + return this.model; + } + + async hasBackups(): Promise { + const model = await this.ready; + + return model.count() > 0; + } + + hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number): boolean { + const backupResource = this.toBackupResource(identifier); + + return this.model.has(backupResource, versionId); + } + + async backup(identifier: IWorkingCopyIdentifier, content?: VSBufferReadable | VSBufferReadableStream, versionId?: number, meta?: IWorkingCopyBackupMeta, token?: CancellationToken): Promise { + const model = await this.ready; + if (token?.isCancellationRequested) { + return; + } + + const backupResource = this.toBackupResource(identifier); + if (model.has(backupResource, versionId, meta)) { + return; // return early if backup version id matches requested one + } + + return this.ioOperationQueues.queueFor(backupResource).queue(async () => { + if (token?.isCancellationRequested) { + return; + } + + // Encode as: Resource + META-START + Meta + END + // and respect max length restrictions in case + // meta is too large. + let preamble = this.createPreamble(identifier, meta); + if (preamble.length >= NativeWorkingCopyBackupServiceImpl.PREAMBLE_MAX_LENGTH) { + preamble = this.createPreamble(identifier); + } + + // Update backup with value + const preambleBuffer = VSBuffer.fromString(preamble); + let backupBuffer: VSBuffer | VSBufferReadableStream | VSBufferReadable; + if (isReadableStream(content)) { + backupBuffer = prefixedBufferStream(preambleBuffer, content); + } else if (content) { + backupBuffer = prefixedBufferReadable(preambleBuffer, content); + } else { + backupBuffer = VSBuffer.concat([preambleBuffer, VSBuffer.fromString('')]); + } + + await this.fileService.writeFile(backupResource, backupBuffer); + + // Update model + model.add(backupResource, versionId, meta); + }); + } + + private createPreamble(identifier: IWorkingCopyIdentifier, meta?: IWorkingCopyBackupMeta): string { + return `${identifier.resource.toString()}${NativeWorkingCopyBackupServiceImpl.PREAMBLE_META_SEPARATOR}${JSON.stringify({ ...meta, typeId: identifier.typeId })}${NativeWorkingCopyBackupServiceImpl.PREAMBLE_END_MARKER}`; + } + + async discardBackups(): Promise { + const model = await this.ready; + + await this.deleteIgnoreFileNotFound(this.backupWorkspaceHome); + + model.clear(); + } + + discardBackup(identifier: IWorkingCopyIdentifier): Promise { + const backupResource = this.toBackupResource(identifier); + + return this.doDiscardBackup(backupResource); + } + + private async doDiscardBackup(backupResource: URI): Promise { + const model = await this.ready; + + return this.ioOperationQueues.queueFor(backupResource).queue(async () => { + await this.deleteIgnoreFileNotFound(backupResource); + + model.remove(backupResource); + }); + } + + private async deleteIgnoreFileNotFound(backupResource: URI): Promise { + try { + await this.fileService.del(backupResource, { recursive: true }); + } catch (error) { + if ((error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) { + throw error; // re-throw any other error than file not found which is OK + } + } + } + + async getBackups(): Promise { + const model = await this.ready; + + const backups = await Promise.all(model.get().map(backupResource => this.resolveIdentifier(backupResource))); + + return coalesce(backups); + } + + private async resolveIdentifier(backupResource: URI): Promise { + + // Read the entire backup preamble by reading up to + // `PREAMBLE_MAX_LENGTH` in the backup file until + // the `PREAMBLE_END_MARKER` is found + const backupPreamble = await this.readToMatchingString(backupResource, NativeWorkingCopyBackupServiceImpl.PREAMBLE_END_MARKER, NativeWorkingCopyBackupServiceImpl.PREAMBLE_MAX_LENGTH); + if (!backupPreamble) { + return undefined; + } + + // Figure out the offset in the preamble where meta + // information possibly starts. This can be `-1` for + // older backups without meta. + const metaStartIndex = backupPreamble.indexOf(NativeWorkingCopyBackupServiceImpl.PREAMBLE_META_SEPARATOR); + + // Extract the preamble content for resource and meta + let resourcePreamble: string; + let metaPreamble: string | undefined; + if (metaStartIndex > 0) { + resourcePreamble = backupPreamble.substring(0, metaStartIndex); + metaPreamble = backupPreamble.substr(metaStartIndex + 1); + } else { + resourcePreamble = backupPreamble; + metaPreamble = undefined; + } + + // Try to find the `typeId` in the meta data if possible + let typeId: string | undefined = undefined; + if (metaPreamble) { + try { + typeId = JSON.parse(metaPreamble).typeId; + } catch (error) { + // ignore JSON parse errors + } + } + + return { + typeId: typeId ?? '', // Fallback for previous backups that do not encode the typeId (TODO@bpasero remove me eventually) + resource: URI.parse(resourcePreamble) + }; + } + + private async readToMatchingString(backupResource: URI, matchingString: string, maximumBytesToRead: number): Promise { + const contents = (await this.fileService.readFile(backupResource, { length: maximumBytesToRead })).value.toString(); + + const matchingStringIndex = contents.indexOf(matchingString); + if (matchingStringIndex >= 0) { + return contents.substr(0, matchingStringIndex); + } + + // Unable to find matching string in file + return undefined; + } + + async resolve(identifier: IWorkingCopyIdentifier): Promise | undefined> { + const backupResource = this.toBackupResource(identifier); + + const model = await this.ready; + if (!model.has(backupResource)) { + return undefined; // require backup to be present + } + + // Load the backup content and peek into the first chunk + // to be able to resolve the meta data + const backupStream = await this.fileService.readFileStream(backupResource); + const peekedBackupStream = await peekStream(backupStream.value, 1); + const firstBackupChunk = VSBuffer.concat(peekedBackupStream.buffer); + + // We have seen reports (e.g. https://github.com/microsoft/vscode/issues/78500) where + // if VSCode goes down while writing the backup file, the file can turn empty because + // it always first gets truncated and then written to. In this case, we will not find + // the meta-end marker ('\n') and as such the backup can only be invalid. We bail out + // here if that is the case. + const preambleEndIndex = firstBackupChunk.buffer.indexOf(NativeWorkingCopyBackupServiceImpl.PREAMBLE_END_MARKER_CHARCODE); + if (preambleEndIndex === -1) { + this.logService.trace(`Backup: Could not find meta end marker in ${backupResource}. The file is probably corrupt (filesize: ${backupStream.size}).`); + + return undefined; + } + + const preambelRaw = firstBackupChunk.slice(0, preambleEndIndex).toString(); + + // Extract meta data (if any) + let meta: T | undefined; + const metaStartIndex = preambelRaw.indexOf(NativeWorkingCopyBackupServiceImpl.PREAMBLE_META_SEPARATOR); + if (metaStartIndex !== -1) { + try { + meta = JSON.parse(preambelRaw.substr(metaStartIndex + 1)); + + // `typeId` is a property that we add so we + // remove it when returning to clients. + if (typeof meta?.typeId === 'string') { + delete meta.typeId; + + if (isEmptyObject(meta)) { + meta = undefined; + } + } + } catch (error) { + // ignore JSON parse errors + } + } + + // Build a new stream without the preamble + const firstBackupChunkWithoutPreamble = firstBackupChunk.slice(preambleEndIndex + 1); + let value: VSBufferReadableStream; + if (peekedBackupStream.ended) { + value = bufferToStream(firstBackupChunkWithoutPreamble); + } else { + value = prefixedBufferStream(firstBackupChunkWithoutPreamble, peekedBackupStream.stream); + } + + return { value, meta }; + } + + toBackupResource(identifier: IWorkingCopyIdentifier): URI { + return joinPath(this.backupWorkspaceHome, identifier.resource.scheme, hashIdentifier(identifier)); + } +} + +export class InMemoryWorkingCopyBackupService implements IWorkingCopyBackupService { + + declare readonly _serviceBrand: undefined; + + private backups = new ResourceMap<{ typeId: string, content: VSBuffer, meta?: IWorkingCopyBackupMeta }>(); + + constructor() { } + + async hasBackups(): Promise { + return this.backups.size > 0; + } + + hasBackupSync(identifier: IWorkingCopyIdentifier, versionId?: number): boolean { + const backupResource = this.toBackupResource(identifier); + + return this.backups.has(backupResource); + } + + async backup(identifier: IWorkingCopyIdentifier, content?: VSBufferReadable | VSBufferReadableStream, versionId?: number, meta?: IWorkingCopyBackupMeta, token?: CancellationToken): Promise { + const backupResource = this.toBackupResource(identifier); + this.backups.set(backupResource, { + typeId: identifier.typeId, + content: content instanceof VSBuffer ? content : content ? isReadableStream(content) ? await streamToBuffer(content) : readableToBuffer(content) : VSBuffer.fromString(''), + meta + }); + } + + async resolve(identifier: IWorkingCopyIdentifier): Promise | undefined> { + const backupResource = this.toBackupResource(identifier); + const backup = this.backups.get(backupResource); + if (backup) { + return { value: bufferToStream(backup.content), meta: backup.meta as T | undefined }; + } + + return undefined; + } + + async getBackups(): Promise { + return Array.from(this.backups.entries()).map(([resource, backup]) => ({ typeId: backup.typeId, resource })); + } + + async discardBackup(identifier: IWorkingCopyIdentifier): Promise { + this.backups.delete(this.toBackupResource(identifier)); + } + + async discardBackups(): Promise { + this.backups.clear(); + } + + toBackupResource(identifier: IWorkingCopyIdentifier): URI { + return URI.from({ scheme: Schemas.inMemory, path: hashIdentifier(identifier) }); + } +} + +/* + * Exported only for testing + */ +export function hashIdentifier(identifier: IWorkingCopyIdentifier): string { + + // IMPORTANT: for backwards compatibility, ensure that + // we ignore the `typeId` unless a value is provided. + // To preserve previous backups without type id, we + // need to just hash the resource. Otherwise we use + // the type id as a seed to the resource path. + let resource: URI; + if (identifier.typeId.length > 0) { + const typeIdHash = hashString(identifier.typeId); + resource = joinPath(identifier.resource, typeIdHash); + } else { + resource = identifier.resource; + } + + return hashPath(resource); +} + +function hashPath(resource: URI): string { + const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); + + return hashString(str); +} + +function hashString(str: string): string { + return hash(str).toString(16); +} + +// Register Backup Restorer +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(WorkingCopyBackupRestorer, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/backup/common/backupTracker.ts b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts similarity index 85% rename from src/vs/workbench/services/backup/common/backupTracker.ts rename to src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts index ad09e07a1b5..6628085cdc1 100644 --- a/src/vs/workbench/services/backup/common/backupTracker.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyBackupTracker.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ILogService } from 'vs/platform/log/common/log'; import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { AutoSaveMode, IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -export abstract class BackupTracker extends Disposable { +export abstract class WorkingCopyBackupTracker extends Disposable { // A map from working copy to a version ID we compute on each content // change. This version ID allows to e.g. ask if a backup for a specific @@ -36,7 +37,7 @@ export abstract class BackupTracker extends Disposable { }; constructor( - protected readonly backupFileService: IBackupFileService, + protected readonly workingCopyBackupService: IWorkingCopyBackupService, protected readonly workingCopyService: IWorkingCopyService, protected readonly logService: ILogService, private readonly lifecycleService: ILifecycleService, @@ -105,7 +106,7 @@ export abstract class BackupTracker extends Disposable { // Clear any running backup operation this.cancelBackup(workingCopy); - this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString()); + this.logService.trace(`[backup tracker] scheduling backup`, workingCopy.resource.toString(true), workingCopy.typeId); // Schedule new backup const cts = new CancellationTokenSource(); @@ -116,7 +117,7 @@ export abstract class BackupTracker extends Disposable { // Backup if dirty if (workingCopy.isDirty()) { - this.logService.trace(`[backup tracker] creating backup`, workingCopy.resource.toString()); + this.logService.trace(`[backup tracker] creating backup`, workingCopy.resource.toString(true), workingCopy.typeId); try { const backup = await workingCopy.backup(cts.token); @@ -125,9 +126,9 @@ export abstract class BackupTracker extends Disposable { } if (workingCopy.isDirty()) { - this.logService.trace(`[backup tracker] storing backup`, workingCopy.resource.toString()); + this.logService.trace(`[backup tracker] storing backup`, workingCopy.resource.toString(true), workingCopy.typeId); - await this.backupFileService.backup(workingCopy.resource, backup.content, this.getContentVersion(workingCopy), backup.meta, cts.token); + await this.workingCopyBackupService.backup(workingCopy, backup.content, this.getContentVersion(workingCopy), backup.meta, cts.token); } } catch (error) { this.logService.error(error); @@ -145,7 +146,7 @@ export abstract class BackupTracker extends Disposable { // Keep in map for disposal as needed this.pendingBackups.set(workingCopy, toDisposable(() => { - this.logService.trace(`[backup tracker] clearing pending backup`, workingCopy.resource.toString()); + this.logService.trace(`[backup tracker] clearing pending backup`, workingCopy.resource.toString(true), workingCopy.typeId); cts.dispose(true); clearTimeout(handle); @@ -158,7 +159,7 @@ export abstract class BackupTracker extends Disposable { autoSaveMode = AutoSaveMode.OFF; // auto-save is never on for untitled working copies } - return BackupTracker.BACKUP_SCHEDULE_DELAYS[autoSaveMode]; + return WorkingCopyBackupTracker.BACKUP_SCHEDULE_DELAYS[autoSaveMode]; } protected getContentVersion(workingCopy: IWorkingCopy): number { @@ -166,13 +167,13 @@ export abstract class BackupTracker extends Disposable { } private discardBackup(workingCopy: IWorkingCopy): void { - this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString()); + this.logService.trace(`[backup tracker] discarding backup`, workingCopy.resource.toString(true), workingCopy.typeId); // Clear any running backup operation this.cancelBackup(workingCopy); - // Forward to backup file service - this.backupFileService.discardBackup(workingCopy.resource); + // Forward to working copy backup service + this.workingCopyBackupService.discardBackup(workingCopy); } private cancelBackup(workingCopy: IWorkingCopy): void { diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts index e546748bbbc..52116ec499f 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyFileService.ts @@ -12,7 +12,8 @@ import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { IFileService, FileOperation, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IWorkingCopyService, IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { WorkingCopyFileOperationParticipant } from 'vs/workbench/services/workingCopy/common/workingCopyFileOperationParticipant'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index 09f1680edd5..e8c5f0b6565 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -9,130 +9,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import { Disposable, IDisposable, toDisposable, DisposableStore, dispose } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; -import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; -import { ITextSnapshot } from 'vs/editor/common/model'; -import { CancellationToken } from 'vs/base/common/cancellation'; - -export const enum WorkingCopyCapabilities { - - /** - * Signals no specific capability for the working copy. - */ - None = 0, - - /** - * Signals that the working copy requires - * additional input when saving, e.g. an - * associated path to save to. - */ - Untitled = 1 << 1 -} - -/** - * Data to be associated with working copy backups. Use - * `IBackupFileService.resolve(workingCopy.resource)` to - * retrieve the backup when loading the working copy. - */ -export interface IWorkingCopyBackup { - - /** - * Any serializable metadata to be associated with the backup. - */ - meta?: MetaType; - - /** - * Use this for larger textual content of the backup. - */ - content?: ITextSnapshot; -} - -/** - * A working copy is an abstract concept to unify handling of - * data that can be worked on (e.g. edited) in an editor. - * - * Every working copy has in common that it is identified by - * a resource `URI` and only one working copy can be registered - * with the same `URI`. - * - * A working copy resource may be the backing store of the data - * (e.g. a file on disk), but that is not a requirement. The - * `URI` is mainly used to uniquely identify a working copy among - * others. - */ -export interface IWorkingCopy { - - /** - * The unique resource of the working copy. There can only be one - * working copy in the system with the same URI. - */ - readonly resource: URI; - - /** - * Human readable name of the working copy. - */ - readonly name: string; - - /** - * The capabilities of the working copy. - */ - readonly capabilities: WorkingCopyCapabilities; - - - //#region Events - - /** - * Used by the workbench to signal if the working copy - * is dirty or not. Typically a working copy is dirty - * once changed until saved or reverted. - */ - readonly onDidChangeDirty: Event; - - /** - * Used by the workbench e.g. to trigger auto-save - * (unless this working copy is untitled) and backups. - */ - readonly onDidChangeContent: Event; - - //#endregion - - - //#region Dirty Tracking - - isDirty(): boolean; - - //#endregion - - - //#region Save / Backup - - /** - * The workbench may call this method often after it receives - * the `onDidChangeContent` event for the working copy. The motivation - * is to allow to quit VSCode with dirty working copies present. - * - * Providers of working copies should use `IBackupFileService.resolve(workingCopy.resource)` - * to retrieve the backup metadata associated when loading the working copy. - * - * @param token support for cancellation - */ - backup(token: CancellationToken): Promise; - - /** - * Asks the working copy to save. If the working copy was dirty, it is - * expected to be non-dirty after this operation has finished. - * - * @returns `true` if the operation was successful and `false` otherwise. - */ - save(options?: ISaveOptions): Promise; - - /** - * Asks the working copy to revert. If the working copy was dirty, it is - * expected to be non-dirty after this operation has finished. - */ - revert(options?: IRevertOptions): Promise; - - //#endregion -} +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; export const IWorkingCopyService = createDecorator('workingCopyService'); @@ -162,7 +39,15 @@ export interface IWorkingCopyService { readonly hasDirty: boolean; - isDirty(resource: URI): boolean; + /** + * Figure out if working copies with the given + * resource are dirty or not. + * + * @param resource the URI of the working copy + * @param typeId optional type identifier to only + * consider working copies of that type. + */ + isDirty(resource: URI, typeId?: string): boolean; //#endregion @@ -210,20 +95,26 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic get workingCopies(): IWorkingCopy[] { return Array.from(this._workingCopies.values()); } private _workingCopies = new Set(); - private readonly mapResourceToWorkingCopy = new ResourceMap(); + private readonly mapResourceToWorkingCopies = new ResourceMap>(); registerWorkingCopy(workingCopy: IWorkingCopy): IDisposable { - if (this.mapResourceToWorkingCopy.has(workingCopy.resource)) { - throw new Error(`Cannot register more than one working copy with the same resource ${workingCopy.resource.toString(true)}.`); + let workingCopiesForResource = this.mapResourceToWorkingCopies.get(workingCopy.resource); + if (workingCopiesForResource?.has(workingCopy.typeId)) { + throw new Error(`Cannot register more than one working copy with the same resource ${workingCopy.resource.toString(true)} and type ${workingCopy.typeId}.`); } - const disposables = new DisposableStore(); - - // Registry + // Registry (all) this._workingCopies.add(workingCopy); - this.mapResourceToWorkingCopy.set(workingCopy.resource, workingCopy); + + // Registry (type based) + if (!workingCopiesForResource) { + workingCopiesForResource = new Map(); + this.mapResourceToWorkingCopies.set(workingCopy.resource, workingCopiesForResource); + } + workingCopiesForResource.set(workingCopy.typeId, workingCopy); // Wire in Events + const disposables = new DisposableStore(); disposables.add(workingCopy.onDidChangeContent(() => this._onDidChangeContent.fire(workingCopy))); disposables.add(workingCopy.onDidChangeDirty(() => this._onDidChangeDirty.fire(workingCopy))); @@ -244,9 +135,14 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic private unregisterWorkingCopy(workingCopy: IWorkingCopy): void { - // Remove from registry + // Registry (all) this._workingCopies.delete(workingCopy); - this.mapResourceToWorkingCopy.delete(workingCopy.resource); + + // Registry (type based) + const workingCopiesForResource = this.mapResourceToWorkingCopies.get(workingCopy.resource); + if (workingCopiesForResource?.delete(workingCopy.typeId) && workingCopiesForResource.size === 0) { + this.mapResourceToWorkingCopies.delete(workingCopy.resource); + } // If copy is dirty, ensure to fire an event to signal the dirty change // (a disposed working copy cannot account for being dirty in our model) @@ -286,10 +182,23 @@ export class WorkingCopyService extends Disposable implements IWorkingCopyServic return this.workingCopies.filter(workingCopy => workingCopy.isDirty()); } - isDirty(resource: URI): boolean { - const workingCopy = this.mapResourceToWorkingCopy.get(resource); - if (workingCopy) { - return workingCopy.isDirty(); + isDirty(resource: URI, typeId?: string): boolean { + const workingCopies = this.mapResourceToWorkingCopies.get(resource); + if (workingCopies) { + + // For a specific type + if (typeof typeId === 'string') { + return workingCopies.get(typeId)?.isDirty() ?? false; + } + + // Across all working copies + else { + for (const [, workingCopy] of workingCopies) { + if (workingCopy.isDirty()) { + return true; + } + } + } } return false; diff --git a/src/vs/workbench/services/backup/electron-sandbox/backupFileService.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts similarity index 71% rename from src/vs/workbench/services/backup/electron-sandbox/backupFileService.ts rename to src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts index 09892c9ffb0..758acf322c8 100644 --- a/src/vs/workbench/services/backup/electron-sandbox/backupFileService.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService.ts @@ -3,19 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; +import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { URI } from 'vs/base/common/uri'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IFileService } from 'vs/platform/files/common/files'; import { ILogService } from 'vs/platform/log/common/log'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { NativeBackupTracker } from 'vs/workbench/services/backup/electron-sandbox/backupTracker'; +import { NativeWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker'; -export class NativeBackupFileService extends BackupFileService { +export class NativeWorkingCopyBackupService extends WorkingCopyBackupService { constructor( @INativeWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService, @@ -27,7 +27,7 @@ export class NativeBackupFileService extends BackupFileService { } // Register Service -registerSingleton(IBackupFileService, NativeBackupFileService); +registerSingleton(IWorkingCopyBackupService, NativeWorkingCopyBackupService); // Register Backup Tracker -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeBackupTracker, LifecyclePhase.Starting); +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeWorkingCopyBackupTracker, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/backup/electron-sandbox/backupTracker.ts b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts similarity index 93% rename from src/vs/workbench/services/backup/electron-sandbox/backupTracker.ts rename to src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts index 4a0296ee2a1..410b74c8861 100644 --- a/src/vs/workbench/services/backup/electron-sandbox/backupTracker.ts +++ b/src/vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyService, IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ILifecycleService, ShutdownReason } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { ConfirmResult, IFileDialogService, IDialogService, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; @@ -15,7 +16,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/ import { isMacintosh } from 'vs/base/common/platform'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { BackupTracker } from 'vs/workbench/services/backup/common/backupTracker'; +import { WorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/common/workingCopyBackupTracker'; import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -25,10 +26,10 @@ import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/ import { Promises, raceCancellation } from 'vs/base/common/async'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -export class NativeBackupTracker extends BackupTracker implements IWorkbenchContribution { +export class NativeWorkingCopyBackupTracker extends WorkingCopyBackupTracker implements IWorkbenchContribution { constructor( - @IBackupFileService backupFileService: IBackupFileService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, @@ -42,7 +43,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont @IProgressService private readonly progressService: IProgressService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { - super(backupFileService, workingCopyService, logService, lifecycleService, filesConfigurationService); + super(workingCopyBackupService, workingCopyService, logService, lifecycleService, filesConfigurationService); } protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise { @@ -199,14 +200,14 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont const contentVersion = this.getContentVersion(workingCopy); // Backup exists - if (this.backupFileService.hasBackupSync(workingCopy.resource, contentVersion)) { + if (this.workingCopyBackupService.hasBackupSync(workingCopy, contentVersion)) { backups.push(workingCopy); } // Backup does not exist else { const backup = await workingCopy.backup(token); - await this.backupFileService.backup(workingCopy.resource, backup.content, contentVersion, backup.meta, token); + await this.workingCopyBackupService.backup(workingCopy, backup.content, contentVersion, backup.meta, token); backups.push(workingCopy); } @@ -316,7 +317,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont return false; // if editors have not restored, we are very likely not up to speed with backups and thus should not discard them } - return Promises.settled(backupsToDiscard.map(workingCopy => this.backupFileService.discardBackup(workingCopy.resource))).then(() => false, () => false); + return Promises.settled(backupsToDiscard.map(workingCopy => this.workingCopyBackupService.discardBackup(workingCopy))).then(() => false, () => false); } private async onBeforeShutdownWithoutDirty(): Promise { @@ -327,7 +328,7 @@ export class NativeBackupTracker extends BackupTracker implements IWorkbenchCont // https://github.com/microsoft/vscode/issues/92962 if (this.editorGroupService.isRestored()) { try { - await this.backupFileService.discardBackups(); + await this.workingCopyBackupService.discardBackups(); } catch (error) { this.logService.error(`[backup tracker] error discarding backups: ${error}`); } diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts index 725498a599b..2e74c9cefea 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopy.test.ts @@ -16,6 +16,7 @@ import { basename } from 'vs/base/common/resources'; import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { SaveReason } from 'vs/workbench/common/editor'; import { Promises } from 'vs/base/common/async'; +import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; export class TestFileWorkingCopyModel extends Disposable implements IFileWorkingCopyModel { @@ -88,7 +89,7 @@ suite('FileWorkingCopy', function () { let workingCopy: FileWorkingCopy; function createWorkingCopy() { - return new FileWorkingCopy(resource, basename(resource), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.backupFileService, accessor.workingCopyService); + return new FileWorkingCopy('testWorkingCopyType', resource, basename(resource), factory, accessor.fileService, accessor.logService, accessor.textFileService, accessor.filesConfigurationService, accessor.workingCopyBackupService, accessor.workingCopyService); } setup(() => { @@ -240,7 +241,9 @@ suite('FileWorkingCopy', function () { await workingCopy.resolve({ contents: bufferToStream(VSBuffer.fromString('hello backup')) }); const backup = await workingCopy.backup(CancellationToken.None); - accessor.backupFileService.backup(workingCopy.resource, backup.content, undefined, backup.meta); + await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); + + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(workingCopy), true); workingCopy.dispose(); @@ -273,7 +276,9 @@ suite('FileWorkingCopy', function () { assert.strictEqual(workingCopy.hasState(FileWorkingCopyState.ORPHAN), true); const backup = await workingCopy.backup(CancellationToken.None); - accessor.backupFileService.backup(workingCopy.resource, backup.content, undefined, backup.meta); + await accessor.workingCopyBackupService.backup(workingCopy, backup.content, undefined, backup.meta); + + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(workingCopy), true); workingCopy.dispose(); @@ -345,7 +350,17 @@ suite('FileWorkingCopy', function () { const backup = await workingCopy.backup(CancellationToken.None); assert.ok(backup.meta); - assert.strictEqual(backup.content?.read(), 'hello backup'); + + let backupContents: string | undefined = undefined; + if (backup.content instanceof VSBuffer) { + backupContents = backup.content.toString(); + } else if (isReadableStream(backup.content)) { + backupContents = (await consumeStream(backup.content, chunks => VSBuffer.concat(chunks))).toString(); + } else if (backup.content) { + backupContents = consumeReadable(backup.content, chunks => VSBuffer.concat(chunks)).toString(); + } + + assert.strictEqual(backupContents, 'hello backup'); }); test('save (no errors)', async () => { diff --git a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts index 53a46392ce2..9370d2f4b33 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/fileWorkingCopyManager.test.ts @@ -27,7 +27,7 @@ suite('FileWorkingCopyManager', () => { accessor = instantiationService.createInstance(TestServiceAccessor); const factory = new TestFileWorkingCopyModelFactory(); - manager = new FileWorkingCopyManager(factory, accessor.fileService, accessor.lifecycleService, accessor.labelService, instantiationService, accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, accessor.uriIdentityService); + manager = new FileWorkingCopyManager('testWorkingCopyType', factory, accessor.fileService, accessor.lifecycleService, accessor.labelService, instantiationService, accessor.logService, accessor.fileDialogService, accessor.workingCopyFileService, accessor.uriIdentityService); }); teardown(() => { @@ -49,6 +49,7 @@ suite('FileWorkingCopyManager', () => { const workingCopy1 = await resolvePromise; assert.ok(workingCopy1); assert.ok(workingCopy1.model); + assert.strictEqual(workingCopy1.typeId, 'testWorkingCopyType'); assert.strictEqual(manager.get(resource), workingCopy1); const workingCopy2 = await manager.resolve(resource); @@ -111,7 +112,7 @@ suite('FileWorkingCopyManager', () => { workingCopy.dispose(); }); - test('multiple resolves execute in sequence', async () => { + test('multiple resolves execute in sequence (same resources)', async () => { const resource = URI.file('/test.html'); const firstPromise = manager.resolve(resource); @@ -128,6 +129,27 @@ suite('FileWorkingCopyManager', () => { workingCopy.dispose(); }); + test('multiple resolves execute in parallel (different resources)', async () => { + const resource1 = URI.file('/test1.html'); + const resource2 = URI.file('/test2.html'); + const resource3 = URI.file('/test3.html'); + + const firstPromise = manager.resolve(resource1); + const secondPromise = manager.resolve(resource2); + const thirdPromise = manager.resolve(resource3); + + const [workingCopy1, workingCopy2, workingCopy3] = await Promise.all([firstPromise, secondPromise, thirdPromise]); + + assert.strictEqual(manager.workingCopies.length, 3); + assert.strictEqual(workingCopy1.resource.toString(), resource1.toString()); + assert.strictEqual(workingCopy2.resource.toString(), resource2.toString()); + assert.strictEqual(workingCopy3.resource.toString(), resource3.toString()); + + workingCopy1.dispose(); + workingCopy2.dispose(); + workingCopy3.dispose(); + }); + test('removed from cache when working copy or model gets disposed', async () => { const resource = URI.file('/test.html'); diff --git a/src/vs/workbench/services/backup/test/browser/backupRestorer.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupRestorer.test.ts similarity index 66% rename from src/vs/workbench/services/backup/test/browser/backupRestorer.test.ts rename to src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupRestorer.test.ts index 5d5d66cb907..78619224371 100644 --- a/src/vs/workbench/services/backup/test/browser/backupRestorer.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupRestorer.test.ts @@ -6,22 +6,21 @@ import * as assert from 'assert'; import { isWindows } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; -import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; -import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { bufferToReadable, VSBuffer } from 'vs/base/common/buffer'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { Schemas } from 'vs/base/common/network'; import { isEqual } from 'vs/base/common/resources'; -import { createEditorPart, InMemoryTestBackupFileService, registerTestResourceEditor, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { BackupRestorer } from 'vs/workbench/services/backup/common/backupRestorer'; -import { BrowserBackupTracker } from 'vs/workbench/services/backup/browser/backupTracker'; +import { createEditorPart, InMemoryTestWorkingCopyBackupService, registerTestResourceEditor, TestServiceAccessor, toUntypedWorkingCopyId, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { WorkingCopyBackupRestorer } from 'vs/workbench/services/workingCopy/common/workingCopyBackupRestorer'; +import { BrowserWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/browser/workingCopyBackupTracker'; import { DisposableStore } from 'vs/base/common/lifecycle'; -suite('BackupRestorer', () => { +suite('WorkingCopyBackupRestorer', () => { - class TestBackupRestorer extends BackupRestorer { + class TestBackupRestorer extends WorkingCopyBackupRestorer { async override doRestoreBackups(): Promise { return super.doRestoreBackups(); } @@ -44,9 +43,9 @@ suite('BackupRestorer', () => { }); test('Restore backups', async function () { - const backupFileService = new InMemoryTestBackupFileService(); + const workingCopyBackupService = new InMemoryTestWorkingCopyBackupService(); const instantiationService = workbenchInstantiationService(); - instantiationService.stub(IBackupFileService, backupFileService); + instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); const part = await createEditorPart(instantiationService, disposables); @@ -57,14 +56,14 @@ suite('BackupRestorer', () => { accessor = instantiationService.createInstance(TestServiceAccessor); - disposables.add(instantiationService.createInstance(BrowserBackupTracker)); + disposables.add(instantiationService.createInstance(BrowserWorkingCopyBackupTracker)); const restorer = instantiationService.createInstance(TestBackupRestorer); - // Backup 2 normal files and 2 untitled file - await backupFileService.backup(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - await backupFileService.backup(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - await backupFileService.backup(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); - await backupFileService.backup(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).textBuffer.createSnapshot(false)); + // Backup 2 normal files and 2 untitled files + await workingCopyBackupService.backup(toUntypedWorkingCopyId(untitledFile1), bufferToReadable(VSBuffer.fromString('untitled-1'))); + await workingCopyBackupService.backup(toUntypedWorkingCopyId(untitledFile2), bufferToReadable(VSBuffer.fromString('untitled-2'))); + await workingCopyBackupService.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString('fooFile'))); + await workingCopyBackupService.backup(toUntypedWorkingCopyId(barFile), bufferToReadable(VSBuffer.fromString('barFile'))); // Verify backups restored and opened as dirty await restorer.doRestoreBackups(); @@ -77,7 +76,7 @@ suite('BackupRestorer', () => { if (isEqual(resource, untitledFile1)) { const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); if (model.textEditorModel?.getValue() !== 'untitled-1') { - const backupContents = await backupFileService.getBackupContents(untitledFile1); + const backupContents = await workingCopyBackupService.getBackupContents(model); assert.fail(`Unable to restore backup for resource ${untitledFile1.toString()}. Backup contents: ${backupContents}`); } model.dispose(); @@ -85,7 +84,7 @@ suite('BackupRestorer', () => { } else if (isEqual(resource, untitledFile2)) { const model = await accessor.textFileService.untitled.resolve({ untitledResource: resource }); if (model.textEditorModel?.getValue() !== 'untitled-2') { - const backupContents = await backupFileService.getBackupContents(untitledFile2); + const backupContents = await workingCopyBackupService.getBackupContents(model); assert.fail(`Unable to restore backup for resource ${untitledFile2.toString()}. Backup contents: ${backupContents}`); } model.dispose(); @@ -94,7 +93,7 @@ suite('BackupRestorer', () => { const model = accessor.textFileService.files.get(fooFile); await model?.resolve(); if (model?.textEditorModel?.getValue() !== 'fooFile') { - const backupContents = await backupFileService.getBackupContents(fooFile); + const backupContents = await workingCopyBackupService.getBackupContents(model!); assert.fail(`Unable to restore backup for resource ${fooFile.toString()}. Backup contents: ${backupContents}`); } counter++; @@ -102,7 +101,7 @@ suite('BackupRestorer', () => { const model = accessor.textFileService.files.get(barFile); await model?.resolve(); if (model?.textEditorModel?.getValue() !== 'barFile') { - const backupContents = await backupFileService.getBackupContents(barFile); + const backupContents = await workingCopyBackupService.getBackupContents(model!); assert.fail(`Unable to restore backup for resource ${barFile.toString()}. Backup contents: ${backupContents}`); } counter++; diff --git a/src/vs/workbench/services/backup/test/browser/backupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts similarity index 62% rename from src/vs/workbench/services/backup/test/browser/backupTracker.test.ts rename to src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts index dc25515045d..15371690ee4 100644 --- a/src/vs/workbench/services/backup/test/browser/backupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyBackupTracker.test.ts @@ -10,35 +10,36 @@ import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; -import { IWorkingCopyBackup, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { ILogService } from 'vs/platform/log/common/log'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; -import { BackupTracker } from 'vs/workbench/services/backup/common/backupTracker'; +import { WorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/common/workingCopyBackupTracker'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { createEditorPart, InMemoryTestBackupFileService, registerTestResourceEditor, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; +import { createEditorPart, InMemoryTestWorkingCopyBackupService, registerTestResourceEditor, TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestWorkingCopy } from 'vs/workbench/test/common/workbenchTestServices'; import { CancellationToken } from 'vs/base/common/cancellation'; import { timeout } from 'vs/base/common/async'; -import { BrowserBackupTracker } from 'vs/workbench/services/backup/browser/backupTracker'; +import { BrowserWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/browser/workingCopyBackupTracker'; import { DisposableStore } from 'vs/base/common/lifecycle'; -suite('BackupTracker (browser)', function () { +suite('WorkingCopyBackupTracker (browser)', function () { let accessor: TestServiceAccessor; - class TestBackupTracker extends BrowserBackupTracker { + class TestBackupTracker extends BrowserWorkingCopyBackupTracker { constructor( - @IBackupFileService backupFileService: IBackupFileService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, @ILogService logService: ILogService, ) { - super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, logService); + super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, logService); } protected override getBackupScheduleDelay(): number { @@ -46,12 +47,12 @@ suite('BackupTracker (browser)', function () { } } - async function createTracker(): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, backupFileService: InMemoryTestBackupFileService, instantiationService: IInstantiationService, cleanup: () => void }> { + async function createTracker(): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: WorkingCopyBackupTracker, workingCopyBackupService: InMemoryTestWorkingCopyBackupService, instantiationService: IInstantiationService, cleanup: () => void }> { const disposables = new DisposableStore(); - const backupFileService = new InMemoryTestBackupFileService(); + const workingCopyBackupService = new InMemoryTestWorkingCopyBackupService(); const instantiationService = workbenchInstantiationService(); - instantiationService.stub(IBackupFileService, backupFileService); + instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); const part = await createEditorPart(instantiationService, disposables); @@ -66,11 +67,11 @@ suite('BackupTracker (browser)', function () { const tracker = disposables.add(instantiationService.createInstance(TestBackupTracker)); - return { accessor, part, tracker, backupFileService, instantiationService, cleanup: () => disposables.dispose() }; + return { accessor, part, tracker, workingCopyBackupService: workingCopyBackupService, instantiationService, cleanup: () => disposables.dispose() }; } async function untitledBackupTest(untitled: IUntitledTextResourceEditorInput = {}): Promise { - const { accessor, cleanup, backupFileService } = await createTracker(); + const { accessor, cleanup, workingCopyBackupService } = await createTracker(); const untitledEditor = (await accessor.editorService.openEditor(untitled))?.input as UntitledTextEditorInput; @@ -80,15 +81,15 @@ suite('BackupTracker (browser)', function () { untitledModel.textEditorModel?.setValue('Super Good'); } - await backupFileService.joinBackupResource(); + await workingCopyBackupService.joinBackupResource(); - assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), true); + assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledModel), true); untitledModel.dispose(); - await backupFileService.joinDiscardBackup(); + await workingCopyBackupService.joinDiscardBackup(); - assert.strictEqual(backupFileService.hasBackupSync(untitledEditor.resource), false); + assert.strictEqual(workingCopyBackupService.hasBackupSync(untitledModel), false); cleanup(); } @@ -102,7 +103,7 @@ suite('BackupTracker (browser)', function () { }); test('Track backups (custom)', async function () { - const { accessor, cleanup, backupFileService } = await createTracker(); + const { accessor, cleanup, workingCopyBackupService } = await createTracker(); class TestBackupWorkingCopy extends TestWorkingCopy { @@ -126,24 +127,24 @@ suite('BackupTracker (browser)', function () { // Normal customWorkingCopy.setDirty(true); - await backupFileService.joinBackupResource(); - assert.strictEqual(backupFileService.hasBackupSync(resource), true); + await workingCopyBackupService.joinBackupResource(); + assert.strictEqual(workingCopyBackupService.hasBackupSync(customWorkingCopy), true); customWorkingCopy.setDirty(false); customWorkingCopy.setDirty(true); - await backupFileService.joinBackupResource(); - assert.strictEqual(backupFileService.hasBackupSync(resource), true); + await workingCopyBackupService.joinBackupResource(); + assert.strictEqual(workingCopyBackupService.hasBackupSync(customWorkingCopy), true); customWorkingCopy.setDirty(false); - await backupFileService.joinDiscardBackup(); - assert.strictEqual(backupFileService.hasBackupSync(resource), false); + await workingCopyBackupService.joinDiscardBackup(); + assert.strictEqual(workingCopyBackupService.hasBackupSync(customWorkingCopy), false); // Cancellation customWorkingCopy.setDirty(true); await timeout(0); customWorkingCopy.setDirty(false); - await backupFileService.joinDiscardBackup(); - assert.strictEqual(backupFileService.hasBackupSync(resource), false); + await workingCopyBackupService.joinDiscardBackup(); + assert.strictEqual(workingCopyBackupService.hasBackupSync(customWorkingCopy), false); customWorkingCopy.dispose(); cleanup(); diff --git a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts index 41c546de3fc..5021663c3f1 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/workingCopyFileService.test.ts @@ -463,7 +463,7 @@ suite('WorkingCopyFileService', () => { eventCounter++; }); - await accessor.workingCopyFileService.delete(models.map(m => ({ resource: m.resource })), CancellationToken.None); + await accessor.workingCopyFileService.delete(models.map(model => ({ resource: model.resource })), CancellationToken.None); for (const model of models) { assert.ok(!accessor.workingCopyService.isDirty(model.resource)); model.dispose(); diff --git a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts index 314cd18f966..f1de51dd04c 100644 --- a/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts +++ b/src/vs/workbench/services/workingCopy/test/common/workingCopyService.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { URI } from 'vs/base/common/uri'; import { TestWorkingCopy, TestWorkingCopyService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -104,12 +104,9 @@ suite('WorkingCopyService', () => { assert.strictEqual(onDidChangeDirty[3], copy2); }); - test('registry - multiple copies on same resource throws', () => { + test('registry - multiple copies on same resource throws (same type ID)', () => { const service = new TestWorkingCopyService(); - const onDidChangeDirty: IWorkingCopy[] = []; - service.onDidChangeDirty(copy => onDidChangeDirty.push(copy)); - const resource = URI.parse('custom://some/folder/custom.txt'); const copy1 = new TestWorkingCopy(resource); @@ -119,4 +116,64 @@ suite('WorkingCopyService', () => { assert.throws(() => service.registerWorkingCopy(copy2)); }); + + test('registry - multiple copies on same resource is supported (different type ID)', () => { + const service = new TestWorkingCopyService(); + + const resource = URI.parse('custom://some/folder/custom.txt'); + + const typeId1 = 'testWorkingCopyTypeId1'; + let copy1 = new TestWorkingCopy(resource, false, typeId1); + let dispose1 = service.registerWorkingCopy(copy1); + + const typeId2 = 'testWorkingCopyTypeId2'; + const copy2 = new TestWorkingCopy(resource, false, typeId2); + const dispose2 = service.registerWorkingCopy(copy2); + + const typeId3 = 'testWorkingCopyTypeId3'; + const copy3 = new TestWorkingCopy(resource, false, typeId3); + const dispose3 = service.registerWorkingCopy(copy3); + + assert.strictEqual(service.dirtyCount, 0); + assert.strictEqual(service.isDirty(resource), false); + assert.strictEqual(service.isDirty(resource, typeId1), false); + + copy1.setDirty(true); + assert.strictEqual(service.dirtyCount, 1); + assert.strictEqual(service.isDirty(resource), true); + assert.strictEqual(service.isDirty(resource, typeId1), true); + assert.strictEqual(service.isDirty(resource, typeId2), false); + + copy2.setDirty(true); + assert.strictEqual(service.dirtyCount, 2); + assert.strictEqual(service.isDirty(resource), true); + assert.strictEqual(service.isDirty(resource, typeId1), true); + assert.strictEqual(service.isDirty(resource, typeId2), true); + + copy3.setDirty(true); + assert.strictEqual(service.dirtyCount, 3); + assert.strictEqual(service.isDirty(resource), true); + assert.strictEqual(service.isDirty(resource, typeId1), true); + assert.strictEqual(service.isDirty(resource, typeId2), true); + assert.strictEqual(service.isDirty(resource, typeId3), true); + + copy1.setDirty(false); + copy2.setDirty(false); + copy3.setDirty(false); + assert.strictEqual(service.dirtyCount, 0); + assert.strictEqual(service.isDirty(resource), false); + assert.strictEqual(service.isDirty(resource, typeId1), false); + assert.strictEqual(service.isDirty(resource, typeId2), false); + assert.strictEqual(service.isDirty(resource, typeId3), false); + + dispose1.dispose(); + copy1 = new TestWorkingCopy(resource, false, typeId1); + dispose1 = service.registerWorkingCopy(copy1); + + dispose1.dispose(); + dispose2.dispose(); + dispose3.dispose(); + + assert.strictEqual(service.workingCopies.length, 0); + }); }); diff --git a/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts new file mode 100644 index 00000000000..8cd80278218 --- /dev/null +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test.ts @@ -0,0 +1,1108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { isWindows } from 'vs/base/common/platform'; +import { tmpdir } from 'os'; +import { insert } from 'vs/base/common/arrays'; +import { hash } from 'vs/base/common/hash'; +import { isEqual } from 'vs/base/common/resources'; +import { promises, existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { dirname, join } from 'vs/base/common/path'; +import { readdirSync, rimraf, writeFile } from 'vs/base/node/pfs'; +import { URI } from 'vs/base/common/uri'; +import { WorkingCopyBackupsModel, hashIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; +import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { Schemas } from 'vs/base/common/network'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { NullLogService } from 'vs/platform/log/common/log'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-sandbox/environmentService'; +import { toBufferOrReadable } from 'vs/workbench/services/textfile/common/textfiles'; +import { IFileService } from 'vs/platform/files/common/files'; +import { NativeWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; +import { bufferToReadable, bufferToStream, streamToBuffer, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { TestWorkbenchConfiguration } from 'vs/workbench/test/electron-browser/workbenchTestServices'; +import { TestProductService, toTypedWorkingCopyId, toUntypedWorkingCopyId } from 'vs/workbench/test/browser/workbenchTestServices'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IWorkingCopyBackupMeta } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; +import { consumeStream } from 'vs/base/common/stream'; + +class TestWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService { + + constructor(testDir: string, backupPath: string) { + super({ ...TestWorkbenchConfiguration, backupPath, 'user-data-dir': testDir }, TestProductService); + } +} + +export class NodeTestWorkingCopyBackupService extends NativeWorkingCopyBackupService { + + override readonly fileService: IFileService; + + private backupResourceJoiners: Function[]; + private discardBackupJoiners: Function[]; + discardedBackups: IWorkingCopyIdentifier[]; + private pendingBackupsArr: Promise[]; + + constructor(testDir: string, workspaceBackupPath: string) { + const environmentService = new TestWorkbenchEnvironmentService(testDir, workspaceBackupPath); + const logService = new NullLogService(); + const fileService = new FileService(logService); + const diskFileSystemProvider = new DiskFileSystemProvider(logService); + fileService.registerProvider(Schemas.file, diskFileSystemProvider); + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(Schemas.file, diskFileSystemProvider, Schemas.userData, logService)); + + super(environmentService, fileService, logService); + + this.fileService = fileService; + this.backupResourceJoiners = []; + this.discardBackupJoiners = []; + this.discardedBackups = []; + this.pendingBackupsArr = []; + } + + async waitForAllBackups(): Promise { + await Promise.all(this.pendingBackupsArr); + } + + joinBackupResource(): Promise { + return new Promise(resolve => this.backupResourceJoiners.push(resolve)); + } + + async override backup(identifier: IWorkingCopyIdentifier, content?: VSBufferReadableStream | VSBufferReadable, versionId?: number, meta?: any, token?: CancellationToken): Promise { + const p = super.backup(identifier, content, versionId, meta, token); + const removeFromPendingBackups = insert(this.pendingBackupsArr, p.then(undefined, undefined)); + + try { + await p; + } finally { + removeFromPendingBackups(); + } + + while (this.backupResourceJoiners.length) { + this.backupResourceJoiners.pop()!(); + } + } + + joinDiscardBackup(): Promise { + return new Promise(resolve => this.discardBackupJoiners.push(resolve)); + } + + async override discardBackup(identifier: IWorkingCopyIdentifier): Promise { + await super.discardBackup(identifier); + this.discardedBackups.push(identifier); + + while (this.discardBackupJoiners.length) { + this.discardBackupJoiners.pop()!(); + } + } + + async getBackupContents(identifier: IWorkingCopyIdentifier): Promise { + const backupResource = this.toBackupResource(identifier); + + const fileContents = await this.fileService.readFile(backupResource); + + return fileContents.value.toString(); + } +} + +suite('WorkingCopyBackupService', () => { + + let testDir: string; + let backupHome: string; + let workspacesJsonPath: string; + let workspaceBackupPath: string; + + let service: NodeTestWorkingCopyBackupService; + + let workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); + let fooFile = URI.file(isWindows ? 'c:\\Foo' : '/Foo'); + let customFile = URI.parse('customScheme://some/path'); + let customFileWithFragment = URI.parse('customScheme2://some/path#fragment'); + let barFile = URI.file(isWindows ? 'c:\\Bar' : '/Bar'); + let fooBarFile = URI.file(isWindows ? 'c:\\Foo Bar' : '/Foo Bar'); + let untitledFile = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + + setup(async () => { + testDir = getRandomTestPath(tmpdir(), 'vsctests', 'workingcopybackupservice'); + backupHome = join(testDir, 'Backups'); + workspacesJsonPath = join(backupHome, 'workspaces.json'); + workspaceBackupPath = join(backupHome, hash(workspaceResource.fsPath).toString(16)); + + service = new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath); + + await promises.mkdir(backupHome, { recursive: true }); + + return writeFile(workspacesJsonPath, ''); + }); + + teardown(() => { + return rimraf(testDir); + }); + + suite('hashIdentifier', () => { + test('should correctly hash the identifier for untitled scheme URIs', () => { + const uri = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // If these hashes change people will lose their backed up files + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + const untypedBackupHash = hashIdentifier(toUntypedWorkingCopyId(uri)); + assert.strictEqual(untypedBackupHash, '-7f9c1a2e'); + assert.strictEqual(untypedBackupHash, hash(uri.fsPath).toString(16)); + + const typedBackupHash = hashIdentifier({ typeId: 'hashTest', resource: uri }); + if (isWindows) { + assert.strictEqual(typedBackupHash, '-17c47cdc'); + } else { + assert.strictEqual(typedBackupHash, '-8ad5f4f'); + } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // If these hashes collide people will lose their backed up files + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + assert.notStrictEqual(untypedBackupHash, typedBackupHash); + }); + + test('should correctly hash the identifier for file scheme URIs', () => { + const uri = URI.file('/foo'); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // If these hashes change people will lose their backed up files + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + const untypedBackupHash = hashIdentifier(toUntypedWorkingCopyId(uri)); + if (isWindows) { + assert.strictEqual(untypedBackupHash, '20ffaa13'); + } else { + assert.strictEqual(untypedBackupHash, '20eb3560'); + } + assert.strictEqual(untypedBackupHash, hash(uri.fsPath).toString(16)); + + const typedBackupHash = hashIdentifier({ typeId: 'hashTest', resource: uri }); + if (isWindows) { + assert.strictEqual(typedBackupHash, '-55fc55db'); + } else { + assert.strictEqual(typedBackupHash, '51e56bf'); + } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // If these hashes collide people will lose their backed up files + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + assert.notStrictEqual(untypedBackupHash, typedBackupHash); + }); + + test('should correctly hash the identifier for custom scheme URIs', () => { + const uri = URI.from({ + scheme: 'vscode-custom', + path: 'somePath' + }); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // If these hashes change people will lose their backed up files + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + const untypedBackupHash = hashIdentifier(toUntypedWorkingCopyId(uri)); + assert.strictEqual(untypedBackupHash, '-44972d98'); + assert.strictEqual(untypedBackupHash, hash(uri.toString()).toString(16)); + + const typedBackupHash = hashIdentifier({ typeId: 'hashTest', resource: uri }); + assert.strictEqual(typedBackupHash, '502149c7'); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // If these hashes collide people will lose their backed up files + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + assert.notStrictEqual(untypedBackupHash, typedBackupHash); + }); + }); + + suite('getBackupResource', () => { + test('should get the correct backup path for text files', () => { + + // Format should be: /// + const backupResource = fooFile; + const workspaceHash = hash(workspaceResource.fsPath).toString(16); + + // No Type ID + let backupId = toUntypedWorkingCopyId(backupResource); + let filePathHash = hashIdentifier(backupId); + let expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString(); + assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); + + // With Type ID + backupId = toTypedWorkingCopyId(backupResource); + filePathHash = hashIdentifier(backupId); + expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.file, filePathHash)).with({ scheme: Schemas.userData }).toString(); + assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); + }); + + test('should get the correct backup path for untitled files', () => { + + // Format should be: /// + const backupResource = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); + const workspaceHash = hash(workspaceResource.fsPath).toString(16); + + // No Type ID + let backupId = toUntypedWorkingCopyId(backupResource); + let filePathHash = hashIdentifier(backupId); + let expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString(); + assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); + + // With Type ID + backupId = toTypedWorkingCopyId(backupResource); + filePathHash = hashIdentifier(backupId); + expectedPath = URI.file(join(backupHome, workspaceHash, Schemas.untitled, filePathHash)).with({ scheme: Schemas.userData }).toString(); + assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); + }); + + test('should get the correct backup path for custom files', () => { + + // Format should be: /// + const backupResource = URI.from({ scheme: 'custom', path: 'custom/file.txt' }); + const workspaceHash = hash(workspaceResource.fsPath).toString(16); + + // No Type ID + let backupId = toUntypedWorkingCopyId(backupResource); + let filePathHash = hashIdentifier(backupId); + let expectedPath = URI.file(join(backupHome, workspaceHash, 'custom', filePathHash)).with({ scheme: Schemas.userData }).toString(); + assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); + + // With Type ID + backupId = toTypedWorkingCopyId(backupResource); + filePathHash = hashIdentifier(backupId); + expectedPath = URI.file(join(backupHome, workspaceHash, 'custom', filePathHash)).with({ scheme: Schemas.userData }).toString(); + assert.strictEqual(service.toBackupResource(backupId).toString(), expectedPath); + }); + }); + + suite('backup', () => { + + function toExpectedPreamble(identifier: IWorkingCopyIdentifier, content = '', meta?: object): string { + return `${identifier.resource.toString()} ${JSON.stringify({ ...meta, typeId: identifier.typeId })}\n${content}`; + } + + test('no text', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier)); + assert.ok(service.hasBackupSync(identifier)); + }); + + test('text file', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test')); + assert.ok(service.hasBackupSync(identifier)); + }); + + test('text file (with version)', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test')), 666); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test')); + assert.ok(!service.hasBackupSync(identifier, 555)); + assert.ok(service.hasBackupSync(identifier, 666)); + }); + + test('text file (with meta)', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + const meta = { etag: '678', orphaned: true }; + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test')), undefined, meta); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test', meta)); + assert.ok(service.hasBackupSync(identifier)); + }); + + test('text file with whitespace in name and type (with meta)', async () => { + let fileWithSpace = URI.file(isWindows ? 'c:\\Foo \n Bar' : '/Foo \n Bar'); + const identifier = toTypedWorkingCopyId(fileWithSpace, ' test id \n'); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + const meta = { etag: '678 \n k', orphaned: true }; + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test')), undefined, meta); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test', meta)); + assert.ok(service.hasBackupSync(identifier)); + }); + + test('text file with unicode character in name and type (with meta)', async () => { + let fileWithUnicode = URI.file(isWindows ? 'c:\\so𒀅meࠄ' : '/so𒀅meࠄ'); + const identifier = toTypedWorkingCopyId(fileWithUnicode, ' test so𒀅meࠄ id \n'); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + const meta = { etag: '678so𒀅meࠄ', orphaned: true }; + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test')), undefined, meta); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test', meta)); + assert.ok(service.hasBackupSync(identifier)); + }); + + test('untitled file', async () => { + const identifier = toUntypedWorkingCopyId(untitledFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test')); + assert.ok(service.hasBackupSync(identifier)); + }); + + test('text file (readable)', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + const model = createTextModel('test'); + + await service.backup(identifier, toBufferOrReadable(model.createSnapshot())); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test')); + assert.ok(service.hasBackupSync(identifier)); + + model.dispose(); + }); + + test('untitled file (readable)', async () => { + const identifier = toUntypedWorkingCopyId(untitledFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + const model = createTextModel('test'); + + await service.backup(identifier, toBufferOrReadable(model.createSnapshot())); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, 'test')); + + model.dispose(); + }); + + test('text file (large file, stream)', () => { + const largeString = (new Array(30 * 1024)).join('Large String\n'); + + return testLargeTextFile(largeString, bufferToStream(VSBuffer.fromString(largeString))); + }); + + test('text file (large file, readable)', async () => { + const largeString = (new Array(30 * 1024)).join('Large String\n'); + const model = createTextModel(largeString); + + await testLargeTextFile(largeString, toBufferOrReadable(model.createSnapshot())); + + model.dispose(); + }); + + async function testLargeTextFile(largeString: string, buffer: VSBufferReadable | VSBufferReadableStream) { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier, buffer, undefined, { largeTest: true }); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, largeString, { largeTest: true })); + assert.ok(service.hasBackupSync(identifier)); + } + + test('untitled file (large file, readable)', async () => { + const identifier = toUntypedWorkingCopyId(untitledFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + const largeString = (new Array(30 * 1024)).join('Large String\n'); + const model = createTextModel(largeString); + + await service.backup(identifier, toBufferOrReadable(model.createSnapshot())); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); + assert.strictEqual(existsSync(backupPath), true); + assert.strictEqual(readFileSync(backupPath).toString(), toExpectedPreamble(identifier, largeString)); + assert.ok(service.hasBackupSync(identifier)); + + model.dispose(); + }); + + test('cancellation', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + const cts = new CancellationTokenSource(); + const promise = service.backup(identifier, undefined, undefined, undefined, cts.token); + cts.cancel(); + await promise; + + assert.strictEqual(existsSync(backupPath), false); + assert.ok(!service.hasBackupSync(identifier)); + }); + + test('multiple same resource, different type id', async () => { + const backupId1 = toUntypedWorkingCopyId(fooFile); + const backupId2 = toTypedWorkingCopyId(fooFile, 'type1'); + const backupId3 = toTypedWorkingCopyId(fooFile, 'type2'); + + await service.backup(backupId1); + await service.backup(backupId2); + await service.backup(backupId3); + + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 3); + + for (const backupId of [backupId1, backupId2, backupId3]) { + const fooBackupPath = join(workspaceBackupPath, backupId.resource.scheme, hashIdentifier(backupId)); + assert.strictEqual(existsSync(fooBackupPath), true); + assert.strictEqual(readFileSync(fooBackupPath).toString(), toExpectedPreamble(backupId)); + assert.ok(service.hasBackupSync(backupId)); + } + }); + }); + + suite('discardBackup', () => { + + test('text file', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + assert.ok(service.hasBackupSync(identifier)); + + await service.discardBackup(identifier); + assert.strictEqual(existsSync(backupPath), false); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 0); + assert.ok(!service.hasBackupSync(identifier)); + }); + + test('untitled file', async () => { + const identifier = toUntypedWorkingCopyId(untitledFile); + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); + + await service.discardBackup(identifier); + assert.strictEqual(existsSync(backupPath), false); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 0); + }); + + test('multiple same resource, different type id', async () => { + const backupId1 = toUntypedWorkingCopyId(fooFile); + const backupId2 = toTypedWorkingCopyId(fooFile, 'type1'); + const backupId3 = toTypedWorkingCopyId(fooFile, 'type2'); + + await service.backup(backupId1); + await service.backup(backupId2); + await service.backup(backupId3); + + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 3); + + for (const backupId of [backupId1, backupId2, backupId3]) { + const backupPath = join(workspaceBackupPath, backupId.resource.scheme, hashIdentifier(backupId)); + await service.discardBackup(backupId); + assert.strictEqual(existsSync(backupPath), false); + } + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 0); + }); + }); + + suite('discardBackups', () => { + test('text file', async () => { + const backupId1 = toUntypedWorkingCopyId(fooFile); + const backupId2 = toUntypedWorkingCopyId(barFile); + const backupId3 = toTypedWorkingCopyId(barFile); + + await service.backup(backupId1, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 1); + + await service.backup(backupId2, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 2); + + await service.backup(backupId3, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'file')).length, 3); + + await service.discardBackups(); + for (const backupId of [backupId1, backupId2, backupId3]) { + const backupPath = join(workspaceBackupPath, backupId.resource.scheme, hashIdentifier(backupId)); + assert.strictEqual(existsSync(backupPath), false); + } + + assert.strictEqual(existsSync(join(workspaceBackupPath, 'file')), false); + }); + + test('untitled file', async () => { + const backupId = toUntypedWorkingCopyId(untitledFile); + const backupPath = join(workspaceBackupPath, backupId.resource.scheme, hashIdentifier(backupId)); + + await service.backup(backupId, bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(readdirSync(join(workspaceBackupPath, 'untitled')).length, 1); + + await service.discardBackups(); + assert.strictEqual(existsSync(backupPath), false); + assert.strictEqual(existsSync(join(workspaceBackupPath, 'untitled')), false); + }); + + test('can backup after discarding all', async () => { + await service.discardBackups(); + await service.backup(toUntypedWorkingCopyId(untitledFile), bufferToReadable(VSBuffer.fromString('test'))); + assert.strictEqual(existsSync(workspaceBackupPath), true); + }); + }); + + suite('getBackups', () => { + test('text file', async () => { + await service.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString('test'))); + await service.backup(toTypedWorkingCopyId(fooFile, 'type1'), bufferToReadable(VSBuffer.fromString('test'))); + await service.backup(toTypedWorkingCopyId(fooFile, 'type2'), bufferToReadable(VSBuffer.fromString('test'))); + + let backups = await service.getBackups(); + assert.strictEqual(backups.length, 3); + + for (const backup of backups) { + if (backup.typeId === '') { + assert.strictEqual(backup.resource.toString(), fooFile.toString()); + } else if (backup.typeId === 'type1') { + assert.strictEqual(backup.resource.toString(), fooFile.toString()); + } else if (backup.typeId === 'type2') { + assert.strictEqual(backup.resource.toString(), fooFile.toString()); + } else { + assert.fail('Unexpected backup'); + } + } + + await service.backup(toUntypedWorkingCopyId(barFile), bufferToReadable(VSBuffer.fromString('test'))); + + backups = await service.getBackups(); + assert.strictEqual(backups.length, 4); + }); + + test('untitled file', async () => { + await service.backup(toUntypedWorkingCopyId(untitledFile), bufferToReadable(VSBuffer.fromString('test'))); + await service.backup(toTypedWorkingCopyId(untitledFile, 'type1'), bufferToReadable(VSBuffer.fromString('test'))); + await service.backup(toTypedWorkingCopyId(untitledFile, 'type2'), bufferToReadable(VSBuffer.fromString('test'))); + + const backups = await service.getBackups(); + assert.strictEqual(backups.length, 3); + + for (const backup of backups) { + if (backup.typeId === '') { + assert.strictEqual(backup.resource.toString(), untitledFile.toString()); + } else if (backup.typeId === 'type1') { + assert.strictEqual(backup.resource.toString(), untitledFile.toString()); + } else if (backup.typeId === 'type2') { + assert.strictEqual(backup.resource.toString(), untitledFile.toString()); + } else { + assert.fail('Unexpected backup'); + } + } + }); + }); + + suite('resolve', () => { + + interface IBackupTestMetaData extends IWorkingCopyBackupMeta { + mtime?: number; + size?: number; + etag?: string; + orphaned?: boolean; + } + + test('should restore the original contents (untitled file)', async () => { + const contents = 'test\nand more stuff'; + + await testResolveBackup(untitledFile, contents); + }); + + test('should restore the original contents (untitled file with metadata)', async () => { + const contents = 'test\nand more stuff'; + + const meta = { + etag: 'the Etag', + size: 666, + mtime: Date.now(), + orphaned: true + }; + + await testResolveBackup(untitledFile, contents, meta); + }); + + test('should restore the original contents (untitled file empty with metadata)', async () => { + const contents = ''; + + const meta = { + etag: 'the Etag', + size: 666, + mtime: Date.now(), + orphaned: true + }; + + await testResolveBackup(untitledFile, contents, meta); + }); + + test('should restore the original contents (untitled large file with metadata)', async () => { + const contents = (new Array(30 * 1024)).join('Large String\n'); + + const meta = { + etag: 'the Etag', + size: 666, + mtime: Date.now(), + orphaned: true + }; + + await testResolveBackup(untitledFile, contents, meta); + }); + + test('should restore the original contents (text file)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'consectetur ', + 'adipiscing ßß elit' + ].join(''); + + await testResolveBackup(fooFile, contents); + }); + + test('should restore the original contents (text file - custom scheme)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'consectetur ', + 'adipiscing ßß elit' + ].join(''); + + await testResolveBackup(customFile, contents); + }); + + test('should restore the original contents (text file with metadata)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(fooFile, contents, meta); + }); + + test('should restore the original contents (empty text file with metadata)', async () => { + const contents = ''; + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(fooFile, contents, meta); + }); + + test('should restore the original contents (large text file with metadata)', async () => { + const contents = (new Array(30 * 1024)).join('Large String\n'); + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(fooFile, contents, meta); + }); + + test('should restore the original contents (text file with metadata changed once)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(fooFile, contents, meta); + + // Change meta and test again + meta.size = 999; + await testResolveBackup(fooFile, contents, meta); + }); + + test('should restore the original contents (text file with metadata and fragment URI)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(customFileWithFragment, contents, meta); + }); + + test('should restore the original contents (text file with space in name with metadata)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(fooBarFile, contents, meta); + }); + + test('should restore the original contents (text file with too large metadata to persist)', async () => { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + + const meta = { + etag: (new Array(100 * 1024)).join('Large String'), + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await testResolveBackup(fooFile, contents, meta, true); + }); + + async function testResolveBackup(resource: URI, contents: string, meta?: IBackupTestMetaData, expectNoMeta?: boolean) { + await doTestResolveBackup(toUntypedWorkingCopyId(resource), contents, meta, expectNoMeta); + await doTestResolveBackup(toTypedWorkingCopyId(resource), contents, meta, expectNoMeta); + } + + async function doTestResolveBackup(identifier: IWorkingCopyIdentifier, contents: string, meta?: IBackupTestMetaData, expectNoMeta?: boolean) { + await service.backup(identifier, bufferToReadable(VSBuffer.fromString(contents)), 1, meta); + + const backup = await service.resolve(identifier); + assert.ok(backup); + assert.strictEqual(contents, (await streamToBuffer(backup.value)).toString()); + + if (expectNoMeta || !meta) { + assert.strictEqual(backup.meta, undefined); + } else { + assert.ok(backup.meta); + assert.strictEqual(backup.meta.etag, meta.etag); + assert.strictEqual(backup.meta.size, meta.size); + assert.strictEqual(backup.meta.mtime, meta.mtime); + assert.strictEqual(backup.meta.orphaned, meta.orphaned); + + assert.strictEqual(Object.keys(meta).length, Object.keys(backup.meta).length); + } + } + + test('should restore the original contents (text file with broken metadata)', async () => { + await testShouldRestoreOriginalContentsWithBrokenBackup(toUntypedWorkingCopyId(fooFile)); + await testShouldRestoreOriginalContentsWithBrokenBackup(toTypedWorkingCopyId(fooFile)); + }); + + async function testShouldRestoreOriginalContentsWithBrokenBackup(identifier: IWorkingCopyIdentifier): Promise { + const contents = [ + 'Lorem ipsum ', + 'dolor öäü sit amet ', + 'adipiscing ßß elit', + 'consectetur ' + ].join(''); + + const meta = { + etag: 'theEtag', + size: 888, + mtime: Date.now(), + orphaned: false + }; + + await service.backup(identifier, bufferToReadable(VSBuffer.fromString(contents)), 1, meta); + + const backupPath = join(workspaceBackupPath, identifier.resource.scheme, hashIdentifier(identifier)); + + const fileContents = readFileSync(backupPath).toString(); + assert.strictEqual(fileContents.indexOf(identifier.resource.toString()), 0); + + const metaIndex = fileContents.indexOf('{'); + const newFileContents = fileContents.substring(0, metaIndex) + '{{' + fileContents.substr(metaIndex); + writeFileSync(backupPath, newFileContents); + + const backup = await service.resolve(identifier); + assert.ok(backup); + assert.strictEqual(contents, (await streamToBuffer(backup.value)).toString()); + assert.strictEqual(backup.meta, undefined); + } + + test('should ignore invalid backups (empty file)', async () => { + const contents = 'test\nand more stuff'; + + await service.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString(contents)), 1); + + let backup = await service.resolve(toUntypedWorkingCopyId(fooFile)); + assert.ok(backup); + + await service.fileService.writeFile(service.toBackupResource(toUntypedWorkingCopyId(fooFile)), VSBuffer.fromString('')); + + backup = await service.resolve(toUntypedWorkingCopyId(fooFile)); + assert.ok(!backup); + }); + + test('should ignore invalid backups (no preamble)', async () => { + const contents = 'testand more stuff'; + + await service.backup(toUntypedWorkingCopyId(fooFile), bufferToReadable(VSBuffer.fromString(contents)), 1); + + let backup = await service.resolve(toUntypedWorkingCopyId(fooFile)); + assert.ok(backup); + + await service.fileService.writeFile(service.toBackupResource(toUntypedWorkingCopyId(fooFile)), VSBuffer.fromString(contents)); + + backup = await service.resolve(toUntypedWorkingCopyId(fooFile)); + assert.ok(!backup); + }); + + test('file with binary data', async () => { + const identifier = toUntypedWorkingCopyId(fooFile); + + const buffer = VSBuffer.alloc(3); + buffer.writeUInt8(10, 0); + buffer.writeUInt8(20, 1); + buffer.writeUInt8(30, 2); + + await service.backup(identifier, bufferToReadable(buffer), undefined, { binaryTest: 'true' }); + + const backup = await service.resolve(toUntypedWorkingCopyId(fooFile)); + assert.ok(backup); + + const backupBuffer = await consumeStream(backup.value, chunks => VSBuffer.concat(chunks)); + assert.strictEqual(backupBuffer.buffer.byteLength, buffer.byteLength); + assert.strictEqual(backupBuffer.readUInt8(0), buffer.readUInt8(0)); + assert.strictEqual(backupBuffer.readUInt8(1), buffer.readUInt8(1)); + assert.strictEqual(backupBuffer.readUInt8(2), buffer.readUInt8(2)); + }); + }); + + suite('WorkingCopyBackupsModel', () => { + + test('simple', async () => { + const model = await WorkingCopyBackupsModel.create(URI.file(workspaceBackupPath), service.fileService); + + const resource1 = URI.file('test.html'); + + assert.strictEqual(model.has(resource1), false); + + model.add(resource1); + + assert.strictEqual(model.has(resource1), true); + assert.strictEqual(model.has(resource1, 0), true); + assert.strictEqual(model.has(resource1, 1), false); + assert.strictEqual(model.has(resource1, 1, { foo: 'bar' }), false); + + model.remove(resource1); + + assert.strictEqual(model.has(resource1), false); + + model.add(resource1); + + assert.strictEqual(model.has(resource1), true); + assert.strictEqual(model.has(resource1, 0), true); + assert.strictEqual(model.has(resource1, 1), false); + + model.clear(); + + assert.strictEqual(model.has(resource1), false); + + model.add(resource1, 1); + + assert.strictEqual(model.has(resource1), true); + assert.strictEqual(model.has(resource1, 0), false); + assert.strictEqual(model.has(resource1, 1), true); + + const resource2 = URI.file('test1.html'); + const resource3 = URI.file('test2.html'); + const resource4 = URI.file('test3.html'); + + model.add(resource2); + model.add(resource3); + model.add(resource4, undefined, { foo: 'bar' }); + + assert.strictEqual(model.has(resource1), true); + assert.strictEqual(model.has(resource2), true); + assert.strictEqual(model.has(resource3), true); + + assert.strictEqual(model.has(resource4), true); + assert.strictEqual(model.has(resource4, undefined, { foo: 'bar' }), true); + assert.strictEqual(model.has(resource4, undefined, { bar: 'foo' }), false); + + const resource5 = URI.file('test4.html'); + model.move(resource4, resource5); + assert.strictEqual(model.has(resource4), false); + assert.strictEqual(model.has(resource5), true); + }); + + test('create', async () => { + const fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashIdentifier(toUntypedWorkingCopyId(fooFile))); + await promises.mkdir(dirname(fooBackupPath), { recursive: true }); + writeFileSync(fooBackupPath, 'foo'); + const model = await WorkingCopyBackupsModel.create(URI.file(workspaceBackupPath), service.fileService); + + assert.strictEqual(model.has(URI.file(fooBackupPath)), true); + }); + + test('get', async () => { + const model = await WorkingCopyBackupsModel.create(URI.file(workspaceBackupPath), service.fileService); + + assert.deepStrictEqual(model.get(), []); + + const file1 = URI.file('/root/file/foo.html'); + const file2 = URI.file('/root/file/bar.html'); + const untitled = URI.file('/root/untitled/bar.html'); + + model.add(file1); + model.add(file2); + model.add(untitled); + + assert.deepStrictEqual(model.get().map(f => f.fsPath), [file1.fsPath, file2.fsPath, untitled.fsPath]); + }); + }); + + suite('Hash migration', () => { + + test('works', async () => { + const fooBackupId = toUntypedWorkingCopyId(fooFile); + const untitledBackupId = toUntypedWorkingCopyId(untitledFile); + const customBackupId = toUntypedWorkingCopyId(customFile); + + const fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashIdentifier(fooBackupId)); + const untitledBackupPath = join(workspaceBackupPath, untitledFile.scheme, hashIdentifier(untitledBackupId)); + const customFileBackupPath = join(workspaceBackupPath, customFile.scheme, hashIdentifier(customBackupId)); + + // Prepare backups of the old MD5 hash format + mkdirSync(join(workspaceBackupPath, fooFile.scheme), { recursive: true }); + mkdirSync(join(workspaceBackupPath, untitledFile.scheme), { recursive: true }); + mkdirSync(join(workspaceBackupPath, customFile.scheme), { recursive: true }); + writeFileSync(join(workspaceBackupPath, fooFile.scheme, '8a8589a2f1c9444b89add38166f50229'), `${fooFile.toString()}\ntest file`); + writeFileSync(join(workspaceBackupPath, untitledFile.scheme, '13264068d108c6901b3592ea654fcd57'), `${untitledFile.toString()}\ntest untitled`); + writeFileSync(join(workspaceBackupPath, customFile.scheme, 'bf018572af7b38746b502893bd0adf6c'), `${customFile.toString()}\ntest custom`); + + service.reinitialize(URI.file(workspaceBackupPath)); + + const backups = await service.getBackups(); + assert.strictEqual(backups.length, 3); + assert.ok(backups.some(backup => isEqual(backup.resource, fooFile))); + assert.ok(backups.some(backup => isEqual(backup.resource, untitledFile))); + assert.ok(backups.some(backup => isEqual(backup.resource, customFile))); + + assert.strictEqual(readdirSync(join(workspaceBackupPath, fooFile.scheme)).length, 1); + assert.strictEqual(existsSync(fooBackupPath), true); + assert.strictEqual(readFileSync(fooBackupPath).toString(), `${fooFile.toString()}\ntest file`); + assert.ok(service.hasBackupSync(fooBackupId)); + + assert.strictEqual(readdirSync(join(workspaceBackupPath, untitledFile.scheme)).length, 1); + assert.strictEqual(existsSync(untitledBackupPath), true); + assert.strictEqual(readFileSync(untitledBackupPath).toString(), `${untitledFile.toString()}\ntest untitled`); + assert.ok(service.hasBackupSync(untitledBackupId)); + + assert.strictEqual(readdirSync(join(workspaceBackupPath, customFile.scheme)).length, 1); + assert.strictEqual(existsSync(customFileBackupPath), true); + assert.strictEqual(readFileSync(customFileBackupPath).toString(), `${customFile.toString()}\ntest custom`); + assert.ok(service.hasBackupSync(customBackupId)); + }); + }); + + suite('typeId migration', () => { + + test('works (when meta is missing)', async () => { + const fooBackupId = toUntypedWorkingCopyId(fooFile); + const untitledBackupId = toUntypedWorkingCopyId(untitledFile); + const customBackupId = toUntypedWorkingCopyId(customFile); + + const fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashIdentifier(fooBackupId)); + const untitledBackupPath = join(workspaceBackupPath, untitledFile.scheme, hashIdentifier(untitledBackupId)); + const customFileBackupPath = join(workspaceBackupPath, customFile.scheme, hashIdentifier(customBackupId)); + + // Prepare backups of the old format without meta + mkdirSync(join(workspaceBackupPath, fooFile.scheme), { recursive: true }); + mkdirSync(join(workspaceBackupPath, untitledFile.scheme), { recursive: true }); + mkdirSync(join(workspaceBackupPath, customFile.scheme), { recursive: true }); + writeFileSync(fooBackupPath, `${fooFile.toString()}\ntest file`); + writeFileSync(untitledBackupPath, `${untitledFile.toString()}\ntest untitled`); + writeFileSync(customFileBackupPath, `${customFile.toString()}\ntest custom`); + + service.reinitialize(URI.file(workspaceBackupPath)); + + const backups = await service.getBackups(); + assert.strictEqual(backups.length, 3); + assert.ok(backups.some(backup => isEqual(backup.resource, fooFile))); + assert.ok(backups.some(backup => isEqual(backup.resource, untitledFile))); + assert.ok(backups.some(backup => isEqual(backup.resource, customFile))); + assert.ok(backups.every(backup => backup.typeId === '')); + }); + + test('works (when typeId in meta is missing)', async () => { + const fooBackupId = toUntypedWorkingCopyId(fooFile); + const untitledBackupId = toUntypedWorkingCopyId(untitledFile); + const customBackupId = toUntypedWorkingCopyId(customFile); + + const fooBackupPath = join(workspaceBackupPath, fooFile.scheme, hashIdentifier(fooBackupId)); + const untitledBackupPath = join(workspaceBackupPath, untitledFile.scheme, hashIdentifier(untitledBackupId)); + const customFileBackupPath = join(workspaceBackupPath, customFile.scheme, hashIdentifier(customBackupId)); + + // Prepare backups of the old format without meta + mkdirSync(join(workspaceBackupPath, fooFile.scheme), { recursive: true }); + mkdirSync(join(workspaceBackupPath, untitledFile.scheme), { recursive: true }); + mkdirSync(join(workspaceBackupPath, customFile.scheme), { recursive: true }); + writeFileSync(fooBackupPath, `${fooFile.toString()} ${JSON.stringify({ foo: 'bar' })}\ntest file`); + writeFileSync(untitledBackupPath, `${untitledFile.toString()} ${JSON.stringify({ foo: 'bar' })}\ntest untitled`); + writeFileSync(customFileBackupPath, `${customFile.toString()} ${JSON.stringify({ foo: 'bar' })}\ntest custom`); + + service.reinitialize(URI.file(workspaceBackupPath)); + + const backups = await service.getBackups(); + assert.strictEqual(backups.length, 3); + assert.ok(backups.some(backup => isEqual(backup.resource, fooFile))); + assert.ok(backups.some(backup => isEqual(backup.resource, untitledFile))); + assert.ok(backups.some(backup => isEqual(backup.resource, customFile))); + assert.ok(backups.every(backup => backup.typeId === '')); + }); + }); +}); diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts similarity index 89% rename from src/vs/workbench/services/backup/test/electron-browser/backupTracker.test.ts rename to src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts index 12467fe9d96..33a453d04f6 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupTracker.test.ts @@ -11,15 +11,15 @@ import { join } from 'vs/base/common/path'; import { rimraf, writeFile } from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { flakySuite, getRandomTestPath } from 'vs/base/test/node/testUtils'; -import { hashPath } from 'vs/workbench/services/backup/common/backupFileService'; -import { NativeBackupTracker } from 'vs/workbench/services/backup/electron-sandbox/backupTracker'; +import { hash } from 'vs/base/common/hash'; +import { NativeWorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupTracker'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { NodeTestWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { toResource } from 'vs/base/test/common/utils'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; @@ -30,7 +30,7 @@ import { ShutdownReason, ILifecycleService } from 'vs/workbench/services/lifecyc import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; -import { BackupTracker } from 'vs/workbench/services/backup/common/backupTracker'; +import { WorkingCopyBackupTracker } from 'vs/workbench/services/workingCopy/common/workingCopyBackupTracker'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -42,12 +42,12 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace'; import { IProgressService } from 'vs/platform/progress/common/progress'; -flakySuite('BackupTracker (native)', function () { +flakySuite('WorkingCopyBackupTracker (native)', function () { - class TestBackupTracker extends NativeBackupTracker { + class TestBackupTracker extends NativeWorkingCopyBackupTracker { constructor( - @IBackupFileService backupFileService: IBackupFileService, + @IWorkingCopyBackupService workingCopyBackupService: IWorkingCopyBackupService, @IFilesConfigurationService filesConfigurationService: IFilesConfigurationService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @ILifecycleService lifecycleService: ILifecycleService, @@ -61,7 +61,7 @@ flakySuite('BackupTracker (native)', function () { @IProgressService progressService: IProgressService, @IEditorGroupsService editorGroupService: IEditorGroupsService ) { - super(backupFileService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService, progressService, editorGroupService); + super(workingCopyBackupService, filesConfigurationService, workingCopyService, lifecycleService, fileDialogService, dialogService, contextService, nativeHostService, logService, editorService, environmentService, progressService, editorGroupService); } protected override getBackupScheduleDelay(): number { @@ -90,7 +90,7 @@ flakySuite('BackupTracker (native)', function () { const workspacesJsonPath = join(backupHome, 'workspaces.json'); const workspaceResource = URI.file(isWindows ? 'c:\\workspace' : '/workspace'); - workspaceBackupPath = join(backupHome, hashPath(workspaceResource)); + workspaceBackupPath = join(backupHome, hash(workspaceResource.fsPath).toString(16)); const instantiationService = workbenchInstantiationService(); accessor = instantiationService.createInstance(TestServiceAccessor); @@ -110,10 +110,10 @@ flakySuite('BackupTracker (native)', function () { return rimraf(testDir); }); - async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: BackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { - const backupFileService = new NodeTestBackupFileService(testDir, workspaceBackupPath); + async function createTracker(autoSaveEnabled = false): Promise<{ accessor: TestServiceAccessor, part: EditorPart, tracker: WorkingCopyBackupTracker, instantiationService: IInstantiationService, cleanup: () => Promise }> { + const workingCopyBackupService = new NodeTestWorkingCopyBackupService(testDir, workspaceBackupPath); const instantiationService = workbenchInstantiationService(); - instantiationService.stub(IBackupFileService, backupFileService); + instantiationService.stub(IWorkingCopyBackupService, workingCopyBackupService); const configurationService = new TestConfigurationService(); if (autoSaveEnabled) { @@ -139,7 +139,7 @@ flakySuite('BackupTracker (native)', function () { const cleanup = async () => { // File changes could also schedule some backup operations so we need to wait for them before finishing the test - await accessor.backupFileService.waitForAllBackups(); + await accessor.workingCopyBackupService.waitForAllBackups(); part.dispose(); tracker.dispose(); @@ -162,17 +162,18 @@ flakySuite('BackupTracker (native)', function () { await accessor.editorService.openEditor({ resource, options: { pinned: true } }); const fileModel = accessor.textFileService.files.get(resource); - fileModel?.textEditorModel?.setValue('Super Good'); + assert.ok(fileModel); + fileModel.textEditorModel?.setValue('Super Good'); - await accessor.backupFileService.joinBackupResource(); + await accessor.workingCopyBackupService.joinBackupResource(); - assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), true); + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(fileModel), true); - fileModel?.dispose(); + fileModel.dispose(); - await accessor.backupFileService.joinDiscardBackup(); + await accessor.workingCopyBackupService.joinDiscardBackup(); - assert.strictEqual(accessor.backupFileService.hasBackupSync(resource), false); + assert.strictEqual(accessor.workingCopyBackupService.hasBackupSync(fileModel), false); await cleanup(); } @@ -258,7 +259,7 @@ flakySuite('BackupTracker (native)', function () { const veto = await event.value; assert.ok(!veto); - assert.ok(accessor.backupFileService.discardedBackups.length > 0); + assert.ok(accessor.workingCopyBackupService.discardedBackups.length > 0); await cleanup(); }); @@ -423,7 +424,7 @@ flakySuite('BackupTracker (native)', function () { accessor.lifecycleService.fireBeforeShutdown(event); const veto = await event.value; - assert.strictEqual(accessor.backupFileService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel + assert.strictEqual(accessor.workingCopyBackupService.discardedBackups.length, 0); // When hot exit is set, backups should never be cleaned since the confirm result is cancel assert.strictEqual(veto, shouldVeto); await cleanup(); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index a286f60b5d5..09240650498 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -5,13 +5,16 @@ import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustStateInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService, IWorkspaceTrustStorageService as IWorkspaceTrustStorageService } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustInfo, IWorkspaceTrustUriInfo, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export const WORKSPACE_TRUST_ENABLED = 'security.workspace.trust.enabled'; @@ -27,34 +30,57 @@ export function isWorkspaceTrustEnabled(configurationService: IConfigurationServ return configurationService.inspect(WORKSPACE_TRUST_ENABLED).userValue ?? false; } -export class WorkspaceTrustStorageService extends Disposable implements IWorkspaceTrustStorageService { +export class WorkspaceTrustManagementService extends Disposable implements IWorkspaceTrustManagementService { + _serviceBrand: undefined; private readonly storageKey = WORKSPACE_TRUST_STORAGE_KEY; - private trustStateInfo: IWorkspaceTrustStateInfo; - private readonly _onDidStorageChange = this._register(new Emitter()); - readonly onDidStorageChange = this._onDidStorageChange.event; + private readonly _onDidChangeTrust = this._register(new Emitter()); + readonly onDidChangeTrust = this._onDidChangeTrust.event; + + private readonly _onDidChangeTrustedFolders = this._register(new Emitter()); + readonly onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; + + private _isWorkspaceTrusted: boolean = false; + private _trustStateInfo: IWorkspaceTrustInfo; constructor( + @IConfigurationService readonly configurationService: IConfigurationService, @IStorageService private readonly storageService: IStorageService, - @IUriIdentityService private readonly uriIdentityService: IUriIdentityService + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, + @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, + @IEnvironmentService private readonly envService: IEnvironmentService, ) { super(); - this.trustStateInfo = this.loadTrustInfo(); + this._trustStateInfo = this.loadTrustInfo(); + this._isWorkspaceTrusted = this.calculateWorkspaceTrust(); + + this.registerListeners(); + } + + private set currentTrustState(trusted: boolean) { + if (this._isWorkspaceTrusted === trusted) { return; } + this._isWorkspaceTrusted = trusted; + + this._onDidChangeTrust.fire(trusted); + } + + private registerListeners(): void { + this._register(this.workspaceService.onDidChangeWorkspaceFolders(() => this.currentTrustState = this.calculateWorkspaceTrust())); this._register(this.storageService.onDidChangeValue(changeEvent => { if (changeEvent.key === this.storageKey) { - this.trustStateInfo = this.loadTrustInfo(); - this._onDidStorageChange.fire(); + this._trustStateInfo = this.loadTrustInfo(); + this.currentTrustState = this.calculateWorkspaceTrust(); } })); } - private loadTrustInfo(): IWorkspaceTrustStateInfo { + private loadTrustInfo(): IWorkspaceTrustInfo { const infoAsString = this.storageService.get(this.storageKey, StorageScope.GLOBAL); - let result: IWorkspaceTrustStateInfo | undefined; + let result: IWorkspaceTrustInfo | undefined; try { if (infoAsString) { result = JSON.parse(infoAsString); @@ -78,16 +104,50 @@ export class WorkspaceTrustStorageService extends Disposable implements IWorkspa } private saveTrustInfo(): void { - this.storageService.store(this.storageKey, JSON.stringify(this.trustStateInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + this.storageService.store(this.storageKey, JSON.stringify(this._trustStateInfo), StorageScope.GLOBAL, StorageTarget.MACHINE); + this._onDidChangeTrustedFolders.fire(); } - getFolderTrustStateInfo(folder: URI): IWorkspaceTrustUriInfo { + private calculateWorkspaceTrust(): boolean { + if (!isWorkspaceTrustEnabled(this.configurationService)) { + return true; + } + + if (this.envService.extensionTestsLocationURI) { + return true; // trust running tests with vscode-test + } + + if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { + return true; + } + + const folderURIs = this.workspaceService.getWorkspace().folders.map(f => f.uri); + const trusted = this.getFoldersTrust(folderURIs); + + return trusted; + } + + private getFoldersTrust(folders: URI[]): boolean { + let state = true; + for (const folder of folders) { + const { trusted } = this.getFolderTrustInfo(folder); + + if (!trusted) { + state = trusted; + return state; + } + } + + return state; + } + + public getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo { let resultState = false; let maxLength = -1; let resultUri = folder; - for (const trustInfo of this.trustStateInfo.uriTrustInfo) { + for (const trustInfo of this._trustStateInfo.uriTrustInfo) { if (this.uriIdentityService.extUri.isEqualOrParent(folder, trustInfo.uri)) { const fsPath = trustInfo.uri.fsPath; if (fsPath.length > maxLength) { @@ -101,26 +161,23 @@ export class WorkspaceTrustStorageService extends Disposable implements IWorkspa return { trusted: resultState, uri: resultUri }; } - private setFolderTrustState(folder: URI, trusted: boolean): boolean { - if (trusted) { - const foundItem = this.trustStateInfo.uriTrustInfo.find(trustInfo => this.uriIdentityService.extUri.isEqual(trustInfo.uri, folder)); - if (!foundItem) { - this.trustStateInfo.uriTrustInfo.push({ uri: folder, trusted: true }); - return true; - } - } else { - const previousLength = this.trustStateInfo.uriTrustInfo.length; - this.trustStateInfo.uriTrustInfo = this.trustStateInfo.uriTrustInfo.filter(trustInfo => !this.uriIdentityService.extUri.isEqual(trustInfo.uri, folder)); - return previousLength !== this.trustStateInfo.uriTrustInfo.length; - } - - return false; - } - setFoldersTrust(folders: URI[], trusted: boolean): void { let changed = false; + for (const folder of folders) { - changed = this.setFolderTrustState(folder, trusted) || changed; + if (trusted) { + const foundItem = this._trustStateInfo.uriTrustInfo.find(trustInfo => this.uriIdentityService.extUri.isEqual(trustInfo.uri, folder)); + if (!foundItem) { + this._trustStateInfo.uriTrustInfo.push({ uri: folder, trusted: true }); + changed = true; + } + } else { + const previousLength = this._trustStateInfo.uriTrustInfo.length; + this._trustStateInfo.uriTrustInfo = this._trustStateInfo.uriTrustInfo.filter(trustInfo => !this.uriIdentityService.extUri.isEqual(trustInfo.uri, folder)); + if (previousLength !== this._trustStateInfo.uriTrustInfo.length) { + changed = true; + } + } } if (changed) { @@ -128,88 +185,43 @@ export class WorkspaceTrustStorageService extends Disposable implements IWorkspa } } - getFoldersTrust(folders: URI[]): boolean { - let state = true; - for (const folder of folders) { - const { trusted } = this.getFolderTrustStateInfo(folder); - - if (!trusted) { - state = trusted; - return state; - } - } - - return state; + canSetWorkspaceTrust(): boolean { + return this.workspaceService.getWorkspace().folders.length > 0; } - setTrustedFolders(folders: URI[]): void { - this.trustStateInfo.uriTrustInfo = []; - for (const folder of folders) { - this.trustStateInfo.uriTrustInfo.push({ - trusted: true, - uri: folder - }); - } - - this.saveTrustInfo(); - } - - getTrustStateInfo(): IWorkspaceTrustStateInfo { - return this.trustStateInfo; - } -} - -export class WorkspaceTrustManagementService extends Disposable implements IWorkspaceTrustManagementService { - - _serviceBrand: undefined; - - private readonly _onDidChangeTrust = this._register(new Emitter()); - readonly onDidChangeTrust = this._onDidChangeTrust.event; - - private _isWorkspaceTrusted: boolean = false; - - constructor( - @IConfigurationService readonly configurationService: IConfigurationService, - @IWorkspaceContextService private readonly workspaceService: IWorkspaceContextService, - @IWorkspaceTrustStorageService private readonly workspaceTrustStorageService: IWorkspaceTrustStorageService - ) { - super(); - - this._isWorkspaceTrusted = this.calculateWorkspaceTrust(); - - this._register(this.workspaceService.onDidChangeWorkspaceFolders(() => this.currentTrustState = this.calculateWorkspaceTrust())); - this._register(this.workspaceTrustStorageService.onDidStorageChange(() => this.currentTrustState = this.calculateWorkspaceTrust())); - } - - private set currentTrustState(trusted: boolean) { - if (this._isWorkspaceTrusted === trusted) { return; } - this._isWorkspaceTrusted = trusted; - - this._onDidChangeTrust.fire(trusted); - } - - private calculateWorkspaceTrust(): boolean { - if (!isWorkspaceTrustEnabled(this.configurationService)) { - return true; - } - - if (this.workspaceService.getWorkbenchState() === WorkbenchState.EMPTY) { - return true; - } - - const folderURIs = this.workspaceService.getWorkspace().folders.map(f => f.uri); - const trusted = this.workspaceTrustStorageService.getFoldersTrust(folderURIs); - - return trusted; + canSetParentFolderTrust(): boolean { + const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); + return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file; } isWorkpaceTrusted(): boolean { return this._isWorkspaceTrusted; } + setParentFolderTrust(trusted: boolean): void { + } + setWorkspaceTrust(trusted: boolean): void { + // TODO: workspace file for multi-root workspaces const folderURIs = this.workspaceService.getWorkspace().folders.map(f => f.uri); - this.workspaceTrustStorageService.setFoldersTrust(folderURIs, trusted); + + this.setFoldersTrust(folderURIs, trusted); + } + + getTrustedFolders(): URI[] { + return this._trustStateInfo.uriTrustInfo.map(info => info.uri); + } + + setTrustedFolders(folders: URI[]): void { + this._trustStateInfo.uriTrustInfo = []; + for (const folder of folders) { + this._trustStateInfo.uriTrustInfo.push({ + trusted: true, + uri: folder + }); + } + + this.saveTrustInfo(); } } diff --git a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts index c0de8e7ac93..a0ed9f2a3d7 100644 --- a/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspaces/electron-sandbox/workspaceEditingService.ts @@ -12,7 +12,7 @@ import { IWorkspacesService, isUntitledWorkspace, IWorkspaceIdentifier, hasWorks import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { basename } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -29,7 +29,7 @@ import { AbstractWorkspaceEditingService } from 'vs/workbench/services/workspace import { INativeHostService } from 'vs/platform/native/electron-sandbox/native'; import { isMacintosh } from 'vs/base/common/platform'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; -import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; +import { WorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingService { @@ -41,7 +41,7 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi @IConfigurationService configurationService: IConfigurationService, @IStorageService private storageService: IStorageService, @IExtensionService private extensionService: IExtensionService, - @IBackupFileService private backupFileService: IBackupFileService, + @IWorkingCopyBackupService private workingCopyBackupService: IWorkingCopyBackupService, @INotificationService notificationService: INotificationService, @ICommandService commandService: ICommandService, @IFileService fileService: IFileService, @@ -165,9 +165,9 @@ export class NativeWorkspaceEditingService extends AbstractWorkspaceEditingServi await this.migrateStorage(result.workspace); // Reinitialize backup service - if (this.backupFileService instanceof BackupFileService) { + if (this.workingCopyBackupService instanceof WorkingCopyBackupService) { const newBackupWorkspaceHome = result.backupPath ? URI.file(result.backupPath).with({ scheme: this.environmentService.userRoamingDataHome.scheme }) : undefined; - this.backupFileService.reinitialize(newBackupWorkspaceHome); + this.workingCopyBackupService.reinitialize(newBackupWorkspaceHome); } } diff --git a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts index fc1269cde8f..c16af475c1c 100644 --- a/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts +++ b/src/vs/workbench/services/workspaces/test/common/testWorkspaceTrustService.ts @@ -5,37 +5,8 @@ import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustStorageService, IWorkspaceTrustStateInfo, IWorkspaceTrustUriInfo } from 'vs/platform/workspace/common/workspaceTrust'; +import { WorkspaceTrustRequestOptions, IWorkspaceTrustManagementService, IWorkspaceTrustRequestService, IWorkspaceTrustUriInfo } from 'vs/platform/workspace/common/workspaceTrust'; -export class TestWorkspaceTrustStorageService implements IWorkspaceTrustStorageService { - _serviceBrand: undefined; - - onDidStorageChange: Event = Event.None; - - setFoldersTrust(folder: URI[], trusted: boolean): void { - throw new Error('Method not implemented.'); - } - - getFoldersTrust(folder: URI[]): boolean { - throw new Error('Method not implemented.'); - } - - setTrustedFolders(folders: URI[]): void { - throw new Error('Method not implemented.'); - } - - setUntrustedFolders(folders: URI[]): void { - throw new Error('Method not implemented.'); - } - - getFolderTrustStateInfo(folder: URI): IWorkspaceTrustUriInfo { - throw new Error('Method not implemented.'); - } - - getTrustStateInfo(): IWorkspaceTrustStateInfo { - throw new Error('Method not implemented.'); - } -} export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManagementService { _serviceBrand: undefined; @@ -43,12 +14,43 @@ export class TestWorkspaceTrustManagementService implements IWorkspaceTrustManag private _onDidChangeTrust = new Emitter(); onDidChangeTrust = this._onDidChangeTrust.event; + private _onDidChangeTrustedFolders = new Emitter(); + onDidChangeTrustedFolders = this._onDidChangeTrustedFolders.event; + private trusted: boolean; constructor(trusted: boolean = true) { this.trusted = trusted; } + getTrustedFolders(): URI[] { + throw new Error('Method not implemented.'); + } + + setParentFolderTrust(trusted: boolean): void { + throw new Error('Method not implemented.'); + } + + getFolderTrustInfo(folder: URI): IWorkspaceTrustUriInfo { + throw new Error('Method not implemented.'); + } + + setTrustedFolders(folders: URI[]): void { + throw new Error('Method not implemented.'); + } + + setFoldersTrust(folders: URI[], trusted: boolean): void { + throw new Error('Method not implemented.'); + } + + canSetParentFolderTrust(): boolean { + throw new Error('Method not implemented.'); + } + + canSetWorkspaceTrust(): boolean { + throw new Error('Method not implemented.'); + } + isWorkpaceTrusted(): boolean { return this.trusted; } diff --git a/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts index 02b4cce8d00..3710f4b9e8a 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookKernel2.test.ts @@ -29,7 +29,7 @@ suite('NotebookKernel', function () { override $registerCommand() { } }); rpcProtocol.set(MainContext.MainThreadNotebookKernels, new class extends mock() { - override $addKernel(handle: number, data: INotebookKernelDto2): void { + async override $addKernel(handle: number, data: INotebookKernelDto2): Promise { kernelData.set(handle, data); } override $removeKernel(handle: number) { @@ -50,7 +50,7 @@ suite('NotebookKernel', function () { test('create/dispose kernel', async function () { - const kernel = extHostNotebookKernels.createKernel(nullExtensionDescription, { id: 'foo', label: 'Foo', selector: '*', executeHandler: () => { }, supportedLanguages: ['plaintext'] }); + const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); assert.ok(kernel); assert.strictEqual(kernel.id, 'foo'); @@ -73,7 +73,7 @@ suite('NotebookKernel', function () { test('update kernel', async function () { - const kernel = extHostNotebookKernels.createKernel(nullExtensionDescription, { id: 'foo', label: 'Foo', selector: '*', executeHandler: () => { }, supportedLanguages: ['plaintext'] }); + const kernel = extHostNotebookKernels.createNotebookController(nullExtensionDescription, 'foo', '*', 'Foo'); await rpcProtocol.sync(); assert.ok(kernel); diff --git a/src/vs/workbench/test/browser/api/extHostTesting.test.ts b/src/vs/workbench/test/browser/api/extHostTesting.test.ts index cb245d0f3b1..1236700012e 100644 --- a/src/vs/workbench/test/browser/api/extHostTesting.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTesting.test.ts @@ -10,7 +10,7 @@ import { stubTest, testStubs } from 'vs/workbench/contrib/testing/common/testStu import { TestOwnedTestCollection, TestSingleUseCollection } from 'vs/workbench/contrib/testing/test/common/ownedTestCollection'; import { TestItem } from 'vscode'; -const simplify = (item: TestItem) => ({ +const simplify = (item: TestItem) => ({ id: item.id, label: item.label, uri: item.uri, @@ -19,13 +19,21 @@ const simplify = (item: TestItem) => ({ debuggable: item.debuggable, }); -const assertTreesEqual = (a: TestItem, b: TestItem) => { +const assertTreesEqual = (a: TestItem | undefined, b: TestItem | undefined) => { + if (!a) { + throw new assert.AssertionError({ message: 'Expected a to be defined', actual: a }); + } + + if (!b) { + throw new assert.AssertionError({ message: 'Expected b to be defined', actual: b }); + } + assert.deepStrictEqual(simplify(a), simplify(b)); - const aChildren = [...a.children].slice().sort(); - const bChildren = [...b.children].slice().sort(); + const aChildren = [...a.children.keys()].slice().sort(); + const bChildren = [...b.children.keys()].slice().sort(); assert.strictEqual(aChildren.length, bChildren.length, `expected ${a.label}.children.length == ${b.label}.children.length`); - aChildren.forEach((_, i) => assertTreesEqual(aChildren[i], bChildren[i])); + aChildren.forEach(key => assertTreesEqual(a.children.get(key), b.children.get(key))); }; // const assertTreeListEqual = (a: ReadonlyArray, b: ReadonlyArray) => { @@ -67,36 +75,32 @@ suite('ExtHost Testing', () => { assert.deepStrictEqual(single.collectDiff(), [ [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')), expandable: true } } + { src: { tree: 0, controller: 'pid' }, parent: null, expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('root')) } } ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-root', expand: TestItemExpandState.Expandable, item: { ...convert.TestItem.from(stubTest('a')), expandable: true } } + { src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.BusyExpanding, item: { ...convert.TestItem.from(stubTest('a')) } } ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) } - ], - [ - TestDiffOpType.Update, - { extId: 'id-root', expand: TestItemExpandState.Expanded } - ], - [ - TestDiffOpType.Update, - { extId: 'id-a', expand: TestItemExpandState.BusyExpanding } + { src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) } ], [ TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('aa')) } - ], - [ - TestDiffOpType.Add, - { src: { tree: 0, provider: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) } + { src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('ab')) } ], [ TestDiffOpType.Update, { extId: 'id-a', expand: TestItemExpandState.Expanded } ], + [ + TestDiffOpType.Add, + { src: { tree: 0, controller: 'pid' }, parent: 'id-root', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(stubTest('b')) } + ], + [ + TestDiffOpType.Update, + { extId: 'id-root', expand: TestItemExpandState.Expanded } + ], ]); }); @@ -126,7 +130,7 @@ suite('ExtHost Testing', () => { single.addRoot(tests, 'pid'); single.expand('id-root', Infinity); single.collectDiff(); - tests.children.delete('id-a'); + tests.children.get('id-a')!.dispose(); assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Remove, 'id-a'], @@ -141,11 +145,11 @@ suite('ExtHost Testing', () => { single.expand('id-root', Infinity); single.collectDiff(); const child = stubTest('ac'); - tests.children.get('id-a')!.children!.add(child); + tests.children.get('id-a')!.addChild(child); assert.deepStrictEqual(single.collectDiff(), [ [TestDiffOpType.Add, { - src: { tree: 0, provider: 'pid' }, + src: { tree: 0, controller: 'pid' }, parent: 'id-a', expand: TestItemExpandState.NotExpandable, item: convert.TestItem.from(child), diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index 4dde707b992..95676be5884 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -51,7 +51,6 @@ import { TestTextResourcePropertiesService, TestContextService } from 'vs/workbe import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { extUri } from 'vs/base/common/resources'; import { ITextSnapshot } from 'vs/editor/common/model'; -import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -112,7 +111,7 @@ suite('MainThreadEditors', () => { } return Promise.resolve(Object.create(null)); } - async override getEncodedReadable(resource: URI, value?: string | ITextSnapshot): Promise { + async override getEncodedReadable(resource: URI, value?: string | ITextSnapshot): Promise { return undefined; } }); diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index e8608159c9f..09baace30db 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -46,9 +46,9 @@ suite('Resource text editors', () => { const model = await input.resolve(); assert.ok(model); - assert.strictEqual(model.textEditorModel.getModeId(), 'resource-input-test'); + assert.strictEqual(model.textEditorModel?.getModeId(), 'resource-input-test'); input.setMode('text'); - assert.strictEqual(model.textEditorModel.getModeId(), PLAINTEXT_MODE_ID); + assert.strictEqual(model.textEditorModel?.getModeId(), PLAINTEXT_MODE_ID); }); }); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index d3f3819285d..24324d1ace3 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -12,7 +12,7 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputSerializer, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext, SideBySideEditorInput, IEditorMoveEvent } from 'vs/workbench/common/editor'; import { EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor, IEditorGroupTitleHeight } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position as PartPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; @@ -62,18 +62,19 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ViewletDescriptor, Viewlet } from 'vs/workbench/browser/viewlet'; import { IViewlet } from 'vs/workbench/common/viewlet'; import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; -import { IProcessEnvironment, isLinux, isWindows } from 'vs/base/common/platform'; +import { IProcessEnvironment, isLinux, isWindows, OperatingSystem } from 'vs/base/common/platform'; import { LabelService } from 'vs/workbench/services/label/common/labelService'; import { Part } from 'vs/workbench/browser/part'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPanel } from 'vs/workbench/common/panel'; import { IBadge } from 'vs/workbench/services/activity/common/activity'; -import { bufferToStream, VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; +import { bufferToStream, VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; import product from 'vs/platform/product/common/product'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyIdentifier } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; @@ -112,9 +113,8 @@ import { EncodingOracle, IEncodingOverride } from 'vs/workbench/services/textfil import { UTF16le, UTF16be, UTF8_with_bom } from 'vs/workbench/services/textfile/common/encoding'; import { ColorScheme } from 'vs/platform/theme/common/theme'; import { Iterable } from 'vs/base/common/iterator'; -import { InMemoryBackupFileService } from 'vs/workbench/services/backup/common/backupFileService'; -import { hash } from 'vs/base/common/hash'; -import { BrowserBackupFileService } from 'vs/workbench/services/backup/browser/backupFileService'; +import { InMemoryWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackupService'; +import { BrowserWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/browser/workingCopyBackupService'; import { FileService } from 'vs/platform/files/common/fileService'; import { TextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; @@ -129,6 +129,7 @@ import { ILocalTerminalService, IShellLaunchConfig, ITerminalChildProcess, ITerm import { IProcessDetails, ISetTerminalLayoutInfoArgs } from 'vs/platform/terminal/common/terminalProcess'; import { ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal'; import { isArray } from 'vs/base/common/types'; +import { IShellLaunchConfigResolveOptions, ITerminalProfile, ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; export function createFileEditorInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined, undefined, undefined, undefined); @@ -205,7 +206,7 @@ export function workbenchInstantiationService( const fileService = new TestFileService(); instantiationService.stub(IFileService, fileService); instantiationService.stub(IUriIdentityService, new UriIdentityService(fileService)); - instantiationService.stub(IBackupFileService, new TestBackupFileService()); + instantiationService.stub(IWorkingCopyBackupService, new TestWorkingCopyBackupService()); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(INotificationService, new TestNotificationService()); instantiationService.stub(IUntitledTextEditorService, disposables.add(instantiationService.createInstance(UntitledTextEditorService))); @@ -253,12 +254,13 @@ export class TestServiceAccessor { @ITextModelService public textModelResolverService: ITextModelService, @IUntitledTextEditorService public untitledTextEditorService: UntitledTextEditorService, @IConfigurationService public testConfigurationService: TestConfigurationService, - @IBackupFileService public backupFileService: TestBackupFileService, + @IWorkingCopyBackupService public workingCopyBackupService: TestWorkingCopyBackupService, @IHostService public hostService: TestHostService, @IQuickInputService public quickInputService: IQuickInputService, @ILabelService public labelService: ILabelService, @ILogService public logService: ILogService, - @IUriIdentityService public uriIdentityService: IUriIdentityService + @IUriIdentityService public uriIdentityService: IUriIdentityService, + @IInstantiationService public instantitionService: IInstantiationService ) { } } @@ -796,7 +798,6 @@ export class TestEditorService implements EditorServiceImpl { saveAll(options?: ISaveEditorsOptions): Promise { throw new Error('Method not implemented.'); } revert(editors: IEditorIdentifier[], options?: IRevertOptions): Promise { throw new Error('Method not implemented.'); } revertAll(options?: IRevertAllEditorsOptions): Promise { throw new Error('Method not implemented.'); } - whenClosed(editors: IResourceEditorInput[], options?: { waitForSaved: boolean; }): Promise { throw new Error('Method not implemented.'); } } export class TestFileService implements IFileService { @@ -923,6 +924,10 @@ export class TestFileService implements IFileService { return toDisposable(() => this.providers.delete(scheme)); } + getProvider(scheme: string) { + return this.providers.get(scheme); + } + activateProvider(_scheme: string): Promise { throw new Error('not implemented'); } canHandleResource(resource: URI): boolean { return resource.scheme === Schemas.file || this.providers.has(resource.scheme); } listCapabilities() { @@ -957,28 +962,37 @@ export class TestFileService implements IFileService { async canDelete(resource: URI, options?: { useTrash?: boolean | undefined; recursive?: boolean | undefined; } | undefined): Promise { return true; } } -export class TestBackupFileService extends InMemoryBackupFileService { +export class TestWorkingCopyBackupService extends InMemoryWorkingCopyBackupService { constructor() { - super(resource => String(hash(resource.path))); + super(); } parseBackupContent(textBufferFactory: ITextBufferFactory): string { const textBuffer = textBufferFactory.create(DefaultEndOfLine.LF).textBuffer; const lineCount = textBuffer.getLineCount(); const range = new Range(1, 1, lineCount, textBuffer.getLineLength(lineCount) + 1); + return textBuffer.getValueInRange(range, EndOfLinePreference.TextDefined); } } -export class InMemoryTestBackupFileService extends BrowserBackupFileService { +export function toUntypedWorkingCopyId(resource: URI): IWorkingCopyIdentifier { + return toTypedWorkingCopyId(resource, ''); +} + +export function toTypedWorkingCopyId(resource: URI, typeId = 'testBackupTypeId'): IWorkingCopyIdentifier { + return { typeId, resource }; +} + +export class InMemoryTestWorkingCopyBackupService extends BrowserWorkingCopyBackupService { override readonly fileService: IFileService; private backupResourceJoiners: Function[]; private discardBackupJoiners: Function[]; - discardedBackups: URI[]; + discardedBackups: IWorkingCopyIdentifier[]; constructor() { const environmentService = TestEnvironmentService; @@ -1003,25 +1017,25 @@ export class InMemoryTestBackupFileService extends BrowserBackupFileService { return new Promise(resolve => this.discardBackupJoiners.push(resolve)); } - async override backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: any, token?: CancellationToken): Promise { - await super.backup(resource, content, versionId, meta, token); + async override backup(identifier: IWorkingCopyIdentifier, content?: VSBufferReadableStream | VSBufferReadable, versionId?: number, meta?: any, token?: CancellationToken): Promise { + await super.backup(identifier, content, versionId, meta, token); while (this.backupResourceJoiners.length) { this.backupResourceJoiners.pop()!(); } } - async override discardBackup(resource: URI): Promise { - await super.discardBackup(resource); - this.discardedBackups.push(resource); + async override discardBackup(identifier: IWorkingCopyIdentifier): Promise { + await super.discardBackup(identifier); + this.discardedBackups.push(identifier); while (this.discardBackupJoiners.length) { this.discardBackupJoiners.pop()!(); } } - async getBackupContents(resource: URI): Promise { - const backupResource = this.toBackupResource(resource); + async getBackupContents(identifier: IWorkingCopyIdentifier): Promise { + const backupResource = this.toBackupResource(identifier); const fileContents = await this.fileService.readFile(backupResource); @@ -1531,8 +1545,17 @@ export class TestTerminalInstanceService implements ITerminalInstanceService { createWindowsShellHelper(shellProcessId: number, xterm: any): any { throw new Error('Method not implemented.'); } } -export class TestLocalTerminalService implements ILocalTerminalService { +export class TestTerminalProfileResolverService implements ITerminalProfileResolverService { + _serviceBrand: undefined; + resolveIcon(shellLaunchConfig: IShellLaunchConfig): void { } + async resolveShellLaunchConfig(shellLaunchConfig: IShellLaunchConfig, options: IShellLaunchConfigResolveOptions): Promise { } + async getDefaultProfile(options: IShellLaunchConfigResolveOptions): Promise { return { path: '/default', profileName: 'Default' }; } + async getDefaultShell(options: IShellLaunchConfigResolveOptions): Promise { return '/default'; } + async getDefaultShellArgs(options: IShellLaunchConfigResolveOptions): Promise { return []; } + async getShellEnvironment(): Promise { return process.env; } +} +export class TestLocalTerminalService implements ILocalTerminalService { declare readonly _serviceBrand: undefined; onPtyHostExit = Event.None; @@ -1545,6 +1568,8 @@ export class TestLocalTerminalService implements ILocalTerminalService { } async attachToProcess(id: number): Promise { throw new Error('Method not implemented.'); } async listProcesses(): Promise { throw new Error('Method not implemented.'); } + getDefaultSystemShell(osOverride?: OperatingSystem): Promise { throw new Error('Method not implemented.'); } + getShellEnvironment(): Promise { throw new Error('Method not implemented.'); } async setTerminalLayoutInfo(argsOrLayout?: ISetTerminalLayoutInfoArgs | ITerminalsLayoutInfoById) { throw new Error('Method not implemented.'); } async getTerminalLayoutInfo(): Promise { throw new Error('Method not implemented.'); } async reduceConnectionGraceTime(): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/test/common/notifications.test.ts b/src/vs/workbench/test/common/notifications.test.ts index 742e4073021..600c0e81579 100644 --- a/src/vs/workbench/test/common/notifications.test.ts +++ b/src/vs/workbench/test/common/notifications.test.ts @@ -38,6 +38,12 @@ suite('Notifications', () => { assert.strictEqual(item1.equals(item4), false); assert.strictEqual(item1.equals(item5), false); + let itemId1 = NotificationViewItem.create({ id: 'same', message: 'Info Message', severity: Severity.Info })!; + let itemId2 = NotificationViewItem.create({ id: 'same', message: 'Error Message', severity: Severity.Error })!; + + assert.strictEqual(itemId1.equals(itemId2), true); + assert.strictEqual(itemId1.equals(item3), false); + // Progress assert.strictEqual(item1.hasProgress, false); assert.strictEqual(item6.hasProgress, true); diff --git a/src/vs/workbench/test/common/workbenchTestServices.ts b/src/vs/workbench/test/common/workbenchTestServices.ts index 258788de2c3..749702baf31 100644 --- a/src/vs/workbench/test/common/workbenchTestServices.ts +++ b/src/vs/workbench/test/common/workbenchTestServices.ts @@ -14,13 +14,15 @@ import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platf import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { isLinux, isMacintosh } from 'vs/base/common/platform'; import { InMemoryStorageService, WillSaveStateReason } from 'vs/platform/storage/common/storage'; -import { WorkingCopyService, IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { WorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; import { NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkingCopyFileService, IWorkingCopyFileOperationParticipant, WorkingCopyFileEvent, IDeleteOperation, ICopyOperation, IMoveOperation, IFileOperationUndoRedoInfo, ICreateFileOperation, ICreateOperation } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { IFileStatWithMetadata } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; +import product from 'vs/platform/product/common/product'; export class TestTextResourcePropertiesService implements ITextResourcePropertiesService { @@ -146,7 +148,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private dirty = false; - constructor(public readonly resource: URI, isDirty = false) { + constructor(public readonly resource: URI, isDirty = false, public readonly typeId = 'testWorkingCopyType') { super(); this.dirty = isDirty; @@ -213,3 +215,5 @@ export interface Ctor { } export class TestExtensionService extends NullExtensionService { } + +export const TestProductService = { _serviceBrand: undefined, ...product }; diff --git a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts index 5db6bc7035a..ed480eec685 100644 --- a/src/vs/workbench/test/electron-browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/electron-browser/workbenchTestServices.ts @@ -30,8 +30,8 @@ import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; -import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; +import { IWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; +import { NodeTestWorkingCopyBackupService } from 'vs/workbench/services/workingCopy/test/electron-browser/workingCopyBackupService.test'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { TestContextService } from 'vs/workbench/test/common/workbenchTestServices'; @@ -39,7 +39,7 @@ import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/ur import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IOSProperties, IOSStatistics } from 'vs/platform/native/common/native'; -import { homedir, release, tmpdir } from 'os'; +import { homedir, release, tmpdir, hostname } from 'os'; import { IEnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { getUserDataPath } from 'vs/platform/environment/node/userDataPath'; @@ -50,7 +50,6 @@ const args = parseArgs(process.argv, OPTIONS); export const TestWorkbenchConfiguration: INativeWorkbenchConfiguration = { windowId: 0, machineId: 'testMachineId', - sessionId: 'testSessionId', logLevel: LogLevel.Error, mainPid: 0, partsSplashPath: '', @@ -59,7 +58,7 @@ export const TestWorkbenchConfiguration: INativeWorkbenchConfiguration = { execPath: process.execPath, perfMarks: [], colorScheme: { dark: true, highContrast: false }, - os: { release: release() }, + os: { release: release(), hostname: hostname() }, product, homeDir: homedir(), tmpDir: tmpdir(), @@ -275,7 +274,7 @@ export class TestServiceAccessor { @IFileService public fileService: TestFileService, @INativeHostService public nativeHostService: TestNativeHostService, @IFileDialogService public fileDialogService: TestFileDialogService, - @IBackupFileService public backupFileService: NodeTestBackupFileService, + @IWorkingCopyBackupService public workingCopyBackupService: NodeTestWorkingCopyBackupService, @IWorkingCopyService public workingCopyService: IWorkingCopyService, @IEditorService public editorService: IEditorService ) { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 89f64a73eb8..c7872b65b09 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -74,6 +74,7 @@ import 'vs/workbench/services/mode/common/workbenchModeService'; import 'vs/workbench/services/commands/common/commandService'; import 'vs/workbench/services/themes/browser/workbenchThemeService'; import 'vs/workbench/services/label/common/labelService'; +import 'vs/workbench/services/extensions/common/extensionManifestPropertiesService'; import 'vs/workbench/services/extensionManagement/common/webExtensionsScannerService'; import 'vs/workbench/services/extensionManagement/browser/extensionEnablementService'; import 'vs/workbench/services/extensionManagement/browser/builtinExtensionsScannerService'; diff --git a/src/vs/workbench/workbench.sandbox.main.ts b/src/vs/workbench/workbench.sandbox.main.ts index 5b65c35d082..5b1d59c1cae 100644 --- a/src/vs/workbench/workbench.sandbox.main.ts +++ b/src/vs/workbench/workbench.sandbox.main.ts @@ -63,7 +63,7 @@ import 'vs/workbench/services/ipc/electron-sandbox/sharedProcessService'; import 'vs/workbench/services/timer/electron-sandbox/timerService'; import 'vs/workbench/services/environment/electron-sandbox/shellEnvironmentService'; import 'vs/workbench/services/integrity/electron-sandbox/integrityService'; -import 'vs/workbench/services/backup/electron-sandbox/backupFileService'; +import 'vs/workbench/services/workingCopy/electron-sandbox/workingCopyBackupService'; import 'vs/platform/diagnostics/electron-sandbox/diagnosticsService'; import 'vs/platform/checksum/electron-sandbox/checksumService'; import 'vs/platform/telemetry/electron-sandbox/customEndpointTelemetryService'; diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index d3f90afd58e..d337f0fd006 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -6,7 +6,6 @@ import 'vs/workbench/workbench.web.main'; import { main } from 'vs/workbench/browser/web.main'; import { UriComponents, URI } from 'vs/base/common/uri'; -import { IFileSystemProvider, FileSystemProviderCapabilities, IFileChange, FileChangeType } from 'vs/platform/files/common/files'; import { IWebSocketFactory, IWebSocket } from 'vs/platform/remote/browser/browserSocketFactory'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; @@ -566,12 +565,6 @@ export { IWorkspace, IWorkspaceProvider, - // FileSystem - IFileSystemProvider, - FileSystemProviderCapabilities, - IFileChange, - FileChangeType, - // WebSockets IWebSocketFactory, IWebSocket, diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index e05cec0c1c4..d6705fab19c 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -56,7 +56,7 @@ import 'vs/workbench/services/extensionResourceLoader/browser/extensionResourceL import 'vs/workbench/services/path/browser/pathService'; import 'vs/workbench/services/themes/browser/browserHostColorSchemeService'; import 'vs/workbench/services/encryption/browser/encryptionService'; -import 'vs/workbench/services/backup/browser/backupFileService'; +import 'vs/workbench/services/workingCopy/browser/workingCopyBackupService'; import 'vs/workbench/services/remote/browser/tunnelServiceImpl'; import 'vs/workbench/services/userDataSync/browser/userDataAutoSyncEnablementService'; diff --git a/yarn.lock b/yarn.lock index 88589d48079..af37b508cde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -202,13 +202,6 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.0.tgz#258fde4098f5004a56db67c35f33033af64810f6" - integrity sha512-GeIK5rfU1Yd7BZJQPTGZMMmcZy5nhRToPXZcjaDwQDRSewdhp648GT2E4dh+L7+Io7AOW6WQ+GR44QSzja4qxg== - dependencies: - cross-spawn "^7.0.1" - "@nodelib/fs.scandir@2.1.3": version "2.1.3" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" @@ -501,15 +494,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== -"@types/node@^12.0.12": - version "12.19.15" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.15.tgz#0de7e978fb43db62da369db18ea088a63673c182" - integrity sha512-lowukE3GUI+VSYSu6VcBXl14d61Rp5hA1D+61r16qnwC0lYNSqdxcvRh0pswejorHfS+HgwBasM8jLXz0/aOsw== - -"@types/node@^12.19.9": - version "12.19.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.19.9.tgz#990ad687ad8b26ef6dcc34a4f69c33d40c95b679" - integrity sha512-yj0DOaQeUrk3nJ0bd3Y5PeDRJ6W0r+kilosLA+dzF3dola/o9hxhMSg2sFvVcA2UHS5JSOsZp4S0c1OEXc4m1Q== +"@types/node@^14.14.37", "@types/node@^14.6.2": + version "14.14.37" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e" + integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw== "@types/q@^1.5.1": version "1.5.4" @@ -582,6 +570,11 @@ "@types/webpack-sources" "*" source-map "^0.6.0" +"@types/wicg-file-system-access@^2020.9.1": + version "2020.9.1" + resolved "https://registry.yarnpkg.com/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.1.tgz#ae1f420b0ca70f545c8621a9b63ed29270ef724a" + integrity sha512-hEN/YpLwvDjhRJrKoBiyiKtIh2zNkmJ/GY9VWIXNgjy7TBZNM9upfb/rnWDGpOoLomnEQtlTBjFBFCDra1oxOQ== + "@types/windows-foreground-love@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/windows-foreground-love/-/windows-foreground-love-0.3.0.tgz#26bc230b2568aa7ab7c56d35bb5653c0a6965a42" @@ -1337,11 +1330,6 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -2003,11 +1991,6 @@ cli-cursor@^3.1.0: dependencies: restore-cursor "^3.1.0" -cli-spinners@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" - integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== - cli-width@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48" @@ -2045,15 +2028,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" - integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -2076,7 +2050,7 @@ clone-stats@^1.0.0: resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= -clone@^1.0.0, clone@^1.0.2: +clone@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= @@ -2195,11 +2169,6 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== -colors@^1.3.3: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -2431,15 +2400,6 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - crypt@~0.0.1: version "0.0.2" resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" @@ -2784,13 +2744,6 @@ default-resolution@^2.0.0: resolved "https://registry.yarnpkg.com/default-resolution/-/default-resolution-2.0.0.tgz#bcb82baa72ad79b426a76732f1a81ad6df26d684" integrity sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ= -defaults@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" - integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= - dependencies: - clone "^1.0.2" - defer-to-connect@^1.0.1: version "1.1.3" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" @@ -3028,33 +2981,18 @@ editorconfig@^0.15.2: semver "^5.6.0" sigmund "^1.0.1" -electron-rebuild@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/electron-rebuild/-/electron-rebuild-2.0.3.tgz#fbcf34d35bf6795a0ded39bfe2aee24526a152c8" - integrity sha512-I8Oeey9afU+trFLd8/qRRiHC083CCoBnmw3q0qQaRFsg0OzMaeJQn7Nl6EYKPpntuQ/3yOqZQ7b3ObNuETN/Ig== - dependencies: - "@malept/cross-spawn-promise" "^1.1.0" - colors "^1.3.3" - debug "^4.1.1" - detect-libc "^1.0.3" - fs-extra "^9.0.1" - node-abi "^2.19.1" - node-gyp "^7.1.0" - ora "^5.1.0" - yargs "^16.0.0" - electron-to-chromium@^1.3.634: version "1.3.642" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.642.tgz#8b884f50296c2ae2a9997f024d0e3e57facc2b94" integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ== -electron@11.4.2: - version "11.4.2" - resolved "https://registry.yarnpkg.com/electron/-/electron-11.4.2.tgz#02005d9f5d77ea6485efeffdb5c5433c769ddda4" - integrity sha512-P0PRLH7cXp8ZdpA9yVPe7jRVM+QeiAtsadqmqS6XY3AYrsH+7bJnVrNuw6p/fcmp+b/UxWaCexobqQpyFJ5Qkw== +electron@12.0.4: + version "12.0.4" + resolved "https://registry.yarnpkg.com/electron/-/electron-12.0.4.tgz#c2ca4710d0e4da7db6d31c4f55777b08bfcb08e5" + integrity sha512-A8Lq3YMZ1CaO1z5z5nsyFxIwkgwXLHUwL2pf9MVUHpq7fv3XUewCMD98EnLL3DdtiyCvw5KMkeT1WGsZh8qFug== dependencies: "@electron/get" "^1.0.1" - "@types/node" "^12.0.12" + "@types/node" "^14.6.2" extract-zip "^1.0.3" elliptic@^6.5.3: @@ -3253,11 +3191,6 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.3: es6-iterator "^2.0.3" es6-symbol "^3.1.1" -escalade@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.0.2.tgz#6a580d70edb87880f22b4c91d0d56078df6962c4" - integrity sha512-gPYAU37hYCUhW5euPeR+Y74F7BL+IBsV93j5cvGriSaD1aG6MGsqsV1yamRdrWrb2j3aiZvb0X+UBOWpx3JWtQ== - escalade@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" @@ -4022,16 +3955,6 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^1.0.0" - fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -4129,7 +4052,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -4389,7 +4312,7 @@ graceful-fs@4.2.3: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.3: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== @@ -5298,11 +5221,6 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-interactive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" - integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== - is-negated-glob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" @@ -5678,15 +5596,6 @@ jsonfile@^4.0.0: optionalDependencies: graceful-fs "^4.1.6" -jsonfile@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" - integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== - dependencies: - universalify "^1.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - jsonparse@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.2.0.tgz#5c0c5685107160e72fe7489bddea0b44c2bc67bd" @@ -5949,7 +5858,7 @@ lodash@^4.17.13: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== -log-symbols@4.0.0, log-symbols@^4.0.0: +log-symbols@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== @@ -6566,13 +6475,6 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-abi@^2.19.1: - version "2.19.3" - resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.19.3.tgz#252f5dcab12dad1b5503b2d27eddd4733930282d" - integrity sha512-9xZrlyfvKhWme2EXFKQhZRp1yNWT/uI1luYPr3sFl+H4keYY4xR+1jO7mvTTijIsHf1M+QDe9uWuKeEpLInIlg== - dependencies: - semver "^5.4.1" - node-abi@^2.21.0: version "2.21.0" resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.21.0.tgz#c2dc9ebad6f4f53d6ea9b531e7b8faad81041d48" @@ -6595,22 +6497,6 @@ node-fetch@^2.6.1: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== -node-gyp@^7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.0.tgz#cb8aed7ab772e73ad592ae0c71b0e3741099fe39" - integrity sha512-rjlHQlnl1dqiDZxZYiKqQdrjias7V+81OVR5PTzZioCBtWkNdrKy06M05HLKxy/pcKikKRCabeDRoZaEc6nIjw== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.3" - nopt "^4.0.3" - npmlog "^4.1.2" - request "^2.88.2" - rimraf "^2.6.3" - semver "^7.3.2" - tar "^6.0.1" - which "^2.0.2" - node-libs-browser@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" @@ -6665,14 +6551,6 @@ noop-logger@^0.1.1: resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= -nopt@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== - dependencies: - abbrev "1" - osenv "^0.1.4" - nopt@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" @@ -6755,7 +6633,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npmlog@^4.0.1, npmlog@^4.1.2: +npmlog@^4.0.1: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -6952,20 +6830,6 @@ optionator@^0.8.2, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" -ora@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" - integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w== - dependencies: - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.4.0" - is-interactive "^1.0.0" - log-symbols "^4.0.0" - mute-stream "0.0.8" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - ordered-read-streams@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz#7137e69b3298bb342247a1bbee3881c80e2fd78b" @@ -7227,11 +7091,6 @@ path-key@^2.0.0, path-key@^2.0.1: resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= -path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" @@ -8193,7 +8052,7 @@ replacestream@^4.0.0: object-assign "^4.0.1" readable-stream "^2.0.2" -"request@>= 2.44.0 < 3.0.0", request@^2.85.0, request@^2.88.0, request@^2.88.2: +"request@>= 2.44.0 < 3.0.0", request@^2.85.0, request@^2.88.0: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== @@ -8595,23 +8454,11 @@ shebang-command@^1.2.0: dependencies: shebang-regex "^1.0.0" -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - shell-quote@^1.6.1: version "1.7.2" resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" @@ -8881,9 +8728,9 @@ sshpk@^1.7.0: tweetnacl "~0.14.0" ssri@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" - integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + version "6.0.2" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.2.tgz#157939134f20464e7301ddba3e90ffa8f7728ac5" + integrity sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q== dependencies: figgy-pudding "^3.5.1" @@ -9276,7 +9123,7 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" -tar@^6.0.1, tar@^6.0.2: +tar@^6.0.2: version "6.0.5" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.5.tgz#bde815086e10b39f1dcd298e89d596e1535e200f" integrity sha512-0b4HOimQHj9nXNEAA7zWwMM91Zhhba3pspja6sQbgTpynOJf+bkjBnfybNYzbpLbnwXnbyB4LOREvlyXLkCHSg== @@ -9746,11 +9593,6 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -universalify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" - integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== - unquote@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544" @@ -10084,10 +9926,10 @@ vscode-ripgrep@^1.11.3: https-proxy-agent "^4.0.0" proxy-from-env "^1.1.0" -vscode-sqlite3@4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/vscode-sqlite3/-/vscode-sqlite3-4.0.10.tgz#cf34cd98e5a49b24d3bb5ff5f2058744931f7cee" - integrity sha512-oYH3Nff3AMfbZpDiOhUh+hVtxClwrXBt9SGHGed+RcKf1Z4/fkLpfaldZeWgn4YBHFjNJjJ8HHRRfVvB8fXj1w== +vscode-sqlite3@4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/vscode-sqlite3/-/vscode-sqlite3-4.0.11.tgz#8f41ec8f4c03e24a284a74c5fad322ea39141f15" + integrity sha512-45oylZEr8sWJFWRoVE9iXI8RQhk7XDeIpwWYdtOAXFnCfmI74n0CwSqZOLfVsMLgtpU9/JZ1CA6s45tgKktSVQ== dependencies: nan "^2.14.0" @@ -10135,13 +9977,6 @@ watchpack@^1.7.4: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1" -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= - dependencies: - defaults "^1.0.3" - webpack-cli@^3.3.12: version "3.3.12" resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.12.tgz#94e9ada081453cd0aa609c99e500012fd3ad2d4a" @@ -10226,7 +10061,7 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@2.0.2, which@^2.0.1, which@^2.0.2: +which@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -10315,15 +10150,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -10450,11 +10276,6 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4" integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ== -y18n@^5.0.1: - version "5.0.5" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18" - integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg== - yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -10502,11 +10323,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.0.0: - version "20.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.0.0.tgz#c65a1daaa977ad63cebdd52159147b789a4e19a9" - integrity sha512-8eblPHTL7ZWRkyjIZJjnGf+TijiKJSwA24svzLRVvtgoi/RZiKa9fFQTrlx0OKLnyHSdt/enrdadji6WFfESVA== - yargs-unparser@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" @@ -10567,19 +10383,6 @@ yargs@^15.3.0: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.0: - version "16.0.3" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" - integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== - dependencies: - cliui "^7.0.0" - escalade "^3.0.2" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.1" - yargs-parser "^20.0.0" - yargs@^7.1.0: version "7.1.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.1.tgz#67f0ef52e228d4ee0d6311acede8850f53464df6"