diff --git a/.eslintrc.json b/.eslintrc.json index 8761c1c1813..df3c5ad560c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -653,6 +653,20 @@ "**/vs/workbench/services/**/{common,browser}/**" ] }, + { + "target": "**/vs/workbench/contrib/notebook/common/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,worker}/**", + "**/vs/platform/**/common/**", + "**/vs/editor/**", + "**/vs/workbench/common/**", + "**/vs/workbench/api/common/**", + "**/vs/workbench/services/**/common/**", + "**/vs/workbench/contrib/**/common/**" + ] + }, { "target": "**/vs/workbench/contrib/**/common/**", "restrictions": [ diff --git a/.github/classifier.json b/.github/classifier.json index c9196fc5691..33c179d3237 100644 --- a/.github/classifier.json +++ b/.github/classifier.json @@ -134,7 +134,7 @@ "snippets": {"assign": ["jrieken"]}, "splitview": {"assign": ["joaomoreno"]}, "suggest": {"assign": ["jrieken"]}, - "tasks": {"assign": ["alexr00"]}, + "tasks": {"assign": ["alexr00"], "accuracy": 0.85}, "telemetry": {"assign": []}, "themes": {"assign": ["aeschli"]}, "timeline": {"assign": ["eamodio"]}, diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index c0f5efc51d4..167f27a5490 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -17,7 +17,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 63e43a17703..61c82aa73ca 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v33 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/deep-classifier-monitor.yml b/.github/workflows/deep-classifier-monitor.yml index 545f74c273b..9425ae0f1f2 100644 --- a/.github/workflows/deep-classifier-monitor.yml +++ b/.github/workflows/deep-classifier-monitor.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-runner.yml b/.github/workflows/deep-classifier-runner.yml index 3db13a01c2b..2f598745e49 100644 --- a/.github/workflows/deep-classifier-runner.yml +++ b/.github/workflows/deep-classifier-runner.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/deep-classifier-scraper.yml b/.github/workflows/deep-classifier-scraper.yml index a78b4d14c2f..b9cb6ec3cd3 100644 --- a/.github/workflows/deep-classifier-scraper.yml +++ b/.github/workflows/deep-classifier-scraper.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index b81f12ab39f..fdfd548291d 100644 --- a/.github/workflows/english-please.yml +++ b/.github/workflows/english-please.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions if: contains(github.event.issue.labels.*.name, '*english-please') diff --git a/.github/workflows/feature-request.yml b/.github/workflows/feature-request.yml index 9d25f4a9f5c..12af1423506 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -18,7 +18,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v33 + ref: v34 - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'feature-request') run: npm install --production --prefix ./actions diff --git a/.github/workflows/latest-release-monitor.yml b/.github/workflows/latest-release-monitor.yml index acca00a07e3..a5777e3a7f1 100644 --- a/.github/workflows/latest-release-monitor.yml +++ b/.github/workflows/latest-release-monitor.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v33 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Install Storage Module diff --git a/.github/workflows/locker.yml b/.github/workflows/locker.yml index 776203c8c6c..64ac30d3717 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v33 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Locker diff --git a/.github/workflows/needs-more-info-closer.yml b/.github/workflows/needs-more-info-closer.yml index 8ecdd5fb9be..018caf24fe0 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v33 + ref: v34 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Needs More Info Closer diff --git a/.github/workflows/on-label.yml b/.github/workflows/on-label.yml index 732842501a6..446c041ebd3 100644 --- a/.github/workflows/on-label.yml +++ b/.github/workflows/on-label.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/on-open.yml b/.github/workflows/on-open.yml index 3c003d0797a..e4f0434dbdc 100644 --- a/.github/workflows/on-open.yml +++ b/.github/workflows/on-open.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/release-pipeline-labeler.yml b/.github/workflows/release-pipeline-labeler.yml index 8386e8f0f9f..14f74f581dd 100644 --- a/.github/workflows/release-pipeline-labeler.yml +++ b/.github/workflows/release-pipeline-labeler.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v33 + ref: v34 path: ./actions - name: Checkout Repo if: github.event_name != 'issues' diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index 73da8e6b928..4365afa03e5 100644 --- a/.github/workflows/test-plan-item-validator.yml +++ b/.github/workflows/test-plan-item-validator.yml @@ -14,7 +14,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v33 + ref: v34 - name: Install Actions if: contains(github.event.issue.labels.*.name, 'testplan-item') || contains(github.event.issue.labels.*.name, 'invalid-testplan-item') run: npm install --production --prefix ./actions diff --git a/.vscode/launch.json b/.vscode/launch.json index 1d966c8b508..33801a60ee3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -241,7 +241,7 @@ "type": "node", "request": "launch", "name": "VS Code (Web)", - "program": "${workspaceFolder}/resources/serverless/code-web.js", + "program": "${workspaceFolder}/resources/web/code-web.js", "presentation": { "group": "0_vscode", "order": 2 diff --git a/.yarnrc b/.yarnrc index 68cb12c1284..3c6eccfb102 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "9.2.0" +target "9.2.1" runtime "electron" diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 58353f1f283..3b186bb1136 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -118,6 +118,13 @@ steps: displayName: Run integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ + ./resources/server/test/test-web-integration.sh --browser webkit + displayName: Run integration tests (Browser) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin @@ -128,13 +135,6 @@ steps: displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) -- script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ - ./resources/server/test/test-web-integration.sh --browser webkit - displayName: Run integration tests (Browser) - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-darwin diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index ea6c0195954..21d963042c8 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -123,6 +123,13 @@ steps: displayName: Run integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) +- script: | + set -e + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-x64" \ + DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium + displayName: Run integration tests (Browser) + condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) + - script: | set -e APP_ROOT=$(agent.builddirectory)/VSCode-linux-x64 @@ -133,13 +140,6 @@ steps: displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) -- script: | - set -e - VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-linux-x64" \ - DISPLAY=:10 ./resources/server/test/test-web-integration.sh --browser chromium - displayName: Run integration tests (Browser) - condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - task: PublishPipelineArtifact@0 inputs: artifactName: crash-dump-linux diff --git a/build/azure-pipelines/win32/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 779bc8a8d57..be80731a7ab 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -135,18 +135,18 @@ steps: - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" - $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json - $AppNameShort = $AppProductJson.nameShort - exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-remote-integration.bat } - displayName: Run remote integration tests (Electron) + exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser firefox } + displayName: Run integration tests (Browser) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - powershell: | . build/azure-pipelines/win32/exec.ps1 $ErrorActionPreference = "Stop" - exec { $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-web-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-web-integration.bat --browser firefox } - displayName: Run integration tests (Browser) + $AppRoot = "$(agent.builddirectory)\VSCode-win32-$(VSCODE_ARCH)" + $AppProductJson = Get-Content -Raw -Path "$AppRoot\resources\app\product.json" | ConvertFrom-Json + $AppNameShort = $AppProductJson.nameShort + exec { $env:INTEGRATION_TEST_ELECTRON_PATH = "$AppRoot\$AppNameShort.exe"; $env:VSCODE_REMOTE_SERVER_PATH = "$(agent.builddirectory)\vscode-reh-win32-$(VSCODE_ARCH)"; .\resources\server\test\test-remote-integration.bat } + displayName: Run remote integration tests (Electron) condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - task: PublishPipelineArtifact@0 diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index fd1d8ceeed9..4952ee1c26d 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -119,12 +119,12 @@ const copyrightFilter = [ '!resources/linux/snap/snapcraft.yaml', '!resources/linux/snap/electron-launch', '!resources/win32/bin/code.js', + '!resources/web/code-web.js', '!resources/completions/**', '!extensions/markdown-language-features/media/highlight.css', '!extensions/html-language-features/server/src/modes/typescript/*', '!extensions/*/server/bin/*', - '!src/vs/editor/test/node/classification/typescript-test.ts', - '!resources/serverless/code-web.js' + '!src/vs/editor/test/node/classification/typescript-test.ts' ]; const jsHygieneFilter = [ diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 1bf7d36a9f6..2c86b2808e0 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -43,6 +43,7 @@ const vscodeEntryPoints = _.flatten([ buildfile.entrypoint('vs/workbench/workbench.desktop.main'), buildfile.base, buildfile.workerExtensionHost, + buildfile.workerNotebook, buildfile.workbenchDesktop, buildfile.code ]); @@ -79,7 +80,7 @@ const vscodeResources = [ 'out-build/vs/code/electron-browser/sharedProcess/sharedProcess.js', 'out-build/vs/code/electron-sandbox/issue/issueReporter.js', 'out-build/vs/code/electron-sandbox/processExplorer/processExplorer.js', - 'out-build/vs/platform/auth/common/auth.css', + 'out-build/vs/code/electron-sandbox/proxy/auth.js', '!**/test/**' ]; diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 9cc40c4e1be..fe0deffc6d0 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -224,7 +224,6 @@ function packageLocalExtensionsStream(forWeb) { const extensionName = path.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) - .filter(({ name }) => (name === 'vscode-web-playground' ? forWeb : true)) // package vscode-web-playground only for web .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true))); diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 7e529f17cb8..dac71c81479 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -275,7 +275,6 @@ export function packageLocalExtensionsStream(forWeb: boolean): Stream { const extensionName = path.basename(extensionPath); return { name: extensionName, path: extensionPath, manifestPath: absoluteManifestPath }; }) - .filter(({ name }) => (name === 'vscode-web-playground' ? forWeb : true)) // package vscode-web-playground only for web .filter(({ name }) => excludedExtensions.indexOf(name) === -1) .filter(({ name }) => builtInExtensions.every(b => b.name !== name)) .filter(({ manifestPath }) => (forWeb ? isWebExtension(require(manifestPath)) : true)) diff --git a/build/package.json b/build/package.json index 7392e4b602c..e185594554b 100644 --- a/build/package.json +++ b/build/package.json @@ -45,7 +45,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^4.0.1-rc", + "typescript": "^4.1.0-dev.20200824", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.6.0", "xml2js": "^0.4.17" diff --git a/build/win32/code.iss b/build/win32/code.iss index 195e63cf12a..6908759b7aa 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -89,7 +89,7 @@ Source: "{#ProductJsonPath}"; DestDir: "{code:GetDestDir}\resources\app"; Flags: [Icons] Name: "{group}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; AppUserModelID: "{#AppUserId}" -Name: "{commondesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" +Name: "{autodesktop}\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#NameLong}"; Filename: "{app}\{#ExeBasename}.exe"; Tasks: quicklaunchicon; AppUserModelID: "{#AppUserId}" [Run] diff --git a/build/yarn.lock b/build/yarn.lock index d610f9cefa7..01ebd186c4c 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -2535,10 +2535,10 @@ typescript@^3.0.1: resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== -typescript@^4.0.1-rc: - version "4.0.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-rc.tgz#8adc78223eae56fe71d906a5fa90c3543b07a677" - integrity sha512-TCkspT3dSKOykbzS3/WSK7pqU2h1d/lEO6i45Afm5Y3XNAEAo8YXTG3kHOQk/wFq/5uPyO1+X8rb/Q+g7UsxJw== +typescript@^4.1.0-dev.20200824: + version "4.1.0-dev.20200824" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200824.tgz#34c92d9b6e5124600658c0d4e9b8c125beaf577d" + integrity sha512-hTJfocmebnMKoqRw/xs3bL61z87XXtvOUwYtM7zaCX9mAvnfdo1x1bzQlLZAsvdzRIgAHPJQYbqYHKygWkDw6g== typical@^4.0.0: version "4.0.0" diff --git a/cgmanifest.json b/cgmanifest.json index 576724e75af..e6e8ce8a5c1 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "0c2cb59b6283fe8d6bb4b14f8a832e2166aeaa0c" + "commitHash": "03c7a54dc534ce1867d4393b9b1a6989d4a7e005" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "9.2.0" + "version": "9.2.1" }, { "component": { diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 3cadb80fd33..477d530a0ed 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -54,7 +54,7 @@ "url": "vscode://schemas/keybindings" }, { - "fileMatch": "vscode://defaultsettings/defaultSettings.json", + "fileMatch": "vscode://defaultsettings/*/*.json", "url": "vscode://schemas/settings/default" }, { diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index ccbdba90a29..331af0883a9 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -10,7 +10,7 @@ "main": "./out/node/cssServerMain", "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.3.1", + "vscode-css-languageservice": "^4.3.3", "vscode-languageserver": "7.0.0-next.3", "vscode-uri": "^2.1.2" }, diff --git a/extensions/css-language-features/server/test/index.js b/extensions/css-language-features/server/test/index.js index 4e9960494a2..4ab853bd503 100644 --- a/extensions/css-language-features/server/test/index.js +++ b/extensions/css-language-features/server/test/index.js @@ -21,7 +21,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/css-language-features/server/yarn.lock b/extensions/css-language-features/server/yarn.lock index 3b9a29250f5..c93e187a941 100644 --- a/extensions/css-language-features/server/yarn.lock +++ b/extensions/css-language-features/server/yarn.lock @@ -696,10 +696,10 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -vscode-css-languageservice@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.1.tgz#a78755b28b8a0cbb1681121f0fa372860f34ef6b" - integrity sha512-Vdz2cyoTP2tLWikhFdouK8dAQ3gVhLPxsFkIscM30Quh6rd/YejTeZEYC/W+b0iKumHYebDeo1GUFbf0ptySRw== +vscode-css-languageservice@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/vscode-css-languageservice/-/vscode-css-languageservice-4.3.3.tgz#fcb8c7442ae7bb8fbe6ff1d3a514be8248bfb52f" + integrity sha512-b2b+0oHvPmBHygDtOXX3xBvpQCa6eIQSvXnGDNSDmIC1894ZTJ2yX10vjplOO/PvV7mwhyvGPwHyY4X2HGxtKw== dependencies: vscode-languageserver-textdocument "^1.0.1" vscode-languageserver-types "3.16.0-next.2" diff --git a/extensions/debug-auto-launch/.vscode/launch.json b/extensions/debug-auto-launch/.vscode/launch.json new file mode 100644 index 00000000000..1c3d9e9661b --- /dev/null +++ b/extensions/debug-auto-launch/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Extension", + "type": "extensionHost", + "request": "launch", + "skipFiles": ["/**"], + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js", + ], + } + ] +} diff --git a/extensions/debug-auto-launch/package.json b/extensions/debug-auto-launch/package.json index 07568c77081..3cb11ef1844 100644 --- a/extensions/debug-auto-launch/package.json +++ b/extensions/debug-auto-launch/package.json @@ -35,6 +35,12 @@ ], "description": "%debug.node.autoAttach.description%", "default": "disabled" + }, + "debug.javascript.usePreviewAutoAttach": { + "scope": "window", + "type": "boolean", + "default": true, + "description": "%debug.javascript.usePreviewAutoAttach%" } } }, diff --git a/extensions/debug-auto-launch/package.nls.json b/extensions/debug-auto-launch/package.nls.json index 030ac5a20a9..1179563a6c5 100644 --- a/extensions/debug-auto-launch/package.nls.json +++ b/extensions/debug-auto-launch/package.nls.json @@ -3,9 +3,10 @@ "description": "Helper for auto-attach feature when node-debug extensions are not active.", "debug.node.autoAttach.description": "Automatically attach node debugger when node.js was launched in debug mode from integrated terminal.", + "debug.javascript.usePreviewAutoAttach": "Whether to use the preview debugger's version of auto attach.", "debug.node.autoAttach.disabled.description": "Auto attach is disabled and not shown in status bar.", "debug.node.autoAttach.on.description": "Auto attach is active.", "debug.node.autoAttach.off.description": "Auto attach is inactive.", "toggle.auto.attach": "Toggle Auto Attach" -} \ No newline at end of file +} diff --git a/extensions/debug-auto-launch/src/extension.ts b/extensions/debug-auto-launch/src/extension.ts index ef58db6a5ea..b70fb3dedbe 100644 --- a/extensions/debug-auto-launch/src/extension.ts +++ b/extensions/debug-auto-launch/src/extension.ts @@ -13,11 +13,11 @@ const OFF_TEXT = localize('status.text.auto.attach.off', 'Auto Attach: Off'); const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach'; const JS_DEBUG_SETTINGS = 'debug.javascript'; -const JS_DEBUG_USEPREVIEW = 'usePreview'; +const JS_DEBUG_USEPREVIEWAA = 'usePreviewAutoAttach'; const JS_DEBUG_IPC_KEY = 'jsDebugIpcState'; const NODE_DEBUG_SETTINGS = 'debug.node'; -const NODE_DEBUG_USEV3 = 'useV3'; const AUTO_ATTACH_SETTING = 'autoAttach'; +const LAST_STATE_STORAGE_KEY = 'lastState'; type AUTO_ATTACH_VALUES = 'disabled' | 'on' | 'off'; @@ -29,33 +29,36 @@ const enum State { } // on activation this feature is always disabled... -let currentState = Promise.resolve({ state: State.Disabled, transitionData: null as unknown }); +let currentState: Promise<{ context: vscode.ExtensionContext, state: State; transitionData: unknown }>; let statusItem: vscode.StatusBarItem | undefined; // and there is no status bar item export function activate(context: vscode.ExtensionContext): void { + const previousState = context.workspaceState.get(LAST_STATE_STORAGE_KEY, State.Disabled); + currentState = Promise.resolve(transitions[previousState].onActivate?.(context, readCurrentState())) + .then(() => ({ context, state: State.Disabled, transitionData: null })); + context.subscriptions.push(vscode.commands.registerCommand(TOGGLE_COMMAND, toggleAutoAttachSetting)); // settings that can result in the "state" being changed--on/off/disable or useV3 toggles const effectualConfigurationSettings = [ `${NODE_DEBUG_SETTINGS}.${AUTO_ATTACH_SETTING}`, - `${NODE_DEBUG_SETTINGS}.${NODE_DEBUG_USEV3}`, - `${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEW}`, + `${JS_DEBUG_SETTINGS}.${JS_DEBUG_USEPREVIEWAA}`, ]; context.subscriptions.push( vscode.workspace.onDidChangeConfiguration((e) => { if (effectualConfigurationSettings.some(setting => e.affectsConfiguration(setting))) { - updateAutoAttach(context); + updateAutoAttach(); } }) ); - updateAutoAttach(context); + updateAutoAttach(); } export async function deactivate(): Promise { - const { state, transitionData } = await currentState; - await transitions[state].exit?.(transitionData); + const { context, state, transitionData } = await currentState; + await transitions[state].exit?.(context, transitionData); } function toggleAutoAttachSetting() { @@ -88,6 +91,11 @@ function toggleAutoAttachSetting() { } } +function autoAttachWithJsDebug() { + const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); + return jsDebugConfig.get(JS_DEBUG_USEPREVIEWAA, true); +} + function readCurrentState(): State { const nodeConfig = vscode.workspace.getConfiguration(NODE_DEBUG_SETTINGS); const autoAttachState = nodeConfig.get(AUTO_ATTACH_SETTING); @@ -95,11 +103,7 @@ function readCurrentState(): State { case 'off': return State.Off; case 'on': - // todo: reenable after resolving https://github.com/microsoft/vscode/issues/102057 - // const jsDebugConfig = vscode.workspace.getConfiguration(JS_DEBUG_SETTINGS); - // const useV3 = nodeConfig.get(NODE_DEBUG_USEV3) || jsDebugConfig.get(JS_DEBUG_USEPREVIEW); - // return useV3 ? State.OnWithJsDebug : State.OnWithNodeDebug; - return State.OnWithNodeDebug; + return autoAttachWithJsDebug() ? State.OnWithJsDebug : State.OnWithNodeDebug; case 'disabled': default: return State.Disabled; @@ -126,37 +130,44 @@ function ensureStatusBarExists(context: vscode.ExtensionContext) { return statusItem; } +async function clearJsDebugAttachState(context: vscode.ExtensionContext) { + await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); + await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables'); +} + interface CachedIpcState { ipcAddress: string; jsDebugPath: string; } interface StateTransition { - exit?(stateData: StateData): Promise | void; + onActivate?(context: vscode.ExtensionContext, currentState: State): Promise; + exit?(context: vscode.ExtensionContext, stateData: StateData): Promise | void; enter?(context: vscode.ExtensionContext): Promise | StateData; } +const makeTransition = (tsn: StateTransition) => tsn; // helper to apply generic type + /** * Map of logic that happens when auto attach states are entered and exited. * All state transitions are queued and run in order; promises are awaited. */ const transitions: { [S in State]: StateTransition } = { - [State.Disabled]: { + [State.Disabled]: makeTransition({ async enter(context) { statusItem?.hide(); - await context.workspaceState.update(JS_DEBUG_IPC_KEY, undefined); - await vscode.commands.executeCommand('extension.js-debug.clearAutoAttachVariables'); + await clearJsDebugAttachState(context); }, - }, + }), - [State.Off]: { + [State.Off]: makeTransition({ enter(context) { const statusItem = ensureStatusBarExists(context); statusItem.text = OFF_TEXT; }, - }, + }), - [State.OnWithNodeDebug]: { + [State.OnWithNodeDebug]: makeTransition({ async enter(context) { const statusItem = ensureStatusBarExists(context); const vscode_pid = process.env['VSCODE_PID']; @@ -168,21 +179,37 @@ const transitions: { [S in State]: StateTransition } = { async exit() { await vscode.commands.executeCommand('extension.node-debug.stopAutoAttach'); }, - }, + }), - [State.OnWithJsDebug]: { + [State.OnWithJsDebug]: makeTransition({ async enter(context) { const ipcAddress = await getIpcAddress(context); - const server = await new Promise((resolve, reject) => { + if (!ipcAddress) { + return null; + } + + const server = await new Promise((resolve, reject) => { const s = createServer((socket) => { let data: Buffer[] = []; - socket.on('data', (chunk) => data.push(chunk)); - socket.on('end', () => - vscode.commands.executeCommand( - 'extension.js-debug.autoAttachToProcess', - JSON.parse(Buffer.concat(data).toString()) - ) - ); + socket.on('data', async (chunk) => { + if (chunk[chunk.length - 1] !== 0) { // terminated with NUL byte + data.push(chunk); + return; + } + + data.push(chunk.slice(0, -1)); + + try { + await vscode.commands.executeCommand( + 'extension.js-debug.autoAttachToProcess', + JSON.parse(Buffer.concat(data).toString()) + ); + socket.write(Buffer.from([0])); + } catch (err) { + socket.write(Buffer.from([1])); + console.error(err); + } + }); }) .on('error', reject) .listen(ipcAddress, () => resolve(s)); @@ -190,33 +217,47 @@ const transitions: { [S in State]: StateTransition } = { const statusItem = ensureStatusBarExists(context); statusItem.text = ON_TEXT; - return server; + return server || null; }, - async exit(server: Server) { + async exit(context, server) { // we don't need to clear the environment variables--the bootloader will // no-op if the debug server is closed. This prevents having to reload // terminals if users want to turn it back on. - await new Promise((resolve) => server.close(resolve)); + if (server) { + await new Promise((resolve) => server.close(resolve)); + } + + // but if they toggled auto attach use js-debug off, go ahead and do so + if (!autoAttachWithJsDebug()) { + await clearJsDebugAttachState(context); + } }, - }, + + async onActivate(context, currentState) { + if (currentState === State.OnWithNodeDebug || currentState === State.Disabled) { + await clearJsDebugAttachState(context); + } + } + }), }; /** * Updates the auto attach feature based on the user or workspace setting */ -function updateAutoAttach(context: vscode.ExtensionContext) { +function updateAutoAttach() { const newState = readCurrentState(); - currentState = currentState.then(async ({ state: oldState, transitionData }) => { + currentState = currentState.then(async ({ context, state: oldState, transitionData }) => { if (newState === oldState) { - return { state: oldState, transitionData }; + return { context, state: oldState, transitionData }; } - await transitions[oldState].exit?.(transitionData); + await transitions[oldState].exit?.(context, transitionData); const newData = await transitions[newState].enter?.(context); + await context.workspaceState.update(LAST_STATE_STORAGE_KEY, newState); - return { state: newState, transitionData: newData }; + return { context, state: newState, transitionData: newData }; }); } @@ -244,8 +285,11 @@ async function getIpcAddress(context: vscode.ExtensionContext) { const result = await vscode.commands.executeCommand<{ ipcAddress: string; }>( 'extension.js-debug.setAutoAttachVariables' ); + if (!result) { + return; + } - const ipcAddress = result!.ipcAddress; + const ipcAddress = result.ipcAddress; await context.workspaceState.update(JS_DEBUG_IPC_KEY, { ipcAddress, jsDebugPath }); return ipcAddress; } diff --git a/extensions/emmet/extension.webpack.config.js b/extensions/emmet/extension.webpack.config.js index 96ada4c23a7..bfac2b59f47 100644 --- a/extensions/emmet/extension.webpack.config.js +++ b/extensions/emmet/extension.webpack.config.js @@ -21,6 +21,6 @@ module.exports = withDefaults({ filename: 'emmetNodeMain.js' }, externals: { - 'vscode-emmet-helper2': 'commonjs vscode-emmet-helper2', + 'vscode-emmet-helper': 'commonjs vscode-emmet-helper', }, }); diff --git a/extensions/emmet/package.json b/extensions/emmet/package.json index aadbba7edea..d02c19a78ac 100644 --- a/extensions/emmet/package.json +++ b/extensions/emmet/package.json @@ -422,7 +422,7 @@ "scripts": { "watch": "gulp watch-extension:emmet", "compile": "gulp compile-extension:emmet", - "deps": "yarn add vscode-emmet-helper2" + "deps": "yarn add vscode-emmet-helper" }, "devDependencies": { "@types/node": "^12.11.7", @@ -435,7 +435,7 @@ "@emmetio/html-matcher": "^0.3.3", "@emmetio/math-expression": "^0.1.1", "image-size": "^0.5.2", - "vscode-emmet-helper2": "^2.0.0-next.0", + "vscode-emmet-helper": "^2.0.0", "vscode-html-languageservice": "^3.0.3" } } diff --git a/extensions/emmet/src/test/index.ts b/extensions/emmet/src/test/index.ts index f3aeb0fefc1..3aeda9dfa42 100644 --- a/extensions/emmet/src/test/index.ts +++ b/extensions/emmet/src/test/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Emmet Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Emmet Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Emmet Tests'; +} else { + suite = 'Integration Emmet Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/emmet/src/util.ts b/extensions/emmet/src/util.ts index 0dc2a0e201d..8ef33e97a83 100644 --- a/extensions/emmet/src/util.ts +++ b/extensions/emmet/src/util.ts @@ -8,7 +8,7 @@ import parse from '@emmetio/html-matcher'; import parseStylesheet from '@emmetio/css-parser'; import { Node, HtmlNode, CssToken, Property, Rule, Stylesheet } from 'EmmetNode'; import { DocumentStreamReader } from './bufferStream'; -import * as EmmetHelper from 'vscode-emmet-helper2'; +import * as EmmetHelper from 'vscode-emmet-helper'; import { TextDocument as LSTextDocument } from 'vscode-html-languageservice'; let _emmetHelper: typeof EmmetHelper; @@ -26,7 +26,7 @@ export function getEmmetHelper() { // Lazy load vscode-emmet-helper instead of importing it // directly to reduce the start-up time of the extension if (!_emmetHelper) { - _emmetHelper = require('vscode-emmet-helper2'); + _emmetHelper = require('vscode-emmet-helper'); } updateEmmetExtensionsPath(); return _emmetHelper; diff --git a/extensions/emmet/yarn.lock b/extensions/emmet/yarn.lock index 06a8845658c..cd26963d81b 100644 --- a/extensions/emmet/yarn.lock +++ b/extensions/emmet/yarn.lock @@ -2469,10 +2469,10 @@ vinyl@~2.0.1: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" -vscode-emmet-helper2@^2.0.0-next.0: - version "2.0.0-next.0" - resolved "https://registry.yarnpkg.com/vscode-emmet-helper2/-/vscode-emmet-helper2-2.0.0-next.0.tgz#86eb4c2e581a577e7eb56a51f662e72fb1c63b47" - integrity sha512-ccm6Fb5dkbdEDNLIAebWwVcb8X3AXZDsACLi4KYdCxyFSMV+pOoNokBf4rsu+rSYWNe+fMqxjXZs9z0G2CxPGg== +vscode-emmet-helper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vscode-emmet-helper/-/vscode-emmet-helper-2.0.0.tgz#0057ec2d4af8ac83b1f7937383714ffdc56fcc07" + integrity sha512-ytR+Ajxs6zeYI0b4bPsl+nPU8xm852piJUtIwO1ajp1Pw7lwn3VeR+f4ynmxOl9IjfOdF2kW9T/qIkeFbKLwYw== dependencies: "@emmetio/extract-abbreviation" "^0.2.0" jsonc-parser "^2.3.0" diff --git a/extensions/git/package.json b/extensions/git/package.json index 748868778dd..1704a89e7e3 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -1915,7 +1915,11 @@ "[git-commit]": { "editor.rulers": [ 72 - ] + ], + "workbench.editor.restoreViewState": false + }, + "[git-rebase]": { + "workbench.editor.restoreViewState": false } }, "viewsWelcome": [ diff --git a/extensions/git/src/test/index.ts b/extensions/git/src/test/index.ts index 747c4562e8a..8773f772e62 100644 --- a/extensions/git/src/test/index.ts +++ b/extensions/git/src/test/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Git Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Git Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Git Tests'; +} else { + suite = 'Integration Git Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts index 5600b6f0e0c..94857f8e210 100644 --- a/extensions/git/src/timelineProvider.ts +++ b/extensions/git/src/timelineProvider.ts @@ -232,7 +232,7 @@ export class GitTimelineProvider implements TimelineProvider { private onRepositoryStatusChanged(_repo: Repository) { // console.log(`GitTimelineProvider.onRepositoryStatusChanged`); - // This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items + // This is less than ideal, but for now just save the last time a status was run and use that as the timestamp for staged items this.repoStatusDate = new Date(); this.fireChanged(); diff --git a/extensions/github-authentication/package.json b/extensions/github-authentication/package.json index c4189e6b7eb..787b4d17497 100644 --- a/extensions/github-authentication/package.json +++ b/extensions/github-authentication/package.json @@ -11,6 +11,11 @@ "categories": [ "Other" ], + "extensionKind": [ + "ui", + "workspace", + "web" + ], "activationEvents": [ "*", "onAuthenticationRequest:github" diff --git a/extensions/groovy/package.json b/extensions/groovy/package.json index 55da6a97fc8..ab2ef0da7ba 100644 --- a/extensions/groovy/package.json +++ b/extensions/groovy/package.json @@ -13,8 +13,8 @@ "languages": [{ "id": "groovy", "aliases": ["Groovy", "groovy"], - "extensions": [".groovy", ".gvy", ".gradle"], - "filenames": [ "Jenkinsfile" ], + "extensions": [".groovy", ".gvy", ".gradle", ".jenkinsfile"], + "filenamePatterns": ["Jenkinsfile.*"], "firstLine": "^#!.*\\bgroovy\\b", "configuration": "./language-configuration.json" }], diff --git a/extensions/html-language-features/client/src/htmlClient.ts b/extensions/html-language-features/client/src/htmlClient.ts index e818d1a2d02..6917b56950b 100644 --- a/extensions/html-language-features/client/src/htmlClient.ts +++ b/extensions/html-language-features/client/src/htmlClient.ts @@ -28,7 +28,7 @@ namespace TagCloseRequest { export const type: RequestType = new RequestType('html/tag'); } namespace OnTypeRenameRequest { - export const type: RequestType = new RequestType('html/onTypeRename'); + export const type: RequestType = new RequestType('html/onTypeRename'); } // experimental: semantic tokens @@ -172,9 +172,14 @@ export function startClient(context: ExtensionContext, newLanguageClient: Langua disposable = languages.registerOnTypeRenameProvider(documentSelector, { async provideOnTypeRenameRanges(document, position) { const param = client.code2ProtocolConverter.asTextDocumentPositionParams(document, position); - const response = await client.sendRequest(OnTypeRenameRequest.type, param); - - return response || []; + return client.sendRequest(OnTypeRenameRequest.type, param).then(response => { + if (response) { + return { + ranges: response.map(r => client.protocol2CodeConverter.asRange(r)) + }; + } + return undefined; + }); } }); toDispose.push(disposable); diff --git a/extensions/html-language-features/server/test/index.js b/extensions/html-language-features/server/test/index.js index d177599c624..5f7aa21e58a 100644 --- a/extensions/html-language-features/server/test/index.js +++ b/extensions/html-language-features/server/test/index.js @@ -21,7 +21,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/image-preview/src/extension.ts b/extensions/image-preview/src/extension.ts index 552b32d39b6..10722360dd5 100644 --- a/extensions/image-preview/src/extension.ts +++ b/extensions/image-preview/src/extension.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { PreviewManager } from './preview'; import { SizeStatusBarEntry } from './sizeStatusBarEntry'; -import { BinarySizeStatusBarEntry } from './binarySizeStatusBarEntry'; import { ZoomStatusBarEntry } from './zoomStatusBarEntry'; export function activate(context: vscode.ExtensionContext) { diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index 60f6ce87547..a3daae76943 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -5737,4 +5737,4 @@ "match": "\\S+" } } -} +} \ No newline at end of file diff --git a/extensions/markdown-basics/snippets/markdown.code-snippets b/extensions/markdown-basics/snippets/markdown.code-snippets index 6ee831ae4a5..b07f984138c 100644 --- a/extensions/markdown-basics/snippets/markdown.code-snippets +++ b/extensions/markdown-basics/snippets/markdown.code-snippets @@ -14,43 +14,54 @@ "body": "> ${1:${TM_SELECTED_TEXT}}", "description": "Insert quoted text" }, - "Insert code": { + "Insert inline code": { "prefix": "code", "body": "`${1:${TM_SELECTED_TEXT}}`$0", - "description": "Insert code" + "description": "Insert inline code" }, "Insert fenced code block": { "prefix": "fenced codeblock", - "body": [ - "```${1:language}", - "${TM_SELECTED_TEXT}$0", - "```" - ], + "body": ["```${1:language}", "${TM_SELECTED_TEXT}$0", "```"], "description": "Insert fenced code block" }, - "Insert heading": { - "prefix": "heading", + "Insert heading level 1": { + "prefix": "heading1", "body": "# ${1:${TM_SELECTED_TEXT}}", - "description": "Insert heading" + "description": "Insert heading level 1" + }, + "Insert heading level 2": { + "prefix": "heading2", + "body": "## ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 2" + }, + "Insert heading level 3": { + "prefix": "heading3", + "body": "### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 3" + }, + "Insert heading level 4": { + "prefix": "heading4", + "body": "#### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 4" + }, + "Insert heading level 5": { + "prefix": "heading5", + "body": "##### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 5" + }, + "Insert heading level 6": { + "prefix": "heading6", + "body": "###### ${1:${TM_SELECTED_TEXT}}", + "description": "Insert heading level 6" }, "Insert unordered list": { "prefix": "unordered list", - "body": [ - "- ${1:first}", - "- ${2:second}", - "- ${3:third}", - "$0" - ], + "body": ["- ${1:first}", "- ${2:second}", "- ${3:third}", "$0"], "description": "Insert unordered list" }, "Insert ordered list": { "prefix": "ordered list", - "body": [ - "1. ${1:first}", - "2. ${2:second}", - "3. ${3:third}", - "$0" - ], + "body": ["1. ${1:first}", "2. ${2:second}", "3. ${3:third}", "$0"], "description": "Insert ordered list" }, "Insert horizontal rule": { @@ -67,5 +78,10 @@ "prefix": "image", "body": "![${TM_SELECTED_TEXT:${1:alt}}](https://${2:link})$0", "description": "Insert image" + }, + "Insert strikethrough": { + "prefix": "strikethrough", + "body": "~~${1:${TM_SELECTED_TEXT}}~~", + "description": "Insert strikethrough" } } diff --git a/extensions/markdown-language-features/src/test/index.ts b/extensions/markdown-language-features/src/test/index.ts index 77019228745..0eb9bc92487 100644 --- a/extensions/markdown-language-features/src/test/index.ts +++ b/extensions/markdown-language-features/src/test/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Markdown Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Markdown Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Markdown Tests'; +} else { + suite = 'Integration Markdown Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/microsoft-authentication/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts index 035c5af7350..67b55889c3c 100644 --- a/extensions/microsoft-authentication/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -5,6 +5,7 @@ import * as randomBytes from 'randombytes'; import * as querystring from 'querystring'; +import { Buffer } from 'buffer'; import * as vscode from 'vscode'; import { createServer, startServer } from './authServer'; diff --git a/extensions/npm/package.json b/extensions/npm/package.json index 4dd9ee6a71c..aa478b26c9b 100644 --- a/extensions/npm/package.json +++ b/extensions/npm/package.json @@ -56,7 +56,6 @@ { "id": "npm", "name": "%view.name%", - "when": "npm:showScriptExplorer", "icon": "images/code.svg", "visibility": "hidden" } @@ -232,6 +231,7 @@ "type": "boolean", "default": false, "scope": "resource", + "deprecationMessage": "The NPM Script Explorer is now available in 'Views' menu in the Explorer in all folders.", "description": "%config.npm.enableScriptExplorer%" }, "npm.enableRunFromFolder": { diff --git a/extensions/npm/src/features/packageJSONContribution.ts b/extensions/npm/src/features/packageJSONContribution.ts index 0e24b45aa28..f154a875239 100644 --- a/extensions/npm/src/features/packageJSONContribution.ts +++ b/extensions/npm/src/features/packageJSONContribution.ts @@ -249,7 +249,27 @@ export class PackageJSONContribution implements IJSONContribution { return null; } + private isValidNPMName(name: string): boolean { + // following rules from https://github.com/npm/validate-npm-package-name + if (!name || name.length > 214 || name.match(/^[_.]/)) { + return false; + } + const match = name.match(/^(?:@([^/]+?)[/])?([^/]+?)$/); + if (match) { + const scope = match[1]; + if (scope && encodeURIComponent(scope) !== scope) { + return false; + } + const name = match[2]; + return encodeURIComponent(name) === name; + } + return true; + } + private async fetchPackageInfo(pack: string): Promise { + if (!this.isValidNPMName(pack)) { + return undefined; // avoid unnecessary lookups + } let info: ViewPackageInfo | undefined; if (this.canRunNPM) { info = await this.npmView(pack); @@ -260,7 +280,6 @@ export class PackageJSONContribution implements IJSONContribution { return info; } - private npmView(pack: string): Promise { return new Promise((resolve, _reject) => { const command = 'npm view --json ' + pack + ' description dist-tags.latest homepage version'; diff --git a/extensions/npm/src/npmMain.ts b/extensions/npm/src/npmMain.ts index 764be6ea0fc..a92f554b759 100644 --- a/extensions/npm/src/npmMain.ts +++ b/extensions/npm/src/npmMain.ts @@ -8,7 +8,7 @@ import * as vscode from 'vscode'; import { addJSONProviders } from './features/jsonContributions'; import { runSelectedScript, selectAndRunScriptFromFolder } from './commands'; import { NpmScriptsTreeDataProvider } from './npmView'; -import { invalidateTasksCache, NpmTaskProvider, hasPackageJson } from './tasks'; +import { invalidateTasksCache, NpmTaskProvider } from './tasks'; import { invalidateHoverScriptsCache, NpmScriptHoverProvider } from './scriptHover'; let treeDataProvider: NpmScriptsTreeDataProvider | undefined; @@ -44,11 +44,6 @@ export async function activate(context: vscode.ExtensionContext): Promise registerHoverProvider(context); context.subscriptions.push(vscode.commands.registerCommand('npm.runSelectedScript', runSelectedScript)); - - if (await hasPackageJson()) { - vscode.commands.executeCommand('setContext', 'npm:showScriptExplorer', true); - } - context.subscriptions.push(vscode.commands.registerCommand('npm.runScriptFromFolder', selectAndRunScriptFromFolder)); } diff --git a/extensions/npm/src/npmView.ts b/extensions/npm/src/npmView.ts index 72ec421f775..c7c7835fa04 100644 --- a/extensions/npm/src/npmView.ts +++ b/extensions/npm/src/npmView.ts @@ -7,7 +7,7 @@ import { JSONVisitor, visit } from 'jsonc-parser'; import * as path from 'path'; import { commands, Event, EventEmitter, ExtensionContext, - Selection, Task2 as Task, + Selection, Task, TaskGroup, tasks, TextDocument, ThemeIcon, TreeDataProvider, TreeItem, TreeItemCollapsibleState, Uri, window, workspace, WorkspaceFolder } from 'vscode'; diff --git a/extensions/npm/src/tasks.ts b/extensions/npm/src/tasks.ts index 040e1f820cc..f7def2f8876 100644 --- a/extensions/npm/src/tasks.ts +++ b/extensions/npm/src/tasks.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { - TaskDefinition, Task2 as Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace, + TaskDefinition, Task, TaskGroup, WorkspaceFolder, RelativePattern, ShellExecution, Uri, workspace, DebugConfiguration, debug, TaskProvider, TextDocument, tasks, TaskScope, QuickPickItem } from 'vscode'; import * as path from 'path'; diff --git a/extensions/package.json b/extensions/package.json index 3307d7709cb..97870c8d51b 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "^4.0.1-insiders.20200813" + "typescript": "4.0.2" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/php/cgmanifest.json b/extensions/php/cgmanifest.json index f265c4b3184..ea157efc34e 100644 --- a/extensions/php/cgmanifest.json +++ b/extensions/php/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "language-php", "repositoryUrl": "https://github.com/atom/language-php", - "commitHash": "882f6c0e19f0ebf9dafa443bf4c3fc5626f76aed" + "commitHash": "11cdaf62a9d949d3aca550f1a58c9754de6b5ab0" } }, "license": "MIT", - "version": "0.44.4" + "version": "0.44.5" } ], "version": 1 diff --git a/extensions/php/syntaxes/php.tmLanguage.json b/extensions/php/syntaxes/php.tmLanguage.json index f04272f72f5..a995a1e82ab 100644 --- a/extensions/php/syntaxes/php.tmLanguage.json +++ b/extensions/php/syntaxes/php.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-php/commit/882f6c0e19f0ebf9dafa443bf4c3fc5626f76aed", + "version": "https://github.com/atom/language-php/commit/11cdaf62a9d949d3aca550f1a58c9754de6b5ab0", "scopeName": "source.php", "patterns": [ { @@ -146,7 +146,7 @@ "name": "keyword.other.use.php" } }, - "end": "(?<=})|(?=;)", + "end": "(?<=})|(?=;)|(?=\\?>)", "name": "meta.use.php", "patterns": [ { diff --git a/extensions/python/cgmanifest.json b/extensions/python/cgmanifest.json index 6b1df10ed8f..37a21b2de54 100644 --- a/extensions/python/cgmanifest.json +++ b/extensions/python/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "MagicStack/MagicPython", "repositoryUrl": "https://github.com/MagicStack/MagicPython", - "commitHash": "b4b2e6eb16fee36aea0788bf0aa1853c25f7d276" + "commitHash": "c9b3409deb69acec31bbf7913830e93a046b30cc" } }, "license": "MIT", diff --git a/extensions/python/syntaxes/MagicPython.tmLanguage.json b/extensions/python/syntaxes/MagicPython.tmLanguage.json index b8822299e63..0df9076dfc9 100644 --- a/extensions/python/syntaxes/MagicPython.tmLanguage.json +++ b/extensions/python/syntaxes/MagicPython.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/MagicStack/MagicPython/commit/b4b2e6eb16fee36aea0788bf0aa1853c25f7d276", + "version": "https://github.com/MagicStack/MagicPython/commit/b2b4f4ae7b4e6284e80bda8080106b93bd588f9e", "name": "MagicPython", "scopeName": "source.python", "patterns": [ @@ -634,9 +634,6 @@ }, "2": { "name": "invalid.illegal.dec.python" - }, - "3": { - "name": "invalid.illegal.dec.python" } } }, diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index eac084ddbc1..fb74d3696ef 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -3,10 +3,9 @@ const mappings = [ ['bat', 'source.batchfile'], ['c', 'source.c'], - ['cc', 'source.cpp'], ['clj', 'source.clojure'], ['coffee', 'source.coffee'], - ['cpp', 'source.cpp'], + ['cpp', 'source.cpp', '\\.(?:cpp|c\\+\\+|cc|cxx|hxx|h\\+\\+|hh)'], ['cs', 'source.cs'], ['cshtml', 'text.html.cshtml'], ['css', 'source.css'], @@ -17,8 +16,7 @@ const mappings = [ ['go', 'source.go'], ['groovy', 'source.groovy'], ['h', 'source.objc'], - ['handlebars', 'text.html.handlebars'], - ['hbs', 'text.html.handlebars'], + ['handlebars', 'text.html.handlebars', '\\.(?:handlebars|hbs)'], ['hlsl', 'source.hlsl'], ['hpp', 'source.objcpp'], ['html', 'text.html.basic'], @@ -35,10 +33,8 @@ const mappings = [ ['md', 'text.html.markdown'], ['mm', 'source.objcpp'], ['p6', 'source.perl.6'], - ['perl', 'source.perl'], + ['perl', 'source.perl', '\\.(?:perl|pl|pm)'], ['php', 'source.php'], - ['pl', 'source.perl'], - ['pm', 'source.perl'], ['ps1', 'source.powershell'], ['pug', 'text.pug'], ['py', 'source.python'], @@ -54,8 +50,7 @@ const mappings = [ ['tsx', 'source.tsx'], ['vb', 'source.asp.vb.net'], ['xml', 'text.xml'], - ['yaml', 'source.yaml'], - ['yml', 'source.yaml'], + ['yaml', 'source.yaml', '\\.(?:ya?ml)'], ]; const scopes = { diff --git a/extensions/search-result/syntaxes/searchResult.tmLanguage.json b/extensions/search-result/syntaxes/searchResult.tmLanguage.json index a8a5557c3e5..e2687fe8a72 100644 --- a/extensions/search-result/syntaxes/searchResult.tmLanguage.json +++ b/extensions/search-result/syntaxes/searchResult.tmLanguage.json @@ -84,9 +84,6 @@ { "include": "#c" }, - { - "include": "#cc" - }, { "include": "#clj" }, @@ -129,9 +126,6 @@ { "include": "#handlebars" }, - { - "include": "#hbs" - }, { "include": "#hlsl" }, @@ -186,12 +180,6 @@ { "include": "#php" }, - { - "include": "#pl" - }, - { - "include": "#pm" - }, { "include": "#ps1" }, @@ -240,9 +228,6 @@ { "include": "#yaml" }, - { - "include": "#yml" - }, { "match": "^(?!\\s)(.*?)([^\\\\\\/\\n]*)(:)$", "name": "meta.resultBlock.search string meta.path.search", @@ -453,92 +438,6 @@ } ] }, - "cc": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.cc)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.cpp" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.cpp" - } - ] - } - ] - }, "clj": { "name": "meta.resultBlock.search", "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.clj)(:)$", @@ -713,7 +612,7 @@ }, "cpp": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.cpp)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:cpp|c\\+\\+|cc|cxx|hxx|h\\+\\+|hh))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -1229,7 +1128,7 @@ }, "dockerfile": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*(?:dockerfile|Dockerfile))(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*(?:dockerfile|Dockerfile|containerfile|Containerfile))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -1659,93 +1558,7 @@ }, "handlebars": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.handlebars)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "text.html.handlebars" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "text.html.handlebars" - } - ] - } - ] - }, - "hbs": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.hbs)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:handlebars|hbs))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -3207,7 +3020,7 @@ }, "perl": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.perl)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:perl|pl|pm))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { @@ -3377,178 +3190,6 @@ } ] }, - "pl": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.pl)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - } - ] - }, - "pm": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.pm)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.perl" - } - ] - } - ] - }, "ps1": { "name": "meta.resultBlock.search", "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.ps1)(:)$", @@ -4841,93 +4482,7 @@ }, "yaml": { "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.yaml)(:)$", - "end": "^(?!\\s)", - "beginCaptures": { - "0": { - "name": "string meta.path.search" - }, - "1": { - "name": "meta.path.dirname.search" - }, - "2": { - "name": "meta.path.basename.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "name": "meta.resultLine.search meta.resultLine.multiLine.search", - "begin": "^ (?:\\s*)((\\d+) )", - "while": "^ (?:\\s*)(?:((\\d+)(:))|((\\d+) ))", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "whileCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - }, - "4": { - "name": "meta.resultLinePrefix.contextLinePrefix.search" - }, - "5": { - "name": "meta.resultLinePrefix.lineNumber.search" - } - }, - "patterns": [ - { - "include": "source.yaml" - } - ] - }, - { - "begin": "^ (?:\\s*)((\\d+)(:))", - "while": "(?=not)possible", - "name": "meta.resultLine.search meta.resultLine.singleLine.search", - "beginCaptures": { - "0": { - "name": "constant.numeric.integer meta.resultLinePrefix.search" - }, - "1": { - "name": "meta.resultLinePrefix.matchLinePrefix.search" - }, - "2": { - "name": "meta.resultLinePrefix.lineNumber.search" - }, - "3": { - "name": "punctuation.separator" - } - }, - "patterns": [ - { - "include": "source.yaml" - } - ] - } - ] - }, - "yml": { - "name": "meta.resultBlock.search", - "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.yml)(:)$", + "begin": "^(?!\\s)(.*?)([^\\\\\\/\\n]*\\.(?:ya?ml))(:)$", "end": "^(?!\\s)", "beginCaptures": { "0": { diff --git a/extensions/swift/syntaxes/swift.tmLanguage.json b/extensions/swift/syntaxes/swift.tmLanguage.json index 33cb2ca044a..91a374d4716 100644 --- a/extensions/swift/syntaxes/swift.tmLanguage.json +++ b/extensions/swift/syntaxes/swift.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/textmate/swift.tmbundle/commit/ecba759c1c2f46f69795fe2d01691030214dd5ff", + "version": "https://github.com/textmate/swift.tmbundle/commit/97d29d2073853c328e42239c5d38c96e2e2ade9c", "name": "Swift", "scopeName": "source.swift", "comment": "See swift.tmbundle/grammar-test.swift for test cases.", @@ -2620,7 +2620,7 @@ "name": "variable.language.swift" }, { - "match": "\\B(?:#file|#filePath|#line|#column|#function|#dsohandle)\\b|\\b(?:__FILE__|__LINE__|__COLUMN__|__FUNCTION__|__DSO_HANDLE__)\\b", + "match": "\\B(?:#file|#filePath|#fileID|#line|#column|#function|#dsohandle)\\b|\\b(?:__FILE__|__LINE__|__COLUMN__|__FUNCTION__|__DSO_HANDLE__)\\b", "name": "support.variable.swift" }, { diff --git a/extensions/theme-abyss/themes/abyss-color-theme.json b/extensions/theme-abyss/themes/abyss-color-theme.json index 39f93305b8d..7afe3bd963e 100644 --- a/extensions/theme-abyss/themes/abyss-color-theme.json +++ b/extensions/theme-abyss/themes/abyss-color-theme.json @@ -233,6 +233,20 @@ "foreground": "#22aa44" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -242,11 +256,14 @@ } }, { - "name": "Markup Setext Header", - "scope": "markup.heading.setext", + "name": "Markup Headings", + "scope": [ + "markup.heading", + "markup.heading.setext" + ], "settings": { - "fontStyle": "", - "foreground": "#ddbb88" + "fontStyle": "bold", + "foreground": "#6688cc" } } ], diff --git a/extensions/theme-defaults/themes/hc_black_defaults.json b/extensions/theme-defaults/themes/hc_black_defaults.json index 1a03010abff..495a15238dc 100644 --- a/extensions/theme-defaults/themes/hc_black_defaults.json +++ b/extensions/theme-defaults/themes/hc_black_defaults.json @@ -136,6 +136,7 @@ { "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#6796e6" } }, diff --git a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json index cdd22307117..38c8fe09968 100644 --- a/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json +++ b/extensions/theme-kimbie-dark/themes/kimbie-dark-color-theme.json @@ -260,7 +260,7 @@ "entity.name.section" ], "settings": { - "fontStyle": "", + "fontStyle": "bold", "foreground": "#8ab1b0" } }, diff --git a/extensions/theme-red/themes/Red-color-theme.json b/extensions/theme-red/themes/Red-color-theme.json index 8964f40a093..dbe80113209 100644 --- a/extensions/theme-red/themes/Red-color-theme.json +++ b/extensions/theme-red/themes/Red-color-theme.json @@ -350,6 +350,20 @@ "foreground": "#fb9a4bff" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -359,17 +373,15 @@ } }, { - "name": "Markup Headings", - "scope": "markup.heading", + "name": "Headings", + "scope": [ + "markup.heading", + "markup.heading.setext", + "punctuation.definition.heading", + "entity.name.section" + ], "settings": { - "foreground": "#fec758ff" - } - }, - { - "name": "Markup Setext Header", - "scope": "markup.heading.setext", - "settings": { - "fontStyle": "", + "fontStyle": "bold", "foreground": "#fec758ff" } }, diff --git a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json index b23ff8bb85c..eaf90258d35 100644 --- a/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json +++ b/extensions/theme-solarized-dark/themes/solarized-dark-color-theme.json @@ -270,6 +270,20 @@ "foreground": "#D33682" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -282,6 +296,7 @@ "name": "Markup Headings", "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#268BD2" } }, diff --git a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json index 21f530d00a3..77aa0f29079 100644 --- a/extensions/theme-solarized-light/themes/solarized-light-color-theme.json +++ b/extensions/theme-solarized-light/themes/solarized-light-color-theme.json @@ -273,6 +273,20 @@ "foreground": "#D33682" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -285,6 +299,7 @@ "name": "Markup Headings", "scope": "markup.heading", "settings": { + "fontStyle": "bold", "foreground": "#268BD2" } }, diff --git a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json index 0baee6822ef..bdccdb49d91 100644 --- a/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json +++ b/extensions/theme-tomorrow-night-blue/themes/tomorrow-night-blue-theme.json @@ -223,6 +223,20 @@ "foreground": "#FFC58F" } }, + { + "name": "Markup: Strong", + "scope": "markup.bold", + "settings": { + "fontStyle": "bold" + } + }, + { + "name": "Markup: Emphasis", + "scope": "markup.italic", + "settings": { + "fontStyle": "italic" + } + }, { "name": "Markup Inline", "scope": "markup.inline.raw", @@ -231,6 +245,13 @@ "foreground": "#FF9DA4" } }, + { + "name": "Markup Headings", + "scope": "markup.heading", + "settings": { + "fontStyle": "bold" + } + }, { "scope": "token.info-token", "settings": { diff --git a/extensions/typescript-basics/snippets/typescript.code-snippets b/extensions/typescript-basics/snippets/typescript.code-snippets index 0587884ee12..8eeb13e2e2d 100644 --- a/extensions/typescript-basics/snippets/typescript.code-snippets +++ b/extensions/typescript-basics/snippets/typescript.code-snippets @@ -167,16 +167,16 @@ "}" ], "description": "For-Of Loop" - }, - "For-Await-Of Loop": { - "prefix": "forawaitof", - "body": [ + }, + "For-Await-Of Loop": { + "prefix": "forawaitof", + "body": [ "for await (const ${1:iterator} of ${2:object}) {", "\t$0", "}" ], - "description": "For-Await-Of Loop" - }, + "description": "For-Await-Of Loop" + }, "Function Statement": { "prefix": "function", "body": [ @@ -278,5 +278,32 @@ "//#endregion" ], "description": "Folding Region End" + }, + "new Promise": { + "prefix": "newpromise", + "body": [ + "new Promise<$1:type>((resolve, reject) => {", + "\t$1", + "})" + ], + "description": "Create a new Promise" + }, + "Async Function Statement": { + "prefix": "async function", + "body": [ + "async function ${1:name}(${2:params}:${3:type}) {", + "\t$0", + "}" + ], + "description": "Async Function Statement" + }, + "Async Function Expression": { + "prefix": "async arrow function", + "body": [ + "async (${1:params}:${2:type}) => {", + "\t$0", + "}" + ], + "description": "Async Function Expression" } } diff --git a/extensions/typescript-language-features/src/languageFeatures/hover.ts b/extensions/typescript-language-features/src/languageFeatures/hover.ts index c6c4860f663..a4de074897f 100644 --- a/extensions/typescript-language-features/src/languageFeatures/hover.ts +++ b/extensions/typescript-language-features/src/languageFeatures/hover.ts @@ -5,7 +5,8 @@ import * as vscode from 'vscode'; import type * as Proto from '../protocol'; -import { ClientCapability, ITypeScriptServiceClient } from '../typescriptService'; +import { localize } from '../tsServer/versionProvider'; +import { ClientCapability, ITypeScriptServiceClient, ServerType } from '../typescriptService'; import { conditionalRegistration, requireSomeCapability } from '../utils/dependentRegistration'; import { DocumentSelector } from '../utils/documentSelector'; import { markdownDocumentation } from '../utils/previewer'; @@ -35,17 +36,30 @@ class TypeScriptHoverProvider implements vscode.HoverProvider { } return new vscode.Hover( - TypeScriptHoverProvider.getContents(response.body), + this.getContents(response.body, response._serverType), typeConverters.Range.fromTextSpan(response.body)); } - private static getContents( - data: Proto.QuickInfoResponseBody + private getContents( + data: Proto.QuickInfoResponseBody, + source: ServerType | undefined, ) { - const parts = []; + const parts: vscode.MarkedString[] = []; if (data.displayString) { - parts.push({ language: 'typescript', value: data.displayString }); + const displayParts: string[] = []; + + if (source === ServerType.Syntax && this.client.capabilities.has(ClientCapability.Semantic)) { + displayParts.push( + localize({ + key: 'loadingPrefix', + comment: ['Prefix displayed for hover entries while the server is still loading'] + }, "(loading...)")); + } + + displayParts.push(data.displayString); + + parts.push({ language: 'typescript', value: displayParts.join(' ') }); } parts.push(markdownDocumentation(data.documentation, data.tags)); return parts; diff --git a/extensions/typescript-language-features/src/lazyClientHost.ts b/extensions/typescript-language-features/src/lazyClientHost.ts index 7f136285215..73dee25e6c3 100644 --- a/extensions/typescript-language-features/src/lazyClientHost.ts +++ b/extensions/typescript-language-features/src/lazyClientHost.ts @@ -11,6 +11,7 @@ import { TsServerProcessFactory } from './tsServer/server'; import { ITypeScriptVersionProvider } from './tsServer/versionProvider'; import TypeScriptServiceClientHost from './typeScriptServiceClientHost'; import { flatten } from './utils/arrays'; +import * as fileSchemes from './utils/fileSchemes'; import { standardLanguageDescriptions } from './utils/languageDescription'; import { lazy, Lazy } from './utils/lazy'; import ManagedFileContextManager from './utils/managedFileContext'; @@ -85,5 +86,6 @@ function isSupportedDocument( supportedLanguage: readonly string[], document: vscode.TextDocument ): boolean { - return supportedLanguage.indexOf(document.languageId) >= 0; + return supportedLanguage.indexOf(document.languageId) >= 0 + && !fileSchemes.disabledSchemes.has(document.uri.scheme); } diff --git a/extensions/typescript-language-features/src/protocol.d.ts b/extensions/typescript-language-features/src/protocol.d.ts index 6e926eb8d7e..e81fe81f2db 100644 --- a/extensions/typescript-language-features/src/protocol.d.ts +++ b/extensions/typescript-language-features/src/protocol.d.ts @@ -1,2 +1,12 @@ import * as Proto from 'typescript/lib/protocol'; export = Proto; + +declare enum ServerType { + Syntax = 'syntax', + Semantic = 'semantic', +} +declare module 'typescript/lib/protocol' { + interface Response { + readonly _serverType?: ServerType; + } +} diff --git a/extensions/typescript-language-features/src/task/taskProvider.ts b/extensions/typescript-language-features/src/task/taskProvider.ts index 0024a3596f3..85d3b574ae3 100644 --- a/extensions/typescript-language-features/src/task/taskProvider.ts +++ b/extensions/typescript-language-features/src/task/taskProvider.ts @@ -203,7 +203,7 @@ class TscTaskProvider implements vscode.TaskProvider { } private getBuildTask(workspaceFolder: vscode.WorkspaceFolder | undefined, label: string, command: string, args: string[], buildTaskidentifier: TypeScriptTaskDefinition): vscode.Task { - const buildTask = new vscode.Task2( + const buildTask = new vscode.Task( buildTaskidentifier, workspaceFolder || vscode.TaskScope.Workspace, localize('buildTscLabel', 'build - {0}', label), diff --git a/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts b/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts index 1289b87b4bc..5c8cf18c73c 100644 --- a/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts +++ b/extensions/typescript-language-features/src/test/functionCallSnippet.test.ts @@ -128,4 +128,13 @@ suite('typescript function call snippets', () => { ).snippet.value, 'foobar(${1:param})$0'); }); + + test('Should skip over this parameter', async () => { + assert.strictEqual( + snippetForFunctionCall( + { label: 'foobar', }, + [{ "text": "function", "kind": "keyword" }, { "text": " ", "kind": "space" }, { "text": "foobar", "kind": "functionName" }, { "text": "(", "kind": "punctuation" }, { "text": "this", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ",", "kind": "punctuation" }, { "text": "param", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "string", "kind": "keyword" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" }] + ).snippet.value, + 'foobar(${1:param})$0'); + }); }); diff --git a/extensions/typescript-language-features/src/test/server.test.ts b/extensions/typescript-language-features/src/test/server.test.ts index 7e27e366b5c..5caad737a48 100644 --- a/extensions/typescript-language-features/src/test/server.test.ts +++ b/extensions/typescript-language-features/src/test/server.test.ts @@ -9,6 +9,7 @@ import * as stream from 'stream'; import type * as Proto from '../protocol'; import { NodeRequestCanceller } from '../tsServer/cancellation.electron'; import { ProcessBasedTsServer, TsServerProcess } from '../tsServer/server'; +import { ServerType } from '../typescriptService'; import { nulToken } from '../utils/cancellation'; import { Logger } from '../utils/logger'; import { TelemetryReporter } from '../utils/telemetry'; @@ -64,7 +65,7 @@ suite('Server', () => { test('should send requests with increasing sequence numbers', async () => { const process = new FakeServerProcess(); - const server = new ProcessBasedTsServer('semantic', process, undefined, new NodeRequestCanceller('semantic', tracer), undefined!, NoopTelemetryReporter, tracer); + const server = new ProcessBasedTsServer('semantic', ServerType.Semantic, process, undefined, new NodeRequestCanceller('semantic', tracer), undefined!, NoopTelemetryReporter, tracer); const onWrite1 = process.onWrite(); server.executeImpl('geterr', {}, { isAsync: false, token: nulToken, expectsResult: true }); diff --git a/extensions/typescript-language-features/src/tsServer/server.ts b/extensions/typescript-language-features/src/tsServer/server.ts index fc7841322bd..6ad3d015679 100644 --- a/extensions/typescript-language-features/src/tsServer/server.ts +++ b/extensions/typescript-language-features/src/tsServer/server.ts @@ -9,7 +9,7 @@ import { EventName } from '../protocol.const'; import { CallbackMap } from '../tsServer/callbackMap'; import { RequestItem, RequestQueue, RequestQueueingType } from '../tsServer/requestQueue'; import { TypeScriptServerError } from '../tsServer/serverError'; -import { ServerResponse, TypeScriptRequests } from '../typescriptService'; +import { ServerResponse, ServerType, TypeScriptRequests } from '../typescriptService'; import { TypeScriptServiceConfiguration } from '../utils/configuration'; import { Disposable } from '../utils/dispose'; import { TelemetryReporter } from '../utils/telemetry'; @@ -77,6 +77,7 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe constructor( private readonly _serverId: string, + private readonly _serverSource: ServerType, private readonly _process: TsServerProcess, private readonly _tsServerLogFile: string | undefined, private readonly _requestCanceller: OngoingRequestCanceller, @@ -130,7 +131,14 @@ export class ProcessBasedTsServer extends Disposable implements ITypeScriptServe try { switch (message.type) { case 'response': - this.dispatchResponse(message as Proto.Response); + if (this._serverSource) { + this.dispatchResponse({ + ...(message as Proto.Response), + _serverType: this._serverSource + }); + } else { + this.dispatchResponse(message as Proto.Response); + } break; case 'event': diff --git a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts index f9d70d858e0..96b0c6b411e 100644 --- a/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts +++ b/extensions/typescript-language-features/src/tsServer/serverProcess.electron.ts @@ -173,12 +173,19 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { } private static getExecArgv(kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration): string[] { + const args: string[] = []; + const debugPort = this.getDebugPort(kind); - const inspectFlag = process.env['TSS_DEBUG_BRK'] ? '--inspect-brk' : '--inspect'; - return [ - ...(debugPort ? [`${inspectFlag}=${debugPort}`] : []), - ...(configuration.maxTsServerMemory ? [`--max-old-space-size=${configuration.maxTsServerMemory}`] : []) - ]; + if (debugPort) { + const inspectFlag = ChildServerProcess.getTssDebugBrk() ? '--inspect-brk' : '--inspect'; + args.push(`${inspectFlag}=${debugPort}`); + } + + if (configuration.maxTsServerMemory) { + args.push(`--max-old-space-size=${configuration.maxTsServerMemory}`); + } + + return args; } private static getDebugPort(kind: TsServerProcessKind): number | undefined { @@ -186,7 +193,7 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { // We typically only want to debug the main semantic server return undefined; } - const value = process.env['TSS_DEBUG_BRK'] || process.env['TSS_DEBUG']; + const value = ChildServerProcess.getTssDebugBrk() || ChildServerProcess.getTssDebug(); if (value) { const port = parseInt(value); if (!isNaN(port)) { @@ -196,6 +203,14 @@ export class ChildServerProcess extends Disposable implements TsServerProcess { return undefined; } + private static getTssDebug(): string | undefined { + return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG' : 'TSS_DEBUG']; + } + + private static getTssDebugBrk(): string | undefined { + return process.env[vscode.env.remoteName ? 'TSS_REMOTE_DEBUG_BRK' : 'TSS_DEBUG_BRK']; + } + private constructor( private readonly _process: child_process.ChildProcess, ) { diff --git a/extensions/typescript-language-features/src/tsServer/spawner.ts b/extensions/typescript-language-features/src/tsServer/spawner.ts index 70f1b41574e..7f4aaeefe13 100644 --- a/extensions/typescript-language-features/src/tsServer/spawner.ts +++ b/extensions/typescript-language-features/src/tsServer/spawner.ts @@ -6,7 +6,7 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { OngoingRequestCancellerFactory } from '../tsServer/cancellation'; -import { ClientCapabilities, ClientCapability } from '../typescriptService'; +import { ClientCapabilities, ClientCapability, ServerType } from '../typescriptService'; import API from '../utils/api'; import { SeparateSyntaxServerConfiguration, TsServerLogLevel, TypeScriptServiceConfiguration } from '../utils/configuration'; import { Logger } from '../utils/logger'; @@ -143,6 +143,7 @@ export class TypeScriptServerSpawner { return new ProcessBasedTsServer( kind, + this.kindToServerType(kind), process!, tsServerLogFile, canceller, @@ -151,6 +152,19 @@ export class TypeScriptServerSpawner { this._tracer); } + private kindToServerType(kind: TsServerProcessKind): ServerType { + switch (kind) { + case TsServerProcessKind.Syntax: + return ServerType.Syntax; + + case TsServerProcessKind.Main: + case TsServerProcessKind.Semantic: + case TsServerProcessKind.Diagnostics: + default: + return ServerType.Semantic; + } + } + private getTsServerArgs( kind: TsServerProcessKind, configuration: TypeScriptServiceConfiguration, diff --git a/extensions/typescript-language-features/src/typescriptService.ts b/extensions/typescript-language-features/src/typescriptService.ts index de32927d816..8a59d97105c 100644 --- a/extensions/typescript-language-features/src/typescriptService.ts +++ b/extensions/typescript-language-features/src/typescriptService.ts @@ -13,6 +13,11 @@ import { TypeScriptServiceConfiguration } from './utils/configuration'; import { PluginManager } from './utils/plugins'; import { TelemetryReporter } from './utils/telemetry'; +export enum ServerType { + Syntax = 'syntax', + Semantic = 'semantic', +} + export namespace ServerResponse { export class Cancelled { diff --git a/extensions/typescript-language-features/src/typescriptServiceClient.ts b/extensions/typescript-language-features/src/typescriptServiceClient.ts index 4d5886049d0..a9e6bf10d7f 100644 --- a/extensions/typescript-language-features/src/typescriptServiceClient.ts +++ b/extensions/typescript-language-features/src/typescriptServiceClient.ts @@ -636,6 +636,10 @@ export default class TypeScriptServiceClient extends Disposable implements IType } public normalizedPath(resource: vscode.Uri): string | undefined { + if (fileSchemes.disabledSchemes.has(resource.scheme)) { + return undefined; + } + switch (resource.scheme) { case fileSchemes.file: { @@ -648,10 +652,6 @@ export default class TypeScriptServiceClient extends Disposable implements IType // Both \ and / must be escaped in regular expressions return result.replace(new RegExp('\\' + this.pathSeparator, 'g'), '/'); } - case fileSchemes.git: - { - return undefined; - } default: { return this.inMemoryResourcePrefix + resource.toString(true); @@ -665,7 +665,9 @@ export default class TypeScriptServiceClient extends Disposable implements IType public toOpenedFilePath(document: vscode.TextDocument): string | undefined { if (!this.bufferSyncSupport.ensureHasBuffer(document.uri)) { - console.error(`Unexpected resource ${document.uri}`); + if (!fileSchemes.disabledSchemes.has(document.uri.scheme)) { + console.error(`Unexpected resource ${document.uri}`); + } return undefined; } return this.toPath(document.uri) || undefined; diff --git a/extensions/typescript-language-features/src/utils/fileSchemes.ts b/extensions/typescript-language-features/src/utils/fileSchemes.ts index 4e94d547bd6..d465a60326e 100644 --- a/extensions/typescript-language-features/src/utils/fileSchemes.ts +++ b/extensions/typescript-language-features/src/utils/fileSchemes.ts @@ -6,9 +6,19 @@ export const file = 'file'; export const untitled = 'untitled'; export const git = 'git'; +/** Live share scheme */ +export const vsls = 'vsls'; export const walkThroughSnippet = 'walkThroughSnippet'; export const semanticSupportedSchemes = [ file, untitled, ]; + +/** + * File scheme for which JS/TS language feature should be disabled + */ +export const disabledSchemes = new Set([ + git, + vsls +]); diff --git a/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts b/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts index 6faf19eff48..e0185db581b 100644 --- a/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts +++ b/extensions/typescript-language-features/src/utils/snippetForFunctionCall.ts @@ -73,7 +73,9 @@ function getParameterListParts( const next = displayParts[i + 1]; // Skip optional parameters const nameIsFollowedByOptionalIndicator = next && next.text === '?'; - if (!nameIsFollowedByOptionalIndicator) { + // Skip this parameter + const nameIsThis = part.text === 'this'; + if (!nameIsFollowedByOptionalIndicator && !nameIsThis) { parts.push(part); } hasOptionalParameters = hasOptionalParameters || nameIsFollowedByOptionalIndicator; diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts index 8379f9b1ab7..ea50f38b1b9 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/index.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Single Folder Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Single Folder Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Single Folder Tests'; +} else { + suite = 'Integration Single Folder Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts index 53acdcf23dc..37426834335 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.tasks.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, Task2, env, UIKind, ShellExecution, TaskExecution, Terminal, Event } from 'vscode'; +import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomExecution, Pseudoterminal, TaskScope, commands, env, UIKind, ShellExecution, TaskExecution, Terminal, Event } from 'vscode'; // Disable tasks tests: // - Web https://github.com/microsoft/vscode/issues/90528 @@ -94,7 +94,7 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx }; return Promise.resolve(pty); }); - const task = new Task2(kind, TaskScope.Workspace, taskName, taskType, execution); + const task = new Task(kind, TaskScope.Workspace, taskName, taskType, execution); result.push(task); return result; }, @@ -151,7 +151,7 @@ import { window, tasks, Disposable, TaskDefinition, Task, EventEmitter, CustomEx }; return Promise.resolve(pty); }); - const task = new Task2(kind, TaskScope.Workspace, taskName, taskType, execution); + const task = new Task(kind, TaskScope.Workspace, taskName, taskType, execution); result.push(task); return result; }, diff --git a/extensions/vscode-api-tests/src/workspace-tests/index.ts b/extensions/vscode-api-tests/src/workspace-tests/index.ts index dfef493b2ab..9486d8ed3e5 100644 --- a/extensions/vscode-api-tests/src/workspace-tests/index.ts +++ b/extensions/vscode-api-tests/src/workspace-tests/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Workspace Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Workspace Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Workspace Tests'; +} else { + suite = 'Integration Workspace Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-colorize-tests/src/index.ts b/extensions/vscode-colorize-tests/src/index.ts index a315ee36112..691ba5c6f07 100644 --- a/extensions/vscode-colorize-tests/src/index.ts +++ b/extensions/vscode-colorize-tests/src/index.ts @@ -20,7 +20,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-custom-editor-tests/src/test/index.ts b/extensions/vscode-custom-editor-tests/src/test/index.ts index 6d80cca8048..a60622b2f28 100644 --- a/extensions/vscode-custom-editor-tests/src/test/index.ts +++ b/extensions/vscode-custom-editor-tests/src/test/index.ts @@ -20,7 +20,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 4ed7605f894..53ba12569a9 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -62,8 +62,9 @@ ], "notebookOutputRenderer": [ { - "viewType": "notebookCoreTestRenderer", + "id": "notebookCoreTestRenderer", "displayName": "Notebook Core Test Renderer", + "entrypoint": "./src/customRenderer.js", "mimeTypes": [ "text/custom" ] diff --git a/extensions/vscode-notebook-tests/src/customRenderer.js b/extensions/vscode-notebook-tests/src/customRenderer.js index 75e2ec1eb7a..f23538e38a7 100644 --- a/extensions/vscode-notebook-tests/src/customRenderer.js +++ b/extensions/vscode-notebook-tests/src/customRenderer.js @@ -11,3 +11,11 @@ vscode.postMessage({ firstMessage: true } }); + +const notebook = acquireNotebookRendererApi('notebookCoreTestRenderer'); + +notebook.onDidCreateOutput(({ element, mimeType }) => { + const div = document.createElement('div'); + div.innerText = `Hello ${mimeType}!`; + element.appendChild(div); +}); diff --git a/extensions/vscode-notebook-tests/src/index.ts b/extensions/vscode-notebook-tests/src/index.ts index 2125e68c3d8..293c02db743 100644 --- a/extensions/vscode-notebook-tests/src/index.ts +++ b/extensions/vscode-notebook-tests/src/index.ts @@ -6,21 +6,31 @@ const path = require('path'); const testRunner = require('vscode/lib/testrunner'); -const suite = 'Integration Notebook Tests'; - const options: any = { ui: 'tdd', useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), timeout: 60000 }; +// These integration tests is being run in multiple environments (electron, web, remote) +// so we need to set the suite name based on the environment as the suite name is used +// for the test results file name +let suite = ''; +if (process.env.VSCODE_BROWSER) { + suite = `${process.env.VSCODE_BROWSER} Browser Integration Notebook Tests`; +} else if (process.env.REMOTE_VSCODE) { + suite = 'Remote Integration Notebook Tests'; +} else { + suite = 'Integration Notebook Tests'; +} + if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { options.reporter = 'mocha-multi-reporters'; options.reporterOptions = { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index c8ea3282c2c..df7ace2781f 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import * as path from 'path'; import { smokeTestActivate } from './notebookSmokeTestMain'; export function activate(context: vscode.ExtensionContext): any { @@ -117,16 +116,4 @@ export function activate(context: vscode.ExtensionContext): any { }, cancelCellExecution: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell) => { } })); - - const preloadUri = vscode.Uri.file(path.resolve(__dirname, '../src/customRenderer.js')); - context.subscriptions.push(vscode.notebook.registerNotebookOutputRenderer('notebookCoreTestRenderer', { - mimeTypes: [ - 'text/custom' - ] - }, { - preloads: [preloadUri], - render(_document: vscode.NotebookDocument, _request: vscode.NotebookRenderRequest): string { - return '
test
'; - } - })); } diff --git a/extensions/vscode-web-playground/.gitignore b/extensions/vscode-web-playground/.gitignore deleted file mode 100644 index c19bd94aaa7..00000000000 --- a/extensions/vscode-web-playground/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -out -node_modules diff --git a/extensions/vscode-web-playground/.vscode/tasks.json b/extensions/vscode-web-playground/.vscode/tasks.json deleted file mode 100644 index 390a93a3a7f..00000000000 --- a/extensions/vscode-web-playground/.vscode/tasks.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "2.0.0", - "command": "npm", - "type": "shell", - "presentation": { - "reveal": "silent" - }, - "args": ["run", "compile"], - "isBackground": true, - "problemMatcher": "$tsc-watch" -} diff --git a/extensions/vscode-web-playground/.vscodeignore b/extensions/vscode-web-playground/.vscodeignore deleted file mode 100644 index 32fe3f03697..00000000000 --- a/extensions/vscode-web-playground/.vscodeignore +++ /dev/null @@ -1,11 +0,0 @@ -.vscode/** -build/** -dist/** -out/** -src/** -typings/** -.gitignore -extension-browser.webpack.config.js -extension.webpack.config.js -tsconfig.json -yarn.lock diff --git a/extensions/vscode-web-playground/extension-browser.webpack.config.js b/extensions/vscode-web-playground/extension-browser.webpack.config.js deleted file mode 100644 index dfd50aeff96..00000000000 --- a/extensions/vscode-web-playground/extension-browser.webpack.config.js +++ /dev/null @@ -1,18 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -//@ts-check - -'use strict'; -const path = require('path'); -const withBrowserDefaults = require('../shared.webpack.config').browser; - -module.exports = withBrowserDefaults({ - context: __dirname, - node: false, - entry: { - extension: './src/extension.ts', - } -}); diff --git a/extensions/vscode-web-playground/package.json b/extensions/vscode-web-playground/package.json deleted file mode 100644 index 67ebd70edd4..00000000000 --- a/extensions/vscode-web-playground/package.json +++ /dev/null @@ -1,106 +0,0 @@ -{ - "name": "vscode-web-playground", - "description": "Web playground for VS Code", - "version": "0.0.1", - "publisher": "vscode", - "license": "MIT", - "enableProposedApi": true, - "private": true, - "activationEvents": [ - "onFileSystem:memfs", - "onDebug" - ], - "browser": "./dist/browser/extension", - "main": "./out/extension", - "engines": { - "vscode": "^1.25.0" - }, - "contributes": { - "taskDefinitions": [ - { - "type": "custombuildscript", - "required": [ - "flavor" - ], - "properties": { - "flavor": { - "type": "string", - "description": "The build flavor. Should be either '32' or '64'." - }, - "flags": { - "type": "array", - "description": "Additional build flags." - } - } - } - ], - "breakpoints": [ - { - "language": "markdown" - } - ], - "debuggers": [ - { - "type": "mock", - "label": "Mock Debug", - "languages": [ - "markdown" - ], - "configurationAttributes": { - "launch": { - "required": [ - "program" - ], - "properties": { - "program": { - "type": "string", - "description": "Absolute path to a text file.", - "default": "${workspaceFolder}/file.md" - }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": true - }, - "trace": { - "type": "boolean", - "description": "Enable logging of the Debug Adapter Protocol.", - "default": true - } - } - } - }, - "initialConfigurations": [ - { - "type": "mock", - "request": "launch", - "name": "Debug file.md", - "program": "${workspaceFolder}/file.md" - } - ] - } - ], - "resourceLabelFormatters": [ - { - "scheme": "github", - "authority": "*", - "formatting": { - "label": "${authority}${path}", - "separator": "/", - "workspaceSuffix": "GitHub" - } - } - ] - }, - "scripts": { - "compile": "node ./node_modules/vscode/bin/compile -watch -p ./", - "compile-web": "npx webpack-cli --config extension.webpack.config --mode none", - "watch-web": "npx webpack-cli --config extension.webpack.config --mode none --watch --info-verbosity verbose", - "vscode:prepublish": "node ../../node_modules/gulp/bin/gulp.js --gulpfile ../../build/gulpfile.extensions.js compile-extension:vscode-web-playground ./tsconfig.json" - }, - "devDependencies": { - "@types/mocha": "2.2.43", - "mocha-junit-reporter": "^1.17.0", - "mocha-multi-reporters": "^1.1.7" - } -} diff --git a/extensions/vscode-web-playground/src/exampleFiles.ts b/extensions/vscode-web-playground/src/exampleFiles.ts deleted file mode 100644 index a385f7b72e7..00000000000 --- a/extensions/vscode-web-playground/src/exampleFiles.ts +++ /dev/null @@ -1,310 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export const largeTSFile = `/// -/// - -module Mankala { -export var storeHouses = [6,13]; -export var svgNS = 'http://www.w3.org/2000/svg'; - -function createSVGRect(r:Rectangle) { - var rect = document.createElementNS(svgNS,'rect'); - rect.setAttribute('x', r.x.toString()); - rect.setAttribute('y', r.y.toString()); - rect.setAttribute('width', r.width.toString()); - rect.setAttribute('height', r.height.toString()); - return rect; -} - -function createSVGEllipse(r:Rectangle) { - var ell = document.createElementNS(svgNS,'ellipse'); - ell.setAttribute('rx',(r.width/2).toString()); - ell.setAttribute('ry',(r.height/2).toString()); - ell.setAttribute('cx',(r.x+r.width/2).toString()); - ell.setAttribute('cy',(r.y+r.height/2).toString()); - return ell; -} - -function createSVGEllipsePolar(angle:number,radius:number,tx:number,ty:number,cxo:number,cyo:number) { - var ell = document.createElementNS(svgNS,'ellipse'); - ell.setAttribute('rx',radius.toString()); - ell.setAttribute('ry',(radius/3).toString()); - ell.setAttribute('cx',cxo.toString()); - ell.setAttribute('cy',cyo.toString()); - var dangle = angle*(180/Math.PI); - ell.setAttribute('transform','rotate('+dangle+','+cxo+','+cyo+') translate('+tx+','+ty+')'); - return ell; -} - -function createSVGInscribedCircle(sq:Square) { - var circle = document.createElementNS(svgNS,'circle'); - circle.setAttribute('r',(sq.length/2).toString()); - circle.setAttribute('cx',(sq.x+(sq.length/2)).toString()); - circle.setAttribute('cy',(sq.y+(sq.length/2)).toString()); - return circle; -} - -export class Position { - - seedCounts:number[]; - startMove:number; - turn:number; - - constructor(seedCounts:number[],startMove:number,turn:number) { - this.seedCounts = seedCounts; - this.startMove = startMove; - this.turn = turn; - } - - score() { - var baseScore = this.seedCounts[storeHouses[1-this.turn]]-this.seedCounts[storeHouses[this.turn]]; - var otherSpaces = homeSpaces[this.turn]; - var sum = 0; - for (var k = 0,len = otherSpaces.length;k0) { - features.clear(); - var len = this.seedCounts.length; - for (var i = 0;i0) { - if (nextSpace==storeHouses[this.turn]) { - features.seedStoredCount++; - } - if ((nextSpace!=storeHouses[1-this.turn])) { - nextSeedCounts[nextSpace]++; - seedCount--; - } - if (seedCount==0) { - if (nextSpace==storeHouses[this.turn]) { - features.turnContinues = true; - } - else { - if ((nextSeedCounts[nextSpace]==1)&& - (nextSpace>=firstHomeSpace[this.turn])&& - (nextSpace<=lastHomeSpace[this.turn])) { - // capture - var capturedSpace = capturedSpaces[nextSpace]; - if (capturedSpace>=0) { - features.spaceCaptured = capturedSpace; - features.capturedCount = nextSeedCounts[capturedSpace]; - nextSeedCounts[capturedSpace] = 0; - nextSeedCounts[storeHouses[this.turn]] += features.capturedCount; - features.seedStoredCount += nextSeedCounts[capturedSpace]; - } - } - } - } - nextSpace = (nextSpace+1)%14; - } - return true; - } - else { - return false; - } - } -} - -export class SeedCoords { - tx:number; - ty:number; - angle:number; - - constructor(tx:number, ty:number, angle:number) { - this.tx = tx; - this.ty = ty; - this.angle = angle; - } -} - -export class DisplayPosition extends Position { - - config:SeedCoords[][]; - - constructor(seedCounts:number[],startMove:number,turn:number) { - super(seedCounts,startMove,turn); - - this.config = []; - - for (var i = 0;i(); - } - } - - - seedCircleRect(rect:Rectangle,seedCount:number,board:Element,seed:number) { - var coords = this.config[seed]; - var sq = rect.inner(0.95).square(); - var cxo = (sq.width/2)+sq.x; - var cyo = (sq.height/2)+sq.y; - var seedNumbers = [5,7,9,11]; - var ringIndex = 0; - var ringRem = seedNumbers[ringIndex]; - var angleDelta = (2*Math.PI)/ringRem; - var angle = angleDelta; - var seedLength = sq.width/(seedNumbers.length<<1); - var crMax = sq.width/2-(seedLength/2); - var pit = createSVGInscribedCircle(sq); - if (seed<7) { - pit.setAttribute('fill','brown'); - } - else { - pit.setAttribute('fill','saddlebrown'); - } - board.appendChild(pit); - var seedsSeen = 0; - while (seedCount > 0) { - if (ringRem == 0) { - ringIndex++; - ringRem = seedNumbers[ringIndex]; - angleDelta = (2*Math.PI)/ringRem; - angle = angleDelta; - } - var tx:number; - var ty:number; - var tangle = angle; - if (coords.length>seedsSeen) { - tx = coords[seedsSeen].tx; - ty = coords[seedsSeen].ty; - tangle = coords[seedsSeen].angle; - } - else { - tx = (Math.random()*crMax)-(crMax/3); - ty = (Math.random()*crMax)-(crMax/3); - coords[seedsSeen] = new SeedCoords(tx,ty,angle); - } - var ell = createSVGEllipsePolar(tangle,seedLength,tx,ty,cxo,cyo); - board.appendChild(ell); - angle += angleDelta; - ringRem--; - seedCount--; - seedsSeen++; - } - } - - toCircleSVG() { - var seedDivisions = 14; - var board = document.createElementNS(svgNS,'svg'); - var boardRect = new Rectangle(0,0,1800,800); - board.setAttribute('width','1800'); - board.setAttribute('height','800'); - var whole = createSVGRect(boardRect); - whole.setAttribute('fill','tan'); - board.appendChild(whole); - var labPlayLab = boardRect.proportionalSplitVert(20,760,20); - var playSurface = labPlayLab[1]; - var storeMainStore = playSurface.proportionalSplitHoriz(8,48,8); - var mainPair = storeMainStore[1].subDivideVert(2); - var playerRects = [mainPair[0].subDivideHoriz(6), mainPair[1].subDivideHoriz(6)]; - // reverse top layer because storehouse on left - for (var k = 0;k<3;k++) { - var temp = playerRects[0][k]; - playerRects[0][k] = playerRects[0][5-k]; - playerRects[0][5-k] = temp; - } - var storehouses = [storeMainStore[0],storeMainStore[2]]; - var playerSeeds = this.seedCounts.length>>1; - for (var i = 0;i<2;i++) { - var player = playerRects[i]; - var storehouse = storehouses[i]; - var r:Rectangle; - for (var j = 0;j(); - } - } - } - return board; - } -} -} -`; - -export const debuggableFile = `# VS Code Mock Debug - -This is a starter sample for developing VS Code debug adapters. - -**Mock Debug** simulates a debug adapter for Visual Studio Code. -It supports *step*, *continue*, *breakpoints*, *exceptions*, and -*variable access* but it is not connected to any real debugger. - -The sample is meant as an educational piece showing how to implement a debug -adapter for VS Code. It can be used as a starting point for developing a real adapter. - -More information about how to develop a new debug adapter can be found -[here](https://code.visualstudio.com/docs/extensions/example-debuggers). -Or discuss debug adapters on Gitter: -[![Gitter Chat](https://img.shields.io/badge/chat-online-brightgreen.svg)](https://gitter.im/Microsoft/vscode) - -## Using Mock Debug - -* Install the **Mock Debug** extension in VS Code. -* Create a new 'program' file 'readme.md' and enter several lines of arbitrary text. -* Switch to the debug viewlet and press the gear dropdown. -* Select the debug environment "Mock Debug". -* Press the green 'play' button to start debugging. - -You can now 'step through' the 'readme.md' file, set and hit breakpoints, and run into exceptions (if the word exception appears in a line). - -![Mock Debug](file.jpg) - -## Build and Run - -[![build status](https://travis-ci.org/Microsoft/vscode-mock-debug.svg?branch=master)](https://travis-ci.org/Microsoft/vscode-mock-debug) -[![build status](https://ci.appveyor.com/api/projects/status/empmw5q1tk6h1fly/branch/master?svg=true)](https://ci.appveyor.com/project/weinand/vscode-mock-debug) - - -* Clone the project [https://github.com/Microsoft/vscode-mock-debug.git](https://github.com/Microsoft/vscode-mock-debug.git) -* Open the project folder in VS Code. -* Press 'F5' to build and launch Mock Debug in another VS Code window. In that window: -* Open a new workspace, create a new 'program' file 'readme.md' and enter several lines of arbitrary text. -* Switch to the debug viewlet and press the gear dropdown. -* Select the debug environment "Mock Debug". -* Press 'F5' to start debugging.`; - -export function getImageFile(): Uint8Array { - const data = atob(`/9j/4AAQSkZJRgABAQAASABIAAD/2wCEAA4ODg4ODhcODhchFxcXIS0hISEhLTktLS0tLTlFOTk5OTk5RUVFRUVFRUVSUlJSUlJgYGBgYGxsbGxsbGxsbGwBERISGxkbLxkZL3FMP0xxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcXFxcf/AABEIAFYAZAMBIgACEQEDEQH/xAB1AAACAwEBAQAAAAAAAAAAAAAABAMFBgIBBxAAAgIBAwMCBQQCAwAAAAAAAQIAAxEEBSESMUFRcRMiIzJhFIGRoQbBQlKxAQEBAQEAAAAAAAAAAAAAAAABAgADEQEBAQADAQEAAAAAAAAAAAAAARESITECQf/aAAwDAQACEQMRAD8A2LEZkLc/bKxbdYEHWoyfEze56zXpqRTTYUyPHiVrY2TVZyMzhFZMg8iYE6jcVXAusY98KMnj2lhRu+4aLoGuTNTYPV5APnyDNyPFp6EY3EsO3kxnVVLZVg8z2tw9YsXkGQpcbGIbxHQzep0vw8Jgc8n28CJJRY30lBwzf1iaa2ku/HmMV01VW/k/6hh0abTDTafpPcTytmckEewjeosAqJEj0yDo6yO/rFLzoGME5nIAXtGSM9uwnjLn8zFECw7QneITMWouR7gj9/Ep94061bjXa32WDGfzOGuCXKy9/wDc0FlFe5aX4OpHJHBHcSfT4w246bWJar6MsCwKnp9DOF0r6XRiu5snvg9hNK217vQeih0tXwzcED895R7voNfWoN9gOT2QH/2T3mHrda3Y+p9ppZuSV/qR0j6r+5ju2oun2ypOwCAASGikISzdySf5lxLsAdRPpIqw91xC/wDHvGbAAh88RnSVCjT9b8E/MYsguerTqWuYKo8k4ESTcttsPSmoQ+zCZPWPbvWqsvLE0IxCL4wPP7xEW7TXeKsvaGABOMdLef2ky7ejevX0tBWy5Qhh6jmS9IIxPm6XazbW69K56M/aeRibnSaqyytWtGCfE0+tazDhrHpCdixT5EJSWD1BPkcjsYxpN21FWEcdu0dG3hl8rIX0YqUgDqkSrq/0+6oyfOOZT7hqxqLMKMk8ARfS0fqGatAR04yCY+u3OpLt38e0rQl0tzsFrc8rxj0lqqDHMzujIXUMGPI4mjS1MTCvG8gRLddYE2811n5nHTJ9RaAsztzZ1AZhlX9fBi0VWgWzbSqahfpWfa/iSnatMuqOpVgVPIHGMzc6erS3aQVOoZSMFTK19i2pTwGA9Axx/E58b+K2M8lP6/Urp6BkA5Y+OPE112nrIFeOw8RMajQ7dWU0iAH8TyrVG0mw8EypMFuk7K9TS5RGJHiEYsuUtmEWO1KO2RGDRSVJzj1MiQhOQIx8QEYK5hGpUUJVc1lTgcDjEe1FPxqGQHBZSMiQqa8/Z38xgOoHB/aIfJNVZrdFqirsVbsfzLXT7+UQLYmcDHBlh/k+g+KP1dOCV+4efcTNbdtGq3CxQiMKyeX7CGqxqtDuK7lYK2BXnAz3JMuNZoPpDAyV5zHNt2bRbcA1S/Pjljyf7jerWxx0V4wQeZgynxrUXoUnIif629GJY595cptr1N9XJYjOfEi1G3LYMLgH1m04qxelrAtnj/qZYIvUPpMcHwYtTT8FzVaMN6+sslqVF6gcQ1sRivPccwjS314+bGYRBnqzws6FhUfL7CQ8gdI7+TDIHHgcSVGBYRznMXfUL2J5ngPUOYCpfM2tiq1tnUpVRnMe0DGtAKyQIw+mU4GJCKmrPy+I6V0lxYYIzxOCtdjZyVIMRqtPsYx8RT37+sdRhsFlHzcyC0J0kmcfqFX5cxC7VAk4OPUQtM+UVtYf7vH8iKP8SnKg5U9xHQwsGV7jxF9QnWACMEcgwlUjT4ZUE+YRRLGRehwciEpLRMAAT6SALlIQkF4kl7HEIQLwuQfac9RPeEJi5H3TruvvmEJo1QOcgGQuvVg+sITM8rDKeDHVItXkQhKgqM6esnJEIQlJf//Z`); - return Uint8Array.from([...data].map(x => x.charCodeAt(0))); -} - -// encoded from 'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюя' -export const windows1251File = Uint8Array.from([192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]); - -// encoded from '中国abc' -export const gbkFile = Uint8Array.from([214, 208, 185, 250, 97, 98, 99]); diff --git a/extensions/vscode-web-playground/src/extension.ts b/extensions/vscode-web-playground/src/extension.ts deleted file mode 100644 index 1d5df92bbcc..00000000000 --- a/extensions/vscode-web-playground/src/extension.ts +++ /dev/null @@ -1,3927 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// -// ############################################################################ -// -// ! USED FOR RUNNING VSCODE OUT OF SOURCES FOR WEB ! -// ! DO NOT REMOVE ! -// -// ############################################################################ -// - -import * as vscode from 'vscode'; -import { MemFS } from './memfs'; - -declare const navigator: unknown; - -export function activate(context: vscode.ExtensionContext) { - if (typeof navigator === 'object') { // do not run under node.js - const memFs = enableFs(context); - - if (vscode.workspace.workspaceFolders?.some(f => f.uri.scheme === MemFS.scheme)) { - memFs.seed(); - enableProblems(context); - enableTasks(); - enableDebug(context, memFs); - - vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(`memfs:/sample-folder/large.ts`)); - } - } -} - -function enableFs(context: vscode.ExtensionContext): MemFS { - const memFs = new MemFS(); - context.subscriptions.push(memFs); - - return memFs; -} - -function enableProblems(context: vscode.ExtensionContext): void { - const collection = vscode.languages.createDiagnosticCollection('test'); - if (vscode.window.activeTextEditor) { - updateDiagnostics(vscode.window.activeTextEditor.document, collection); - } - context.subscriptions.push(vscode.window.onDidChangeActiveTextEditor(editor => { - if (editor) { - updateDiagnostics(editor.document, collection); - } - })); -} - -function updateDiagnostics(document: vscode.TextDocument, collection: vscode.DiagnosticCollection): void { - if (document && document.fileName === '/sample-folder/large.ts') { - collection.set(document.uri, [{ - code: '', - message: 'cannot assign twice to immutable variable `storeHouses`', - range: new vscode.Range(new vscode.Position(4, 12), new vscode.Position(4, 32)), - severity: vscode.DiagnosticSeverity.Error, - source: '', - relatedInformation: [ - new vscode.DiagnosticRelatedInformation(new vscode.Location(document.uri, new vscode.Range(new vscode.Position(1, 8), new vscode.Position(1, 9))), 'first assignment to `x`') - ] - }, { - code: '', - message: 'function does not follow naming conventions', - range: new vscode.Range(new vscode.Position(7, 10), new vscode.Position(7, 23)), - severity: vscode.DiagnosticSeverity.Warning, - source: '' - }]); - } else { - collection.clear(); - } -} - -function enableTasks(): void { - - interface CustomBuildTaskDefinition extends vscode.TaskDefinition { - /** - * The build flavor. Should be either '32' or '64'. - */ - flavor: string; - - /** - * Additional build flags - */ - flags?: string[]; - } - - class CustomBuildTaskProvider implements vscode.TaskProvider { - static CustomBuildScriptType: string = 'custombuildscript'; - private tasks: vscode.Task[] | undefined; - - // We use a CustomExecution task when state needs to be shared accross runs of the task or when - // the task requires use of some VS Code API to run. - // If you don't need to share state between runs and if you don't need to execute VS Code API in your task, - // then a simple ShellExecution or ProcessExecution should be enough. - // Since our build has this shared state, the CustomExecution is used below. - private sharedState: string | undefined; - - constructor(private workspaceRoot: string) { } - - public async provideTasks(): Promise { - return this.getTasks(); - } - - public resolveTask(_task: vscode.Task): vscode.Task | undefined { - const flavor: string = _task.definition.flavor; - if (flavor) { - const definition: CustomBuildTaskDefinition = _task.definition; - return this.getTask(definition.flavor, definition.flags ? definition.flags : [], definition); - } - return undefined; - } - - private getTasks(): vscode.Task[] { - if (this.tasks !== undefined) { - return this.tasks; - } - // In our fictional build, we have two build flavors - const flavors: string[] = ['32', '64']; - // Each flavor can have some options. - const flags: string[][] = [['watch', 'incremental'], ['incremental'], []]; - - this.tasks = []; - flavors.forEach(flavor => { - flags.forEach(flagGroup => { - this.tasks!.push(this.getTask(flavor, flagGroup)); - }); - }); - return this.tasks; - } - - private getTask(flavor: string, flags: string[], definition?: CustomBuildTaskDefinition): vscode.Task { - if (definition === undefined) { - definition = { - type: CustomBuildTaskProvider.CustomBuildScriptType, - flavor, - flags - }; - } - return new vscode.Task2(definition, vscode.TaskScope.Workspace, `${flavor} ${flags.join(' ')}`, - CustomBuildTaskProvider.CustomBuildScriptType, new vscode.CustomExecution(async (): Promise => { - // When the task is executed, this callback will run. Here, we setup for running the task. - return new CustomBuildTaskTerminal(this.workspaceRoot, flavor, flags, () => this.sharedState, (state: string) => this.sharedState = state); - })); - } - } - - class CustomBuildTaskTerminal implements vscode.Pseudoterminal { - private writeEmitter = new vscode.EventEmitter(); - onDidWrite: vscode.Event = this.writeEmitter.event; - private closeEmitter = new vscode.EventEmitter(); - onDidClose?: vscode.Event = this.closeEmitter.event; - - private fileWatcher: vscode.FileSystemWatcher | undefined; - - constructor(private workspaceRoot: string, _flavor: string, private flags: string[], private getSharedState: () => string | undefined, private setSharedState: (state: string) => void) { - } - - open(_initialDimensions: vscode.TerminalDimensions | undefined): void { - // At this point we can start using the terminal. - if (this.flags.indexOf('watch') > -1) { - let pattern = this.workspaceRoot + '/customBuildFile'; - this.fileWatcher = vscode.workspace.createFileSystemWatcher(pattern); - this.fileWatcher.onDidChange(() => this.doBuild()); - this.fileWatcher.onDidCreate(() => this.doBuild()); - this.fileWatcher.onDidDelete(() => this.doBuild()); - } - this.doBuild(); - } - - close(): void { - // The terminal has been closed. Shutdown the build. - if (this.fileWatcher) { - this.fileWatcher.dispose(); - } - } - - private async doBuild(): Promise { - return new Promise((resolve) => { - this.writeEmitter.fire('Starting build...\r\n'); - let isIncremental = this.flags.indexOf('incremental') > -1; - if (isIncremental) { - if (this.getSharedState()) { - this.writeEmitter.fire('Using last build results: ' + this.getSharedState() + '\r\n'); - } else { - isIncremental = false; - this.writeEmitter.fire('No result from last build. Doing full build.\r\n'); - } - } - - // Since we don't actually build anything in this example set a timeout instead. - setTimeout(() => { - const date = new Date(); - this.setSharedState(date.toTimeString() + ' ' + date.toDateString()); - this.writeEmitter.fire('Build complete.\r\n\r\n'); - if (this.flags.indexOf('watch') === -1) { - this.closeEmitter.fire(); - resolve(); - } - }, isIncremental ? 1000 : 4000); - }); - } - } - - vscode.tasks.registerTaskProvider(CustomBuildTaskProvider.CustomBuildScriptType, new CustomBuildTaskProvider(vscode.workspace.rootPath!)); -} - -//--------------------------------------------------------------------------- -// DEBUG -//--------------------------------------------------------------------------- - -function enableDebug(context: vscode.ExtensionContext, memFs: MemFS): void { - context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider('mock', new MockConfigurationProvider())); - context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory('mock', new MockDebugAdapterDescriptorFactory(memFs))); -} - -/** - * Declaration module describing the VS Code debug protocol. - * Auto-generated from json schema. Do not edit manually. - */ -declare module DebugProtocol { - - /** Base class of requests, responses, and events. */ - export interface ProtocolMessage { - /** Sequence number (also known as message ID). For protocol messages of type 'request' this ID can be used to cancel the request. */ - seq: number; - /** Message type. - Values: 'request', 'response', 'event', etc. - */ - type: string; - } - - /** A client or debug adapter initiated request. */ - export interface Request extends ProtocolMessage { - // type: 'request'; - /** The command to execute. */ - command: string; - /** Object containing arguments for the command. */ - arguments?: any; - } - - /** A debug adapter initiated event. */ - export interface Event extends ProtocolMessage { - // type: 'event'; - /** Type of event. */ - event: string; - /** Event-specific information. */ - body?: any; - } - - /** Response for a request. */ - export interface Response extends ProtocolMessage { - // type: 'response'; - /** Sequence number of the corresponding request. */ - request_seq: number; - /** Outcome of the request. - If true, the request was successful and the 'body' attribute may contain the result of the request. - If the value is false, the attribute 'message' contains the error in short form and the 'body' may contain additional information (see 'ErrorResponse.body.error'). - */ - success: boolean; - /** The command requested. */ - command: string; - /** Contains the raw error in short form if 'success' is false. - This raw error might be interpreted by the frontend and is not shown in the UI. - Some predefined values exist. - Values: - 'cancelled': request was cancelled. - etc. - */ - message?: string; - /** Contains request result if success is true and optional error details if success is false. */ - body?: any; - } - - /** On error (whenever 'success' is false), the body can provide more details. */ - export interface ErrorResponse extends Response { - body: { - /** An optional, structured error message. */ - error?: Message; - }; - } - - /** Cancel request; value of command field is 'cancel'. - The 'cancel' request is used by the frontend to indicate that it is no longer interested in the result produced by a specific request issued earlier. - This request has a hint characteristic: a debug adapter can only be expected to make a 'best effort' in honouring this request but there are no guarantees. - The 'cancel' request may return an error if it could not cancel an operation but a frontend should refrain from presenting this error to end users. - A frontend client should only call this request if the capability 'supportsCancelRequest' is true. - The request that got canceled still needs to send a response back. - This can either be a normal result ('success' attribute true) or an error response ('success' attribute false and the 'message' set to 'cancelled'). - Returning partial results from a cancelled request is possible but please note that a frontend client has no generic way for detecting that a response is partial or not. - */ - export interface CancelRequest extends Request { - // command: 'cancel'; - arguments?: CancelArguments; - } - - /** Arguments for 'cancel' request. */ - export interface CancelArguments { - /** The ID (attribute 'seq') of the request to cancel. */ - requestId?: number; - } - - /** Response to 'cancel' request. This is just an acknowledgement, so no body field is required. */ - export interface CancelResponse extends Response { - } - - /** Event message for 'initialized' event type. - This event indicates that the debug adapter is ready to accept configuration requests (e.g. SetBreakpointsRequest, SetExceptionBreakpointsRequest). - A debug adapter is expected to send this event when it is ready to accept configuration requests (but not before the 'initialize' request has finished). - The sequence of events/requests is as follows: - - adapters sends 'initialized' event (after the 'initialize' request has returned) - - frontend sends zero or more 'setBreakpoints' requests - - frontend sends one 'setFunctionBreakpoints' request - - frontend sends a 'setExceptionBreakpoints' request if one or more 'exceptionBreakpointFilters' have been defined (or if 'supportsConfigurationDoneRequest' is not defined or false) - - frontend sends other future configuration requests - - frontend sends one 'configurationDone' request to indicate the end of the configuration. - */ - export interface InitializedEvent extends Event { - // event: 'initialized'; - } - - /** Event message for 'stopped' event type. - The event indicates that the execution of the debuggee has stopped due to some condition. - This can be caused by a break point previously set, a stepping action has completed, by executing a debugger statement etc. - */ - export interface StoppedEvent extends Event { - // event: 'stopped'; - body: { - /** The reason for the event. - For backward compatibility this string is shown in the UI if the 'description' attribute is missing (but it must not be translated). - Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', 'function breakpoint', 'data breakpoint', etc. - */ - reason: string; - /** The full reason for the event, e.g. 'Paused on exception'. This string is shown in the UI as is and must be translated. */ - description?: string; - /** The thread which was stopped. */ - threadId?: number; - /** A value of true hints to the frontend that this event should not change the focus. */ - preserveFocusHint?: boolean; - /** Additional information. E.g. if reason is 'exception', text contains the exception name. This string is shown in the UI. */ - text?: string; - /** If 'allThreadsStopped' is true, a debug adapter can announce that all threads have stopped. - - The client should use this information to enable that all threads can be expanded to access their stacktraces. - - If the attribute is missing or false, only the thread with the given threadId can be expanded. - */ - allThreadsStopped?: boolean; - }; - } - - /** Event message for 'continued' event type. - The event indicates that the execution of the debuggee has continued. - Please note: a debug adapter is not expected to send this event in response to a request that implies that execution continues, e.g. 'launch' or 'continue'. - It is only necessary to send a 'continued' event if there was no previous request that implied this. - */ - export interface ContinuedEvent extends Event { - // event: 'continued'; - body: { - /** The thread which was continued. */ - threadId: number; - /** If 'allThreadsContinued' is true, a debug adapter can announce that all threads have continued. */ - allThreadsContinued?: boolean; - }; - } - - /** Event message for 'exited' event type. - The event indicates that the debuggee has exited and returns its exit code. - */ - export interface ExitedEvent extends Event { - // event: 'exited'; - body: { - /** The exit code returned from the debuggee. */ - exitCode: number; - }; - } - - /** Event message for 'terminated' event type. - The event indicates that debugging of the debuggee has terminated. This does **not** mean that the debuggee itself has exited. - */ - export interface TerminatedEvent extends Event { - // event: 'terminated'; - body?: { - /** A debug adapter may set 'restart' to true (or to an arbitrary object) to request that the front end restarts the session. - The value is not interpreted by the client and passed unmodified as an attribute '__restart' to the 'launch' and 'attach' requests. - */ - restart?: any; - }; - } - - /** Event message for 'thread' event type. - The event indicates that a thread has started or exited. - */ - export interface ThreadEvent extends Event { - // event: 'thread'; - body: { - /** The reason for the event. - Values: 'started', 'exited', etc. - */ - reason: string; - /** The identifier of the thread. */ - threadId: number; - }; - } - - /** Event message for 'output' event type. - The event indicates that the target has produced some output. - */ - export interface OutputEvent extends Event { - // event: 'output'; - body: { - /** The output category. If not specified, 'console' is assumed. - Values: 'console', 'stdout', 'stderr', 'telemetry', etc. - */ - category?: string; - /** The output to report. */ - output: string; - /** If an attribute 'variablesReference' exists and its value is > 0, the output contains objects which can be retrieved by passing 'variablesReference' to the 'variables' request. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference?: number; - /** An optional source location where the output was produced. */ - source?: Source; - /** An optional source location line where the output was produced. */ - line?: number; - /** An optional source location column where the output was produced. */ - column?: number; - /** Optional data to report. For the 'telemetry' category the data will be sent to telemetry, for the other categories the data is shown in JSON format. */ - data?: any; - }; - } - - /** Event message for 'breakpoint' event type. - The event indicates that some information about a breakpoint has changed. - */ - export interface BreakpointEvent extends Event { - // event: 'breakpoint'; - body: { - /** The reason for the event. - Values: 'changed', 'new', 'removed', etc. - */ - reason: string; - /** The 'id' attribute is used to find the target breakpoint and the other attributes are used as the new values. */ - breakpoint: Breakpoint; - }; - } - - /** Event message for 'module' event type. - The event indicates that some information about a module has changed. - */ - export interface ModuleEvent extends Event { - // event: 'module'; - body: { - /** The reason for the event. */ - reason: 'new' | 'changed' | 'removed'; - /** The new, changed, or removed module. In case of 'removed' only the module id is used. */ - module: Module; - }; - } - - /** Event message for 'loadedSource' event type. - The event indicates that some source has been added, changed, or removed from the set of all loaded sources. - */ - export interface LoadedSourceEvent extends Event { - // event: 'loadedSource'; - body: { - /** The reason for the event. */ - reason: 'new' | 'changed' | 'removed'; - /** The new, changed, or removed source. */ - source: Source; - }; - } - - /** Event message for 'process' event type. - The event indicates that the debugger has begun debugging a new process. Either one that it has launched, or one that it has attached to. - */ - export interface ProcessEvent extends Event { - // event: 'process'; - body: { - /** The logical name of the process. This is usually the full path to process's executable file. Example: /home/example/myproj/program.js. */ - name: string; - /** The system process id of the debugged process. This property will be missing for non-system processes. */ - systemProcessId?: number; - /** If true, the process is running on the same computer as the debug adapter. */ - isLocalProcess?: boolean; - /** Describes how the debug engine started debugging this process. - 'launch': Process was launched under the debugger. - 'attach': Debugger attached to an existing process. - 'attachForSuspendedLaunch': A project launcher component has launched a new process in a suspended state and then asked the debugger to attach. - */ - startMethod?: 'launch' | 'attach' | 'attachForSuspendedLaunch'; - /** The size of a pointer or address for this process, in bits. This value may be used by clients when formatting addresses for display. */ - pointerSize?: number; - }; - } - - /** Event message for 'capabilities' event type. - The event indicates that one or more capabilities have changed. - Since the capabilities are dependent on the frontend and its UI, it might not be possible to change that at random times (or too late). - Consequently this event has a hint characteristic: a frontend can only be expected to make a 'best effort' in honouring individual capabilities but there are no guarantees. - Only changed capabilities need to be included, all other capabilities keep their values. - */ - export interface CapabilitiesEvent extends Event { - // event: 'capabilities'; - body: { - /** The set of updated capabilities. */ - capabilities: Capabilities; - }; - } - - /** RunInTerminal request; value of command field is 'runInTerminal'. - This request is sent from the debug adapter to the client to run a command in a terminal. This is typically used to launch the debuggee in a terminal provided by the client. - */ - export interface RunInTerminalRequest extends Request { - // command: 'runInTerminal'; - arguments: RunInTerminalRequestArguments; - } - - /** Arguments for 'runInTerminal' request. */ - export interface RunInTerminalRequestArguments { - /** What kind of terminal to launch. */ - kind?: 'integrated' | 'external'; - /** Optional title of the terminal. */ - title?: string; - /** Working directory of the command. */ - cwd: string; - /** List of arguments. The first argument is the command to run. */ - args: string[]; - /** Environment key-value pairs that are added to or removed from the default environment. */ - env?: { [key: string]: string | null; }; - } - - /** Response to 'runInTerminal' request. */ - export interface RunInTerminalResponse extends Response { - body: { - /** The process ID. The value should be less than or equal to 2147483647 (2^31 - 1). */ - processId?: number; - /** The process ID of the terminal shell. The value should be less than or equal to 2147483647 (2^31 - 1). */ - shellProcessId?: number; - }; - } - - /** Initialize request; value of command field is 'initialize'. - The 'initialize' request is sent as the first request from the client to the debug adapter in order to configure it with client capabilities and to retrieve capabilities from the debug adapter. - Until the debug adapter has responded to with an 'initialize' response, the client must not send any additional requests or events to the debug adapter. In addition the debug adapter is not allowed to send any requests or events to the client until it has responded with an 'initialize' response. - The 'initialize' request may only be sent once. - */ - export interface InitializeRequest extends Request { - // command: 'initialize'; - arguments: InitializeRequestArguments; - } - - /** Arguments for 'initialize' request. */ - export interface InitializeRequestArguments { - /** The ID of the (frontend) client using this adapter. */ - clientID?: string; - /** The human readable name of the (frontend) client using this adapter. */ - clientName?: string; - /** The ID of the debug adapter. */ - adapterID: string; - /** The ISO-639 locale of the (frontend) client using this adapter, e.g. en-US or de-CH. */ - locale?: string; - /** If true all line numbers are 1-based (default). */ - linesStartAt1?: boolean; - /** If true all column numbers are 1-based (default). */ - columnsStartAt1?: boolean; - /** Determines in what format paths are specified. The default is 'path', which is the native format. - Values: 'path', 'uri', etc. - */ - pathFormat?: string; - /** Client supports the optional type attribute for variables. */ - supportsVariableType?: boolean; - /** Client supports the paging of variables. */ - supportsVariablePaging?: boolean; - /** Client supports the runInTerminal request. */ - supportsRunInTerminalRequest?: boolean; - /** Client supports memory references. */ - supportsMemoryReferences?: boolean; - } - - /** Response to 'initialize' request. */ - export interface InitializeResponse extends Response { - /** The capabilities of this debug adapter. */ - body?: Capabilities; - } - - /** ConfigurationDone request; value of command field is 'configurationDone'. - The client of the debug protocol must send this request at the end of the sequence of configuration requests (which was started by the 'initialized' event). - */ - export interface ConfigurationDoneRequest extends Request { - // command: 'configurationDone'; - arguments?: ConfigurationDoneArguments; - } - - /** Arguments for 'configurationDone' request. */ - export interface ConfigurationDoneArguments { - } - - /** Response to 'configurationDone' request. This is just an acknowledgement, so no body field is required. */ - export interface ConfigurationDoneResponse extends Response { - } - - /** Launch request; value of command field is 'launch'. - The launch request is sent from the client to the debug adapter to start the debuggee with or without debugging (if 'noDebug' is true). Since launching is debugger/runtime specific, the arguments for this request are not part of this specification. - */ - export interface LaunchRequest extends Request { - // command: 'launch'; - arguments: LaunchRequestArguments; - } - - /** Arguments for 'launch' request. Additional attributes are implementation specific. */ - export interface LaunchRequestArguments { - /** If noDebug is true the launch request should launch the program without enabling debugging. */ - noDebug?: boolean; - /** Optional data from the previous, restarted session. - The data is sent as the 'restart' attribute of the 'terminated' event. - The client should leave the data intact. - */ - __restart?: any; - } - - /** Response to 'launch' request. This is just an acknowledgement, so no body field is required. */ - export interface LaunchResponse extends Response { - } - - /** Attach request; value of command field is 'attach'. - The attach request is sent from the client to the debug adapter to attach to a debuggee that is already running. Since attaching is debugger/runtime specific, the arguments for this request are not part of this specification. - */ - export interface AttachRequest extends Request { - // command: 'attach'; - arguments: AttachRequestArguments; - } - - /** Arguments for 'attach' request. Additional attributes are implementation specific. */ - export interface AttachRequestArguments { - /** Optional data from the previous, restarted session. - The data is sent as the 'restart' attribute of the 'terminated' event. - The client should leave the data intact. - */ - __restart?: any; - } - - /** Response to 'attach' request. This is just an acknowledgement, so no body field is required. */ - export interface AttachResponse extends Response { - } - - /** Restart request; value of command field is 'restart'. - Restarts a debug session. If the capability 'supportsRestartRequest' is missing or has the value false, - the client will implement 'restart' by terminating the debug adapter first and then launching it anew. - A debug adapter can override this default behaviour by implementing a restart request - and setting the capability 'supportsRestartRequest' to true. - */ - export interface RestartRequest extends Request { - // command: 'restart'; - arguments?: RestartArguments; - } - - /** Arguments for 'restart' request. */ - export interface RestartArguments { - } - - /** Response to 'restart' request. This is just an acknowledgement, so no body field is required. */ - export interface RestartResponse extends Response { - } - - /** Disconnect request; value of command field is 'disconnect'. - The 'disconnect' request is sent from the client to the debug adapter in order to stop debugging. It asks the debug adapter to disconnect from the debuggee and to terminate the debug adapter. If the debuggee has been started with the 'launch' request, the 'disconnect' request terminates the debuggee. If the 'attach' request was used to connect to the debuggee, 'disconnect' does not terminate the debuggee. This behavior can be controlled with the 'terminateDebuggee' argument (if supported by the debug adapter). - */ - export interface DisconnectRequest extends Request { - // command: 'disconnect'; - arguments?: DisconnectArguments; - } - - /** Arguments for 'disconnect' request. */ - export interface DisconnectArguments { - /** A value of true indicates that this 'disconnect' request is part of a restart sequence. */ - restart?: boolean; - /** Indicates whether the debuggee should be terminated when the debugger is disconnected. - If unspecified, the debug adapter is free to do whatever it thinks is best. - A client can only rely on this attribute being properly honored if a debug adapter returns true for the 'supportTerminateDebuggee' capability. - */ - terminateDebuggee?: boolean; - } - - /** Response to 'disconnect' request. This is just an acknowledgement, so no body field is required. */ - export interface DisconnectResponse extends Response { - } - - /** Terminate request; value of command field is 'terminate'. - The 'terminate' request is sent from the client to the debug adapter in order to give the debuggee a chance for terminating itself. - */ - export interface TerminateRequest extends Request { - // command: 'terminate'; - arguments?: TerminateArguments; - } - - /** Arguments for 'terminate' request. */ - export interface TerminateArguments { - /** A value of true indicates that this 'terminate' request is part of a restart sequence. */ - restart?: boolean; - } - - /** Response to 'terminate' request. This is just an acknowledgement, so no body field is required. */ - export interface TerminateResponse extends Response { - } - - /** BreakpointLocations request; value of command field is 'breakpointLocations'. - The 'breakpointLocations' request returns all possible locations for source breakpoints in a given range. - */ - export interface BreakpointLocationsRequest extends Request { - // command: 'breakpointLocations'; - arguments?: BreakpointLocationsArguments; - } - - /** Arguments for 'breakpointLocations' request. */ - export interface BreakpointLocationsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ - source: Source; - /** Start line of range to search possible breakpoint locations in. If only the line is specified, the request returns all possible locations in that line. */ - line: number; - /** Optional start column of range to search possible breakpoint locations in. If no start column is given, the first column in the start line is assumed. */ - column?: number; - /** Optional end line of range to search possible breakpoint locations in. If no end line is given, then the end line is assumed to be the start line. */ - endLine?: number; - /** Optional end column of range to search possible breakpoint locations in. If no end column is given, then it is assumed to be in the last column of the end line. */ - endColumn?: number; - } - - /** Response to 'breakpointLocations' request. - Contains possible locations for source breakpoints. - */ - export interface BreakpointLocationsResponse extends Response { - body: { - /** Sorted set of possible breakpoint locations. */ - breakpoints: BreakpointLocation[]; - }; - } - - /** SetBreakpoints request; value of command field is 'setBreakpoints'. - Sets multiple breakpoints for a single source and clears all previous breakpoints in that source. - To clear all breakpoint for a source, specify an empty array. - When a breakpoint is hit, a 'stopped' event (with reason 'breakpoint') is generated. - */ - export interface SetBreakpointsRequest extends Request { - // command: 'setBreakpoints'; - arguments: SetBreakpointsArguments; - } - - /** Arguments for 'setBreakpoints' request. */ - export interface SetBreakpointsArguments { - /** The source location of the breakpoints; either 'source.path' or 'source.reference' must be specified. */ - source: Source; - /** The code locations of the breakpoints. */ - breakpoints?: SourceBreakpoint[]; - /** Deprecated: The code locations of the breakpoints. */ - lines?: number[]; - /** A value of true indicates that the underlying source has been modified which results in new breakpoint locations. */ - sourceModified?: boolean; - } - - /** Response to 'setBreakpoints' request. - Returned is information about each breakpoint created by this request. - This includes the actual code location and whether the breakpoint could be verified. - The breakpoints returned are in the same order as the elements of the 'breakpoints' - (or the deprecated 'lines') array in the arguments. - */ - export interface SetBreakpointsResponse extends Response { - body: { - /** Information about the breakpoints. The array elements are in the same order as the elements of the 'breakpoints' (or the deprecated 'lines') array in the arguments. */ - breakpoints: Breakpoint[]; - }; - } - - /** SetFunctionBreakpoints request; value of command field is 'setFunctionBreakpoints'. - Replaces all existing function breakpoints with new function breakpoints. - To clear all function breakpoints, specify an empty array. - When a function breakpoint is hit, a 'stopped' event (with reason 'function breakpoint') is generated. - */ - export interface SetFunctionBreakpointsRequest extends Request { - // command: 'setFunctionBreakpoints'; - arguments: SetFunctionBreakpointsArguments; - } - - /** Arguments for 'setFunctionBreakpoints' request. */ - export interface SetFunctionBreakpointsArguments { - /** The function names of the breakpoints. */ - breakpoints: FunctionBreakpoint[]; - } - - /** Response to 'setFunctionBreakpoints' request. - Returned is information about each breakpoint created by this request. - */ - export interface SetFunctionBreakpointsResponse extends Response { - body: { - /** Information about the breakpoints. The array elements correspond to the elements of the 'breakpoints' array. */ - breakpoints: Breakpoint[]; - }; - } - - /** SetExceptionBreakpoints request; value of command field is 'setExceptionBreakpoints'. - The request configures the debuggers response to thrown exceptions. If an exception is configured to break, a 'stopped' event is fired (with reason 'exception'). - */ - export interface SetExceptionBreakpointsRequest extends Request { - // command: 'setExceptionBreakpoints'; - arguments: SetExceptionBreakpointsArguments; - } - - /** Arguments for 'setExceptionBreakpoints' request. */ - export interface SetExceptionBreakpointsArguments { - /** IDs of checked exception options. The set of IDs is returned via the 'exceptionBreakpointFilters' capability. */ - filters: string[]; - /** Configuration options for selected exceptions. */ - exceptionOptions?: ExceptionOptions[]; - } - - /** Response to 'setExceptionBreakpoints' request. This is just an acknowledgement, so no body field is required. */ - export interface SetExceptionBreakpointsResponse extends Response { - } - - /** DataBreakpointInfo request; value of command field is 'dataBreakpointInfo'. - Obtains information on a possible data breakpoint that could be set on an expression or variable. - */ - export interface DataBreakpointInfoRequest extends Request { - // command: 'dataBreakpointInfo'; - arguments: DataBreakpointInfoArguments; - } - - /** Arguments for 'dataBreakpointInfo' request. */ - export interface DataBreakpointInfoArguments { - /** Reference to the Variable container if the data breakpoint is requested for a child of the container. */ - variablesReference?: number; - /** The name of the Variable's child to obtain data breakpoint information for. If variableReference isn’t provided, this can be an expression. */ - name: string; - } - - /** Response to 'dataBreakpointInfo' request. */ - export interface DataBreakpointInfoResponse extends Response { - body: { - /** An identifier for the data on which a data breakpoint can be registered with the setDataBreakpoints request or null if no data breakpoint is available. */ - dataId: string | null; - /** UI string that describes on what data the breakpoint is set on or why a data breakpoint is not available. */ - description: string; - /** Optional attribute listing the available access types for a potential data breakpoint. A UI frontend could surface this information. */ - accessTypes?: DataBreakpointAccessType[]; - /** Optional attribute indicating that a potential data breakpoint could be persisted across sessions. */ - canPersist?: boolean; - }; - } - - /** SetDataBreakpoints request; value of command field is 'setDataBreakpoints'. - Replaces all existing data breakpoints with new data breakpoints. - To clear all data breakpoints, specify an empty array. - When a data breakpoint is hit, a 'stopped' event (with reason 'data breakpoint') is generated. - */ - export interface SetDataBreakpointsRequest extends Request { - // command: 'setDataBreakpoints'; - arguments: SetDataBreakpointsArguments; - } - - /** Arguments for 'setDataBreakpoints' request. */ - export interface SetDataBreakpointsArguments { - /** The contents of this array replaces all existing data breakpoints. An empty array clears all data breakpoints. */ - breakpoints: DataBreakpoint[]; - } - - /** Response to 'setDataBreakpoints' request. - Returned is information about each breakpoint created by this request. - */ - export interface SetDataBreakpointsResponse extends Response { - body: { - /** Information about the data breakpoints. The array elements correspond to the elements of the input argument 'breakpoints' array. */ - breakpoints: Breakpoint[]; - }; - } - - /** Continue request; value of command field is 'continue'. - The request starts the debuggee to run again. - */ - export interface ContinueRequest extends Request { - // command: 'continue'; - arguments: ContinueArguments; - } - - /** Arguments for 'continue' request. */ - export interface ContinueArguments { - /** Continue execution for the specified thread (if possible). If the backend cannot continue on a single thread but will continue on all threads, it should set the 'allThreadsContinued' attribute in the response to true. */ - threadId: number; - } - - /** Response to 'continue' request. */ - export interface ContinueResponse extends Response { - body: { - /** If true, the 'continue' request has ignored the specified thread and continued all threads instead. If this attribute is missing a value of 'true' is assumed for backward compatibility. */ - allThreadsContinued?: boolean; - }; - } - - /** Next request; value of command field is 'next'. - The request starts the debuggee to run again for one step. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - */ - export interface NextRequest extends Request { - // command: 'next'; - arguments: NextArguments; - } - - /** Arguments for 'next' request. */ - export interface NextArguments { - /** Execute 'next' for this thread. */ - threadId: number; - } - - /** Response to 'next' request. This is just an acknowledgement, so no body field is required. */ - export interface NextResponse extends Response { - } - - /** StepIn request; value of command field is 'stepIn'. - The request starts the debuggee to step into a function/method if possible. - If it cannot step into a target, 'stepIn' behaves like 'next'. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - If there are multiple function/method calls (or other targets) on the source line, - the optional argument 'targetId' can be used to control into which target the 'stepIn' should occur. - The list of possible targets for a given source line can be retrieved via the 'stepInTargets' request. - */ - export interface StepInRequest extends Request { - // command: 'stepIn'; - arguments: StepInArguments; - } - - /** Arguments for 'stepIn' request. */ - export interface StepInArguments { - /** Execute 'stepIn' for this thread. */ - threadId: number; - /** Optional id of the target to step into. */ - targetId?: number; - } - - /** Response to 'stepIn' request. This is just an acknowledgement, so no body field is required. */ - export interface StepInResponse extends Response { - } - - /** StepOut request; value of command field is 'stepOut'. - The request starts the debuggee to run again for one step. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. - */ - export interface StepOutRequest extends Request { - // command: 'stepOut'; - arguments: StepOutArguments; - } - - /** Arguments for 'stepOut' request. */ - export interface StepOutArguments { - /** Execute 'stepOut' for this thread. */ - threadId: number; - } - - /** Response to 'stepOut' request. This is just an acknowledgement, so no body field is required. */ - export interface StepOutResponse extends Response { - } - - /** StepBack request; value of command field is 'stepBack'. - The request starts the debuggee to run one step backwards. - The debug adapter first sends the response and then a 'stopped' event (with reason 'step') after the step has completed. Clients should only call this request if the capability 'supportsStepBack' is true. - */ - export interface StepBackRequest extends Request { - // command: 'stepBack'; - arguments: StepBackArguments; - } - - /** Arguments for 'stepBack' request. */ - export interface StepBackArguments { - /** Execute 'stepBack' for this thread. */ - threadId: number; - } - - /** Response to 'stepBack' request. This is just an acknowledgement, so no body field is required. */ - export interface StepBackResponse extends Response { - } - - /** ReverseContinue request; value of command field is 'reverseContinue'. - The request starts the debuggee to run backward. Clients should only call this request if the capability 'supportsStepBack' is true. - */ - export interface ReverseContinueRequest extends Request { - // command: 'reverseContinue'; - arguments: ReverseContinueArguments; - } - - /** Arguments for 'reverseContinue' request. */ - export interface ReverseContinueArguments { - /** Execute 'reverseContinue' for this thread. */ - threadId: number; - } - - /** Response to 'reverseContinue' request. This is just an acknowledgement, so no body field is required. */ - export interface ReverseContinueResponse extends Response { - } - - /** RestartFrame request; value of command field is 'restartFrame'. - The request restarts execution of the specified stackframe. - The debug adapter first sends the response and then a 'stopped' event (with reason 'restart') after the restart has completed. - */ - export interface RestartFrameRequest extends Request { - // command: 'restartFrame'; - arguments: RestartFrameArguments; - } - - /** Arguments for 'restartFrame' request. */ - export interface RestartFrameArguments { - /** Restart this stackframe. */ - frameId: number; - } - - /** Response to 'restartFrame' request. This is just an acknowledgement, so no body field is required. */ - export interface RestartFrameResponse extends Response { - } - - /** Goto request; value of command field is 'goto'. - The request sets the location where the debuggee will continue to run. - This makes it possible to skip the execution of code or to executed code again. - The code between the current location and the goto target is not executed but skipped. - The debug adapter first sends the response and then a 'stopped' event with reason 'goto'. - */ - export interface GotoRequest extends Request { - // command: 'goto'; - arguments: GotoArguments; - } - - /** Arguments for 'goto' request. */ - export interface GotoArguments { - /** Set the goto target for this thread. */ - threadId: number; - /** The location where the debuggee will continue to run. */ - targetId: number; - } - - /** Response to 'goto' request. This is just an acknowledgement, so no body field is required. */ - export interface GotoResponse extends Response { - } - - /** Pause request; value of command field is 'pause'. - The request suspends the debuggee. - The debug adapter first sends the response and then a 'stopped' event (with reason 'pause') after the thread has been paused successfully. - */ - export interface PauseRequest extends Request { - // command: 'pause'; - arguments: PauseArguments; - } - - /** Arguments for 'pause' request. */ - export interface PauseArguments { - /** Pause execution for this thread. */ - threadId: number; - } - - /** Response to 'pause' request. This is just an acknowledgement, so no body field is required. */ - export interface PauseResponse extends Response { - } - - /** StackTrace request; value of command field is 'stackTrace'. - The request returns a stacktrace from the current execution state. - */ - export interface StackTraceRequest extends Request { - // command: 'stackTrace'; - arguments: StackTraceArguments; - } - - /** Arguments for 'stackTrace' request. */ - export interface StackTraceArguments { - /** Retrieve the stacktrace for this thread. */ - threadId: number; - /** The index of the first frame to return; if omitted frames start at 0. */ - startFrame?: number; - /** The maximum number of frames to return. If levels is not specified or 0, all frames are returned. */ - levels?: number; - /** Specifies details on how to format the stack frames. */ - format?: StackFrameFormat; - } - - /** Response to 'stackTrace' request. */ - export interface StackTraceResponse extends Response { - body: { - /** The frames of the stackframe. If the array has length zero, there are no stackframes available. - This means that there is no location information available. - */ - stackFrames: StackFrame[]; - /** The total number of frames available. */ - totalFrames?: number; - }; - } - - /** Scopes request; value of command field is 'scopes'. - The request returns the variable scopes for a given stackframe ID. - */ - export interface ScopesRequest extends Request { - // command: 'scopes'; - arguments: ScopesArguments; - } - - /** Arguments for 'scopes' request. */ - export interface ScopesArguments { - /** Retrieve the scopes for this stackframe. */ - frameId: number; - } - - /** Response to 'scopes' request. */ - export interface ScopesResponse extends Response { - body: { - /** The scopes of the stackframe. If the array has length zero, there are no scopes available. */ - scopes: Scope[]; - }; - } - - /** Variables request; value of command field is 'variables'. - Retrieves all child variables for the given variable reference. - An optional filter can be used to limit the fetched children to either named or indexed children. - */ - export interface VariablesRequest extends Request { - // command: 'variables'; - arguments: VariablesArguments; - } - - /** Arguments for 'variables' request. */ - export interface VariablesArguments { - /** The Variable reference. */ - variablesReference: number; - /** Optional filter to limit the child variables to either named or indexed. If omitted, both types are fetched. */ - filter?: 'indexed' | 'named'; - /** The index of the first variable to return; if omitted children start at 0. */ - start?: number; - /** The number of variables to return. If count is missing or 0, all variables are returned. */ - count?: number; - /** Specifies details on how to format the Variable values. */ - format?: ValueFormat; - } - - /** Response to 'variables' request. */ - export interface VariablesResponse extends Response { - body: { - /** All (or a range) of variables for the given variable reference. */ - variables: Variable[]; - }; - } - - /** SetVariable request; value of command field is 'setVariable'. - Set the variable with the given name in the variable container to a new value. - */ - export interface SetVariableRequest extends Request { - // command: 'setVariable'; - arguments: SetVariableArguments; - } - - /** Arguments for 'setVariable' request. */ - export interface SetVariableArguments { - /** The reference of the variable container. */ - variablesReference: number; - /** The name of the variable in the container. */ - name: string; - /** The value of the variable. */ - value: string; - /** Specifies details on how to format the response value. */ - format?: ValueFormat; - } - - /** Response to 'setVariable' request. */ - export interface SetVariableResponse extends Response { - body: { - /** The new value of the variable. */ - value: string; - /** The type of the new value. Typically shown in the UI when hovering over the value. */ - type?: string; - /** If variablesReference is > 0, the new value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference?: number; - /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - indexedVariables?: number; - }; - } - - /** Source request; value of command field is 'source'. - The request retrieves the source code for a given source reference. - */ - export interface SourceRequest extends Request { - // command: 'source'; - arguments: SourceArguments; - } - - /** Arguments for 'source' request. */ - export interface SourceArguments { - /** Specifies the source content to load. Either source.path or source.sourceReference must be specified. */ - source?: Source; - /** The reference to the source. This is the same as source.sourceReference. This is provided for backward compatibility since old backends do not understand the 'source' attribute. */ - sourceReference: number; - } - - /** Response to 'source' request. */ - export interface SourceResponse extends Response { - body: { - /** Content of the source reference. */ - content: string; - /** Optional content type (mime type) of the source. */ - mimeType?: string; - }; - } - - /** Threads request; value of command field is 'threads'. - The request retrieves a list of all threads. - */ - export interface ThreadsRequest extends Request { - // command: 'threads'; - } - - /** Response to 'threads' request. */ - export interface ThreadsResponse extends Response { - body: { - /** All threads. */ - threads: Thread[]; - }; - } - - /** TerminateThreads request; value of command field is 'terminateThreads'. - The request terminates the threads with the given ids. - */ - export interface TerminateThreadsRequest extends Request { - // command: 'terminateThreads'; - arguments: TerminateThreadsArguments; - } - - /** Arguments for 'terminateThreads' request. */ - export interface TerminateThreadsArguments { - /** Ids of threads to be terminated. */ - threadIds?: number[]; - } - - /** Response to 'terminateThreads' request. This is just an acknowledgement, so no body field is required. */ - export interface TerminateThreadsResponse extends Response { - } - - /** Modules request; value of command field is 'modules'. - Modules can be retrieved from the debug adapter with the ModulesRequest which can either return all modules or a range of modules to support paging. - */ - export interface ModulesRequest extends Request { - // command: 'modules'; - arguments: ModulesArguments; - } - - /** Arguments for 'modules' request. */ - export interface ModulesArguments { - /** The index of the first module to return; if omitted modules start at 0. */ - startModule?: number; - /** The number of modules to return. If moduleCount is not specified or 0, all modules are returned. */ - moduleCount?: number; - } - - /** Response to 'modules' request. */ - export interface ModulesResponse extends Response { - body: { - /** All modules or range of modules. */ - modules: Module[]; - /** The total number of modules available. */ - totalModules?: number; - }; - } - - /** LoadedSources request; value of command field is 'loadedSources'. - Retrieves the set of all sources currently loaded by the debugged process. - */ - export interface LoadedSourcesRequest extends Request { - // command: 'loadedSources'; - arguments?: LoadedSourcesArguments; - } - - /** Arguments for 'loadedSources' request. */ - export interface LoadedSourcesArguments { - } - - /** Response to 'loadedSources' request. */ - export interface LoadedSourcesResponse extends Response { - body: { - /** Set of loaded sources. */ - sources: Source[]; - }; - } - - /** Evaluate request; value of command field is 'evaluate'. - Evaluates the given expression in the context of the top most stack frame. - The expression has access to any variables and arguments that are in scope. - */ - export interface EvaluateRequest extends Request { - // command: 'evaluate'; - arguments: EvaluateArguments; - } - - /** Arguments for 'evaluate' request. */ - export interface EvaluateArguments { - /** The expression to evaluate. */ - expression: string; - /** Evaluate the expression in the scope of this stack frame. If not specified, the expression is evaluated in the global scope. */ - frameId?: number; - /** The context in which the evaluate request is run. - Values: - 'watch': evaluate is run in a watch. - 'repl': evaluate is run from REPL console. - 'hover': evaluate is run from a data hover. - etc. - */ - context?: string; - /** Specifies details on how to format the Evaluate result. */ - format?: ValueFormat; - } - - /** Response to 'evaluate' request. */ - export interface EvaluateResponse extends Response { - body: { - /** The result of the evaluate request. */ - result: string; - /** The optional type of the evaluate result. */ - type?: string; - /** Properties of a evaluate result that can be used to determine how to render the result in the UI. */ - presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the evaluate result is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference: number; - /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - indexedVariables?: number; - /** Memory reference to a location appropriate for this result. For pointer type eval results, this is generally a reference to the memory address contained in the pointer. */ - memoryReference?: string; - }; - } - - /** SetExpression request; value of command field is 'setExpression'. - Evaluates the given 'value' expression and assigns it to the 'expression' which must be a modifiable l-value. - The expressions have access to any variables and arguments that are in scope of the specified frame. - */ - export interface SetExpressionRequest extends Request { - // command: 'setExpression'; - arguments: SetExpressionArguments; - } - - /** Arguments for 'setExpression' request. */ - export interface SetExpressionArguments { - /** The l-value expression to assign to. */ - expression: string; - /** The value expression to assign to the l-value expression. */ - value: string; - /** Evaluate the expressions in the scope of this stack frame. If not specified, the expressions are evaluated in the global scope. */ - frameId?: number; - /** Specifies how the resulting value should be formatted. */ - format?: ValueFormat; - } - - /** Response to 'setExpression' request. */ - export interface SetExpressionResponse extends Response { - body: { - /** The new value of the expression. */ - value: string; - /** The optional type of the value. */ - type?: string; - /** Properties of a value that can be used to determine how to render the result in the UI. */ - presentationHint?: VariablePresentationHint; - /** If variablesReference is > 0, the value is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. The value should be less than or equal to 2147483647 (2^31 - 1). */ - variablesReference?: number; - /** The number of named child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. The value should be less than or equal to 2147483647 (2^31 - 1). - */ - indexedVariables?: number; - }; - } - - /** StepInTargets request; value of command field is 'stepInTargets'. - This request retrieves the possible stepIn targets for the specified stack frame. - These targets can be used in the 'stepIn' request. - The StepInTargets may only be called if the 'supportsStepInTargetsRequest' capability exists and is true. - */ - export interface StepInTargetsRequest extends Request { - // command: 'stepInTargets'; - arguments: StepInTargetsArguments; - } - - /** Arguments for 'stepInTargets' request. */ - export interface StepInTargetsArguments { - /** The stack frame for which to retrieve the possible stepIn targets. */ - frameId: number; - } - - /** Response to 'stepInTargets' request. */ - export interface StepInTargetsResponse extends Response { - body: { - /** The possible stepIn targets of the specified source location. */ - targets: StepInTarget[]; - }; - } - - /** GotoTargets request; value of command field is 'gotoTargets'. - This request retrieves the possible goto targets for the specified source location. - These targets can be used in the 'goto' request. - The GotoTargets request may only be called if the 'supportsGotoTargetsRequest' capability exists and is true. - */ - export interface GotoTargetsRequest extends Request { - // command: 'gotoTargets'; - arguments: GotoTargetsArguments; - } - - /** Arguments for 'gotoTargets' request. */ - export interface GotoTargetsArguments { - /** The source location for which the goto targets are determined. */ - source: Source; - /** The line location for which the goto targets are determined. */ - line: number; - /** An optional column location for which the goto targets are determined. */ - column?: number; - } - - /** Response to 'gotoTargets' request. */ - export interface GotoTargetsResponse extends Response { - body: { - /** The possible goto targets of the specified location. */ - targets: GotoTarget[]; - }; - } - - /** Completions request; value of command field is 'completions'. - Returns a list of possible completions for a given caret position and text. - The CompletionsRequest may only be called if the 'supportsCompletionsRequest' capability exists and is true. - */ - export interface CompletionsRequest extends Request { - // command: 'completions'; - arguments: CompletionsArguments; - } - - /** Arguments for 'completions' request. */ - export interface CompletionsArguments { - /** Returns completions in the scope of this stack frame. If not specified, the completions are returned for the global scope. */ - frameId?: number; - /** One or more source lines. Typically this is the text a user has typed into the debug console before he asked for completion. */ - text: string; - /** The character position for which to determine the completion proposals. */ - column: number; - /** An optional line for which to determine the completion proposals. If missing the first line of the text is assumed. */ - line?: number; - } - - /** Response to 'completions' request. */ - export interface CompletionsResponse extends Response { - body: { - /** The possible completions for . */ - targets: CompletionItem[]; - }; - } - - /** ExceptionInfo request; value of command field is 'exceptionInfo'. - Retrieves the details of the exception that caused this event to be raised. - */ - export interface ExceptionInfoRequest extends Request { - // command: 'exceptionInfo'; - arguments: ExceptionInfoArguments; - } - - /** Arguments for 'exceptionInfo' request. */ - export interface ExceptionInfoArguments { - /** Thread for which exception information should be retrieved. */ - threadId: number; - } - - /** Response to 'exceptionInfo' request. */ - export interface ExceptionInfoResponse extends Response { - body: { - /** ID of the exception that was thrown. */ - exceptionId: string; - /** Descriptive text for the exception provided by the debug adapter. */ - description?: string; - /** Mode that caused the exception notification to be raised. */ - breakMode: ExceptionBreakMode; - /** Detailed information about the exception. */ - details?: ExceptionDetails; - }; - } - - /** ReadMemory request; value of command field is 'readMemory'. - Reads bytes from memory at the provided location. - */ - export interface ReadMemoryRequest extends Request { - // command: 'readMemory'; - arguments: ReadMemoryArguments; - } - - /** Arguments for 'readMemory' request. */ - export interface ReadMemoryArguments { - /** Memory reference to the base location from which data should be read. */ - memoryReference: string; - /** Optional offset (in bytes) to be applied to the reference location before reading data. Can be negative. */ - offset?: number; - /** Number of bytes to read at the specified location and offset. */ - count: number; - } - - /** Response to 'readMemory' request. */ - export interface ReadMemoryResponse extends Response { - body?: { - /** The address of the first byte of data returned. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ - address: string; - /** The number of unreadable bytes encountered after the last successfully read byte. This can be used to determine the number of bytes that must be skipped before a subsequent 'readMemory' request will succeed. */ - unreadableBytes?: number; - /** The bytes read from memory, encoded using base64. */ - data?: string; - }; - } - - /** Disassemble request; value of command field is 'disassemble'. - Disassembles code stored at the provided location. - */ - export interface DisassembleRequest extends Request { - // command: 'disassemble'; - arguments: DisassembleArguments; - } - - /** Arguments for 'disassemble' request. */ - export interface DisassembleArguments { - /** Memory reference to the base location containing the instructions to disassemble. */ - memoryReference: string; - /** Optional offset (in bytes) to be applied to the reference location before disassembling. Can be negative. */ - offset?: number; - /** Optional offset (in instructions) to be applied after the byte offset (if any) before disassembling. Can be negative. */ - instructionOffset?: number; - /** Number of instructions to disassemble starting at the specified location and offset. An adapter must return exactly this number of instructions - any unavailable instructions should be replaced with an implementation-defined 'invalid instruction' value. */ - instructionCount: number; - /** If true, the adapter should attempt to resolve memory addresses and other values to symbolic names. */ - resolveSymbols?: boolean; - } - - /** Response to 'disassemble' request. */ - export interface DisassembleResponse extends Response { - body?: { - /** The list of disassembled instructions. */ - instructions: DisassembledInstruction[]; - }; - } - - /** Information about the capabilities of a debug adapter. */ - export interface Capabilities { - /** The debug adapter supports the 'configurationDone' request. */ - supportsConfigurationDoneRequest?: boolean; - /** The debug adapter supports function breakpoints. */ - supportsFunctionBreakpoints?: boolean; - /** The debug adapter supports conditional breakpoints. */ - supportsConditionalBreakpoints?: boolean; - /** The debug adapter supports breakpoints that break execution after a specified number of hits. */ - supportsHitConditionalBreakpoints?: boolean; - /** The debug adapter supports a (side effect free) evaluate request for data hovers. */ - supportsEvaluateForHovers?: boolean; - /** Available filters or options for the setExceptionBreakpoints request. */ - exceptionBreakpointFilters?: ExceptionBreakpointsFilter[]; - /** The debug adapter supports stepping back via the 'stepBack' and 'reverseContinue' requests. */ - supportsStepBack?: boolean; - /** The debug adapter supports setting a variable to a value. */ - supportsSetVariable?: boolean; - /** The debug adapter supports restarting a frame. */ - supportsRestartFrame?: boolean; - /** The debug adapter supports the 'gotoTargets' request. */ - supportsGotoTargetsRequest?: boolean; - /** The debug adapter supports the 'stepInTargets' request. */ - supportsStepInTargetsRequest?: boolean; - /** The debug adapter supports the 'completions' request. */ - supportsCompletionsRequest?: boolean; - /** The set of characters that should trigger completion in a REPL. If not specified, the UI should assume the '.' character. */ - completionTriggerCharacters?: string[]; - /** The debug adapter supports the 'modules' request. */ - supportsModulesRequest?: boolean; - /** The set of additional module information exposed by the debug adapter. */ - additionalModuleColumns?: ColumnDescriptor[]; - /** Checksum algorithms supported by the debug adapter. */ - supportedChecksumAlgorithms?: ChecksumAlgorithm[]; - /** The debug adapter supports the 'restart' request. In this case a client should not implement 'restart' by terminating and relaunching the adapter but by calling the RestartRequest. */ - supportsRestartRequest?: boolean; - /** The debug adapter supports 'exceptionOptions' on the setExceptionBreakpoints request. */ - supportsExceptionOptions?: boolean; - /** The debug adapter supports a 'format' attribute on the stackTraceRequest, variablesRequest, and evaluateRequest. */ - supportsValueFormattingOptions?: boolean; - /** The debug adapter supports the 'exceptionInfo' request. */ - supportsExceptionInfoRequest?: boolean; - /** The debug adapter supports the 'terminateDebuggee' attribute on the 'disconnect' request. */ - supportTerminateDebuggee?: boolean; - /** The debug adapter supports the delayed loading of parts of the stack, which requires that both the 'startFrame' and 'levels' arguments and the 'totalFrames' result of the 'StackTrace' request are supported. */ - supportsDelayedStackTraceLoading?: boolean; - /** The debug adapter supports the 'loadedSources' request. */ - supportsLoadedSourcesRequest?: boolean; - /** The debug adapter supports logpoints by interpreting the 'logMessage' attribute of the SourceBreakpoint. */ - supportsLogPoints?: boolean; - /** The debug adapter supports the 'terminateThreads' request. */ - supportsTerminateThreadsRequest?: boolean; - /** The debug adapter supports the 'setExpression' request. */ - supportsSetExpression?: boolean; - /** The debug adapter supports the 'terminate' request. */ - supportsTerminateRequest?: boolean; - /** The debug adapter supports data breakpoints. */ - supportsDataBreakpoints?: boolean; - /** The debug adapter supports the 'readMemory' request. */ - supportsReadMemoryRequest?: boolean; - /** The debug adapter supports the 'disassemble' request. */ - supportsDisassembleRequest?: boolean; - /** The debug adapter supports the 'cancel' request. */ - supportsCancelRequest?: boolean; - /** The debug adapter supports the 'breakpointLocations' request. */ - supportsBreakpointLocationsRequest?: boolean; - } - - /** An ExceptionBreakpointsFilter is shown in the UI as an option for configuring how exceptions are dealt with. */ - export interface ExceptionBreakpointsFilter { - /** The internal ID of the filter. This value is passed to the setExceptionBreakpoints request. */ - filter: string; - /** The name of the filter. This will be shown in the UI. */ - label: string; - /** Initial value of the filter. If not specified a value 'false' is assumed. */ - default?: boolean; - } - - /** A structured message object. Used to return errors from requests. */ - export interface Message { - /** Unique identifier for the message. */ - id: number; - /** A format string for the message. Embedded variables have the form '{name}'. - If variable name starts with an underscore character, the variable does not contain user data (PII) and can be safely used for telemetry purposes. - */ - format: string; - /** An object used as a dictionary for looking up the variables in the format string. */ - variables?: { [key: string]: string; }; - /** If true send to telemetry. */ - sendTelemetry?: boolean; - /** If true show user. */ - showUser?: boolean; - /** An optional url where additional information about this message can be found. */ - url?: string; - /** An optional label that is presented to the user as the UI for opening the url. */ - urlLabel?: string; - } - - /** A Module object represents a row in the modules view. - Two attributes are mandatory: an id identifies a module in the modules view and is used in a ModuleEvent for identifying a module for adding, updating or deleting. - The name is used to minimally render the module in the UI. - - Additional attributes can be added to the module. They will show up in the module View if they have a corresponding ColumnDescriptor. - - To avoid an unnecessary proliferation of additional attributes with similar semantics but different names - we recommend to re-use attributes from the 'recommended' list below first, and only introduce new attributes if nothing appropriate could be found. - */ - export interface Module { - /** Unique identifier for the module. */ - id: number | string; - /** A name of the module. */ - name: string; - /** optional but recommended attributes. - always try to use these first before introducing additional attributes. - - Logical full path to the module. The exact definition is implementation defined, but usually this would be a full path to the on-disk file for the module. - */ - path?: string; - /** True if the module is optimized. */ - isOptimized?: boolean; - /** True if the module is considered 'user code' by a debugger that supports 'Just My Code'. */ - isUserCode?: boolean; - /** Version of Module. */ - version?: string; - /** User understandable description of if symbols were found for the module (ex: 'Symbols Loaded', 'Symbols not found', etc. */ - symbolStatus?: string; - /** Logical full path to the symbol file. The exact definition is implementation defined. */ - symbolFilePath?: string; - /** Module created or modified. */ - dateTimeStamp?: string; - /** Address range covered by this module. */ - addressRange?: string; - } - - /** A ColumnDescriptor specifies what module attribute to show in a column of the ModulesView, how to format it, and what the column's label should be. - It is only used if the underlying UI actually supports this level of customization. - */ - export interface ColumnDescriptor { - /** Name of the attribute rendered in this column. */ - attributeName: string; - /** Header UI label of column. */ - label: string; - /** Format to use for the rendered values in this column. TBD how the format strings looks like. */ - format?: string; - /** Datatype of values in this column. Defaults to 'string' if not specified. */ - type?: 'string' | 'number' | 'boolean' | 'unixTimestampUTC'; - /** Width of this column in characters (hint only). */ - width?: number; - } - - /** The ModulesViewDescriptor is the container for all declarative configuration options of a ModuleView. - For now it only specifies the columns to be shown in the modules view. - */ - export interface ModulesViewDescriptor { - columns: ColumnDescriptor[]; - } - - /** A Thread */ - export interface Thread { - /** Unique identifier for the thread. */ - id: number; - /** A name of the thread. */ - name: string; - } - - /** A Source is a descriptor for source code. It is returned from the debug adapter as part of a StackFrame and it is used by clients when specifying breakpoints. */ - export interface Source { - /** The short name of the source. Every source returned from the debug adapter has a name. When sending a source to the debug adapter this name is optional. */ - name?: string; - /** The path of the source to be shown in the UI. It is only used to locate and load the content of the source if no sourceReference is specified (or its value is 0). */ - path?: string; - /** If sourceReference > 0 the contents of the source must be retrieved through the SourceRequest (even if a path is specified). A sourceReference is only valid for a session, so it must not be used to persist a source. The value should be less than or equal to 2147483647 (2^31 - 1). */ - sourceReference?: number; - /** An optional hint for how to present the source in the UI. A value of 'deemphasize' can be used to indicate that the source is not available or that it is skipped on stepping. */ - presentationHint?: 'normal' | 'emphasize' | 'deemphasize'; - /** The (optional) origin of this source: possible values 'internal module', 'inlined content from source map', etc. */ - origin?: string; - /** An optional list of sources that are related to this source. These may be the source that generated this source. */ - sources?: Source[]; - /** Optional data that a debug adapter might want to loop through the client. The client should leave the data intact and persist it across sessions. The client should not interpret the data. */ - adapterData?: any; - /** The checksums associated with this file. */ - checksums?: Checksum[]; - } - - /** A Stackframe contains the source location. */ - export interface StackFrame { - /** An identifier for the stack frame. It must be unique across all threads. This id can be used to retrieve the scopes of the frame with the 'scopesRequest' or to restart the execution of a stackframe. */ - id: number; - /** The name of the stack frame, typically a method name. */ - name: string; - /** The optional source of the frame. */ - source?: Source; - /** The line within the file of the frame. If source is null or doesn't exist, line is 0 and must be ignored. */ - line: number; - /** The column within the line. If source is null or doesn't exist, column is 0 and must be ignored. */ - column: number; - /** An optional end line of the range covered by the stack frame. */ - endLine?: number; - /** An optional end column of the range covered by the stack frame. */ - endColumn?: number; - /** Optional memory reference for the current instruction pointer in this frame. */ - instructionPointerReference?: string; - /** The module associated with this frame, if any. */ - moduleId?: number | string; - /** An optional hint for how to present this frame in the UI. A value of 'label' can be used to indicate that the frame is an artificial frame that is used as a visual label or separator. A value of 'subtle' can be used to change the appearance of a frame in a 'subtle' way. */ - presentationHint?: 'normal' | 'label' | 'subtle'; - } - - /** A Scope is a named container for variables. Optionally a scope can map to a source or a range within a source. */ - export interface Scope { - /** Name of the scope such as 'Arguments', 'Locals', or 'Registers'. This string is shown in the UI as is and can be translated. */ - name: string; - /** An optional hint for how to present this scope in the UI. If this attribute is missing, the scope is shown with a generic UI. - Values: - 'arguments': Scope contains method arguments. - 'locals': Scope contains local variables. - 'registers': Scope contains registers. Only a single 'registers' scope should be returned from a 'scopes' request. - etc. - */ - presentationHint?: string; - /** The variables of this scope can be retrieved by passing the value of variablesReference to the VariablesRequest. */ - variablesReference: number; - /** The number of named variables in this scope. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. - */ - namedVariables?: number; - /** The number of indexed variables in this scope. - The client can use this optional information to present the variables in a paged UI and fetch them in chunks. - */ - indexedVariables?: number; - /** If true, the number of variables in this scope is large or expensive to retrieve. */ - expensive: boolean; - /** Optional source for this scope. */ - source?: Source; - /** Optional start line of the range covered by this scope. */ - line?: number; - /** Optional start column of the range covered by this scope. */ - column?: number; - /** Optional end line of the range covered by this scope. */ - endLine?: number; - /** Optional end column of the range covered by this scope. */ - endColumn?: number; - } - - /** A Variable is a name/value pair. - Optionally a variable can have a 'type' that is shown if space permits or when hovering over the variable's name. - An optional 'kind' is used to render additional properties of the variable, e.g. different icons can be used to indicate that a variable is public or private. - If the value is structured (has children), a handle is provided to retrieve the children with the VariablesRequest. - If the number of named or indexed children is large, the numbers should be returned via the optional 'namedVariables' and 'indexedVariables' attributes. - The client can use this optional information to present the children in a paged UI and fetch them in chunks. - */ - export interface Variable { - /** The variable's name. */ - name: string; - /** The variable's value. This can be a multi-line text, e.g. for a function the body of a function. */ - value: string; - /** The type of the variable's value. Typically shown in the UI when hovering over the value. */ - type?: string; - /** Properties of a variable that can be used to determine how to render the variable in the UI. */ - presentationHint?: VariablePresentationHint; - /** Optional evaluatable name of this variable which can be passed to the 'EvaluateRequest' to fetch the variable's value. */ - evaluateName?: string; - /** If variablesReference is > 0, the variable is structured and its children can be retrieved by passing variablesReference to the VariablesRequest. */ - variablesReference: number; - /** The number of named child variables. - The client can use this optional information to present the children in a paged UI and fetch them in chunks. - */ - namedVariables?: number; - /** The number of indexed child variables. - The client can use this optional information to present the children in a paged UI and fetch them in chunks. - */ - indexedVariables?: number; - /** Optional memory reference for the variable if the variable represents executable code, such as a function pointer. */ - memoryReference?: string; - } - - /** Optional properties of a variable that can be used to determine how to render the variable in the UI. */ - export interface VariablePresentationHint { - /** The kind of variable. Before introducing additional values, try to use the listed values. - Values: - 'property': Indicates that the object is a property. - 'method': Indicates that the object is a method. - 'class': Indicates that the object is a class. - 'data': Indicates that the object is data. - 'event': Indicates that the object is an event. - 'baseClass': Indicates that the object is a base class. - 'innerClass': Indicates that the object is an inner class. - 'interface': Indicates that the object is an interface. - 'mostDerivedClass': Indicates that the object is the most derived class. - 'virtual': Indicates that the object is virtual, that means it is a synthetic object introduced by the adapter for rendering purposes, e.g. an index range for large arrays. - 'dataBreakpoint': Indicates that a data breakpoint is registered for the object. - etc. - */ - kind?: string; - /** Set of attributes represented as an array of strings. Before introducing additional values, try to use the listed values. - Values: - 'static': Indicates that the object is static. - 'constant': Indicates that the object is a constant. - 'readOnly': Indicates that the object is read only. - 'rawString': Indicates that the object is a raw string. - 'hasObjectId': Indicates that the object can have an Object ID created for it. - 'canHaveObjectId': Indicates that the object has an Object ID associated with it. - 'hasSideEffects': Indicates that the evaluation had side effects. - etc. - */ - attributes?: string[]; - /** Visibility of variable. Before introducing additional values, try to use the listed values. - Values: 'public', 'private', 'protected', 'internal', 'final', etc. - */ - visibility?: string; - } - - /** Properties of a breakpoint location returned from the 'breakpointLocations' request. */ - export interface BreakpointLocation { - /** Start line of breakpoint location. */ - line: number; - /** Optional start column of breakpoint location. */ - column?: number; - /** Optional end line of breakpoint location if the location covers a range. */ - endLine?: number; - /** Optional end column of breakpoint location if the location covers a range. */ - endColumn?: number; - } - - /** Properties of a breakpoint or logpoint passed to the setBreakpoints request. */ - export interface SourceBreakpoint { - /** The source line of the breakpoint or logpoint. */ - line: number; - /** An optional source column of the breakpoint. */ - column?: number; - /** An optional expression for conditional breakpoints. */ - condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ - hitCondition?: string; - /** If this attribute exists and is non-empty, the backend must not 'break' (stop) but log the message instead. Expressions within {} are interpolated. */ - logMessage?: string; - } - - /** Properties of a breakpoint passed to the setFunctionBreakpoints request. */ - export interface FunctionBreakpoint { - /** The name of the function. */ - name: string; - /** An optional expression for conditional breakpoints. */ - condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ - hitCondition?: string; - } - - /** This enumeration defines all possible access types for data breakpoints. */ - export type DataBreakpointAccessType = 'read' | 'write' | 'readWrite'; - - /** Properties of a data breakpoint passed to the setDataBreakpoints request. */ - export interface DataBreakpoint { - /** An id representing the data. This id is returned from the dataBreakpointInfo request. */ - dataId: string; - /** The access type of the data. */ - accessType?: DataBreakpointAccessType; - /** An optional expression for conditional breakpoints. */ - condition?: string; - /** An optional expression that controls how many hits of the breakpoint are ignored. The backend is expected to interpret the expression as needed. */ - hitCondition?: string; - } - - /** Information about a Breakpoint created in setBreakpoints or setFunctionBreakpoints. */ - export interface Breakpoint { - /** An optional identifier for the breakpoint. It is needed if breakpoint events are used to update or remove breakpoints. */ - id?: number; - /** If true breakpoint could be set (but not necessarily at the desired location). */ - verified: boolean; - /** An optional message about the state of the breakpoint. This is shown to the user and can be used to explain why a breakpoint could not be verified. */ - message?: string; - /** The source where the breakpoint is located. */ - source?: Source; - /** The start line of the actual range covered by the breakpoint. */ - line?: number; - /** An optional start column of the actual range covered by the breakpoint. */ - column?: number; - /** An optional end line of the actual range covered by the breakpoint. */ - endLine?: number; - /** An optional end column of the actual range covered by the breakpoint. If no end line is given, then the end column is assumed to be in the start line. */ - endColumn?: number; - } - - /** A StepInTarget can be used in the 'stepIn' request and determines into which single target the stepIn request should step. */ - export interface StepInTarget { - /** Unique identifier for a stepIn target. */ - id: number; - /** The name of the stepIn target (shown in the UI). */ - label: string; - } - - /** A GotoTarget describes a code location that can be used as a target in the 'goto' request. - The possible goto targets can be determined via the 'gotoTargets' request. - */ - export interface GotoTarget { - /** Unique identifier for a goto target. This is used in the goto request. */ - id: number; - /** The name of the goto target (shown in the UI). */ - label: string; - /** The line of the goto target. */ - line: number; - /** An optional column of the goto target. */ - column?: number; - /** An optional end line of the range covered by the goto target. */ - endLine?: number; - /** An optional end column of the range covered by the goto target. */ - endColumn?: number; - /** Optional memory reference for the instruction pointer value represented by this target. */ - instructionPointerReference?: string; - } - - /** CompletionItems are the suggestions returned from the CompletionsRequest. */ - export interface CompletionItem { - /** The label of this completion item. By default this is also the text that is inserted when selecting this completion. */ - label: string; - /** If text is not falsy then it is inserted instead of the label. */ - text?: string; - /** A string that should be used when comparing this item with other items. When `falsy` the label is used. */ - sortText?: string; - /** The item's type. Typically the client uses this information to render the item in the UI with an icon. */ - type?: CompletionItemType; - /** This value determines the location (in the CompletionsRequest's 'text' attribute) where the completion text is added. - If missing the text is added at the location specified by the CompletionsRequest's 'column' attribute. - */ - start?: number; - /** This value determines how many characters are overwritten by the completion text. - If missing the value 0 is assumed which results in the completion text being inserted. - */ - length?: number; - } - - /** Some predefined types for the CompletionItem. Please note that not all clients have specific icons for all of them. */ - export type CompletionItemType = 'method' | 'function' | 'constructor' | 'field' | 'variable' | 'class' | 'interface' | 'module' | 'property' | 'unit' | 'value' | 'enum' | 'keyword' | 'snippet' | 'text' | 'color' | 'file' | 'reference' | 'customcolor'; - - /** Names of checksum algorithms that may be supported by a debug adapter. */ - export type ChecksumAlgorithm = 'MD5' | 'SHA1' | 'SHA256' | 'timestamp'; - - /** The checksum of an item calculated by the specified algorithm. */ - export interface Checksum { - /** The algorithm used to calculate this checksum. */ - algorithm: ChecksumAlgorithm; - /** Value of the checksum. */ - checksum: string; - } - - /** Provides formatting information for a value. */ - export interface ValueFormat { - /** Display the value in hex. */ - hex?: boolean; - } - - /** Provides formatting information for a stack frame. */ - export interface StackFrameFormat extends ValueFormat { - /** Displays parameters for the stack frame. */ - parameters?: boolean; - /** Displays the types of parameters for the stack frame. */ - parameterTypes?: boolean; - /** Displays the names of parameters for the stack frame. */ - parameterNames?: boolean; - /** Displays the values of parameters for the stack frame. */ - parameterValues?: boolean; - /** Displays the line number of the stack frame. */ - line?: boolean; - /** Displays the module of the stack frame. */ - module?: boolean; - /** Includes all stack frames, including those the debug adapter might otherwise hide. */ - includeAll?: boolean; - } - - /** An ExceptionOptions assigns configuration options to a set of exceptions. */ - export interface ExceptionOptions { - /** A path that selects a single or multiple exceptions in a tree. If 'path' is missing, the whole tree is selected. By convention the first segment of the path is a category that is used to group exceptions in the UI. */ - path?: ExceptionPathSegment[]; - /** Condition when a thrown exception should result in a break. */ - breakMode: ExceptionBreakMode; - } - - /** This enumeration defines all possible conditions when a thrown exception should result in a break. - never: never breaks, - always: always breaks, - unhandled: breaks when exception unhandled, - userUnhandled: breaks if the exception is not handled by user code. - */ - export type ExceptionBreakMode = 'never' | 'always' | 'unhandled' | 'userUnhandled'; - - /** An ExceptionPathSegment represents a segment in a path that is used to match leafs or nodes in a tree of exceptions. If a segment consists of more than one name, it matches the names provided if 'negate' is false or missing or it matches anything except the names provided if 'negate' is true. */ - export interface ExceptionPathSegment { - /** If false or missing this segment matches the names provided, otherwise it matches anything except the names provided. */ - negate?: boolean; - /** Depending on the value of 'negate' the names that should match or not match. */ - names: string[]; - } - - /** Detailed information about an exception that has occurred. */ - export interface ExceptionDetails { - /** Message contained in the exception. */ - message?: string; - /** Short type name of the exception object. */ - typeName?: string; - /** Fully-qualified type name of the exception object. */ - fullTypeName?: string; - /** Optional expression that can be evaluated in the current scope to obtain the exception object. */ - evaluateName?: string; - /** Stack trace at the time the exception was thrown. */ - stackTrace?: string; - /** Details of the exception contained by this exception, if any. */ - innerException?: ExceptionDetails[]; - } - - /** Represents a single disassembled instruction. */ - export interface DisassembledInstruction { - /** The address of the instruction. Treated as a hex value if prefixed with '0x', or as a decimal value otherwise. */ - address: string; - /** Optional raw bytes representing the instruction and its operands, in an implementation-defined format. */ - instructionBytes?: string; - /** Text representing the instruction and its operands, in an implementation-defined format. */ - instruction: string; - /** Name of the symbol that corresponds with the location of this instruction, if any. */ - symbol?: string; - /** Source location that corresponds to this instruction, if any. Should always be set (if available) on the first instruction returned, but can be omitted afterwards if this instruction maps to the same source file as the previous instruction. */ - location?: Source; - /** The line within the source location that corresponds to this instruction, if any. */ - line?: number; - /** The column within the line that corresponds to this instruction, if any. */ - column?: number; - /** The end line of the range that corresponds to this instruction, if any. */ - endLine?: number; - /** The end column of the range that corresponds to this instruction, if any. */ - endColumn?: number; - } -} - -//------------------------------------------------------------------------------------------------------------------------------ - -export class Message implements DebugProtocol.ProtocolMessage { - seq: number; - type: string; - - public constructor(type: string) { - this.seq = 0; - this.type = type; - } -} - -export class Response extends Message implements DebugProtocol.Response { - request_seq: number; - success: boolean; - command: string; - - public constructor(request: DebugProtocol.Request, message?: string) { - super('response'); - this.request_seq = request.seq; - this.command = request.command; - if (message) { - this.success = false; - (this).message = message; - } else { - this.success = true; - } - } -} - -export class Event extends Message implements DebugProtocol.Event { - event: string; - - public constructor(event: string, body?: any) { - super('event'); - this.event = event; - if (body) { - (this).body = body; - } - } -} - -//-------------------------------------------------------------------------------------------------------------------------------- - -export class ProtocolServer implements vscode.DebugAdapter { - - private close = new vscode.EventEmitter(); - onClose: vscode.Event = this.close.event; - - private error = new vscode.EventEmitter(); - onError: vscode.Event = this.error.event; - - private sendMessage = new vscode.EventEmitter(); - readonly onDidSendMessage: vscode.Event = this.sendMessage.event; - - private _sequence: number = 1; - private _pendingRequests = new Map void>(); - - - public handleMessage(message: DebugProtocol.ProtocolMessage): void { - this.dispatch(message); - } - - public dispose() { - } - - public sendEvent(event: DebugProtocol.Event): void { - this._send('event', event); - } - - public sendResponse(response: DebugProtocol.Response): void { - if (response.seq > 0) { - console.error(`attempt to send more than one response for command ${response.command}`); - } else { - this._send('response', response); - } - } - - public sendRequest(command: string, args: any, timeout: number, cb: (response: DebugProtocol.Response) => void): void { - - const request: any = { - command: command - }; - if (args && Object.keys(args).length > 0) { - request.arguments = args; - } - - this._send('request', request); - - if (cb) { - this._pendingRequests.set(request.seq, cb); - - const timer = setTimeout(() => { - clearTimeout(timer); - const clb = this._pendingRequests.get(request.seq); - if (clb) { - this._pendingRequests.delete(request.seq); - clb(new Response(request, 'timeout')); - } - }, timeout); - } - } - - // ---- protected ---------------------------------------------------------- - - protected dispatchRequest(_request: DebugProtocol.Request): void { - } - - // ---- private ------------------------------------------------------------ - - private dispatch(msg: DebugProtocol.ProtocolMessage) { - if (msg.type === 'request') { - this.dispatchRequest(msg); - } else if (msg.type === 'response') { - const response = msg; - const clb = this._pendingRequests.get(response.request_seq); - if (clb) { - this._pendingRequests.delete(response.request_seq); - clb(response); - } - } - } - - private _send(typ: 'request' | 'response' | 'event', message: DebugProtocol.ProtocolMessage): void { - - message.type = typ; - message.seq = this._sequence++; - - this.sendMessage.fire(message); - } -} - -//------------------------------------------------------------------------------------------------------------------------------- - -export class Source implements DebugProtocol.Source { - name: string; - path?: string; - sourceReference: number; - - public constructor(name: string, path?: string, id: number = 0, origin?: string, data?: any) { - this.name = name; - this.path = path; - this.sourceReference = id; - if (origin) { - (this).origin = origin; - } - if (data) { - (this).adapterData = data; - } - } -} - -export class Scope implements DebugProtocol.Scope { - name: string; - variablesReference: number; - expensive: boolean; - - public constructor(name: string, reference: number, expensive: boolean = false) { - this.name = name; - this.variablesReference = reference; - this.expensive = expensive; - } -} - -export class StackFrame implements DebugProtocol.StackFrame { - id: number; - source?: Source; - line: number; - column: number; - name: string; - - public constructor(i: number, nm: string, src?: Source, ln: number = 0, col: number = 0) { - this.id = i; - this.source = src; - this.line = ln; - this.column = col; - this.name = nm; - } -} - -export class Thread implements DebugProtocol.Thread { - id: number; - name: string; - - public constructor(id: number, name: string) { - this.id = id; - if (name) { - this.name = name; - } else { - this.name = 'Thread #' + id; - } - } -} - -export class Variable implements DebugProtocol.Variable { - name: string; - value: string; - variablesReference: number; - - public constructor(name: string, value: string, ref: number = 0, indexedVariables?: number, namedVariables?: number) { - this.name = name; - this.value = value; - this.variablesReference = ref; - if (typeof namedVariables === 'number') { - (this).namedVariables = namedVariables; - } - if (typeof indexedVariables === 'number') { - (this).indexedVariables = indexedVariables; - } - } -} - -export class Breakpoint implements DebugProtocol.Breakpoint { - verified: boolean; - - public constructor(verified: boolean, line?: number, column?: number, source?: Source) { - this.verified = verified; - const e: DebugProtocol.Breakpoint = this; - if (typeof line === 'number') { - e.line = line; - } - if (typeof column === 'number') { - e.column = column; - } - if (source) { - e.source = source; - } - } -} - -export class Module implements DebugProtocol.Module { - id: number | string; - name: string; - - public constructor(id: number | string, name: string) { - this.id = id; - this.name = name; - } -} - -export class CompletionItem implements DebugProtocol.CompletionItem { - label: string; - start: number; - length: number; - - public constructor(label: string, start: number, length: number = 0) { - this.label = label; - this.start = start; - this.length = length; - } -} - -export class StoppedEvent extends Event implements DebugProtocol.StoppedEvent { - body: { - reason: string; - }; - - public constructor(reason: string, threadId?: number, exceptionText?: string) { - super('stopped'); - this.body = { - reason: reason - }; - if (typeof threadId === 'number') { - (this as DebugProtocol.StoppedEvent).body.threadId = threadId; - } - if (typeof exceptionText === 'string') { - (this as DebugProtocol.StoppedEvent).body.text = exceptionText; - } - } -} - -export class ContinuedEvent extends Event implements DebugProtocol.ContinuedEvent { - body: { - threadId: number; - }; - - public constructor(threadId: number, allThreadsContinued?: boolean) { - super('continued'); - this.body = { - threadId: threadId - }; - - if (typeof allThreadsContinued === 'boolean') { - (this).body.allThreadsContinued = allThreadsContinued; - } - } -} - -export class InitializedEvent extends Event implements DebugProtocol.InitializedEvent { - public constructor() { - super('initialized'); - } -} - -export class TerminatedEvent extends Event implements DebugProtocol.TerminatedEvent { - public constructor(restart?: any) { - super('terminated'); - if (typeof restart === 'boolean' || restart) { - const e: DebugProtocol.TerminatedEvent = this; - e.body = { - restart: restart - }; - } - } -} - -export class OutputEvent extends Event implements DebugProtocol.OutputEvent { - body: { - category: string, - output: string, - data?: any - }; - - public constructor(output: string, category: string = 'console', data?: any) { - super('output'); - this.body = { - category: category, - output: output - }; - if (data !== undefined) { - this.body.data = data; - } - } -} - -export class ThreadEvent extends Event implements DebugProtocol.ThreadEvent { - body: { - reason: string, - threadId: number - }; - - public constructor(reason: string, threadId: number) { - super('thread'); - this.body = { - reason: reason, - threadId: threadId - }; - } -} - -export class BreakpointEvent extends Event implements DebugProtocol.BreakpointEvent { - body: { - reason: string, - breakpoint: Breakpoint - }; - - public constructor(reason: string, breakpoint: Breakpoint) { - super('breakpoint'); - this.body = { - reason: reason, - breakpoint: breakpoint - }; - } -} - -export class ModuleEvent extends Event implements DebugProtocol.ModuleEvent { - body: { - reason: 'new' | 'changed' | 'removed', - module: Module - }; - - public constructor(reason: 'new' | 'changed' | 'removed', module: Module) { - super('module'); - this.body = { - reason: reason, - module: module - }; - } -} - -export class LoadedSourceEvent extends Event implements DebugProtocol.LoadedSourceEvent { - body: { - reason: 'new' | 'changed' | 'removed', - source: Source - }; - - public constructor(reason: 'new' | 'changed' | 'removed', source: Source) { - super('loadedSource'); - this.body = { - reason: reason, - source: source - }; - } -} - -export class CapabilitiesEvent extends Event implements DebugProtocol.CapabilitiesEvent { - body: { - capabilities: DebugProtocol.Capabilities - }; - - public constructor(capabilities: DebugProtocol.Capabilities) { - super('capabilities'); - this.body = { - capabilities: capabilities - }; - } -} - -export enum ErrorDestination { - User = 1, - Telemetry = 2 -} - -export class DebugSession extends ProtocolServer { - - private _debuggerLinesStartAt1: boolean; - private _debuggerColumnsStartAt1: boolean; - private _debuggerPathsAreURIs: boolean; - - private _clientLinesStartAt1: boolean; - private _clientColumnsStartAt1: boolean; - private _clientPathsAreURIs: boolean; - - protected _isServer: boolean; - - public constructor(obsolete_debuggerLinesAndColumnsStartAt1?: boolean, obsolete_isServer?: boolean) { - super(); - - const linesAndColumnsStartAt1 = typeof obsolete_debuggerLinesAndColumnsStartAt1 === 'boolean' ? obsolete_debuggerLinesAndColumnsStartAt1 : false; - this._debuggerLinesStartAt1 = linesAndColumnsStartAt1; - this._debuggerColumnsStartAt1 = linesAndColumnsStartAt1; - this._debuggerPathsAreURIs = false; - - this._clientLinesStartAt1 = true; - this._clientColumnsStartAt1 = true; - this._clientPathsAreURIs = false; - - this._isServer = typeof obsolete_isServer === 'boolean' ? obsolete_isServer : false; - - this.onClose(() => { - this.shutdown(); - }); - this.onError((_error) => { - this.shutdown(); - }); - } - - public setDebuggerPathFormat(format: string) { - this._debuggerPathsAreURIs = format !== 'path'; - } - - public setDebuggerLinesStartAt1(enable: boolean) { - this._debuggerLinesStartAt1 = enable; - } - - public setDebuggerColumnsStartAt1(enable: boolean) { - this._debuggerColumnsStartAt1 = enable; - } - - public setRunAsServer(enable: boolean) { - this._isServer = enable; - } - - public shutdown(): void { - if (this._isServer) { - // shutdown ignored in server mode - } else { - // TODO@AW - /* - // wait a bit before shutting down - setTimeout(() => { - process.exit(0); - }, 100); - */ - } - } - - protected sendErrorResponse(response: DebugProtocol.Response, codeOrMessage: number | DebugProtocol.Message, format?: string, variables?: any, dest: ErrorDestination = ErrorDestination.User): void { - - let msg: DebugProtocol.Message; - if (typeof codeOrMessage === 'number') { - msg = { - id: codeOrMessage, - format: format - }; - if (variables) { - msg.variables = variables; - } - if (dest & ErrorDestination.User) { - msg.showUser = true; - } - if (dest & ErrorDestination.Telemetry) { - msg.sendTelemetry = true; - } - } else { - msg = codeOrMessage; - } - - response.success = false; - response.message = DebugSession.formatPII(msg.format, true, msg.variables); - if (!response.body) { - response.body = {}; - } - response.body.error = msg; - - this.sendResponse(response); - } - - public runInTerminalRequest(args: DebugProtocol.RunInTerminalRequestArguments, timeout: number, cb: (response: DebugProtocol.Response) => void) { - this.sendRequest('runInTerminal', args, timeout, cb); - } - - protected dispatchRequest(request: DebugProtocol.Request): void { - - const response = new Response(request); - - try { - if (request.command === 'initialize') { - const args = request.arguments; - - if (typeof args.linesStartAt1 === 'boolean') { - this._clientLinesStartAt1 = args.linesStartAt1; - } - if (typeof args.columnsStartAt1 === 'boolean') { - this._clientColumnsStartAt1 = args.columnsStartAt1; - } - - if (args.pathFormat !== 'path') { - this.sendErrorResponse(response, 2018, 'debug adapter only supports native paths', null, ErrorDestination.Telemetry); - } else { - const initializeResponse = response; - initializeResponse.body = {}; - this.initializeRequest(initializeResponse, args); - } - - } else if (request.command === 'launch') { - this.launchRequest(response, request.arguments, request); - - } else if (request.command === 'attach') { - this.attachRequest(response, request.arguments, request); - - } else if (request.command === 'disconnect') { - this.disconnectRequest(response, request.arguments, request); - - } else if (request.command === 'terminate') { - this.terminateRequest(response, request.arguments, request); - - } else if (request.command === 'restart') { - this.restartRequest(response, request.arguments, request); - - } else if (request.command === 'setBreakpoints') { - this.setBreakPointsRequest(response, request.arguments, request); - - } else if (request.command === 'setFunctionBreakpoints') { - this.setFunctionBreakPointsRequest(response, request.arguments, request); - - } else if (request.command === 'setExceptionBreakpoints') { - this.setExceptionBreakPointsRequest(response, request.arguments, request); - - } else if (request.command === 'configurationDone') { - this.configurationDoneRequest(response, request.arguments, request); - - } else if (request.command === 'continue') { - this.continueRequest(response, request.arguments, request); - - } else if (request.command === 'next') { - this.nextRequest(response, request.arguments, request); - - } else if (request.command === 'stepIn') { - this.stepInRequest(response, request.arguments, request); - - } else if (request.command === 'stepOut') { - this.stepOutRequest(response, request.arguments, request); - - } else if (request.command === 'stepBack') { - this.stepBackRequest(response, request.arguments, request); - - } else if (request.command === 'reverseContinue') { - this.reverseContinueRequest(response, request.arguments, request); - - } else if (request.command === 'restartFrame') { - this.restartFrameRequest(response, request.arguments, request); - - } else if (request.command === 'goto') { - this.gotoRequest(response, request.arguments, request); - - } else if (request.command === 'pause') { - this.pauseRequest(response, request.arguments, request); - - } else if (request.command === 'stackTrace') { - this.stackTraceRequest(response, request.arguments, request); - - } else if (request.command === 'scopes') { - this.scopesRequest(response, request.arguments, request); - - } else if (request.command === 'variables') { - this.variablesRequest(response, request.arguments, request); - - } else if (request.command === 'setVariable') { - this.setVariableRequest(response, request.arguments, request); - - } else if (request.command === 'setExpression') { - this.setExpressionRequest(response, request.arguments, request); - - } else if (request.command === 'source') { - this.sourceRequest(response, request.arguments, request); - - } else if (request.command === 'threads') { - this.threadsRequest(response, request); - - } else if (request.command === 'terminateThreads') { - this.terminateThreadsRequest(response, request.arguments, request); - - } else if (request.command === 'evaluate') { - this.evaluateRequest(response, request.arguments, request); - - } else if (request.command === 'stepInTargets') { - this.stepInTargetsRequest(response, request.arguments, request); - - } else if (request.command === 'gotoTargets') { - this.gotoTargetsRequest(response, request.arguments, request); - - } else if (request.command === 'completions') { - this.completionsRequest(response, request.arguments, request); - - } else if (request.command === 'exceptionInfo') { - this.exceptionInfoRequest(response, request.arguments, request); - - } else if (request.command === 'loadedSources') { - this.loadedSourcesRequest(response, request.arguments, request); - - } else if (request.command === 'dataBreakpointInfo') { - this.dataBreakpointInfoRequest(response, request.arguments, request); - - } else if (request.command === 'setDataBreakpoints') { - this.setDataBreakpointsRequest(response, request.arguments, request); - - } else if (request.command === 'readMemory') { - this.readMemoryRequest(response, request.arguments, request); - - } else if (request.command === 'disassemble') { - this.disassembleRequest(response, request.arguments, request); - - } else if (request.command === 'cancel') { - this.cancelRequest(response, request.arguments, request); - - } else if (request.command === 'breakpointLocations') { - this.breakpointLocationsRequest(response, request.arguments, request); - - } else { - this.customRequest(request.command, response, request.arguments, request); - } - } catch (e) { - this.sendErrorResponse(response, 1104, '{_stack}', { _exception: e.message, _stack: e.stack }, ErrorDestination.Telemetry); - } - } - - protected initializeRequest(response: DebugProtocol.InitializeResponse, _args: DebugProtocol.InitializeRequestArguments): void { - - response.body = response.body || {}; - - // This default debug adapter does not support conditional breakpoints. - response.body.supportsConditionalBreakpoints = false; - - // This default debug adapter does not support hit conditional breakpoints. - response.body.supportsHitConditionalBreakpoints = false; - - // This default debug adapter does not support function breakpoints. - response.body.supportsFunctionBreakpoints = false; - - // This default debug adapter implements the 'configurationDone' request. - response.body.supportsConfigurationDoneRequest = true; - - // This default debug adapter does not support hovers based on the 'evaluate' request. - response.body.supportsEvaluateForHovers = false; - - // This default debug adapter does not support the 'stepBack' request. - response.body.supportsStepBack = false; - - // This default debug adapter does not support the 'setVariable' request. - response.body.supportsSetVariable = false; - - // This default debug adapter does not support the 'restartFrame' request. - response.body.supportsRestartFrame = false; - - // This default debug adapter does not support the 'stepInTargets' request. - response.body.supportsStepInTargetsRequest = false; - - // This default debug adapter does not support the 'gotoTargets' request. - response.body.supportsGotoTargetsRequest = false; - - // This default debug adapter does not support the 'completions' request. - response.body.supportsCompletionsRequest = false; - - // This default debug adapter does not support the 'restart' request. - response.body.supportsRestartRequest = false; - - // This default debug adapter does not support the 'exceptionOptions' attribute on the 'setExceptionBreakpoints' request. - response.body.supportsExceptionOptions = false; - - // This default debug adapter does not support the 'format' attribute on the 'variables', 'evaluate', and 'stackTrace' request. - response.body.supportsValueFormattingOptions = false; - - // This debug adapter does not support the 'exceptionInfo' request. - response.body.supportsExceptionInfoRequest = false; - - // This debug adapter does not support the 'TerminateDebuggee' attribute on the 'disconnect' request. - response.body.supportTerminateDebuggee = false; - - // This debug adapter does not support delayed loading of stack frames. - response.body.supportsDelayedStackTraceLoading = false; - - // This debug adapter does not support the 'loadedSources' request. - response.body.supportsLoadedSourcesRequest = false; - - // This debug adapter does not support the 'logMessage' attribute of the SourceBreakpoint. - response.body.supportsLogPoints = false; - - // This debug adapter does not support the 'terminateThreads' request. - response.body.supportsTerminateThreadsRequest = false; - - // This debug adapter does not support the 'setExpression' request. - response.body.supportsSetExpression = false; - - // This debug adapter does not support the 'terminate' request. - response.body.supportsTerminateRequest = false; - - // This debug adapter does not support data breakpoints. - response.body.supportsDataBreakpoints = false; - - /** This debug adapter does not support the 'readMemory' request. */ - response.body.supportsReadMemoryRequest = false; - - /** The debug adapter does not support the 'disassemble' request. */ - response.body.supportsDisassembleRequest = false; - - /** The debug adapter does not support the 'cancel' request. */ - response.body.supportsCancelRequest = false; - - /** The debug adapter does not support the 'breakpointLocations' request. */ - response.body.supportsBreakpointLocationsRequest = false; - - this.sendResponse(response); - } - - protected disconnectRequest(response: DebugProtocol.DisconnectResponse, _args: DebugProtocol.DisconnectArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - this.shutdown(); - } - - protected launchRequest(response: DebugProtocol.LaunchResponse, _args: DebugProtocol.LaunchRequestArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected attachRequest(response: DebugProtocol.AttachResponse, _args: DebugProtocol.AttachRequestArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected terminateRequest(response: DebugProtocol.TerminateResponse, _args: DebugProtocol.TerminateArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected restartRequest(response: DebugProtocol.RestartResponse, _args: DebugProtocol.RestartArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, _args: DebugProtocol.SetBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setFunctionBreakPointsRequest(response: DebugProtocol.SetFunctionBreakpointsResponse, _args: DebugProtocol.SetFunctionBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, _args: DebugProtocol.SetExceptionBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, _args: DebugProtocol.ConfigurationDoneArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, _args: DebugProtocol.ContinueArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected nextRequest(response: DebugProtocol.NextResponse, _args: DebugProtocol.NextArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepInRequest(response: DebugProtocol.StepInResponse, _args: DebugProtocol.StepInArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepOutRequest(response: DebugProtocol.StepOutResponse, _args: DebugProtocol.StepOutArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepBackRequest(response: DebugProtocol.StepBackResponse, _args: DebugProtocol.StepBackArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, _args: DebugProtocol.ReverseContinueArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected restartFrameRequest(response: DebugProtocol.RestartFrameResponse, _args: DebugProtocol.RestartFrameArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected gotoRequest(response: DebugProtocol.GotoResponse, _args: DebugProtocol.GotoArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected pauseRequest(response: DebugProtocol.PauseResponse, _args: DebugProtocol.PauseArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected sourceRequest(response: DebugProtocol.SourceResponse, _args: DebugProtocol.SourceArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected terminateThreadsRequest(response: DebugProtocol.TerminateThreadsResponse, _args: DebugProtocol.TerminateThreadsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, _args: DebugProtocol.StackTraceArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, _args: DebugProtocol.ScopesArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected variablesRequest(response: DebugProtocol.VariablesResponse, _args: DebugProtocol.VariablesArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setVariableRequest(response: DebugProtocol.SetVariableResponse, _args: DebugProtocol.SetVariableArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setExpressionRequest(response: DebugProtocol.SetExpressionResponse, _args: DebugProtocol.SetExpressionArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, _args: DebugProtocol.EvaluateArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected stepInTargetsRequest(response: DebugProtocol.StepInTargetsResponse, _args: DebugProtocol.StepInTargetsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected gotoTargetsRequest(response: DebugProtocol.GotoTargetsResponse, _args: DebugProtocol.GotoTargetsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected completionsRequest(response: DebugProtocol.CompletionsResponse, _args: DebugProtocol.CompletionsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected exceptionInfoRequest(response: DebugProtocol.ExceptionInfoResponse, _args: DebugProtocol.ExceptionInfoArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected loadedSourcesRequest(response: DebugProtocol.LoadedSourcesResponse, _args: DebugProtocol.LoadedSourcesArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected dataBreakpointInfoRequest(response: DebugProtocol.DataBreakpointInfoResponse, _args: DebugProtocol.DataBreakpointInfoArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected setDataBreakpointsRequest(response: DebugProtocol.SetDataBreakpointsResponse, _args: DebugProtocol.SetDataBreakpointsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected readMemoryRequest(response: DebugProtocol.ReadMemoryResponse, _args: DebugProtocol.ReadMemoryArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected disassembleRequest(response: DebugProtocol.DisassembleResponse, _args: DebugProtocol.DisassembleArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected cancelRequest(response: DebugProtocol.CancelResponse, _args: DebugProtocol.CancelArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - protected breakpointLocationsRequest(response: DebugProtocol.BreakpointLocationsResponse, _args: DebugProtocol.BreakpointLocationsArguments, _request?: DebugProtocol.Request): void { - this.sendResponse(response); - } - - /** - * Override this hook to implement custom requests. - */ - protected customRequest(_command: string, response: DebugProtocol.Response, _args: any, _request?: DebugProtocol.Request): void { - this.sendErrorResponse(response, 1014, 'unrecognized request', null, ErrorDestination.Telemetry); - } - - //---- protected ------------------------------------------------------------------------------------------------- - - protected convertClientLineToDebugger(line: number): number { - if (this._debuggerLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line + 1; - } - return this._clientLinesStartAt1 ? line - 1 : line; - } - - protected convertDebuggerLineToClient(line: number): number { - if (this._debuggerLinesStartAt1) { - return this._clientLinesStartAt1 ? line : line - 1; - } - return this._clientLinesStartAt1 ? line + 1 : line; - } - - protected convertClientColumnToDebugger(column: number): number { - if (this._debuggerColumnsStartAt1) { - return this._clientColumnsStartAt1 ? column : column + 1; - } - return this._clientColumnsStartAt1 ? column - 1 : column; - } - - protected convertDebuggerColumnToClient(column: number): number { - if (this._debuggerColumnsStartAt1) { - return this._clientColumnsStartAt1 ? column : column - 1; - } - return this._clientColumnsStartAt1 ? column + 1 : column; - } - - protected convertClientPathToDebugger(clientPath: string): string { - if (this._clientPathsAreURIs !== this._debuggerPathsAreURIs) { - if (this._clientPathsAreURIs) { - return DebugSession.uri2path(clientPath); - } else { - return DebugSession.path2uri(clientPath); - } - } - return clientPath; - } - - protected convertDebuggerPathToClient(debuggerPath: string): string { - if (this._debuggerPathsAreURIs !== this._clientPathsAreURIs) { - if (this._debuggerPathsAreURIs) { - return DebugSession.uri2path(debuggerPath); - } else { - return DebugSession.path2uri(debuggerPath); - } - } - return debuggerPath; - } - - //---- private ------------------------------------------------------------------------------- - - private static path2uri(path: string): string { - - path = encodeURI(path); - - let uri = new URL(`file:`); // ignore 'path' for now - uri.pathname = path; // now use 'path' to get the correct percent encoding (see https://url.spec.whatwg.org) - return uri.toString(); - } - - private static uri2path(sourceUri: string): string { - - let uri = new URL(sourceUri); - let s = decodeURIComponent(uri.pathname); - return s; - } - - private static _formatPIIRegexp = /{([^}]+)}/g; - - /* - * If argument starts with '_' it is OK to send its value to telemetry. - */ - private static formatPII(format: string, excludePII: boolean, args?: { [key: string]: string }): string { - return format.replace(DebugSession._formatPIIRegexp, function (match, paramName) { - if (excludePII && paramName.length > 0 && paramName[0] !== '_') { - return match; - } - return args && args[paramName] && args.hasOwnProperty(paramName) ? - args[paramName] : - match; - }); - } -} - -//--------------------------------------------------------------------------- - -export class Handles { - - private START_HANDLE = 1000; - - private _nextHandle: number; - private _handleMap = new Map(); - - public constructor(startHandle?: number) { - this._nextHandle = typeof startHandle === 'number' ? startHandle : this.START_HANDLE; - } - - public reset(): void { - this._nextHandle = this.START_HANDLE; - this._handleMap = new Map(); - } - - public create(value: T): number { - const handle = this._nextHandle++; - this._handleMap.set(handle, value); - return handle; - } - - public get(handle: number, dflt?: T): T | undefined { - return this._handleMap.get(handle) || dflt; - } -} - -//--------------------------------------------------------------------------- - -class MockConfigurationProvider implements vscode.DebugConfigurationProvider { - - /** - * Massage a debug configuration just before a debug session is being launched, - * e.g. add all missing attributes to the debug configuration. - */ - resolveDebugConfiguration(_folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration, _token?: vscode.CancellationToken): vscode.ProviderResult { - - // if launch.json is missing or empty - if (!config.type && !config.request && !config.name) { - const editor = vscode.window.activeTextEditor; - if (editor && editor.document.languageId === 'markdown') { - config.type = 'mock'; - config.name = 'Launch'; - config.request = 'launch'; - config.program = '${file}'; - config.stopOnEntry = true; - } - } - - if (!config.program) { - return vscode.window.showInformationMessage('Cannot find a program to debug').then(_ => { - return undefined; // abort launch - }); - } - - return config; - } -} - -export class MockDebugAdapterDescriptorFactory implements vscode.DebugAdapterDescriptorFactory { - - constructor(private memfs: MemFS) { - } - - createDebugAdapterDescriptor(_session: vscode.DebugSession, _executable: vscode.DebugAdapterExecutable | undefined): vscode.ProviderResult { - return new vscode.DebugAdapterInlineImplementation(new MockDebugSession(this.memfs)); - } -} - -function basename(path: string): string { - const pos = path.lastIndexOf('/'); - if (pos >= 0) { - return path.substring(pos + 1); - } - return path; -} - -function timeout(ms: number) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -/** - * This interface describes the mock-debug specific launch attributes - * (which are not part of the Debug Adapter Protocol). - * The schema for these attributes lives in the package.json of the mock-debug extension. - * The interface should always match this schema. - */ -interface LaunchRequestArguments extends DebugProtocol.LaunchRequestArguments { - /** An absolute path to the "program" to debug. */ - program: string; - /** Automatically stop target after launch. If not specified, target does not stop. */ - stopOnEntry?: boolean; - /** enable logging the Debug Adapter Protocol */ - trace?: boolean; -} - -export class MockDebugSession extends DebugSession { - - // we don't support multiple threads, so we can use a hardcoded ID for the default thread - private static THREAD_ID = 1; - - // a Mock runtime (or debugger) - private _runtime: MockRuntime; - - private _variableHandles = new Handles(); - - //private _configurationDone = new Subject(); - - private promiseResolve?: () => void; - private _configurationDone = new Promise((r, _e) => { - this.promiseResolve = r; - setTimeout(r, 1000); - }); - - private _cancelationTokens = new Map(); - private _isLongrunning = new Map(); - - /** - * Creates a new debug adapter that is used for one debug session. - * We configure the default implementation of a debug adapter here. - */ - public constructor(memfs: MemFS) { - - super(); - - // this debugger uses zero-based lines and columns - this.setDebuggerLinesStartAt1(false); - this.setDebuggerColumnsStartAt1(false); - - this._runtime = new MockRuntime(memfs); - - // setup event handlers - this._runtime.onStopOnEntry(() => { - this.sendEvent(new StoppedEvent('entry', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnStep(() => { - this.sendEvent(new StoppedEvent('step', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnBreakpoint(() => { - this.sendEvent(new StoppedEvent('breakpoint', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnDataBreakpoint(() => { - this.sendEvent(new StoppedEvent('data breakpoint', MockDebugSession.THREAD_ID)); - }); - this._runtime.onStopOnException(() => { - this.sendEvent(new StoppedEvent('exception', MockDebugSession.THREAD_ID)); - }); - this._runtime.onBreakpointValidated((bp: MockBreakpoint) => { - this.sendEvent(new BreakpointEvent('changed', { verified: bp.verified, id: bp.id })); - }); - this._runtime.onOutput(oe => { - const e: DebugProtocol.OutputEvent = new OutputEvent(`${oe.text}\n`); - e.body.source = this.createSource(oe.filePath); - e.body.line = this.convertDebuggerLineToClient(oe.line); - e.body.column = this.convertDebuggerColumnToClient(oe.column); - this.sendEvent(e); - }); - this._runtime.onEnd(() => { - this.sendEvent(new TerminatedEvent()); - }); - } - - /** - * The 'initialize' request is the first request called by the frontend - * to interrogate the features the debug adapter provides. - */ - protected initializeRequest(response: DebugProtocol.InitializeResponse, _args: DebugProtocol.InitializeRequestArguments): void { - - // build and return the capabilities of this debug adapter: - response.body = response.body || {}; - - // the adapter implements the configurationDoneRequest. - response.body.supportsConfigurationDoneRequest = true; - - // make VS Code to use 'evaluate' when hovering over source - response.body.supportsEvaluateForHovers = true; - - // make VS Code to show a 'step back' button - response.body.supportsStepBack = true; - - // make VS Code to support data breakpoints - response.body.supportsDataBreakpoints = true; - - // make VS Code to support completion in REPL - response.body.supportsCompletionsRequest = true; - response.body.completionTriggerCharacters = ['.', '[']; - - // make VS Code to send cancelRequests - response.body.supportsCancelRequest = true; - - // make VS Code send the breakpointLocations request - response.body.supportsBreakpointLocationsRequest = true; - - this.sendResponse(response); - - // since this debug adapter can accept configuration requests like 'setBreakpoint' at any time, - // we request them early by sending an 'initializeRequest' to the frontend. - // The frontend will end the configuration sequence by calling 'configurationDone' request. - this.sendEvent(new InitializedEvent()); - } - - /** - * Called at the end of the configuration sequence. - * Indicates that all breakpoints etc. have been sent to the DA and that the 'launch' can start. - */ - protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { - super.configurationDoneRequest(response, args); - - // notify the launchRequest that configuration has finished - //this._configurationDone.notify(); - if (this.promiseResolve) { - this.promiseResolve(); - } - } - - protected async launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments) { - - // make sure to 'Stop' the buffered logging if 'trace' is not set - //logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Stop, false); - - // wait until configuration has finished (and configurationDoneRequest has been called) - await this._configurationDone; - - // start the program in the runtime - this._runtime.start(`memfs:${args.program}`, !!args.stopOnEntry); - - this.sendResponse(response); - } - - protected setBreakPointsRequest(response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): void { - - const path = args.source.path; - const clientLines = args.lines || []; - - // clear all breakpoints for this file - this._runtime.clearBreakpoints(path); - - // set and verify breakpoint locations - const actualBreakpoints = clientLines.map(l => { - let { verified, line, id } = this._runtime.setBreakPoint(path, this.convertClientLineToDebugger(l)); - const bp = new Breakpoint(verified, this.convertDebuggerLineToClient(line)); - bp.id = id; - return bp; - }); - - // send back the actual breakpoint positions - response.body = { - breakpoints: actualBreakpoints - }; - this.sendResponse(response); - } - - protected breakpointLocationsRequest(response: DebugProtocol.BreakpointLocationsResponse, args: DebugProtocol.BreakpointLocationsArguments, _request?: DebugProtocol.Request): void { - - if (args.source.path) { - const bps = this._runtime.getBreakpoints(args.source.path, this.convertClientLineToDebugger(args.line)); - response.body = { - breakpoints: bps.map(col => { - return { - line: args.line, - column: this.convertDebuggerColumnToClient(col) - }; - }) - }; - } else { - response.body = { - breakpoints: [] - }; - } - this.sendResponse(response); - } - - protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - - // runtime supports no threads so just return a default thread. - response.body = { - threads: [ - new Thread(MockDebugSession.THREAD_ID, 'thread 1') - ] - }; - this.sendResponse(response); - } - - protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { - - const startFrame = typeof args.startFrame === 'number' ? args.startFrame : 0; - const maxLevels = typeof args.levels === 'number' ? args.levels : 1000; - const endFrame = startFrame + maxLevels; - - const stk = this._runtime.stack(startFrame, endFrame); - - response.body = { - stackFrames: stk.frames.map(f => new StackFrame(f.index, f.name, this.createSource(f.file), this.convertDebuggerLineToClient(f.line))), - totalFrames: stk.count - }; - this.sendResponse(response); - } - - protected scopesRequest(response: DebugProtocol.ScopesResponse, _args: DebugProtocol.ScopesArguments): void { - - response.body = { - scopes: [ - new Scope('Local', this._variableHandles.create('local'), false), - new Scope('Global', this._variableHandles.create('global'), true) - ] - }; - this.sendResponse(response); - } - - protected async variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request) { - - const variables: DebugProtocol.Variable[] = []; - - if (this._isLongrunning.get(args.variablesReference)) { - // long running - - if (request) { - this._cancelationTokens.set(request.seq, false); - } - - for (let i = 0; i < 100; i++) { - await timeout(1000); - variables.push({ - name: `i_${i}`, - type: 'integer', - value: `${i}`, - variablesReference: 0 - }); - if (request && this._cancelationTokens.get(request.seq)) { - break; - } - } - - if (request) { - this._cancelationTokens.delete(request.seq); - } - - } else { - - const id = this._variableHandles.get(args.variablesReference); - - if (id) { - variables.push({ - name: id + '_i', - type: 'integer', - value: '123', - variablesReference: 0 - }); - variables.push({ - name: id + '_f', - type: 'float', - value: '3.14', - variablesReference: 0 - }); - variables.push({ - name: id + '_s', - type: 'string', - value: 'hello world', - variablesReference: 0 - }); - variables.push({ - name: id + '_o', - type: 'object', - value: 'Object', - variablesReference: this._variableHandles.create(id + '_o') - }); - - // cancelation support for long running requests - const nm = id + '_long_running'; - const ref = this._variableHandles.create(id + '_lr'); - variables.push({ - name: nm, - type: 'object', - value: 'Object', - variablesReference: ref - }); - this._isLongrunning.set(ref, true); - } - } - - response.body = { - variables: variables - }; - this.sendResponse(response); - } - - protected continueRequest(response: DebugProtocol.ContinueResponse, _args: DebugProtocol.ContinueArguments): void { - this._runtime.continue(); - this.sendResponse(response); - } - - protected reverseContinueRequest(response: DebugProtocol.ReverseContinueResponse, _args: DebugProtocol.ReverseContinueArguments): void { - this._runtime.continue(true); - this.sendResponse(response); - } - - protected nextRequest(response: DebugProtocol.NextResponse, _args: DebugProtocol.NextArguments): void { - this._runtime.step(); - this.sendResponse(response); - } - - protected stepBackRequest(response: DebugProtocol.StepBackResponse, _args: DebugProtocol.StepBackArguments): void { - this._runtime.step(true); - this.sendResponse(response); - } - - protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { - - let reply: string | undefined = undefined; - - if (args.context === 'repl') { - // 'evaluate' supports to create and delete breakpoints from the 'repl': - const matches = /new +([0-9]+)/.exec(args.expression); - if (matches && matches.length === 2) { - if (this._runtime.sourceFile) { - const mbp = this._runtime.setBreakPoint(this._runtime.sourceFile, this.convertClientLineToDebugger(parseInt(matches[1]))); - const bp = new Breakpoint(mbp.verified, this.convertDebuggerLineToClient(mbp.line), undefined, this.createSource(this._runtime.sourceFile)); - bp.id = mbp.id; - this.sendEvent(new BreakpointEvent('new', bp)); - reply = `breakpoint created`; - } - } else { - const matches = /del +([0-9]+)/.exec(args.expression); - if (matches && matches.length === 2) { - const mbp = this._runtime.sourceFile ? this._runtime.clearBreakPoint(this._runtime.sourceFile, this.convertClientLineToDebugger(parseInt(matches[1]))) : undefined; - if (mbp) { - const bp = new Breakpoint(false); - bp.id = mbp.id; - this.sendEvent(new BreakpointEvent('removed', bp)); - reply = `breakpoint deleted`; - } - } - } - } - - response.body = { - result: reply ? reply : `evaluate(context: '${args.context}', '${args.expression}')`, - variablesReference: 0 - }; - this.sendResponse(response); - } - - protected dataBreakpointInfoRequest(response: DebugProtocol.DataBreakpointInfoResponse, args: DebugProtocol.DataBreakpointInfoArguments): void { - - response.body = { - dataId: null, - description: 'cannot break on data access', - accessTypes: undefined, - canPersist: false - }; - - if (args.variablesReference && args.name) { - const id = this._variableHandles.get(args.variablesReference); - if (id && id.startsWith('global_')) { - response.body.dataId = args.name; - response.body.description = args.name; - response.body.accessTypes = ['read']; - response.body.canPersist = false; - } - } - - this.sendResponse(response); - } - - protected setDataBreakpointsRequest(response: DebugProtocol.SetDataBreakpointsResponse, args: DebugProtocol.SetDataBreakpointsArguments): void { - - // clear all data breakpoints - this._runtime.clearAllDataBreakpoints(); - - response.body = { - breakpoints: [] - }; - - for (let dbp of args.breakpoints) { - // assume that id is the "address" to break on - const ok = this._runtime.setDataBreakpoint(dbp.dataId); - response.body.breakpoints.push({ - verified: ok - }); - } - - this.sendResponse(response); - } - - protected completionsRequest(response: DebugProtocol.CompletionsResponse, _args: DebugProtocol.CompletionsArguments): void { - - response.body = { - targets: [ - { - label: 'item 10', - sortText: '10' - }, - { - label: 'item 1', - sortText: '01' - }, - { - label: 'item 2', - sortText: '02' - } - ] - }; - this.sendResponse(response); - } - - protected cancelRequest(_response: DebugProtocol.CancelResponse, args: DebugProtocol.CancelArguments) { - if (args.requestId) { - this._cancelationTokens.set(args.requestId, true); - } - } - - //---- helpers - - private createSource(filePath: string): Source { - return new Source(basename(filePath), this.convertDebuggerPathToClient(filePath), undefined, undefined, 'mock-adapter-data'); - } -} - -//------------------------------------------------------------------------------------------------------------------------------------------ - - -/*--------------------------------------------------------- - * Copyright (C) Microsoft Corporation. All rights reserved. - *--------------------------------------------------------*/ - -export interface MockBreakpoint { - id: number; - line: number; - verified: boolean; -} - -export interface MockOutputEvent { - text: string; - filePath: string; - line: number; - column: number; -} - -/** - * A Mock runtime with minimal debugger functionality. - */ -export class MockRuntime { - - private stopOnEntry = new vscode.EventEmitter(); - onStopOnEntry: vscode.Event = this.stopOnEntry.event; - - private stopOnStep = new vscode.EventEmitter(); - onStopOnStep: vscode.Event = this.stopOnStep.event; - - private stopOnBreakpoint = new vscode.EventEmitter(); - onStopOnBreakpoint: vscode.Event = this.stopOnBreakpoint.event; - - private stopOnDataBreakpoint = new vscode.EventEmitter(); - onStopOnDataBreakpoint: vscode.Event = this.stopOnDataBreakpoint.event; - - private stopOnException = new vscode.EventEmitter(); - onStopOnException: vscode.Event = this.stopOnException.event; - - private breakpointValidated = new vscode.EventEmitter(); - onBreakpointValidated: vscode.Event = this.breakpointValidated.event; - - private output = new vscode.EventEmitter(); - onOutput: vscode.Event = this.output.event; - - private end = new vscode.EventEmitter(); - onEnd: vscode.Event = this.end.event; - - - // the initial (and one and only) file we are 'debugging' - private _sourceFile?: string; - public get sourceFile() { - return this._sourceFile; - } - - // the contents (= lines) of the one and only file - private _sourceLines: string[] = []; - - // This is the next line that will be 'executed' - private _currentLine = 0; - - // maps from sourceFile to array of Mock breakpoints - private _breakPoints = new Map(); - - // since we want to send breakpoint events, we will assign an id to every event - // so that the frontend can match events with breakpoints. - private _breakpointId = 1; - - private _breakAddresses = new Set(); - - constructor(private memfs: MemFS) { - } - - /** - * Start executing the given program. - */ - public start(program: string, stopOnEntry: boolean) { - - this.loadSource(program); - this._currentLine = -1; - - if (this._sourceFile) { - this.verifyBreakpoints(this._sourceFile); - } - - if (stopOnEntry) { - // we step once - this.step(false, this.stopOnEntry); - } else { - // we just start to run until we hit a breakpoint or an exception - this.continue(); - } - } - - /** - * Continue execution to the end/beginning. - */ - public continue(reverse = false) { - this.run(reverse, undefined); - } - - /** - * Step to the next/previous non empty line. - */ - public step(reverse = false, event = this.stopOnStep) { - this.run(reverse, event); - } - - /** - * Returns a fake 'stacktrace' where every 'stackframe' is a word from the current line. - */ - public stack(startFrame: number, endFrame: number): { frames: any[], count: number } { - - const words = this._sourceLines[this._currentLine].trim().split(/\s+/); - - const frames = new Array(); - // every word of the current line becomes a stack frame. - for (let i = startFrame; i < Math.min(endFrame, words.length); i++) { - const name = words[i]; // use a word of the line as the stackframe name - frames.push({ - index: i, - name: `${name}(${i})`, - file: this._sourceFile, - line: this._currentLine - }); - } - return { - frames: frames, - count: words.length - }; - } - - public getBreakpoints(_path: string, line: number): number[] { - - const l = this._sourceLines[line]; - - let sawSpace = true; - const bps: number[] = []; - for (let i = 0; i < l.length; i++) { - if (l[i] !== ' ') { - if (sawSpace) { - bps.push(i); - sawSpace = false; - } - } else { - sawSpace = true; - } - } - - return bps; - } - - /* - * Set breakpoint in file with given line. - */ - public setBreakPoint(path: string, line: number): MockBreakpoint { - - const bp = { verified: false, line, id: this._breakpointId++ }; - let bps = this._breakPoints.get(path); - if (!bps) { - bps = new Array(); - this._breakPoints.set(path, bps); - } - bps.push(bp); - - this.verifyBreakpoints(path); - - return bp; - } - - /* - * Clear breakpoint in file with given line. - */ - public clearBreakPoint(path: string, line: number): MockBreakpoint | undefined { - let bps = this._breakPoints.get(path); - if (bps) { - const index = bps.findIndex(bp => bp.line === line); - if (index >= 0) { - const bp = bps[index]; - bps.splice(index, 1); - return bp; - } - } - return undefined; - } - - /* - * Clear all breakpoints for file. - */ - public clearBreakpoints(path: string): void { - this._breakPoints.delete(path); - } - - /* - * Set data breakpoint. - */ - public setDataBreakpoint(address: string): boolean { - if (address) { - this._breakAddresses.add(address); - return true; - } - return false; - } - - /* - * Clear all data breakpoints. - */ - public clearAllDataBreakpoints(): void { - this._breakAddresses.clear(); - } - - // private methods - - private loadSource(file: string) { - if (this._sourceFile !== file) { - this._sourceFile = file; - - const _textDecoder = new TextDecoder(); - - const uri = vscode.Uri.parse(file); - const content = _textDecoder.decode(this.memfs.readFile(uri)); - this._sourceLines = content.split('\n'); - - //this._sourceLines = readFileSync(this._sourceFile).toString().split('\n'); - } - } - - /** - * Run through the file. - * If stepEvent is specified only run a single step and emit the stepEvent. - */ - private run(reverse = false, stepEvent?: vscode.EventEmitter): void { - if (reverse) { - for (let ln = this._currentLine - 1; ln >= 0; ln--) { - if (this.fireEventsForLine(ln, stepEvent)) { - this._currentLine = ln; - return; - } - } - // no more lines: stop at first line - this._currentLine = 0; - this.stopOnEntry.fire(); - } else { - for (let ln = this._currentLine + 1; ln < this._sourceLines.length; ln++) { - if (this.fireEventsForLine(ln, stepEvent)) { - this._currentLine = ln; - return; - } - } - // no more lines: run to end - this.end.fire(); - } - } - - private verifyBreakpoints(path: string): void { - let bps = this._breakPoints.get(path); - if (bps) { - this.loadSource(path); - bps.forEach(bp => { - if (!bp.verified && bp.line < this._sourceLines.length) { - const srcLine = this._sourceLines[bp.line].trim(); - - // if a line is empty or starts with '+' we don't allow to set a breakpoint but move the breakpoint down - if (srcLine.length === 0 || srcLine.indexOf('+') === 0) { - bp.line++; - } - // if a line starts with '-' we don't allow to set a breakpoint but move the breakpoint up - if (srcLine.indexOf('-') === 0) { - bp.line--; - } - // don't set 'verified' to true if the line contains the word 'lazy' - // in this case the breakpoint will be verified 'lazy' after hitting it once. - if (srcLine.indexOf('lazy') < 0) { - bp.verified = true; - this.breakpointValidated.fire(bp); - } - } - }); - } - } - - /** - * Fire events if line has a breakpoint or the word 'exception' is found. - * Returns true is execution needs to stop. - */ - private fireEventsForLine(ln: number, stepEvent?: vscode.EventEmitter): boolean { - - const line = this._sourceLines[ln].trim(); - - // if 'log(...)' found in source -> send argument to debug console - const matches = /log\((.*)\)/.exec(line); - if (matches && matches.length === 2) { - if (this._sourceFile) { - this.output.fire({ text: matches[1], filePath: this._sourceFile, line: ln, column: matches.index }); - } - } - - // if a word in a line matches a data breakpoint, fire a 'dataBreakpoint' event - const words = line.split(' '); - for (let word of words) { - if (this._breakAddresses.has(word)) { - this.stopOnDataBreakpoint.fire(); - return true; - } - } - - // if word 'exception' found in source -> throw exception - if (line.indexOf('exception') >= 0) { - this.stopOnException.fire(); - return true; - } - - // is there a breakpoint? - const breakpoints = this._sourceFile ? this._breakPoints.get(this._sourceFile) : undefined; - if (breakpoints) { - const bps = breakpoints.filter(bp => bp.line === ln); - if (bps.length > 0) { - - // send 'stopped' event - this.stopOnBreakpoint.fire(); - - // the following shows the use of 'breakpoint' events to update properties of a breakpoint in the UI - // if breakpoint is not yet verified, verify it now and send a 'breakpoint' update event - if (!bps[0].verified) { - bps[0].verified = true; - this.breakpointValidated.fire(bps[0]); - } - return true; - } - } - - // non-empty line - if (stepEvent && line.length > 0) { - stepEvent.fire(); - return true; - } - - // nothing interesting found -> continue - return false; - } -} diff --git a/extensions/vscode-web-playground/src/memfs.ts b/extensions/vscode-web-playground/src/memfs.ts deleted file mode 100644 index 76c6f3f0e2e..00000000000 --- a/extensions/vscode-web-playground/src/memfs.ts +++ /dev/null @@ -1,449 +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 { - CancellationToken, - Disposable, - Event, - EventEmitter, - FileChangeEvent, - FileChangeType, - FileSearchOptions, - FileSearchProvider, - FileSearchQuery, - FileStat, - FileSystemError, - FileSystemProvider, - FileType, - Position, - Progress, - ProviderResult, - Range, - TextSearchComplete, - TextSearchOptions, - TextSearchQuery, - TextSearchProvider, - TextSearchResult, - Uri, - workspace, -} from 'vscode'; -import { largeTSFile, getImageFile, debuggableFile, windows1251File, gbkFile } from './exampleFiles'; - -export class File implements FileStat { - - type: FileType; - ctime: number; - mtime: number; - size: number; - - name: string; - data?: Uint8Array; - - constructor(public uri: Uri, name: string) { - this.type = FileType.File; - this.ctime = Date.now(); - this.mtime = Date.now(); - this.size = 0; - this.name = name; - } -} - -export class Directory implements FileStat { - - type: FileType; - ctime: number; - mtime: number; - size: number; - - name: string; - entries: Map; - - constructor(public uri: Uri, name: string) { - this.type = FileType.Directory; - this.ctime = Date.now(); - this.mtime = Date.now(); - this.size = 0; - this.name = name; - this.entries = new Map(); - } -} - -export type Entry = File | Directory; - -const textEncoder = new TextEncoder(); - -export class MemFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable { - static scheme = 'memfs'; - - private readonly disposable: Disposable; - - constructor() { - this.disposable = Disposable.from( - workspace.registerFileSystemProvider(MemFS.scheme, this, { isCaseSensitive: true }), - workspace.registerFileSearchProvider(MemFS.scheme, this), - workspace.registerTextSearchProvider(MemFS.scheme, this) - ); - } - - dispose() { - this.disposable?.dispose(); - } - - seed() { - this.createDirectory(Uri.parse(`memfs:/sample-folder/`)); - - // most common files types - this.writeFile(Uri.parse(`memfs:/sample-folder/large.ts`), textEncoder.encode(largeTSFile), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.txt`), textEncoder.encode('foo'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.html`), textEncoder.encode('

Hello

'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.js`), textEncoder.encode('console.log("JavaScript")'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.json`), textEncoder.encode('{ "json": true }'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.ts`), textEncoder.encode('console.log("TypeScript")'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.css`), textEncoder.encode('* { color: green; }'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.md`), textEncoder.encode(debuggableFile), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.xml`), textEncoder.encode(''), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.py`), textEncoder.encode('import base64, sys; base64.decode(open(sys.argv[1], "rb"), open(sys.argv[2], "wb"))'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.yaml`), textEncoder.encode('- just: write something'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.jpg`), getImageFile(), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/file.php`), textEncoder.encode(''), { create: true, overwrite: true }); - - // some more files & folders - this.createDirectory(Uri.parse(`memfs:/sample-folder/folder/`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/large/`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/xyz/`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/xyz/abc`)); - this.createDirectory(Uri.parse(`memfs:/sample-folder/xyz/def`)); - - this.writeFile(Uri.parse(`memfs:/sample-folder/folder/empty.txt`), new Uint8Array(0), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/folder/empty.foo`), new Uint8Array(0), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/folder/file.ts`), textEncoder.encode('let a:number = true; console.log(a);'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/large/rnd.foo`), randomData(50000), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/UPPER.txt`), textEncoder.encode('UPPER'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/upper.txt`), textEncoder.encode('upper'), { create: true, overwrite: true }); - this.writeFile(Uri.parse(`memfs:/sample-folder/xyz/def/foo.md`), textEncoder.encode('*MemFS*'), { create: true, overwrite: true }); - - // some files in different encodings - this.createDirectory(Uri.parse(`memfs:/sample-folder/encodings/`)); - this.writeFile( - Uri.parse(`memfs:/sample-folder/encodings/windows1251.txt`), - windows1251File, - { create: true, overwrite: true } - ); - this.writeFile( - Uri.parse(`memfs:/sample-folder/encodings/gbk.txt`), - gbkFile, - { create: true, overwrite: true } - ); - } - - root = new Directory(Uri.parse('memfs:/'), ''); - - // --- manage file metadata - - stat(uri: Uri): FileStat { - return this._lookup(uri, false); - } - - readDirectory(uri: Uri): [string, FileType][] { - const entry = this._lookupAsDirectory(uri, false); - let result: [string, FileType][] = []; - for (const [name, child] of entry.entries) { - result.push([name, child.type]); - } - return result; - } - - // --- manage file contents - - readFile(uri: Uri): Uint8Array { - const data = this._lookupAsFile(uri, false).data; - if (data) { - return data; - } - throw FileSystemError.FileNotFound(); - } - - writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): void { - let basename = this._basename(uri.path); - let parent = this._lookupParentDirectory(uri); - let entry = parent.entries.get(basename); - if (entry instanceof Directory) { - throw FileSystemError.FileIsADirectory(uri); - } - if (!entry && !options.create) { - throw FileSystemError.FileNotFound(uri); - } - if (entry && options.create && !options.overwrite) { - throw FileSystemError.FileExists(uri); - } - if (!entry) { - entry = new File(uri, basename); - parent.entries.set(basename, entry); - this._fireSoon({ type: FileChangeType.Created, uri }); - } - entry.mtime = Date.now(); - entry.size = content.byteLength; - entry.data = content; - - this._fireSoon({ type: FileChangeType.Changed, uri }); - } - - // --- manage files/folders - - rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): void { - if (!options.overwrite && this._lookup(newUri, true)) { - throw FileSystemError.FileExists(newUri); - } - - let entry = this._lookup(oldUri, false); - let oldParent = this._lookupParentDirectory(oldUri); - - let newParent = this._lookupParentDirectory(newUri); - let newName = this._basename(newUri.path); - - oldParent.entries.delete(entry.name); - entry.name = newName; - newParent.entries.set(newName, entry); - - this._fireSoon( - { type: FileChangeType.Deleted, uri: oldUri }, - { type: FileChangeType.Created, uri: newUri } - ); - } - - delete(uri: Uri): void { - let dirname = uri.with({ path: this._dirname(uri.path) }); - let basename = this._basename(uri.path); - let parent = this._lookupAsDirectory(dirname, false); - if (!parent.entries.has(basename)) { - throw FileSystemError.FileNotFound(uri); - } - parent.entries.delete(basename); - parent.mtime = Date.now(); - parent.size -= 1; - this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { uri, type: FileChangeType.Deleted }); - } - - createDirectory(uri: Uri): void { - let basename = this._basename(uri.path); - let dirname = uri.with({ path: this._dirname(uri.path) }); - let parent = this._lookupAsDirectory(dirname, false); - - let entry = new Directory(uri, basename); - parent.entries.set(entry.name, entry); - parent.mtime = Date.now(); - parent.size += 1; - this._fireSoon({ type: FileChangeType.Changed, uri: dirname }, { type: FileChangeType.Created, uri }); - } - - // --- lookup - - private _lookup(uri: Uri, silent: false): Entry; - private _lookup(uri: Uri, silent: boolean): Entry | undefined; - private _lookup(uri: Uri, silent: boolean): Entry | undefined { - let parts = uri.path.split('/'); - let entry: Entry = this.root; - for (const part of parts) { - if (!part) { - continue; - } - let child: Entry | undefined; - if (entry instanceof Directory) { - child = entry.entries.get(part); - } - if (!child) { - if (!silent) { - throw FileSystemError.FileNotFound(uri); - } else { - return undefined; - } - } - entry = child; - } - return entry; - } - - private _lookupAsDirectory(uri: Uri, silent: boolean): Directory { - let entry = this._lookup(uri, silent); - if (entry instanceof Directory) { - return entry; - } - throw FileSystemError.FileNotADirectory(uri); - } - - private _lookupAsFile(uri: Uri, silent: boolean): File { - let entry = this._lookup(uri, silent); - if (entry instanceof File) { - return entry; - } - throw FileSystemError.FileIsADirectory(uri); - } - - private _lookupParentDirectory(uri: Uri): Directory { - const dirname = uri.with({ path: this._dirname(uri.path) }); - return this._lookupAsDirectory(dirname, false); - } - - // --- manage file events - - private _emitter = new EventEmitter(); - private _bufferedEvents: FileChangeEvent[] = []; - private _fireSoonHandle?: any; - - readonly onDidChangeFile: Event = this._emitter.event; - - watch(_resource: Uri): Disposable { - // ignore, fires for all changes... - return new Disposable(() => { }); - } - - private _fireSoon(...events: FileChangeEvent[]): void { - this._bufferedEvents.push(...events); - - if (this._fireSoonHandle) { - clearTimeout(this._fireSoonHandle); - } - - this._fireSoonHandle = setTimeout(() => { - this._emitter.fire(this._bufferedEvents); - this._bufferedEvents.length = 0; - }, 5); - } - - // --- path utils - - private _basename(path: string): string { - path = this._rtrim(path, '/'); - if (!path) { - return ''; - } - - return path.substr(path.lastIndexOf('/') + 1); - } - - private _dirname(path: string): string { - path = this._rtrim(path, '/'); - if (!path) { - return '/'; - } - - return path.substr(0, path.lastIndexOf('/')); - } - - private _rtrim(haystack: string, needle: string): string { - if (!haystack || !needle) { - return haystack; - } - - const needleLen = needle.length, - haystackLen = haystack.length; - - if (needleLen === 0 || haystackLen === 0) { - return haystack; - } - - let offset = haystackLen, - idx = -1; - - while (true) { - idx = haystack.lastIndexOf(needle, offset - 1); - if (idx === -1 || idx + needleLen !== offset) { - break; - } - if (idx === 0) { - return ''; - } - offset = idx; - } - - return haystack.substring(0, offset); - } - - private _getFiles(): Set { - const files = new Set(); - - this._doGetFiles(this.root, files); - - return files; - } - - private _doGetFiles(dir: Directory, files: Set): void { - dir.entries.forEach(entry => { - if (entry instanceof File) { - files.add(entry); - } else { - this._doGetFiles(entry, files); - } - }); - } - - private _convertSimple2RegExpPattern(pattern: string): string { - return pattern.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g, '\\$&').replace(/[\*]/g, '.*'); - } - - // --- search provider - - provideFileSearchResults(query: FileSearchQuery, _options: FileSearchOptions, _token: CancellationToken): ProviderResult { - return this._findFiles(query.pattern); - } - - private _findFiles(query: string | undefined): Uri[] { - const files = this._getFiles(); - const result: Uri[] = []; - - const pattern = query ? new RegExp(this._convertSimple2RegExpPattern(query)) : null; - - for (const file of files) { - if (!pattern || pattern.exec(file.name)) { - result.push(file.uri); - } - } - - return result; - } - - private _textDecoder = new TextDecoder(); - - provideTextSearchResults(query: TextSearchQuery, options: TextSearchOptions, progress: Progress, _token: CancellationToken) { - const result: TextSearchComplete = { limitHit: false }; - - const files = this._findFiles(options.includes[0]); - if (files) { - for (const file of files) { - const content = this._textDecoder.decode(this.readFile(file)); - - const lines = content.split('\n'); - for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - const index = line.indexOf(query.pattern); - if (index !== -1) { - progress.report({ - uri: file, - ranges: new Range(new Position(i, index), new Position(i, index + query.pattern.length)), - preview: { - text: line, - matches: new Range(new Position(0, index), new Position(0, index + query.pattern.length)) - } - }); - } - } - } - } - - return result; - } -} - -function randomData(lineCnt: number, lineLen = 155): Uint8Array { - let lines: string[] = []; - for (let i = 0; i < lineCnt; i++) { - let line = ''; - while (line.length < lineLen) { - line += Math.random().toString(2 + (i % 34)).substr(2); - } - lines.push(line.substr(0, lineLen)); - } - return textEncoder.encode(lines.join('\n')); -} diff --git a/extensions/vscode-web-playground/src/typings/ref.d.ts b/extensions/vscode-web-playground/src/typings/ref.d.ts deleted file mode 100644 index 9abc416f7e8..00000000000 --- a/extensions/vscode-web-playground/src/typings/ref.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/// -/// -/// -/// -/// diff --git a/extensions/vscode-web-playground/tsconfig.json b/extensions/vscode-web-playground/tsconfig.json deleted file mode 100644 index 633da7fad77..00000000000 --- a/extensions/vscode-web-playground/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "extends": "../shared.tsconfig.json", - "compilerOptions": { - "outDir": "./out", - "lib": [ - "dom", - "dom.iterable", - "es2018" - ] - }, - "include": [ - "src/**/*" - ] -} diff --git a/extensions/vscode-web-playground/yarn.lock b/extensions/vscode-web-playground/yarn.lock deleted file mode 100644 index b29fc8fc61d..00000000000 --- a/extensions/vscode-web-playground/yarn.lock +++ /dev/null @@ -1,109 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/mocha@2.2.43": - version "2.2.43" - resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-2.2.43.tgz#03c54589c43ad048cbcbfd63999b55d0424eec27" - integrity sha512-xNlAmH+lRJdUMXClMTI9Y0pRqIojdxfm7DHsIxoB2iTzu3fnPmSMEN8SsSx0cdwV36d02PWCWaDUoZPDSln+xw== - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - -debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -is-buffer@~1.1.1: - version "1.1.6" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -lodash@^4.16.4: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -md5@^2.1.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - -minimist@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== - -mkdirp@~0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" - -mocha-junit-reporter@^1.17.0: - version "1.23.3" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.3.tgz#941e219dd759ed732f8641e165918aa8b167c981" - integrity sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA== - dependencies: - debug "^2.2.0" - md5 "^2.1.0" - mkdirp "~0.5.1" - strip-ansi "^4.0.0" - xml "^1.0.0" - -mocha-multi-reporters@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" - integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= - dependencies: - debug "^3.1.0" - lodash "^4.16.4" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -xml@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" - integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 9a140984799..8ed194dd356 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@^4.0.1-insiders.20200813: - version "4.0.1-insiders.20200813" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-insiders.20200813.tgz#0b17335a7517023be0f1ce947052662ab2bde1f0" - integrity sha512-mTQPs9uyxv6jLEO5Z+LJpFUSQwx9KI3ZD+2Uv9e5O32Oz/16snCB5skBHw5k1PchsXOZCG6xcB902qmgjI0tWQ== +typescript@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" + integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== diff --git a/package.json b/package.json index e40c9e33ec3..65ae6ee27f3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.49.0", - "distro": "af085a7e151908a14d22a08a22374f924b8a593d", + "distro": "0891ad485f85afec1bf016d33948b877f5562476", "author": { "name": "Microsoft Corporation" }, @@ -40,7 +40,7 @@ "valid-layers-check": "node build/lib/layersChecker.js", "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", - "web": "node resources/serverless/code-web.js", + "web": "node resources/web/code-web.js", "compile-web": "gulp compile-web --max_old_space_size=4095", "watch-web": "gulp watch-web --max_old_space_size=4095", "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" @@ -56,7 +56,7 @@ "keytar": "^5.5.0", "minimist": "^1.2.5", "native-is-elevated": "0.4.1", - "native-keymap": "2.1.2", + "native-keymap": "2.2.0", "native-watchdog": "1.3.0", "node-pty": "0.10.0-beta8", "semver-umd": "^5.5.7", @@ -109,7 +109,7 @@ "css-loader": "^3.2.0", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "9.2.0", + "electron": "9.2.1", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", @@ -164,7 +164,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^4.0.1-rc", + "typescript": "^4.1.0-dev.20200824", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", @@ -191,4 +191,4 @@ "windows-mutex": "0.3.0", "windows-process-tree": "0.2.4" } -} +} \ No newline at end of file diff --git a/product.json b/product.json index efbb04d8b55..3b18619d1fe 100644 --- a/product.json +++ b/product.json @@ -31,7 +31,7 @@ "builtInExtensions": [ { "name": "ms-vscode.node-debug", - "version": "1.44.9", + "version": "1.44.11", "repo": "https://github.com/Microsoft/vscode-node-debug", "metadata": { "id": "b6ded8fb-a0a0-4c1c-acbd-ab2a3bc995a6", @@ -76,7 +76,7 @@ }, { "name": "ms-vscode.js-debug-companion", - "version": "1.0.2", + "version": "1.0.5", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -91,7 +91,7 @@ }, { "name": "ms-vscode.js-debug", - "version": "1.48.1", + "version": "1.49.2", "repo": "https://github.com/Microsoft/vscode-js-debug", "metadata": { "id": "25629058-ddac-4e17-abba-74678e126c5d", diff --git a/resources/serverless/callback.html b/resources/web/callback.html similarity index 100% rename from resources/serverless/callback.html rename to resources/web/callback.html diff --git a/resources/serverless/code-web.js b/resources/web/code-web.js similarity index 83% rename from resources/serverless/code-web.js rename to resources/web/code-web.js index 90d81b3fb9d..74dd39e8cb8 100644 --- a/resources/serverless/code-web.js +++ b/resources/web/code-web.js @@ -16,14 +16,19 @@ const opn = require('opn'); const minimist = require('minimist'); const fancyLog = require('fancy-log'); const ansiColors = require('ansi-colors'); +const remote = require('gulp-remote-retry-src'); +const vfs = require('vinyl-fs'); const extensions = require('../../build/lib/extensions'); const APP_ROOT = path.join(__dirname, '..', '..'); const BUILTIN_EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); const BUILTIN_MARKETPLACE_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInExtensions'); +const WEB_DEV_EXTENSIONS_ROOT = path.join(APP_ROOT, '.build', 'builtInWebDevExtensions'); const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench', 'workbench-dev.html'); +const WEB_PLAYGROUND_VERSION = '0.0.2'; + const args = minimist(process.argv, { boolean: [ 'no-launch', @@ -72,9 +77,10 @@ async function getBuiltInExtensionInfos() { /** @type {Object.} */ const locations = {}; - const [localExtensions, marketplaceExtensions] = await Promise.all([ + const [localExtensions, marketplaceExtensions, webDevExtensions] = await Promise.all([ extensions.scanBuiltinExtensions(BUILTIN_EXTENSIONS_ROOT), extensions.scanBuiltinExtensions(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT), + ensureWebDevExtensions().then(() => extensions.scanBuiltinExtensions(WEB_DEV_EXTENSIONS_ROOT)) ]); for (const ext of localExtensions) { allExtensions.push(ext); @@ -84,6 +90,10 @@ async function getBuiltInExtensionInfos() { allExtensions.push(ext); locations[ext.extensionPath] = path.join(BUILTIN_MARKETPLACE_EXTENSIONS_ROOT, ext.extensionPath); } + for (const ext of webDevExtensions) { + allExtensions.push(ext); + locations[ext.extensionPath] = path.join(WEB_DEV_EXTENSIONS_ROOT, ext.extensionPath); + } for (const ext of allExtensions) { if (ext.packageJSON.browser) { let mainFilePath = path.join(locations[ext.extensionPath], ext.packageJSON.browser); @@ -98,7 +108,43 @@ async function getBuiltInExtensionInfos() { return { extensions: allExtensions, locations }; } -async function getDefaultExtensionInfos() { +async function ensureWebDevExtensions() { + + // Playground (https://github.com/microsoft/vscode-web-playground) + const webDevPlaygroundRoot = path.join(WEB_DEV_EXTENSIONS_ROOT, 'vscode-web-playground'); + const webDevPlaygroundExists = await exists(webDevPlaygroundRoot); + + let downloadPlayground = false; + if (webDevPlaygroundExists) { + try { + const webDevPlaygroundPackageJson = JSON.parse(((await readFile(path.join(webDevPlaygroundRoot, 'package.json'))).toString())); + if (webDevPlaygroundPackageJson.version !== WEB_PLAYGROUND_VERSION) { + downloadPlayground = true; + } + } catch (error) { + downloadPlayground = true; + } + } else { + downloadPlayground = true; + } + + if (downloadPlayground) { + if (args.verbose) { + fancyLog(`${ansiColors.magenta('Web Development extensions')}: Downloading vscode-web-playground to ${webDevPlaygroundRoot}`); + } + await new Promise((resolve, reject) => { + remote(['package.json', 'dist/extension.js', 'dist/extension.js.map'], { + base: 'https://raw.githubusercontent.com/microsoft/vscode-web-playground/main/' + }).pipe(vfs.dest(webDevPlaygroundRoot)).on('end', resolve).on('error', reject); + }); + } else { + if (args.verbose) { + fancyLog(`${ansiColors.magenta('Web Development extensions')}: Using existing vscode-web-playground in ${webDevPlaygroundRoot}`); + } + } +} + +async function getCommandlineProvidedExtensionInfos() { const extensions = []; /** @type {Object.} */ @@ -150,7 +196,7 @@ async function getExtensionPackageJSON(extensionPath) { } const builtInExtensionsPromise = getBuiltInExtensionInfos(); -const defaultExtensionsPromise = getDefaultExtensionInfos(); +const commandlineProvidedExtensionsPromise = getCommandlineProvidedExtensionInfos(); const mapCallbackUriToRequestId = new Map(); @@ -245,7 +291,7 @@ async function handleStatic(req, res, parsedUrl) { async function handleExtension(req, res, parsedUrl) { // Strip `/extension/` from the path const relativePath = decodeURIComponent(parsedUrl.pathname.substr('/extension/'.length)); - const filePath = getExtensionFilePath(relativePath, (await defaultExtensionsPromise).locations); + const filePath = getExtensionFilePath(relativePath, (await commandlineProvidedExtensionsPromise).locations); if (!filePath) { return serveError(req, res, 400, `Bad request.`); } @@ -289,10 +335,21 @@ async function handleRoot(req, res) { } const { extensions: builtInExtensions } = await builtInExtensionsPromise; - const { extensions: staticExtensions } = await defaultExtensionsPromise; + const { extensions: staticExtensions, locations: staticLocations } = await commandlineProvidedExtensionsPromise; + + const dedupedBuiltInExtensions = []; + for (const builtInExtension of builtInExtensions) { + const extensionId = `${builtInExtension.packageJSON.publisher}.${builtInExtension.packageJSON.name}`; + if (staticLocations[extensionId]) { + fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: Ignoring built-in ${extensionId} because it was overridden via --extension argument`); + continue; + } + + dedupedBuiltInExtensions.push(builtInExtension); + } if (args.verbose) { - fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: ${builtInExtensions.map(e => path.basename(e.extensionPath)).join(', ')}`); + fancyLog(`${ansiColors.magenta('BuiltIn extensions')}: ${dedupedBuiltInExtensions.map(e => path.basename(e.extensionPath)).join(', ')}`); fancyLog(`${ansiColors.magenta('Additional extensions')}: ${staticExtensions.map(e => path.basename(e.extensionLocation.path)).join(', ') || 'None'}`); } @@ -306,7 +363,7 @@ async function handleRoot(req, res) { const data = (await readFile(WEB_MAIN)).toString() .replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => escapeAttribute(JSON.stringify(webConfigJSON))) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied - .replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(builtInExtensions))) + .replace('{{WORKBENCH_BUILTIN_EXTENSIONS}}', () => escapeAttribute(JSON.stringify(dedupedBuiltInExtensions))) .replace('{{WEBVIEW_ENDPOINT}}', '') .replace('{{REMOTE_USER_DATA_URI}}', ''); @@ -351,7 +408,7 @@ async function handleCallback(req, res, parsedUrl) { // add to map of known callbacks mapCallbackUriToRequestId.set(requestId, JSON.stringify({ scheme: vscodeScheme || 'code-oss', authority: vscodeAuthority, path: vscodePath, query, fragment: vscodeFragment })); - return serveFile(req, res, path.join(APP_ROOT, 'resources', 'serverless', 'callback.html'), { 'Content-Type': 'text/html' }); + return serveFile(req, res, path.join(APP_ROOT, 'resources', 'web', 'callback.html'), { 'Content-Type': 'text/html' }); } /** diff --git a/resources/win32/bin/code.sh b/resources/win32/bin/code.sh index 9f029e5522a..d86b6e0574a 100644 --- a/resources/win32/bin/code.sh +++ b/resources/win32/bin/code.sh @@ -37,7 +37,7 @@ else fi if [ $IN_WSL = true ]; then - export WSLENV=ELECTRON_RUN_AS_NODE/w:$WSLENV + export WSLENV="ELECTRON_RUN_AS_NODE/w:$WSLENV" CLI=$(wslpath -m "$VSCODE_PATH/resources/app/out/cli.js") # use the Remote WSL extension if installed diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 3133c7869a5..5a493b0b1c5 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -72,8 +72,11 @@ mkdir %GITWORKSPACE% call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --enable-proposed-api=vscode.git --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --no-cached-data --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% -:: Tests in commonJS (HTML, CSS, JSON language server tests...) -call .\scripts\node-electron.bat .\node_modules\mocha\bin\_mocha .\extensions\*\server\out\test\**\*.test.js +:: Tests in commonJS (CSS, HTML) +call %~dp0\node-electron.bat %~dp0\..\extensions\css-language-features/server/test/index.js +if %errorlevel% neq 0 exit /b %errorlevel% + +call %~dp0\node-electron.bat %~dp0\..\extensions\html-language-features/server/test/index.js if %errorlevel% neq 0 exit /b %errorlevel% rmdir /s /q %VSCODEUSERDATADIR% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 5412a5c0ecd..6856ebd525f 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -59,7 +59,7 @@ fi "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --enable-proposed-api=vscode.git --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-notebook-tests/test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=$ROOT/extensions/vscode-notebook-tests --extensionTestsPath=$ROOT/extensions/vscode-notebook-tests/out/ --disable-telemetry --crash-reporter-directory=$VSCODECRASHDIR --no-cached-data --disable-updates --disable-extensions --user-data-dir=$VSCODEUSERDATADIR -# Tests in commonJS +# Tests in commonJS (CSS, HTML) cd $ROOT/extensions/css-language-features/server && $ROOT/scripts/node-electron.sh test/index.js cd $ROOT/extensions/html-language-features/server && $ROOT/scripts/node-electron.sh test/index.js diff --git a/src/buildfile.js b/src/buildfile.js index 2df9bf24e73..f6d1c647d9d 100644 --- a/src/buildfile.js +++ b/src/buildfile.js @@ -16,6 +16,7 @@ exports.base = [{ }]; exports.workerExtensionHost = [entrypoint('vs/workbench/services/extensions/worker/extensionHostWorker')]; +exports.workerNotebook = [entrypoint('vs/workbench/contrib/notebook/common/services/notebookSimpleWorker')]; exports.workbenchDesktop = require('./vs/workbench/buildfile.desktop').collectModules(); exports.workbenchWeb = require('./vs/workbench/buildfile.web').collectModules(); diff --git a/src/main.js b/src/main.js index 9d68c984c66..e4662daa6b4 100644 --- a/src/main.js +++ b/src/main.js @@ -316,7 +316,7 @@ function createDefaultArgvConfigSync(argvConfigPath) { // Default argv content const defaultArgvConfigContent = [ '// This configuration file allows you to pass permanent command line arguments to VS Code.', - '// Only a subset of arguments is currently supported to reduce the likelyhood of breaking', + '// Only a subset of arguments is currently supported to reduce the likelihood of breaking', '// the installation.', '//', '// PLEASE DO NOT CHANGE WITHOUT UNDERSTANDING THE IMPACT', diff --git a/src/tsconfig.base.json b/src/tsconfig.base.json index 19165d97b72..6decaae056e 100644 --- a/src/tsconfig.base.json +++ b/src/tsconfig.base.json @@ -5,6 +5,7 @@ "experimentalDecorators": true, "noImplicitReturns": true, "noUnusedLocals": true, + "allowUnreachableCode": false, "strict": true, "forceConsistentCasingInFileNames": true, "baseUrl": ".", @@ -15,6 +16,7 @@ }, "lib": [ "ES2015", + "ES2016.Array.Include", "ES2017.String", "ES2018.Promise", "DOM", diff --git a/src/tsconfig.monaco.json b/src/tsconfig.monaco.json index 825a83761f2..86b2926a1ab 100644 --- a/src/tsconfig.monaco.json +++ b/src/tsconfig.monaco.json @@ -15,7 +15,6 @@ "include": [ "typings/require.d.ts", "typings/thenable.d.ts", - "typings/lib.array-ext.d.ts", "vs/css.d.ts", "vs/monaco.d.ts", "vs/nls.d.ts", diff --git a/src/typings/lib.array-ext.d.ts b/src/typings/lib.array-ext.d.ts deleted file mode 100644 index 5a77b70a9f2..00000000000 --- a/src/typings/lib.array-ext.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -interface ArrayConstructor { - isArray(arg: ReadonlyArray | null | undefined): arg is ReadonlyArray; - isArray(arg: Array | null | undefined): arg is Array; - isArray(arg: any): arg is Array; - isArray(arg: any): arg is Array; -} \ No newline at end of file diff --git a/src/typings/node.processEnv-ext.d.ts b/src/typings/node.processEnv-ext.d.ts deleted file mode 100644 index 4ca44ec5b37..00000000000 --- a/src/typings/node.processEnv-ext.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare namespace NodeJS { - - export interface Process { - - /** - * The lazy environment is a promise that resolves to `process.env` - * once the process is resolved. The use-case is VS Code running - * on Linux/macOS when being launched via a launcher. Then the env - * (as defined in .bashrc etc) isn't properly set and needs to be - * resolved lazy. - */ - lazyEnv: Thenable | undefined; - } -} diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index f203119f8f6..29490d6fc1e 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -23,6 +23,9 @@ export function clearNode(node: HTMLElement): void { } } +/** + * @deprecated use `node.remove()` instead + */ export function removeNode(node: HTMLElement): void { if (node.parentNode) { node.parentNode.removeChild(node); @@ -1004,7 +1007,7 @@ export function prepend(parent: HTMLElement, child: T): T { return child; } -const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((.([\w\-]+))*)/; +const SELECTOR_REGEX = /([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/; export enum Namespace { HTML = 'http://www.w3.org/1999/xhtml', diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index e79f159100b..48ead97d48f 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -17,6 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { renderCodicons, markdownEscapeEscapedCodicons } from 'vs/base/common/codicons'; import { resolvePath } from 'vs/base/common/resources'; +import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; export interface MarkedOptions extends marked.MarkedOptions { baseUrl?: never; @@ -171,25 +172,32 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende const actionHandler = options.actionHandler; if (actionHandler) { - actionHandler.disposeables.add(DOM.addStandardDisposableListener(element, 'click', event => { - let target: HTMLElement | null = event.target; - if (target.tagName !== 'A') { - target = target.parentElement; - if (!target || target.tagName !== 'A') { + [DOM.EventType.CLICK, DOM.EventType.AUXCLICK].forEach(event => { + actionHandler.disposeables.add(DOM.addDisposableListener(element, event, (e: MouseEvent) => { + const mouseEvent = new StandardMouseEvent(e); + if (!mouseEvent.leftButton && !mouseEvent.middleButton) { return; } - } - try { - const href = target.dataset['href']; - if (href) { - actionHandler.callback(href, event); + + let target: HTMLElement | null = mouseEvent.target; + if (target.tagName !== 'A') { + target = target.parentElement; + if (!target || target.tagName !== 'A') { + return; + } } - } catch (err) { - onUnexpectedError(err); - } finally { - event.preventDefault(); - } - })); + try { + const href = target.dataset['href']; + if (href) { + actionHandler.callback(href, mouseEvent); + } + } catch (err) { + onUnexpectedError(err); + } finally { + mouseEvent.preventDefault(); + } + })); + }); } // Use our own sanitizer so that we can let through only spans. diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index bbc0918db24..aef1d1c1e1d 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -285,6 +285,10 @@ export class ActionBar extends Disposable implements IActionRunner { index++; } }); + if (this.focusedItem) { + // After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128 + this.focus(this.focusedItem); + } } getWidth(index: number): number { diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index cf9786a2076..d140e2ab635 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -590,6 +590,9 @@ export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } +/** + * @deprecated Use `Array.from` or `[...iter]` + */ export function toArray(iterable: IterableIterator): T[] { const result: T[] = []; for (let element of iterable) { diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index d6e7d6ef5d7..05d343a5e1b 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -142,7 +142,7 @@ export function isUNC(path: string): boolean { // Reference: https://en.wikipedia.org/wiki/Filename const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; const UNIX_INVALID_FILE_CHARS = /[\\/]/g; -const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])$/i; +const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])(\.(.*?))?$/i; export function isValidBasename(name: string | null | undefined, isWindowsOS: boolean = isWindows): boolean { const invalidFileChars = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 1c8e5ef5fb5..12c3e4ad739 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { compareAnything } from 'vs/base/common/comparers'; -import { IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches } from 'vs/base/common/filters'; +import { matchesPrefix, IMatch, isUpper, fuzzyScore, createMatches as createFuzzyMatches } from 'vs/base/common/filters'; import { sep } from 'vs/base/common/path'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { stripWildcards, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -369,7 +369,8 @@ export interface IItemAccessor { } const PATH_IDENTITY_SCORE = 1 << 18; -const LABEL_SCORE_THRESHOLD = 1 << 17; +const LABEL_PREFIX_SCORE_THRESHOLD = 1 << 17; +const LABEL_SCORE_THRESHOLD = 1 << 16; export function scoreItemFuzzy(item: T, query: IPreparedQuery, fuzzy: boolean, accessor: IItemAccessor, cache: FuzzyScorerCache): IItemScore { if (!item || !query.normalized) { @@ -458,11 +459,24 @@ function doScoreItemFuzzyMultiple(label: string, description: string | undefined function doScoreItemFuzzySingle(label: string, description: string | undefined, path: string | undefined, query: IPreparedQueryPiece, preferLabelMatches: boolean, fuzzy: boolean): IItemScore { - // Prefer label matches if told so - if (preferLabelMatches) { + // Prefer label matches if told so or we have no description + if (preferLabelMatches || !description) { const [labelScore, labelPositions] = scoreFuzzy(label, query.normalized, query.normalizedLowercase, fuzzy); if (labelScore) { - return { score: labelScore + LABEL_SCORE_THRESHOLD, labelMatch: createMatches(labelPositions) }; + // If we have a prefix match on the label, we give a much + // higher baseScore to elevate these matches over others + // This ensures that typing a file name wins over results + // that are present somewhere in the label, but not the + // beginning. + const labelPrefixMatch = matchesPrefix(query.normalized, label); + let baseScore: number; + if (labelPrefixMatch) { + baseScore = LABEL_PREFIX_SCORE_THRESHOLD; + } else { + baseScore = LABEL_SCORE_THRESHOLD; + } + + return { score: baseScore + labelScore, labelMatch: labelPrefixMatch || createMatches(labelPositions) }; } } @@ -595,10 +609,13 @@ export function compareItemsByFuzzyScore(itemA: T, itemB: T, query: IPrepared return scoreA > scoreB ? -1 : 1; } - // prefer more compact matches over longer in label - const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); - if (comparedByMatchLength !== 0) { - return comparedByMatchLength; + // prefer more compact matches over longer in label (unless this is a prefix match where + // longer prefix matches are actually preferred) + if (scoreA < LABEL_PREFIX_SCORE_THRESHOLD && scoreB < LABEL_PREFIX_SCORE_THRESHOLD) { + const comparedByMatchLength = compareByMatchLength(itemScoreA.labelMatch, itemScoreB.labelMatch); + if (comparedByMatchLength !== 0) { + return comparedByMatchLength; + } } // prefer shorter labels over longer labels diff --git a/src/vs/base/common/hash.ts b/src/vs/base/common/hash.ts index 4b47073d8e5..d5770516836 100644 --- a/src/vs/base/common/hash.ts +++ b/src/vs/base/common/hash.ts @@ -8,7 +8,12 @@ import * as strings from 'vs/base/common/strings'; /** * Return a hash value for an object. */ -export function hash(obj: any, hashVal = 0): number { +export function hash(obj: any): number { + return doHash(obj, 0); +} + + +export function doHash(obj: any, hashVal: number): number { switch (typeof obj) { case 'object': if (obj === null) { @@ -24,9 +29,9 @@ export function hash(obj: any, hashVal = 0): number { case 'number': return numberHash(obj, hashVal); case 'undefined': - return numberHash(0, 937); + return numberHash(937, hashVal); default: - return numberHash(0, 617); + return numberHash(617, hashVal); } } @@ -48,14 +53,14 @@ export function stringHash(s: string, hashVal: number) { function arrayHash(arr: any[], initialHashVal: number): number { initialHashVal = numberHash(104579, initialHashVal); - return arr.reduce((hashVal, item) => hash(item, hashVal), initialHashVal); + return arr.reduce((hashVal, item) => doHash(item, hashVal), initialHashVal); } function objectHash(obj: any, initialHashVal: number): number { initialHashVal = numberHash(181387, initialHashVal); return Object.keys(obj).sort().reduce((hashVal, key) => { hashVal = stringHash(key, hashVal); - return hash(obj[key], hashVal); + return doHash(obj[key], hashVal); }, initialHashVal); } @@ -68,7 +73,7 @@ export class Hasher { } hash(obj: any): number { - this._value = hash(obj, this._value); + this._value = doHash(obj, this._value); return this._value; } } diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 406a9c801d4..79bbb910aac 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -45,6 +45,14 @@ function trackDisposable(x: T): T { return x; } +export class MultiDisposeError extends Error { + constructor( + public readonly errors: any[] + ) { + super(`Encounter errors while disposing of store. Errors: [${errors.join(', ')}]`); + } +} + export interface IDisposable { dispose(): void; } @@ -60,12 +68,25 @@ export function dispose(disposables: Array): Array; export function dispose(disposables: ReadonlyArray): ReadonlyArray; export function dispose(arg: T | IterableIterator | undefined): any { if (Iterable.is(arg)) { - for (let d of arg) { + let errors: any[] = []; + + for (const d of arg) { if (d) { markTracked(d); - d.dispose(); + try { + d.dispose(); + } catch (e) { + errors.push(e); + } } } + + if (errors.length === 1) { + throw errors[0]; + } else if (errors.length > 1) { + throw new MultiDisposeError(errors); + } + return Array.isArray(arg) ? [] : arg; } else if (arg) { markTracked(arg); @@ -116,8 +137,11 @@ export class DisposableStore implements IDisposable { * Dispose of all registered disposables but do not mark this object as disposed. */ public clear(): void { - this._toDispose.forEach(item => item.dispose()); - this._toDispose.clear(); + try { + dispose(this._toDispose.values()); + } finally { + this._toDispose.clear(); + } } public add(t: T): T { diff --git a/src/vs/base/common/marshalling.ts b/src/vs/base/common/marshalling.ts index e76ba91738f..4af920357fd 100644 --- a/src/vs/base/common/marshalling.ts +++ b/src/vs/base/common/marshalling.ts @@ -5,7 +5,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { regExpFlags } from 'vs/base/common/strings'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; export function stringify(obj: any): string { return JSON.stringify(obj, replacer); @@ -33,7 +33,15 @@ function replacer(key: string, value: any): any { return value; } -export function revive(obj: any, depth = 0): any { + +type Deserialize = T extends UriComponents ? URI + : T extends object + ? Revived + : T; + +export type Revived = { [K in keyof T]: Deserialize }; + +export function revive(obj: any, depth = 0): Revived { if (!obj || depth > 200) { return obj; } @@ -41,15 +49,15 @@ export function revive(obj: any, depth = 0): any { if (typeof obj === 'object') { switch ((obj).$mid) { - case 1: return URI.revive(obj); - case 2: return new RegExp(obj.source, obj.flags); + case 1: return URI.revive(obj); + case 2: return new RegExp(obj.source, obj.flags); } if ( obj instanceof VSBuffer || obj instanceof Uint8Array ) { - return obj; + return obj; } if (Array.isArray(obj)) { diff --git a/src/vs/base/common/strings.ts b/src/vs/base/common/strings.ts index 5003acabe09..ec727797b87 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -855,10 +855,6 @@ export function stripUTF8BOM(str: string): string { return startsWithUTF8BOM(str) ? str.substr(1) : str; } -export function safeBtoa(str: string): string { - return btoa(encodeURIComponent(str)); // we use encodeURIComponent because btoa fails for non Latin 1 values -} - /** * @deprecated ES6 */ diff --git a/src/vs/base/common/types.ts b/src/vs/base/common/types.ts index ee9172a4f84..eef27600bd7 100644 --- a/src/vs/base/common/types.ts +++ b/src/vs/base/common/types.ts @@ -258,14 +258,12 @@ export type UriDto = { [K in keyof T]: T[K] extends URI /** * Mapped-type that replaces all occurrences of URI with UriComponents and * drops all functions. - * todo@joh use toJSON-results */ -export type Dto = { [K in keyof T]: T[K] extends URI - ? UriComponents - : T[K] extends Function - ? never - : UriDto }; - +export type Dto = T extends { toJSON(): infer U } + ? U + : T extends object + ? { [k in keyof T]: Dto; } + : T; export function NotImplementedProxy(name: string): { new(): T } { return class { diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts index 81019137540..d2ca01cf849 100644 --- a/src/vs/base/parts/quickinput/browser/quickInput.ts +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -381,7 +381,7 @@ class QuickPick extends QuickInput implements IQuickPi private static readonly DEFAULT_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); private _value = ''; - private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL; + private _ariaLabel: string | undefined; private _placeholder: string | undefined; private readonly onDidChangeValueEmitter = this._register(new Emitter()); private readonly onDidAcceptEmitter = this._register(new Emitter()); @@ -435,8 +435,8 @@ class QuickPick extends QuickInput implements IQuickPi filterValue = (value: string) => value; - set ariaLabel(ariaLabel: string) { - this._ariaLabel = ariaLabel || QuickPick.DEFAULT_ARIA_LABEL; + set ariaLabel(ariaLabel: string | undefined) { + this._ariaLabel = ariaLabel; this.update(); } @@ -884,8 +884,11 @@ class QuickPick extends QuickInput implements IQuickPi } if (inputShownJustForScreenReader) { this.ui.inputBox.ariaLabel = ''; - } else if (this.ui.inputBox.ariaLabel !== this.ariaLabel) { - this.ui.inputBox.ariaLabel = this.ariaLabel; + } else { + const ariaLabel = this.ariaLabel || this.placeholder || QuickPick.DEFAULT_ARIA_LABEL; + if (this.ui.inputBox.ariaLabel !== ariaLabel) { + this.ui.inputBox.ariaLabel = ariaLabel; + } } this.ui.list.matchOnDescription = this.matchOnDescription; this.ui.list.matchOnDetail = this.matchOnDetail; @@ -1384,9 +1387,6 @@ export class QuickInputController extends Disposable { ]; input.canSelectMany = !!options.canPickMany; input.placeholder = options.placeHolder; - if (options.placeHolder) { - input.ariaLabel = options.placeHolder; - } input.ignoreFocusOut = !!options.ignoreFocusLost; input.matchOnDescription = !!options.matchOnDescription; input.matchOnDetail = !!options.matchOnDetail; diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index ebad819fffa..de7339e6757 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -199,7 +199,7 @@ export interface IQuickPick extends IQuickInput { */ filterValue: (value: string) => string; - ariaLabel: string; + ariaLabel: string | undefined; placeholder: string | undefined; diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js index 4dbe1b48a1d..d6bea974701 100644 --- a/src/vs/base/parts/sandbox/electron-browser/preload.js +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -93,6 +93,14 @@ process: { platform: process.platform, env: process.env, + _whenEnvResolved: undefined, + get whenEnvResolved() { + if (!this._whenEnvResolved) { + this._whenEnvResolved = resolveEnv(); + } + + return this._whenEnvResolved; + }, on: /** * @param {string} type @@ -157,5 +165,33 @@ return true; } + /** + * If VSCode is not run from a terminal, we should resolve additional + * shell specific environment from the OS shell to ensure we are seeing + * all development related environment variables. We do this from the + * main process because it may involve spawning a shell. + */ + function resolveEnv() { + return new Promise(function (resolve) { + const handle = setTimeout(function () { + console.warn('Preload: Unable to resolve shell environment in a reasonable time'); + + // It took too long to fetch the shell environment, return + resolve(); + }, 3000); + + ipcRenderer.once('vscode:acceptShellEnv', function (event, shellEnv) { + clearTimeout(handle); + + // Assign all keys of the shell environment to our process environment + Object.assign(process.env, shellEnv); + + resolve(); + }); + + ipcRenderer.send('vscode:fetchShellEnv'); + }); + } + //#endregion }()); diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts index 0acd55cb3fe..a305df1c8a3 100644 --- a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts @@ -84,6 +84,12 @@ export const process = (window as any).vscode.process as { */ env: { [key: string]: string | undefined }; + /** + * Allows to await resolving the full process environment by checking for the shell environment + * of the OS in certain cases (e.g. when the app is started from the Dock on macOS). + */ + whenEnvResolved: Promise; + /** * A listener on the process. Only a small subset of listener types are allowed. */ diff --git a/src/vs/base/test/browser/dom.test.ts b/src/vs/base/test/browser/dom.test.ts index 61252159d23..f5c8e65dac6 100644 --- a/src/vs/base/test/browser/dom.test.ts +++ b/src/vs/base/test/browser/dom.test.ts @@ -93,6 +93,22 @@ suite('dom', () => { assert(!div.firstChild); }); + test('should buld nodes with id', () => { + const div = $('div#foo'); + assert(div); + assert(div instanceof HTMLElement); + assert.equal(div.tagName, 'DIV'); + assert.equal(div.id, 'foo'); + }); + + test('should buld nodes with class-name', () => { + const div = $('div.foo'); + assert(div); + assert(div instanceof HTMLElement); + assert.equal(div.tagName, 'DIV'); + assert.equal(div.className, 'foo'); + }); + test('should build nodes with attributes', () => { let div = $('div', { class: 'test' }); assert.equal(div.className, 'test'); @@ -111,5 +127,12 @@ suite('dom', () => { assert.equal(div.firstChild && div.firstChild.textContent, 'hello'); }); + + test('should build nodes with text children', () => { + let div = $('div', undefined, 'foobar'); + let firstChild = div.firstChild as HTMLElement; + assert.equal(firstChild.tagName, undefined); + assert.equal(firstChild.textContent, 'foobar'); + }); }); }); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 0760e2c8b91..03993437ffc 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -57,6 +57,13 @@ suite('Paths', () => { assert.ok(!extpath.isValidBasename('aux')); assert.ok(!extpath.isValidBasename('Aux')); assert.ok(!extpath.isValidBasename('LPT0')); + assert.ok(!extpath.isValidBasename('aux.txt')); + assert.ok(!extpath.isValidBasename('com0.abc')); + assert.ok(extpath.isValidBasename('LPT00')); + assert.ok(extpath.isValidBasename('aux1')); + assert.ok(extpath.isValidBasename('aux1.txt')); + assert.ok(extpath.isValidBasename('aux1.aux.txt')); + assert.ok(!extpath.isValidBasename('test.txt.')); assert.ok(!extpath.isValidBasename('test.txt..')); assert.ok(!extpath.isValidBasename('test.txt ')); diff --git a/src/vs/base/test/common/fuzzyScorer.test.ts b/src/vs/base/test/common/fuzzyScorer.test.ts index 2064ec5a075..31f211e7ce7 100644 --- a/src/vs/base/test/common/fuzzyScorer.test.ts +++ b/src/vs/base/test/common/fuzzyScorer.test.ts @@ -985,6 +985,61 @@ suite('Fuzzy Scorer', () => { } }); + test('compareFilesByScore - prefer strict case prefix', function () { + const resourceA = URI.file('app/constants/color.js'); + const resourceB = URI.file('app/components/model/input/Color.js'); + + let query = 'Color'; + + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + query = 'color'; + + res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + }); + + test('compareFilesByScore - prefer prefix (bug #103052)', function () { + const resourceA = URI.file('test/smoke/src/main.ts'); + const resourceB = URI.file('src/vs/editor/common/services/semantikTokensProviderStyling.ts'); + + let query = 'smoke main.ts'; + + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceA); + assert.equal(res[1], resourceB); + }); + + test('compareFilesByScore - boost better prefix match if multiple queries are used', function () { + const resourceA = URI.file('src/vs/workbench/services/host/browser/browserHostService.ts'); + const resourceB = URI.file('src/vs/workbench/browser/workbench.ts'); + + for (const query of ['workbench.ts browser', 'browser workbench.ts', 'browser workbench', 'workbench browser']) { + let res = [resourceA, resourceB].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + + res = [resourceB, resourceA].sort((r1, r2) => compareItemsByScore(r1, r2, query, true, ResourceAccessor)); + assert.equal(res[0], resourceB); + assert.equal(res[1], resourceA); + } + }); + test('prepareQuery', () => { assert.equal(scorer.prepareQuery(' f*a ').normalized, 'fa'); assert.equal(scorer.prepareQuery('model Tester.ts').original, 'model Tester.ts'); diff --git a/src/vs/base/test/common/hash.test.ts b/src/vs/base/test/common/hash.test.ts index 3225caf7b23..b5074f4ffa5 100644 --- a/src/vs/base/test/common/hash.test.ts +++ b/src/vs/base/test/common/hash.test.ts @@ -32,12 +32,18 @@ suite('Hash', () => { assert.equal(hash([1, 2, 3]), hash([1, 2, 3])); assert.equal(hash(['foo', 'bar']), hash(['foo', 'bar'])); assert.equal(hash([]), hash([])); + assert.equal(hash([]), hash(new Array())); assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo'])); assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', null])); + assert.notEqual(hash(['foo', 'bar', null]), hash(['bar', 'foo', null])); + assert.notEqual(hash(['foo', 'bar']), hash(['bar', 'foo', undefined])); + assert.notEqual(hash(['foo', 'bar', undefined]), hash(['bar', 'foo', undefined])); + assert.notEqual(hash(['foo', 'bar', null]), hash(['foo', 'bar', undefined])); }); test('object', () => { assert.equal(hash({}), hash({})); + assert.equal(hash({}), hash(Object.create(null))); assert.equal(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar' })); assert.equal(hash({ 'foo': 'bar', 'foo2': undefined }), hash({ 'foo2': undefined, 'foo': 'bar' })); assert.notEqual(hash({ 'foo': 'bar' }), hash({ 'foo': 'bar2' })); @@ -45,14 +51,26 @@ suite('Hash', () => { }); test('array - unexpected collision', function () { - this.skip(); const a = hash([undefined, undefined, undefined, undefined, undefined]); const b = hash([undefined, undefined, 'HHHHHH', [{ line: 0, character: 0 }, { line: 0, character: 0 }], undefined]); - // console.log(a); - // console.log(b); assert.notEqual(a, b); }); + test('all different', () => { + const candidates: any[] = [ + null, undefined, {}, [], 0, false, true, '', ' ', [null], [undefined], [undefined, undefined], { '': undefined }, { [' ']: undefined }, + 'ab', 'ba', ['ab'] + ]; + const hashes: number[] = candidates.map(hash); + for (let i = 0; i < hashes.length; i++) { + assert.equal(hashes[i], hash(candidates[i])); // verify that repeated invocation returns the same hash + for (let k = i + 1; k < hashes.length; k++) { + assert.notEqual(hashes[i], hashes[k], `Same hash ${hashes[i]} for ${JSON.stringify(candidates[i])} and ${JSON.stringify(candidates[k])}`); + } + } + }); + + function checkSHA1(strings: string[], expected: string) { const hash = new StringSHA1(); for (const str of strings) { diff --git a/src/vs/base/test/common/lifecycle.test.ts b/src/vs/base/test/common/lifecycle.test.ts index 91f17aedb44..7aa87cc6b0f 100644 --- a/src/vs/base/test/common/lifecycle.test.ts +++ b/src/vs/base/test/common/lifecycle.test.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IDisposable, dispose, ReferenceCollection } from 'vs/base/common/lifecycle'; +import { DisposableStore, dispose, IDisposable, MultiDisposeError, ReferenceCollection, toDisposable } from 'vs/base/common/lifecycle'; class Disposable implements IDisposable { isDisposed = false; @@ -49,6 +49,48 @@ suite('Lifecycle', () => { assert(disposable2.isDisposed); }); + test('dispose array should dispose all if a child throws on dispose', () => { + const disposedValues = new Set(); + + let thrownError: any; + try { + dispose([ + toDisposable(() => { disposedValues.add(1); }), + toDisposable(() => { throw new Error('I am error'); }), + toDisposable(() => { disposedValues.add(3); }), + ]); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(3)); + assert.strictEqual(thrownError.message, 'I am error'); + }); + + test('dispose array should rethrow composite error if multiple entries throw on dispose', () => { + const disposedValues = new Set(); + + let thrownError: any; + try { + dispose([ + toDisposable(() => { disposedValues.add(1); }), + toDisposable(() => { throw new Error('I am error 1'); }), + toDisposable(() => { throw new Error('I am error 2'); }), + toDisposable(() => { disposedValues.add(4); }), + ]); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(4)); + assert.ok(thrownError instanceof MultiDisposeError); + assert.strictEqual((thrownError as MultiDisposeError).errors.length, 2); + assert.strictEqual((thrownError as MultiDisposeError).errors[0].message, 'I am error 1'); + assert.strictEqual((thrownError as MultiDisposeError).errors[1].message, 'I am error 2'); + }); + test('Action bar has broken accessibility #100273', function () { let array = [{ dispose() { } }, { dispose() { } }]; let array2 = dispose(array); @@ -61,7 +103,52 @@ suite('Lifecycle', () => { let setValues = set.values(); let setValues2 = dispose(setValues); assert.ok(setValues === setValues2); + }); +}); +suite('DisposableStore', () => { + test('dispose should call all child disposes even if a child throws on dispose', () => { + const disposedValues = new Set(); + + const store = new DisposableStore(); + store.add(toDisposable(() => { disposedValues.add(1); })); + store.add(toDisposable(() => { throw new Error('I am error'); })); + store.add(toDisposable(() => { disposedValues.add(3); })); + + let thrownError: any; + try { + store.dispose(); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(3)); + assert.strictEqual(thrownError.message, 'I am error'); + }); + + test('dispose should throw composite error if multiple children throw on dispose', () => { + const disposedValues = new Set(); + + const store = new DisposableStore(); + store.add(toDisposable(() => { disposedValues.add(1); })); + store.add(toDisposable(() => { throw new Error('I am error 1'); })); + store.add(toDisposable(() => { throw new Error('I am error 2'); })); + store.add(toDisposable(() => { disposedValues.add(4); })); + + let thrownError: any; + try { + store.dispose(); + } catch (e) { + thrownError = e; + } + + assert.ok(disposedValues.has(1)); + assert.ok(disposedValues.has(4)); + assert.ok(thrownError instanceof MultiDisposeError); + assert.strictEqual((thrownError as MultiDisposeError).errors.length, 2); + assert.strictEqual((thrownError as MultiDisposeError).errors[0].message, 'I am error 1'); + assert.strictEqual((thrownError as MultiDisposeError).errors[1].message, 'I am error 2'); }); }); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index c82436e3b82..fd324076b26 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -224,7 +224,6 @@ suite('PFS', function () { } catch (error) { assert.fail(error); - throw error; } }); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index 6196015ad64..55ae7a39871 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -30,7 +30,6 @@ diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 941f2e55399..f9c46333e2e 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -31,7 +31,6 @@ + + + + + + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.js b/src/vs/code/electron-sandbox/workbench/workbench.js new file mode 100644 index 00000000000..bac5dd6d6e8 --- /dev/null +++ b/src/vs/code/electron-sandbox/workbench/workbench.js @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// + +//@ts-check +'use strict'; + +const perf = (function () { + globalThis.MonacoPerformanceMarks = globalThis.MonacoPerformanceMarks || []; + return { + /** + * @param {string} name + */ + mark(name) { + globalThis.MonacoPerformanceMarks.push(name, Date.now()); + } + }; +})(); + +perf.mark('renderer/started'); + +/** + * @type {{ + * load: (modules: string[], resultCallback: (result, configuration: object) => any, options: object) => unknown, + * globals: () => typeof import('../../../base/parts/sandbox/electron-sandbox/globals') + * }} + */ +const bootstrapWindow = (() => { + // @ts-ignore (defined in bootstrap-window.js) + return window.MonacoBootstrapWindow; +})(); + +// Load environment in parallel to workbench loading to avoid waterfall +const whenEnvResolved = bootstrapWindow.globals().process.whenEnvResolved; + +// Load workbench main JS, CSS and NLS all in parallel. This is an +// optimization to prevent a waterfall of loading to happen, because +// we know for a fact that workbench.desktop.sandbox.main will depend on +// the related CSS and NLS counterparts. +bootstrapWindow.load([ + 'vs/workbench/workbench.desktop.sandbox.main', + 'vs/nls!vs/workbench/workbench.desktop.main', + 'vs/css!vs/workbench/workbench.desktop.main' +], + async function (workbench, configuration) { + + // Mark start of workbench + perf.mark('didLoadWorkbenchMain'); + performance.mark('workbench-start'); + + // Wait for process environment being fully resolved + await whenEnvResolved; + + perf.mark('main/startup'); + + // @ts-ignore + return require('vs/workbench/electron-sandbox/desktop.main').main(configuration); + }, + { + removeDeveloperKeybindingsAfterLoad: true, + canModifyDOM: function (windowConfig) { + // TODO@sandbox part-splash is non-sandboxed only + }, + beforeLoaderConfig: function (windowConfig, loaderConfig) { + loaderConfig.recordStats = true; + }, + beforeRequire: function () { + perf.mark('willLoadWorkbenchMain'); + } + } +); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index 9b06d39b0bc..b528f7dd343 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -7,7 +7,6 @@ import { localize } from 'vs/nls'; import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; import * as path from 'vs/base/common/path'; -import * as semver from 'semver-umd'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -214,6 +213,8 @@ export class Main { throw new Error('Invalid vsix'); } + const semver = await import('semver-umd'); + const extensionIdentifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; const installedExtensions = await this.extensionManagementService.getInstalled(ExtensionType.User); const newer = installedExtensions.find(local => areSameExtensions(extensionIdentifier, local.identifier) && semver.gt(local.manifest.version, manifest.version)); diff --git a/src/vs/code/node/shellEnv.ts b/src/vs/code/node/shellEnv.ts index 174bb673a4a..0383550627a 100644 --- a/src/vs/code/node/shellEnv.ts +++ b/src/vs/code/node/shellEnv.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as cp from 'child_process'; +import { spawn } from 'child_process'; import { generateUuid } from 'vs/base/common/uuid'; import { isWindows } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; @@ -30,7 +30,7 @@ function getUnixShellEnvironment(logService: ILogService): Promise ({})); } - -let _shellEnv: Promise; +let shellEnvPromise: Promise | undefined = undefined; /** * We need to get the environment from a user's shell. @@ -91,21 +90,21 @@ let _shellEnv: Promise; * from within a shell. */ export function getShellEnvironment(logService: ILogService, environmentService: INativeEnvironmentService): Promise { - if (_shellEnv === undefined) { + if (!shellEnvPromise) { if (environmentService.args['disable-user-env-probe']) { logService.trace('getShellEnvironment: disable-user-env-probe set, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else if (isWindows) { logService.trace('getShellEnvironment: running on Windows, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else if (process.env['VSCODE_CLI'] === '1' && process.env['VSCODE_FORCE_USER_ENV'] !== '1') { logService.trace('getShellEnvironment: running on CLI, skipping'); - _shellEnv = Promise.resolve({}); + shellEnvPromise = Promise.resolve({}); } else { logService.trace('getShellEnvironment: running on Unix'); - _shellEnv = getUnixShellEnvironment(logService); + shellEnvPromise = getUnixShellEnvironment(logService); } } - return _shellEnv; + return shellEnvPromise; } diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 2013ad47528..63e9488203a 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -559,9 +559,9 @@ export namespace CoreNavigationCommands { case CursorMove_.Direction.ViewPortCenter: case CursorMove_.Direction.ViewPortIfOutside: return CursorMoveCommands.viewportMove(viewModel, cursors, args.direction, inSelectionMode, value); + default: + return null; } - - return null; } } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 9eee0dfe476..220c6c440ea 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -178,14 +178,7 @@ export class TextAreaHandler extends ViewPart { mode }; }, - getScreenReaderContent: (currentState: TextAreaState): TextAreaState => { - - if (browser.isIPad) { - // Do not place anything in the textarea for the iPad - return TextAreaState.EMPTY; - } - if (this._accessibilitySupport === AccessibilitySupport.Disabled) { // We know for a fact that a screen reader is not attached // On OSX, we write the character before the cursor to allow for "long-press" composition diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index 238e47cd542..7b2868aa701 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -72,7 +72,7 @@ interface InMemoryClipboardMetadata { * Every time we read from the cipboard, if the text matches our last written text, * we can fetch the previous metadata. */ -class InMemoryClipboardMetadataManager { +export class InMemoryClipboardMetadataManager { public static readonly INSTANCE = new InMemoryClipboardMetadataManager(); private _lastState: InMemoryClipboardMetadata | null; diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 3f9eef1b816..aa1ea51f927 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -348,6 +348,14 @@ export interface IEditorConstructionOptions extends IEditorOptions { overflowWidgetsDomNode?: HTMLElement; } +export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; +} + /** * A rich code editor. */ @@ -580,6 +588,11 @@ export interface ICodeEditor extends editorCommon.IEditor { */ getRawOptions(): IEditorOptions; + /** + * @internal + */ + getOverflowWidgetsDomNode(): HTMLElement | undefined; + /** * @internal */ diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index 37393b426e3..7a1f3346e46 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -4,13 +4,64 @@ *--------------------------------------------------------------------------------------------*/ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; +import { TextEdit, WorkspaceEdit, WorkspaceEditMetadata, WorkspaceFileEdit, WorkspaceFileEditOptions, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { isObject } from 'vs/base/common/types'; export const IBulkEditService = createDecorator('IWorkspaceEditService'); +function isWorkspaceFileEdit(thing: any): thing is WorkspaceFileEdit { + return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); +} + +function isWorkspaceTextEdit(thing: any): thing is WorkspaceTextEdit { + return isObject(thing) && URI.isUri((thing).resource) && isObject((thing).edit); +} + +export class ResourceEdit { + + protected constructor(readonly metadata?: WorkspaceEditMetadata) { } + + static convert(edit: WorkspaceEdit): ResourceEdit[] { + + + return edit.edits.map(edit => { + if (isWorkspaceTextEdit(edit)) { + return new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata); + } + if (isWorkspaceFileEdit(edit)) { + return new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata); + } + throw new Error('Unsupported edit'); + }); + } +} + +export class ResourceTextEdit extends ResourceEdit { + constructor( + readonly resource: URI, + readonly textEdit: TextEdit, + readonly versionId?: number, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class ResourceFileEdit extends ResourceEdit { + constructor( + readonly oldResource: URI | undefined, + readonly newResource: URI | undefined, + readonly options?: WorkspaceFileEditOptions, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + export interface IBulkEditOptions { editor?: ICodeEditor; progress?: IProgress; @@ -23,7 +74,7 @@ export interface IBulkEditResult { ariaSummary: string; } -export type IBulkEditPreviewHandler = (edit: WorkspaceEdit, options?: IBulkEditOptions) => Promise; +export type IBulkEditPreviewHandler = (edits: ResourceEdit[], options?: IBulkEditOptions) => Promise; export interface IBulkEditService { readonly _serviceBrand: undefined; @@ -32,6 +83,5 @@ export interface IBulkEditService { setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable; - apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise; + apply(edit: ResourceEdit[], options?: IBulkEditOptions): Promise; } - diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 69880b0133d..519a26bb5f8 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -109,8 +109,9 @@ export class ViewController { return data.ctrlKey; case 'metaKey': return data.metaKey; + default: + return false; } - return false; } private _hasNonMulticursorModifier(data: IMouseDispatchData): boolean { @@ -121,8 +122,9 @@ export class ViewController { return data.altKey || data.metaKey; case 'metaKey': return data.ctrlKey || data.altKey; + default: + return false; } - return false; } public dispatchMouse(data: IMouseDispatchData): void { diff --git a/src/vs/editor/browser/viewParts/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index 47a977fe45c..cdb00570ce0 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -217,7 +217,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { endStyle.top = CornerStyle.INTERN; } } else if (previousFrameTop) { - // Accept some hick-ups near the viewport edges to save on repaints + // Accept some hiccups near the viewport edges to save on repaints startStyle.top = previousFrameTop.startStyle!.top; endStyle.top = previousFrameTop.endStyle!.top; } @@ -239,7 +239,7 @@ export class SelectionsOverlay extends DynamicViewOverlay { endStyle.bottom = CornerStyle.INTERN; } } else if (previousFrameBottom) { - // Accept some hick-ups near the viewport edges to save on repaints + // Accept some hiccups near the viewport edges to save on repaints startStyle.bottom = previousFrameBottom.startStyle!.bottom; endStyle.bottom = previousFrameBottom.endStyle!.bottom; } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index aec462f71f0..17539558c91 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -379,6 +379,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._configuration.getRawOptions(); } + public getOverflowWidgetsDomNode(): HTMLElement | undefined { + return this._overflowWidgetsDomNode; + } + public getConfiguredWordAtPosition(position: Position): IWordAtPosition | null { if (!this._modelData) { return null; diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 3ad36126dc8..d7206b4d844 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -70,7 +70,7 @@ interface IEditorsZones { modified: IMyViewZone[]; } -interface IDiffEditorWidgetStyle { +export interface IDiffEditorWidgetStyle { getEditorsDiffDecorations(lineChanges: editorCommon.ILineChange[], ignoreTrimWhitespace: boolean, renderIndicators: boolean, originalWhitespaces: IEditorWhitespace[], modifiedWhitespaces: IEditorWhitespace[], originalEditor: editorBrowser.ICodeEditor, modifiedEditor: editorBrowser.ICodeEditor): IEditorsDiffDecorationsWithZones; setEnableSplitViewResizing(enableSplitViewResizing: boolean): void; applyColors(theme: IColorTheme): boolean; @@ -175,6 +175,9 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE private readonly _onDidUpdateDiff: Emitter = this._register(new Emitter()); public readonly onDidUpdateDiff: Event = this._onDidUpdateDiff.event; + private readonly _onDidContentSizeChange: Emitter = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + private readonly id: number; private _state: editorBrowser.DiffEditorState; private _updatingDiffProgress: IProgressRunner | null; @@ -227,7 +230,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE constructor( domElement: HTMLElement, - options: IDiffEditorOptions, + options: editorBrowser.IDiffEditorConstructionOptions, @IClipboardService clipboardService: IClipboardService, @IEditorWorkerService editorWorkerService: IEditorWorkerService, @IContextKeyService contextKeyService: IContextKeyService, @@ -421,6 +424,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return this._renderIndicators; } + public getContentHeight(): number { + return this.modifiedEditor.getContentHeight(); + } + private _setState(newState: editorBrowser.DiffEditorState): void { if (this._state === newState) { return; @@ -480,7 +487,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._layoutOverviewRulers(); } - private _createLeftHandSideEditor(options: IDiffEditorOptions, instantiationService: IInstantiationService): CodeEditorWidget { + private _createLeftHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService): CodeEditorWidget { const editor = this._createInnerEditor(instantiationService, this._originalDomNode, this._adjustOptionsForLeftHandSide(options, this._originalIsEditable, this._originalCodeLens)); this._register(editor.onDidScrollChange((e) => { @@ -510,10 +517,22 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + this._register(editor.onDidContentSizeChange(e => { + const width = this.originalEditor.getContentWidth() + this.modifiedEditor.getContentWidth() + DiffEditorWidget.ONE_OVERVIEW_WIDTH; + const height = Math.max(this.modifiedEditor.getContentHeight(), this.originalEditor.getContentHeight()); + + this._onDidContentSizeChange.fire({ + contentHeight: height, + contentWidth: width, + contentHeightChanged: e.contentHeightChanged, + contentWidthChanged: e.contentWidthChanged + }); + })); + return editor; } - private _createRightHandSideEditor(options: IDiffEditorOptions, instantiationService: IInstantiationService): CodeEditorWidget { + private _createRightHandSideEditor(options: editorBrowser.IDiffEditorConstructionOptions, instantiationService: IInstantiationService): CodeEditorWidget { const editor = this._createInnerEditor(instantiationService, this._modifiedDomNode, this._adjustOptionsForRightHandSide(options, this._modifiedCodeLens)); this._register(editor.onDidScrollChange((e) => { @@ -555,6 +574,18 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } })); + this._register(editor.onDidContentSizeChange(e => { + const width = this.originalEditor.getContentWidth() + this.modifiedEditor.getContentWidth() + DiffEditorWidget.ONE_OVERVIEW_WIDTH; + const height = Math.max(this.modifiedEditor.getContentHeight(), this.originalEditor.getContentHeight()); + + this._onDidContentSizeChange.fire({ + contentHeight: height, + contentWidth: width, + contentHeightChanged: e.contentHeightChanged, + contentWidthChanged: e.contentWidthChanged + }); + })); + return editor; } @@ -1058,8 +1089,8 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE } } - private _adjustOptionsForSubEditor(options: IDiffEditorOptions): IDiffEditorOptions { - let clonedOptions: IDiffEditorOptions = objects.deepClone(options || {}); + private _adjustOptionsForSubEditor(options: editorBrowser.IDiffEditorConstructionOptions): editorBrowser.IDiffEditorConstructionOptions { + let clonedOptions: editorBrowser.IDiffEditorConstructionOptions = objects.deepClone(options || {}); clonedOptions.inDiffEditor = true; clonedOptions.wordWrap = 'off'; clonedOptions.wordWrapMinified = false; @@ -1069,6 +1100,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE clonedOptions.folding = false; clonedOptions.codeLens = false; clonedOptions.fixedOverflowWidgets = true; + clonedOptions.overflowWidgetsDomNode = options.overflowWidgetsDomNode; // clonedOptions.lineDecorationsWidth = '2ch'; if (!clonedOptions.minimap) { clonedOptions.minimap = {}; @@ -1077,7 +1109,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return clonedOptions; } - private _adjustOptionsForLeftHandSide(options: IDiffEditorOptions, isEditable: boolean, isCodeLensEnabled: boolean): IEditorOptions { + private _adjustOptionsForLeftHandSide(options: editorBrowser.IDiffEditorConstructionOptions, isEditable: boolean, isCodeLensEnabled: boolean): editorBrowser.IEditorConstructionOptions { let result = this._adjustOptionsForSubEditor(options); if (isCodeLensEnabled) { result.codeLens = true; @@ -1087,7 +1119,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE return result; } - private _adjustOptionsForRightHandSide(options: IDiffEditorOptions, isCodeLensEnabled: boolean): IEditorOptions { + private _adjustOptionsForRightHandSide(options: editorBrowser.IDiffEditorConstructionOptions, isCodeLensEnabled: boolean): editorBrowser.IEditorConstructionOptions { let result = this._adjustOptionsForSubEditor(options); if (isCodeLensEnabled) { result.codeLens = true; @@ -1610,14 +1642,14 @@ abstract class ViewZonesComputer { protected abstract _produceModifiedFromDiff(lineChange: editorCommon.ILineChange, lineChangeOriginalLength: number, lineChangeModifiedLength: number): IMyViewZone | null; } -function createDecoration(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, options: ModelDecorationOptions) { +export function createDecoration(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, options: ModelDecorationOptions) { return { range: new Range(startLineNumber, startColumn, endLineNumber, endColumn), options: options }; } -const DECORATIONS = { +export const DECORATIONS = { charDelete: ModelDecorationOptions.register({ className: 'char-delete' @@ -1665,7 +1697,7 @@ const DECORATIONS = { }; -class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEditorWidgetStyle, IVerticalSashLayoutProvider { +export class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffEditorWidgetStyle, IVerticalSashLayoutProvider { static readonly MINIMUM_EDITOR_WIDTH = 100; @@ -2193,11 +2225,11 @@ class InlineViewZonesComputer extends ViewZonesComputer { } } -function isChangeOrInsert(lineChange: editorCommon.IChange): boolean { +export function isChangeOrInsert(lineChange: editorCommon.IChange): boolean { return lineChange.modifiedEndLineNumber > 0; } -function isChangeOrDelete(lineChange: editorCommon.IChange): boolean { +export function isChangeOrDelete(lineChange: editorCommon.IChange): boolean { return lineChange.originalEndLineNumber > 0; } diff --git a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts index 09a18fcf097..5dd98c444c7 100644 --- a/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts +++ b/src/vs/editor/browser/widget/embeddedCodeEditorWidget.ts @@ -37,7 +37,7 @@ export class EmbeddedCodeEditorWidget extends CodeEditorWidget { @INotificationService notificationService: INotificationService, @IAccessibilityService accessibilityService: IAccessibilityService ) { - super(domElement, parentEditor.getRawOptions(), {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); + super(domElement, { ...parentEditor.getRawOptions(), overflowWidgetsDomNode: parentEditor.getOverflowWidgetsDomNode() }, {}, instantiationService, codeEditorService, commandService, contextKeyService, themeService, notificationService, accessibilityService); this._parentEditor = parentEditor; this._overwriteOptions = options; diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index 68d4e69ebf0..bc9bbb1c390 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -317,9 +317,10 @@ export class CursorMoveCommands { // Move to the last non-whitespace column of the current view line return this._moveToViewLastNonWhitespaceColumn(viewModel, cursors, inSelectionMode); } + default: + return null; } - return null; } public static viewportMove(viewModel: IViewModel, cursors: CursorState[], direction: CursorMove.ViewportDirection, inSelectionMode: boolean, value: number): PartialCursorState[] | null { @@ -353,9 +354,9 @@ export class CursorMoveCommands { } return result; } + default: + return null; } - - return null; } public static findPositionInViewportIfOutside(viewModel: IViewModel, cursor: CursorState, visibleViewRange: Range, inSelectionMode: boolean): PartialCursorState { diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index 1773d1011cd..d15e79eab7d 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -1312,6 +1312,7 @@ export interface IReadonlyTextBuffer { getLinesContent(): string[]; getLineContent(lineNumber: number): string; getLineCharCode(lineNumber: number, index: number): number; + getCharCode(offset: number): number; getLineLength(lineNumber: number): number; getLineFirstNonWhitespaceColumn(lineNumber: number): number; getLineLastNonWhitespaceColumn(lineNumber: number): number; diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index e0c7e80cfa6..03a518eb523 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -626,8 +626,7 @@ export class PieceTreeBase { return this._lastVisitedLine.value; } - public getLineCharCode(lineNumber: number, index: number): number { - let nodePos = this.nodeAt2(lineNumber, index + 1); + private _getCharCode(nodePos: NodePosition): number { if (nodePos.remainder === nodePos.node.piece.length) { // the char we want to fetch is at the head of next node. let matchingNode = nodePos.node.next(); @@ -647,6 +646,11 @@ export class PieceTreeBase { } } + public getLineCharCode(lineNumber: number, index: number): number { + let nodePos = this.nodeAt2(lineNumber, index + 1); + return this._getCharCode(nodePos); + } + public getLineLength(lineNumber: number): number { if (lineNumber === this.getLineCount()) { let startOffset = this.getOffsetAt(lineNumber, 1); @@ -655,6 +659,11 @@ export class PieceTreeBase { return this.getOffsetAt(lineNumber + 1, 1) - this.getOffsetAt(lineNumber, 1) - this._EOLLength; } + public getCharCode(offset: number): number { + let nodePos = this.nodeAt(offset); + return this._getCharCode(nodePos); + } + public findMatchesInNode(node: TreeNode, searcher: Searcher, startLineNumber: number, startColumn: number, startCursor: BufferCursor, endCursor: BufferCursor, searchData: SearchData, captureMatches: boolean, limitResultCount: number, resultLen: number, result: FindMatch[]) { let buffer = this._buffers[node.piece.bufferIndex]; let startOffsetInBuffer = this.offsetInBuffer(node.piece.bufferIndex, node.piece.start); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 1c81c18a0a6..62ab2929910 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -178,6 +178,10 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { return this._pieceTree.getLineCharCode(lineNumber, index); } + public getCharCode(offset: number): number { + return this._pieceTree.getCharCode(offset); + } + public getLineLength(lineNumber: number): number { return this._pieceTree.getLineLength(lineNumber); } @@ -214,8 +218,9 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { return '\r\n'; case EndOfLinePreference.TextDefined: return this.getEOL(); + default: + throw new Error('Unknown EOL preference'); } - throw new Error('Unknown EOL preference'); } public setEOL(newEOL: '\r\n' | '\n'): void { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index b89285449aa..3d34f627b1c 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -8,7 +8,6 @@ import { Color } from 'vs/base/common/color'; import { Event } from 'vs/base/common/event'; import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isObject } from 'vs/base/common/types'; import { URI, UriComponents } from 'vs/base/common/uri'; import { Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -813,12 +812,12 @@ export interface DocumentHighlightProvider { */ export interface OnTypeRenameProvider { - stopPattern?: RegExp; + wordPattern?: RegExp; /** * Provide a list of ranges that can be live-renamed together. */ - provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideOnTypeRenameRanges(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult<{ ranges: IRange[]; wordPattern?: RegExp; }>; } /** @@ -1337,29 +1336,6 @@ export class FoldingRangeKind { } } -/** - * @internal - */ -export namespace WorkspaceFileEdit { - /** - * @internal - */ - export function is(thing: any): thing is WorkspaceFileEdit { - return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); - } -} - -/** - * @internal - */ -export namespace WorkspaceTextEdit { - /** - * @internal - */ - export function is(thing: any): thing is WorkspaceTextEdit { - return isObject(thing) && URI.isUri((thing).resource) && isObject((thing).edit); - } -} export interface WorkspaceEditMetadata { needsConfirmation: boolean; diff --git a/src/vs/editor/contrib/clipboard/clipboard.ts b/src/vs/editor/contrib/clipboard/clipboard.ts index 114004565c5..6860a7bb610 100644 --- a/src/vs/editor/contrib/clipboard/clipboard.ts +++ b/src/vs/editor/contrib/clipboard/clipboard.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import * as browser from 'vs/base/browser/browser'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; -import { CopyOptions } from 'vs/editor/browser/controller/textAreaInput'; +import { CopyOptions, InMemoryClipboardMetadataManager } from 'vs/editor/browser/controller/textAreaInput'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, registerEditorAction, Command, MultiCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; @@ -16,6 +16,8 @@ import { MenuId } from 'vs/platform/actions/common/actions'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { Handler } from 'vs/editor/common/editorCommon'; const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste'; @@ -23,10 +25,9 @@ const supportsCut = (platform.isNative || document.queryCommandSupported('cut')) const supportsCopy = (platform.isNative || document.queryCommandSupported('copy')); // IE and Edge have trouble with setting html content in clipboard const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdge); -// Chrome incorrectly returns true for document.queryCommandSupported('paste') -// when the paste feature is available but the calling script has insufficient -// privileges to actually perform the action -const supportsPaste = (platform.isNative || (!browser.isChrome && document.queryCommandSupported('paste'))); +// Firefox only supports navigator.clipboard.readText() in browser extensions. +// See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility +const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true); function registerCommand(command: T): T { command.register(); @@ -160,7 +161,7 @@ class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction { } } -function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy' | 'paste'): void { +function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy'): void { if (!target) { return; } @@ -170,13 +171,11 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // Only if editor text focus (i.e. not if editor has widget focus). const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); if (focusedEditor && focusedEditor.hasTextFocus()) { - if (browserCommand === 'cut' || browserCommand === 'copy') { - // Do not execute if there is no selection and empty selection clipboard is off - const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard); - const selection = focusedEditor.getSelection(); - if (selection && selection.isEmpty() && !emptySelectionClipboard) { - return true; - } + // Do not execute if there is no selection and empty selection clipboard is off + const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard); + const selection = focusedEditor.getSelection(); + if (selection && selection.isEmpty() && !emptySelectionClipboard) { + return true; } document.execCommand(browserCommand); return true; @@ -186,7 +185,6 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman // 2. (default) handle case when focus is somewhere else. target.addImplementation(0, (accessor: ServicesAccessor, args: any) => { - // Only if editor text focus (i.e. not if editor has widget focus). document.execCommand(browserCommand); return true; }); @@ -194,7 +192,52 @@ function registerExecCommandImpl(target: MultiCommand | undefined, browserComman registerExecCommandImpl(CutAction, 'cut'); registerExecCommandImpl(CopyAction, 'copy'); -registerExecCommandImpl(PasteAction, 'paste'); + +if (PasteAction) { + // 1. Paste: handle case when focus is in editor. + PasteAction.addImplementation(10000, (accessor: ServicesAccessor, args: any) => { + const codeEditorService = accessor.get(ICodeEditorService); + const clipboardService = accessor.get(IClipboardService); + + // Only if editor text focus (i.e. not if editor has widget focus). + const focusedEditor = codeEditorService.getFocusedCodeEditor(); + if (focusedEditor && focusedEditor.hasTextFocus()) { + const result = document.execCommand('paste'); + // Use the clipboard service if document.execCommand('paste') was not successful + if (!result && platform.isWeb) { + (async () => { + const clipboardText = await clipboardService.readText(); + if (clipboardText !== '') { + const metadata = InMemoryClipboardMetadataManager.INSTANCE.get(clipboardText); + let pasteOnNewLine = false; + let multicursorText: string[] | null = null; + let mode: string | null = null; + if (metadata) { + pasteOnNewLine = (focusedEditor.getOption(EditorOption.emptySelectionClipboard) && !!metadata.isFromEmptySelection); + multicursorText = (typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null); + mode = metadata.mode; + } + focusedEditor.trigger('keyboard', Handler.Paste, { + text: clipboardText, + pasteOnNewLine, + multicursorText, + mode + }); + } + })(); + return true; + } + return true; + } + return false; + }); + + // 2. Paste: (default) handle case when focus is somewhere else. + PasteAction.addImplementation(0, (accessor: ServicesAccessor, args: any) => { + document.execCommand('paste'); + return true; + }); +} if (supportsCopyWithSyntaxHighlighting) { registerEditorAction(ExecCommandCopyWithSyntaxHighlightingAction); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 4acfb917796..699e05a4699 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -11,7 +11,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { IPosition } from 'vs/editor/common/core/position'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -163,7 +163,7 @@ export async function applyCodeAction( }); if (action.edit) { - await bulkEditService.apply(action.edit, { editor, label: action.title }); + await bulkEditService.apply(ResourceEdit.convert(action.edit), { editor, label: action.title }); } if (action.command) { diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 2f619c371d8..df4b5cdcd4f 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { alert } from 'vs/base/browser/ui/aria/aria'; -import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { asArray, isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; @@ -120,11 +120,12 @@ export abstract class FormattingConflicts { } } -export async function formatDocumentRangeWithSelectedProvider( +export async function formatDocumentRangesWithSelectedProvider( accessor: ServicesAccessor, editorOrModel: ITextModel | IActiveCodeEditor, - range: Range, + rangeOrRanges: Range | Range[], mode: FormattingMode, + progress: IProgress, token: CancellationToken ): Promise { @@ -133,15 +134,16 @@ export async function formatDocumentRangeWithSelectedProvider( const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); const selected = await FormattingConflicts.select(provider, model, mode); if (selected) { - await instaService.invokeFunction(formatDocumentRangeWithProvider, selected, editorOrModel, range, token); + progress.report(selected); + await instaService.invokeFunction(formatDocumentRangesWithProvider, selected, editorOrModel, rangeOrRanges, token); } } -export async function formatDocumentRangeWithProvider( +export async function formatDocumentRangesWithProvider( accessor: ServicesAccessor, provider: DocumentRangeFormattingEditProvider, editorOrModel: ITextModel | IActiveCodeEditor, - range: Range, + rangeOrRanges: Range | Range[], token: CancellationToken ): Promise { const workerService = accessor.get(IEditorWorkerService); @@ -156,39 +158,53 @@ export async function formatDocumentRangeWithProvider( cts = new TextModelCancellationTokenSource(editorOrModel, token); } - let edits: TextEdit[] | undefined; - try { - const rawEdits = await provider.provideDocumentRangeFormattingEdits( - model, - range, - model.getFormattingOptions(), - cts.token - ); - edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); - - if (cts.token.isCancellationRequested) { - return true; + // make sure that ranges don't overlap nor touch each other + let ranges: Range[] = []; + let len = 0; + for (let range of asArray(rangeOrRanges).sort(Range.compareRangesUsingStarts)) { + if (len > 0 && Range.areIntersectingOrTouching(ranges[len - 1], range)) { + ranges[len - 1] = Range.fromPositions(ranges[len - 1].getStartPosition(), range.getEndPosition()); + } else { + len = ranges.push(range); } - - } finally { - cts.dispose(); } - if (!edits || edits.length === 0) { + const allEdits: TextEdit[] = []; + for (let range of ranges) { + try { + const rawEdits = await provider.provideDocumentRangeFormattingEdits( + model, + range, + model.getFormattingOptions(), + cts.token + ); + const minEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits); + if (minEdits) { + allEdits.push(...minEdits); + } + if (cts.token.isCancellationRequested) { + return true; + } + } finally { + cts.dispose(); + } + } + + if (allEdits.length === 0) { return false; } if (isCodeEditor(editorOrModel)) { // use editor to apply edits - FormattingEdit.execute(editorOrModel, edits, true); - alertFormattingEdits(edits); + FormattingEdit.execute(editorOrModel, allEdits, true); + alertFormattingEdits(allEdits); editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); } else { // use model to apply edits - const [{ range }] = edits; + const [{ range }] = allEdits; const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - model.pushEditOperations([initialSelection], edits.map(edit => { + model.pushEditOperations([initialSelection], allEdits.map(edit => { return { text: edit.text, range: Range.lift(edit.range), diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 8296fe06333..bedb20cdd97 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -16,7 +16,7 @@ import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; -import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangeWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit'; import * as nls from 'vs/nls'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; @@ -202,7 +202,7 @@ class FormatOnPaste implements IEditorContribution { if (this.editor.getSelections().length > 1) { return; } - this._instantiationService.invokeFunction(formatDocumentRangeWithSelectedProvider, this.editor, range, FormattingMode.Silent, CancellationToken.None).catch(onUnexpectedError); + this._instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, this.editor, range, FormattingMode.Silent, Progress.None, CancellationToken.None).catch(onUnexpectedError); } } @@ -267,14 +267,16 @@ class FormatSelectionAction extends EditorAction { } const instaService = accessor.get(IInstantiationService); const model = editor.getModel(); - let range: Range = editor.getSelection(); - if (range.isEmpty()) { - range = new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)); - } + + const ranges = editor.getSelections().map(range => { + return range.isEmpty() + ? new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber)) + : range; + }); const progressService = accessor.get(IEditorProgressService); await progressService.showWhile( - instaService.invokeFunction(formatDocumentRangeWithSelectedProvider, editor, range, FormattingMode.Explicit, CancellationToken.None), + instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, ranges, FormattingMode.Explicit, Progress.None, CancellationToken.None), 250 ); } diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index 4bf81a1ec8a..6a7c295e827 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -971,7 +971,7 @@ export class SelectionHighlighter extends Disposable implements IEditorContribut return; } - const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model); + const hasFindOccurrences = DocumentHighlightProviderRegistry.has(model) && this.editor.getOption(EditorOption.occurrencesHighlight); let allMatches = model.findMatches(this.state.searchText, true, false, this.state.matchCase, this.state.wordSeparators, false).map(m => m.range); allMatches.sort(Range.compareRangesUsingStarts); diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts index 51cc31bbefe..105dfb2ddb2 100644 --- a/src/vs/editor/contrib/rename/onTypeRename.ts +++ b/src/vs/editor/contrib/rename/onTypeRename.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { registerEditorContribution, registerModelAndPositionCommand, EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import * as arrays from 'vs/base/common/arrays'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position, IPosition } from 'vs/editor/common/core/position'; @@ -16,7 +16,7 @@ import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness, IIdentifiedS import { CancellationToken } from 'vs/base/common/cancellation'; import { IRange, Range } from 'vs/editor/common/core/range'; import { OnTypeRenameProviderRegistry } from 'vs/editor/common/modes'; -import { first, createCancelablePromise, CancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; +import { first, createCancelablePromise, CancelablePromise, Delayer } from 'vs/base/common/async'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; import { ContextKeyExpr, RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; @@ -24,11 +24,12 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; +import { isPromiseCanceledError, onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { Color } from 'vs/base/common/color'; +import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; export const CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE = new RawContextKey('onTypeRenameInputVisible', false); @@ -45,19 +46,26 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr return editor.getContribution(OnTypeRenameContribution.ID); } + private _debounceDuration = 200; + private readonly _editor: ICodeEditor; private _enabled: boolean; private readonly _visibleContextKey: IContextKey; - private _currentRequest: CancelablePromise<{ - ranges: IRange[], - stopPattern?: RegExp - } | null | undefined> | null; + private _rangeUpdateTriggerPromise: Promise | null; + private _rangeSyncTriggerPromise: Promise | null; + + private _currentRequest: CancelablePromise | null; + private _currentRequestPosition: Position | null; + private _currentRequestModelVersion: number | null; + private _currentDecorations: string[]; // The one at index 0 is the reference one - private _stopPattern: RegExp; + private _languageWordPattern: RegExp | null; + private _currentWordPattern: RegExp | null; private _ignoreChangeEvent: boolean; - private _updateMirrors: RunOnceScheduler; + + private readonly _localToDispose = this._register(new DisposableStore()); constructor( editor: ICodeEditor, @@ -65,103 +73,117 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr ) { super(); this._editor = editor; - this._enabled = this._editor.getOption(EditorOption.renameOnType); + this._enabled = false; this._visibleContextKey = CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); - this._currentRequest = null; + this._currentDecorations = []; - this._stopPattern = /^\s/; + this._languageWordPattern = null; + this._currentWordPattern = null; this._ignoreChangeEvent = false; - this._updateMirrors = this._register(new RunOnceScheduler(() => this._doUpdateMirrors(), 0)); + this._localToDispose = this._register(new DisposableStore()); - this._register(this._editor.onDidChangeModel((e) => { - this.stopAll(); - this.run(); - })); + this._rangeUpdateTriggerPromise = null; + this._rangeSyncTriggerPromise = null; - this._register(this._editor.onDidChangeConfiguration((e) => { + this._currentRequest = null; + this._currentRequestPosition = null; + this._currentRequestModelVersion = null; + + this._register(this._editor.onDidChangeModel(() => this.reinitialize())); + + this._register(this._editor.onDidChangeConfiguration(e => { if (e.hasChanged(EditorOption.renameOnType)) { - this._enabled = this._editor.getOption(EditorOption.renameOnType); - this.stopAll(); - this.run(); + this.reinitialize(); } })); + this._register(OnTypeRenameProviderRegistry.onDidChange(() => this.reinitialize())); + this._register(this._editor.onDidChangeModelLanguage(() => this.reinitialize())); - this._register(this._editor.onDidChangeCursorPosition((e) => { - // no regions, run - if (this._currentDecorations.length === 0) { - this.run(e.position); - } - - // has cached regions, don't run - if (!this._editor.hasModel()) { - return; - } - if (this._currentDecorations.length === 0) { - return; - } - const model = this._editor.getModel(); - const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!); - - // just moving cursor around, don't run again - if (Range.containsPosition(currentRanges[0], e.position)) { - return; - } - - // moving cursor out of primary region, run - this.run(e.position); - })); - - this._register(OnTypeRenameProviderRegistry.onDidChange(() => { - this.run(); - })); - - this._register(this._editor.onDidChangeModelContent((e) => { - if (this._ignoreChangeEvent) { - return; - } - if (!this._editor.hasModel()) { - return; - } - if (this._currentDecorations.length === 0) { - // nothing to do - return; - } - if (e.isUndoing || e.isRedoing) { - return; - } - if (e.changes[0] && this._stopPattern.test(e.changes[0].text)) { - this.stopAll(); - return; - } - this._updateMirrors.schedule(); - })); + this.reinitialize(); } - private _doUpdateMirrors(): void { - if (!this._editor.hasModel()) { + private reinitialize() { + const model = this._editor.getModel(); + const isEnabled = model !== null && this._editor.getOption(EditorOption.renameOnType) && OnTypeRenameProviderRegistry.has(model); + if (isEnabled === this._enabled) { return; } - if (this._currentDecorations.length === 0) { + + this._enabled = isEnabled; + + this.clearRanges(); + this._localToDispose.clear(); + + if (!isEnabled || model === null) { + return; + } + + this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + this._localToDispose.add(model.onDidChangeLanguageConfiguration(() => { + this._languageWordPattern = LanguageConfigurationRegistry.getWordDefinition(model.getLanguageIdentifier().id); + })); + + const rangeUpdateScheduler = new Delayer(this._debounceDuration); + const triggerRangeUpdate = () => { + this._rangeUpdateTriggerPromise = rangeUpdateScheduler.trigger(() => this.updateRanges(), this._debounceDuration); + }; + const rangeSyncScheduler = new Delayer(0); + const triggerRangeSync = (decorations: string[]) => { + this._rangeSyncTriggerPromise = rangeSyncScheduler.trigger(() => this._syncRanges(decorations)); + }; + this._localToDispose.add(this._editor.onDidChangeCursorPosition(() => { + triggerRangeUpdate(); + })); + this._localToDispose.add(this._editor.onDidChangeModelContent((e) => { + if (!this._ignoreChangeEvent) { + if (this._currentDecorations.length > 0) { + const referenceRange = model.getDecorationRange(this._currentDecorations[0]); + if (referenceRange && e.changes.every(c => referenceRange.intersectRanges(c.range))) { + triggerRangeSync(this._currentDecorations); + return; + } + } + } + triggerRangeUpdate(); + })); + this._localToDispose.add({ + dispose: () => { + rangeUpdateScheduler.cancel(); + rangeSyncScheduler.cancel(); + } + }); + this.updateRanges(); + } + + private _syncRanges(decorations: string[]): void { + // dalayed invocation, make sure we're still on + if (!this._editor.hasModel() || decorations !== this._currentDecorations || decorations.length === 0) { // nothing to do return; } const model = this._editor.getModel(); - const currentRanges = this._currentDecorations.map(decId => model.getDecorationRange(decId)!); + const referenceRange = model.getDecorationRange(decorations[0]); - const referenceRange = currentRanges[0]; - if (referenceRange.startLineNumber !== referenceRange.endLineNumber) { - return this.stopAll(); + if (!referenceRange || referenceRange.startLineNumber !== referenceRange.endLineNumber) { + return this.clearRanges(); } const referenceValue = model.getValueInRange(referenceRange); - if (this._stopPattern.test(referenceValue)) { - return this.stopAll(); + if (this._currentWordPattern) { + const match = referenceValue.match(this._currentWordPattern); + const matchLength = match ? match[0].length : 0; + if (matchLength !== referenceValue.length) { + return this.clearRanges(); + } } let edits: IIdentifiedSingleEditOperation[] = []; - for (let i = 1, len = currentRanges.length; i < len; i++) { - const mirrorRange = currentRanges[i]; + for (let i = 1, len = decorations.length; i < len; i++) { + const mirrorRange = model.getDecorationRange(decorations[i]); + if (!mirrorRange) { + continue; + } if (mirrorRange.startLineNumber !== mirrorRange.endLineNumber) { edits.push({ range: mirrorRange, @@ -207,72 +229,136 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr } public dispose(): void { + this.clearRanges(); super.dispose(); - this.stopAll(); } - stopAll(): void { + public clearRanges(): void { this._visibleContextKey.set(false); this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, []); - } - - async run(position: Position | null = this._editor.getPosition(), force = false): Promise { - if (!position) { - return; - } - if (!this._enabled && !force) { - return; - } - if (!this._editor.hasModel()) { - return; - } - if (this._currentRequest) { this._currentRequest.cancel(); this._currentRequest = null; + this._currentRequestPosition = null; + } + } + + public get currentUpdateTriggerPromise(): Promise { + return this._rangeUpdateTriggerPromise || Promise.resolve(); + } + + public get currentSyncTriggerPromise(): Promise { + return this._rangeSyncTriggerPromise || Promise.resolve(); + } + + public async updateRanges(force = false): Promise { + if (!this._editor.hasModel()) { + this.clearRanges(); + return; + } + + const position = this._editor.getPosition(); + if (!this._enabled && !force || this._editor.getSelections().length > 1) { + // disabled or multicursor + this.clearRanges(); + return; } const model = this._editor.getModel(); - - this._currentRequest = createCancelablePromise(token => getOnTypeRenameRanges(model, position, token)); - try { - const response = await this._currentRequest; - - let ranges: IRange[] = []; - if (response?.ranges) { - ranges = response.ranges; + const modelVersionId = model.getVersionId(); + if (this._currentRequestPosition && this._currentRequestModelVersion === modelVersionId) { + if (position.equals(this._currentRequestPosition)) { + return; // same position } - if (response?.stopPattern) { - this._stopPattern = response.stopPattern; - } - - let foundReferenceRange = false; - for (let i = 0, len = ranges.length; i < len; i++) { - if (Range.containsPosition(ranges[i], position)) { - foundReferenceRange = true; - if (i !== 0) { - const referenceRange = ranges[i]; - ranges.splice(i, 1); - ranges.unshift(referenceRange); - } - break; + if (this._currentDecorations && this._currentDecorations.length > 0) { + const range = model.getDecorationRange(this._currentDecorations[0]); + if (range && range.containsPosition(position)) { + return; // just moving inside the existing primary range } } - - if (!foundReferenceRange) { - // Cannot do on type rename if the ranges are not where the cursor is... - this.stopAll(); - return; - } - - const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION })); - this._visibleContextKey.set(true); - this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations); - } catch (err) { - onUnexpectedError(err); - this.stopAll(); } + + this._currentRequestPosition = position; + this._currentRequestModelVersion = modelVersionId; + const request = createCancelablePromise(async token => { + try { + const response = await getOnTypeRenameRanges(model, position, token); + if (request !== this._currentRequest) { + return; + } + this._currentRequest = null; + if (modelVersionId !== model.getVersionId()) { + return; + } + + let ranges: IRange[] = []; + if (response?.ranges) { + ranges = response.ranges; + } + + this._currentWordPattern = response?.wordPattern || this._languageWordPattern; + + let foundReferenceRange = false; + for (let i = 0, len = ranges.length; i < len; i++) { + if (Range.containsPosition(ranges[i], position)) { + foundReferenceRange = true; + if (i !== 0) { + const referenceRange = ranges[i]; + ranges.splice(i, 1); + ranges.unshift(referenceRange); + } + break; + } + } + + if (!foundReferenceRange) { + // Cannot do on type rename if the ranges are not where the cursor is... + this.clearRanges(); + return; + } + + const decorations: IModelDeltaDecoration[] = ranges.map(range => ({ range: range, options: OnTypeRenameContribution.DECORATION })); + this._visibleContextKey.set(true); + this._currentDecorations = this._editor.deltaDecorations(this._currentDecorations, decorations); + } catch (err) { + if (!isPromiseCanceledError(err)) { + onUnexpectedError(err); + } + if (this._currentRequest === request || !this._currentRequest) { + // stop if we are still the latest request + this.clearRanges(); + } + } + }); + this._currentRequest = request; + return request; } + + // for testing + public setDebounceDuration(timeInMS: number) { + this._debounceDuration = timeInMS; + } + + // private printDecorators(model: ITextModel) { + // return this._currentDecorations.map(d => { + // const range = model.getDecorationRange(d); + // if (range) { + // return this.printRange(range); + // } + // return 'invalid'; + // }).join(','); + // } + + // private printChanges(changes: IModelContentChange[]) { + // return changes.map(c => { + // return `${this.printRange(c.range)} - ${c.text}`; + // } + // ).join(','); + // } + + // private printRange(range: IRange) { + // return `${range.startLineNumber},${range.startColumn}/${range.endLineNumber},${range.endColumn}`; + // } } export class OnTypeRenameAction extends EditorAction { @@ -310,10 +396,10 @@ export class OnTypeRenameAction extends EditorAction { return super.runCommand(accessor, args); } - run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { const controller = OnTypeRenameContribution.get(editor); if (controller) { - return Promise.resolve(controller.run(editor.getPosition(), true)); + return Promise.resolve(controller.updateRanges(true)); } return Promise.resolve(); } @@ -323,7 +409,7 @@ const OnTypeRenameCommand = EditorCommand.bindToContribution x.stopAll(), + handler: x => x.clearRanges(), kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, weight: KeybindingWeight.EditorContrib + 99, @@ -335,7 +421,7 @@ registerEditorCommand(new OnTypeRenameCommand({ export function getOnTypeRenameRanges(model: ITextModel, position: Position, token: CancellationToken): Promise<{ ranges: IRange[], - stopPattern?: RegExp + wordPattern?: RegExp } | undefined | null> { const orderedByScore = OnTypeRenameProviderRegistry.ordered(model); @@ -344,16 +430,16 @@ export function getOnTypeRenameRanges(model: ITextModel, position: Position, tok // (good = none empty array) return first<{ ranges: IRange[], - stopPattern?: RegExp + wordPattern?: RegExp } | undefined>(orderedByScore.map(provider => () => { - return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((ranges) => { - if (!ranges) { + return Promise.resolve(provider.provideOnTypeRenameRanges(model, position, token)).then((res) => { + if (!res) { return undefined; } return { - ranges, - stopPattern: provider.stopPattern + ranges: res.ranges, + wordPattern: res.wordPattern || provider.wordPattern }; }, (err) => { onUnexpectedExternalError(err); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 7ae883bcd2e..214d00ffc63 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -22,7 +22,7 @@ import { MessageController } from 'vs/editor/contrib/message/messageController'; import { CodeEditorStateFlag, EditorStateCancellationTokenSource } from 'vs/editor/browser/core/editorState'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; import { URI } from 'vs/base/common/uri'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -226,7 +226,7 @@ class RenameController implements IEditorContribution { return; } - this._bulkEditService.apply(renameResult, { + this._bulkEditService.apply(ResourceEdit.convert(renameResult), { editor: this.editor, showPreview: inputFieldResult.wantsPreview, label: nls.localize('label', "Renaming '{0}'", loc?.text), diff --git a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts index 86177205b90..303b3554445 100644 --- a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts +++ b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts @@ -6,19 +6,29 @@ import * as assert from 'assert'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { Handler } from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; import { OnTypeRenameContribution } from 'vs/editor/contrib/rename/onTypeRename'; import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { ITextModel } from 'vs/editor/common/model'; +import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper'; const mockFile = URI.parse('test:somefile.ttt'); const mockFileSelector = { scheme: 'test' }; const timeout = 30; +interface TestEditor { + setPosition(pos: Position): Promise; + setSelection(sel: IRange): Promise; + trigger(source: string | null | undefined, handlerId: string, payload: any): Promise; + undo(): void; + redo(): void; +} + suite('On type rename', () => { const disposables = new DisposableStore(); @@ -45,26 +55,54 @@ suite('On type rename', () => { function testCase( name: string, - initialState: { text: string | string[], ranges: Range[], stopPattern?: RegExp }, - operations: (editor: ITestCodeEditor, contrib: OnTypeRenameContribution) => Promise, + initialState: { text: string | string[], responseWordPattern?: RegExp, providerWordPattern?: RegExp }, + operations: (editor: TestEditor) => Promise, expectedEndText: string | string[] ) { test(name, async () => { disposables.add(modes.OnTypeRenameProviderRegistry.register(mockFileSelector, { - stopPattern: initialState.stopPattern || /^\s/, - - provideOnTypeRenameRanges() { - return initialState.ranges; + wordPattern: initialState.providerWordPattern, + provideOnTypeRenameRanges(model: ITextModel, pos: IPosition) { + const wordAtPos = model.getWordAtPosition(pos); + if (wordAtPos) { + const matches = model.findMatches(wordAtPos.word, false, false, true, USUAL_WORD_SEPARATORS, false); + assert.ok(matches.length > 0); + return { ranges: matches.map(m => m.range), wordPattern: initialState.responseWordPattern }; + } + return { ranges: [], wordPattern: initialState.responseWordPattern }; } })); const editor = createMockEditor(initialState.text); + editor.updateOptions({ renameOnType: true }); const ontypeRenameContribution = editor.registerAndInstantiateContribution( OnTypeRenameContribution.ID, OnTypeRenameContribution ); + ontypeRenameContribution.setDebounceDuration(0); - await operations(editor, ontypeRenameContribution); + const testEditor: TestEditor = { + setPosition(pos: Position) { + editor.setPosition(pos); + return ontypeRenameContribution.currentUpdateTriggerPromise; + }, + setSelection(sel: IRange) { + editor.setSelection(sel); + return ontypeRenameContribution.currentUpdateTriggerPromise; + }, + trigger(source: string | null | undefined, handlerId: string, payload: any) { + editor.trigger(source, handlerId, payload); + return ontypeRenameContribution.currentSyncTriggerPromise; + }, + undo() { + CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + }, + redo() { + CoreEditingCommands.Redo.runEditorCommand(null, editor, null); + } + }; + + await operations(testEditor); return new Promise((resolve) => { setTimeout(() => { @@ -80,349 +118,322 @@ suite('On type rename', () => { } const state = { - text: '', - ranges: [ - new Range(1, 2, 1, 5), - new Range(1, 8, 1, 11), - ] + text: '' }; /** * Simple insertion */ - testCase('Simple insert - initial', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - initial', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert - middle', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - middle', state, async (editor) => { const pos = new Position(1, 3); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert - end', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - end', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Simple insertion - end */ - testCase('Simple insert end - initial', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert end - initial', state, async (editor) => { const pos = new Position(1, 8); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert end - middle', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert end - middle', state, async (editor) => { const pos = new Position(1, 9); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert end - end', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert end - end', state, async (editor) => { const pos = new Position(1, 11); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); /** * Boundary insertion */ - testCase('Simple insert - out of boundary', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary', state, async (editor) => { const pos = new Position(1, 1); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); - testCase('Simple insert - out of boundary 2', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary 2', state, async (editor) => { const pos = new Position(1, 6); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); - testCase('Simple insert - out of boundary 3', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary 3', state, async (editor) => { const pos = new Position(1, 7); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Simple insert - out of boundary 4', state, async (editor, ontypeRenameContribution) => { + testCase('Simple insert - out of boundary 4', state, async (editor) => { const pos = new Position(1, 12); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); /** * Insert + Move */ - testCase('Continuous insert', state, async (editor, ontypeRenameContribution) => { + testCase('Continuous insert', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Insert - move - insert', state, async (editor, ontypeRenameContribution) => { + testCase('Insert - move - insert', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - editor.setPosition(new Position(1, 4)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(new Position(1, 4)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Insert - move - insert outside region', state, async (editor, ontypeRenameContribution) => { + testCase('Insert - move - insert outside region', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - editor.setPosition(new Position(1, 7)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(new Position(1, 7)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'i'); /** * Selection insert */ - testCase('Selection insert - simple', state, async (editor, ontypeRenameContribution) => { + testCase('Selection insert - simple', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 2, 1, 3)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 2, 1, 3)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Selection insert - whole', state, async (editor, ontypeRenameContribution) => { + testCase('Selection insert - whole', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 2, 1, 5)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 2, 1, 5)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Selection insert - across boundary', state, async (editor, ontypeRenameContribution) => { + testCase('Selection insert - across boundary', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 1, 1, 3)); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 1, 1, 3)); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, 'ioo>'); /** * @todo * Undefined behavior */ - // testCase('Selection insert - across two boundary', state, async (editor, ontypeRenameContribution) => { + // testCase('Selection insert - across two boundary', state, async (editor) => { // const pos = new Position(1, 2); - // editor.setPosition(pos); - // await ontypeRenameContribution.run(pos, true); - // editor.setSelection(new Range(1, 4, 1, 9)); - // editor.trigger('keyboard', Handler.Type, { text: 'i' }); + // await editor.setPosition(pos); + // await ontypeRenameContribution.updateLinkedUI(pos); + // await editor.setSelection(new Range(1, 4, 1, 9)); + // await editor.trigger('keyboard', Handler.Type, { text: 'i' }); // }, ''); /** * Break out behavior */ - testCase('Breakout - type space', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - type space', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: ' ' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: ' ' }); }, ''); - testCase('Breakout - type space then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - type space then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: ' ' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: ' ' }); + editor.undo(); }, ''); - testCase('Breakout - type space in middle', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - type space in middle', state, async (editor) => { const pos = new Position(1, 4); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: ' ' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: ' ' }); }, ''); - testCase('Breakout - paste content starting with space', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - paste content starting with space', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); }, ''); - testCase('Breakout - paste content starting with space then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - paste content starting with space then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: ' i="i"' }); + editor.undo(); }, ''); - testCase('Breakout - paste content starting with space in middle', state, async (editor, ontypeRenameContribution) => { + testCase('Breakout - paste content starting with space in middle', state, async (editor) => { const pos = new Position(1, 4); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: ' i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: ' i' }); }, ''); /** - * Break out with custom stopPattern + * Break out with custom provider wordPattern */ const state3 = { ...state, - stopPattern: /^s/ + providerWordPattern: /[a-yA-Y]+/ }; - testCase('Breakout with stop pattern - insert', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - insert', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, ''); - testCase('Breakout with stop pattern - insert stop char', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - insert stop char', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 's' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'z' }); + }, ''); - testCase('Breakout with stop pattern - paste char', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - paste char', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: 's' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: 'z' }); + }, ''); - testCase('Breakout with stop pattern - paste string', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - paste string', state3, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Paste, { text: 'so' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Paste, { text: 'zo' }); + }, ''); - testCase('Breakout with stop pattern - insert at end', state3, async (editor, ontypeRenameContribution) => { + testCase('Breakout with stop pattern - insert at end', state3, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 's' }); - }, ''); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'z' }); + }, ''); + + const state4 = { + ...state, + providerWordPattern: /[a-yA-Y]+/, + responseWordPattern: /[a-eA-E]+/ + }; + + testCase('Breakout with stop pattern - insert stop char, respos', state4, async (editor) => { + const pos = new Position(1, 2); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + }, ''); /** * Delete */ - testCase('Delete - left char', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left char', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteLeft', {}); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteLeft', {}); }, ''); - testCase('Delete - left char then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left char then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteLeft', {}); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteLeft', {}); + editor.undo(); }, ''); - testCase('Delete - left word', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left word', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteWordLeft', {}); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteWordLeft', {}); }, '<>'); - testCase('Delete - left word then undo', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left word then undo', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteWordLeft', {}); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteWordLeft', {}); + editor.undo(); + editor.undo(); }, ''); /** * Todo: Fix test */ - // testCase('Delete - left all', state, async (editor, ontypeRenameContribution) => { + // testCase('Delete - left all', state, async (editor) => { // const pos = new Position(1, 3); - // editor.setPosition(pos); - // await ontypeRenameContribution.run(pos, true); - // editor.trigger('keyboard', 'deleteAllLeft', {}); + // await editor.setPosition(pos); + // await ontypeRenameContribution.updateLinkedUI(pos); + // await editor.trigger('keyboard', 'deleteAllLeft', {}); // }, '>'); /** * Todo: Fix test */ - // testCase('Delete - left all then undo', state, async (editor, ontypeRenameContribution) => { + // testCase('Delete - left all then undo', state, async (editor) => { // const pos = new Position(1, 5); - // editor.setPosition(pos); - // await ontypeRenameContribution.run(pos, true); - // editor.trigger('keyboard', 'deleteAllLeft', {}); - // CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + // await editor.setPosition(pos); + // await ontypeRenameContribution.updateLinkedUI(pos); + // await editor.trigger('keyboard', 'deleteAllLeft', {}); + // editor.undo(); // }, '>'); - testCase('Delete - left all then undo twice', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - left all then undo twice', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', 'deleteAllLeft', {}); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', 'deleteAllLeft', {}); + editor.undo(); + editor.undo(); }, ''); - testCase('Delete - selection', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - selection', state, async (editor) => { const pos = new Position(1, 5); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 2, 1, 3)); - editor.trigger('keyboard', 'deleteLeft', {}); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 2, 1, 3)); + await editor.trigger('keyboard', 'deleteLeft', {}); }, ''); - testCase('Delete - selection across boundary', state, async (editor, ontypeRenameContribution) => { + testCase('Delete - selection across boundary', state, async (editor) => { const pos = new Position(1, 3); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.setSelection(new Range(1, 1, 1, 3)); - editor.trigger('keyboard', 'deleteLeft', {}); + await editor.setPosition(pos); + await editor.setSelection(new Range(1, 1, 1, 3)); + await editor.trigger('keyboard', 'deleteLeft', {}); }, 'oo>'); /** * Undo / redo */ - testCase('Undo/redo - simple undo', state, async (editor, ontypeRenameContribution) => { + testCase('Undo/redo - simple undo', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.undo(); + editor.undo(); }, ''); - testCase('Undo/redo - simple undo/redo', state, async (editor, ontypeRenameContribution) => { + testCase('Undo/redo - simple undo/redo', state, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); - CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - CoreEditingCommands.Redo.runEditorCommand(null, editor, null); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); + editor.undo(); + editor.redo(); }, ''); /** @@ -432,18 +443,13 @@ suite('On type rename', () => { text: [ '', '' - ], - ranges: [ - new Range(1, 2, 1, 5), - new Range(2, 3, 2, 6), ] }; - testCase('Multiline insert', state2, async (editor, ontypeRenameContribution) => { + testCase('Multiline insert', state2, async (editor) => { const pos = new Position(1, 2); - editor.setPosition(pos); - await ontypeRenameContribution.run(pos, true); - editor.trigger('keyboard', Handler.Type, { text: 'i' }); + await editor.setPosition(pos); + await editor.trigger('keyboard', Handler.Type, { text: 'i' }); }, [ '', '' diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index 791e831aab0..2e2b7013238 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -101,13 +101,7 @@ export class CursorWordStartLeft extends WordLeftCommand { inSelectionMode: false, wordNavigationType: WordNavigationType.WordStart, id: 'cursorWordStartLeft', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, - weight: KeybindingWeight.EditorContrib - } + precondition: undefined }); } } @@ -129,7 +123,13 @@ export class CursorWordLeft extends WordLeftCommand { inSelectionMode: false, wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeft', - precondition: undefined + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyCode.LeftArrow, + mac: { primary: KeyMod.Alt | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } }); } } @@ -140,13 +140,7 @@ export class CursorWordStartLeftSelect extends WordLeftCommand { inSelectionMode: true, wordNavigationType: WordNavigationType.WordStart, id: 'cursorWordStartLeftSelect', - precondition: undefined, - kbOpts: { - kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, - mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, - weight: KeybindingWeight.EditorContrib - } + precondition: undefined }); } } @@ -168,7 +162,13 @@ export class CursorWordLeftSelect extends WordLeftCommand { inSelectionMode: true, wordNavigationType: WordNavigationType.WordStartFast, id: 'cursorWordLeftSelect', - precondition: undefined + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.LeftArrow, + mac: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow }, + weight: KeybindingWeight.EditorContrib + } }); } } diff --git a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts index 08d629e4260..f219c6cafec 100644 --- a/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts +++ b/src/vs/editor/standalone/browser/inspectTokens/inspectTokens.ts @@ -265,8 +265,8 @@ class InspectTokensWidget extends Disposable implements IContentWidget { case StandardTokenType.Comment: return 'Comment'; case StandardTokenType.String: return 'String'; case StandardTokenType.RegEx: return 'RegEx'; + default: return '??'; } - return '??'; } private _fontStyleToString(fontStyle: FontStyle): string { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 9051fb2cc78..01554deb137 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -13,14 +13,13 @@ import { OS, isLinux, isMacintosh } from 'vs/base/common/platform'; import Severity from 'vs/base/common/severity'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor, IDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { isDiffEditorConfigurationKey, isEditorConfigurationKey } from 'vs/editor/common/config/commonEditorConfig'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditor } from 'vs/editor/common/editorCommon'; -import { ITextModel, ITextSnapshot } from 'vs/editor/common/model'; -import { TextEdit, WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IIdentifiedSingleEditOperation, ITextModel, ITextSnapshot } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from 'vs/editor/common/services/resolverService'; import { ITextResourceConfigurationService, ITextResourcePropertiesService, ITextResourceConfigurationChangeEvent } from 'vs/editor/common/services/textResourceConfigurationService'; @@ -665,42 +664,43 @@ export class SimpleBulkEditService implements IBulkEditService { return Disposable.None; } - apply(workspaceEdit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(edits: ResourceEdit[], _options?: IBulkEditOptions): Promise { - let edits = new Map(); + const textEdits = new Map(); - if (workspaceEdit.edits) { - for (let edit of workspaceEdit.edits) { - if (!WorkspaceTextEdit.is(edit)) { - return Promise.reject(new Error('bad edit - only text edits are supported')); - } - let model = this._modelService.getModel(edit.resource); - if (!model) { - return Promise.reject(new Error('bad edit - model not found')); - } - let array = edits.get(model); - if (!array) { - array = []; - edits.set(model, array); - } - array.push(edit.edit); + for (let edit of edits) { + if (!(edit instanceof ResourceTextEdit)) { + throw new Error('bad edit - only text edits are supported'); } + const model = this._modelService.getModel(edit.resource); + if (!model) { + throw new Error('bad edit - model not found'); + } + if (typeof edit.versionId === 'number' && model.getVersionId() !== edit.versionId) { + throw new Error('bad state - model changed in the meantime'); + } + let array = textEdits.get(model); + if (!array) { + array = []; + textEdits.set(model, array); + } + array.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text)); } + let totalEdits = 0; let totalFiles = 0; - edits.forEach((edits, model) => { + for (const [model, edits] of textEdits) { model.pushStackElement(); - model.pushEditOperations([], edits.map((e) => EditOperation.replaceMove(Range.lift(e.range), e.text)), () => []); + model.pushEditOperations([], edits, () => []); model.pushStackElement(); totalFiles += 1; totalEdits += edits.length; - }); + } - return Promise.resolve({ - selection: undefined, + return { ariaSummary: strings.format(SimpleServicesNLS.bulkEditServiceSummary, totalEdits, totalFiles) - }); + }; } } diff --git a/src/vs/editor/test/browser/controller/imeTester.ts b/src/vs/editor/test/browser/controller/imeTester.ts index d1d625087f8..4a3f4e196df 100644 --- a/src/vs/editor/test/browser/controller/imeTester.ts +++ b/src/vs/editor/test/browser/controller/imeTester.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as browser from 'vs/base/browser/browser'; import { createFastDomNode } from 'vs/base/browser/fastDomNode'; import { ITextAreaInputHost, TextAreaInput } from 'vs/editor/browser/controller/textAreaInput'; import { ISimpleModel, PagedScreenReaderStrategy, TextAreaState } from 'vs/editor/browser/controller/textAreaState'; @@ -96,12 +95,6 @@ function doCreateTest(description: string, inputStr: string, expectedStr: string }; }, getScreenReaderContent: (currentState: TextAreaState): TextAreaState => { - - if (browser.isIPad) { - // Do not place anything in the textarea for the iPad - return TextAreaState.EMPTY; - } - const selection = new Range(1, 1 + cursorOffset, 1, 1 + cursorOffset + cursorLength); return PagedScreenReaderStrategy.fromEditorSelection(currentState, model, selection, 10, true); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index d371020b873..7f438e97ee9 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4414,6 +4414,14 @@ declare namespace monaco.editor { overflowWidgetsDomNode?: HTMLElement; } + export interface IDiffEditorConstructionOptions extends IDiffEditorOptions { + /** + * Place overflow widgets inside an external DOM node. + * Defaults to an internal DOM node. + */ + overflowWidgetsDomNode?: HTMLElement; + } + /** * A rich code editor. */ @@ -5793,11 +5801,14 @@ declare namespace monaco.languages { * the live-rename feature. */ export interface OnTypeRenameProvider { - stopPattern?: RegExp; + wordPattern?: RegExp; /** * Provide a list of ranges that can be live-renamed together. */ - provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideOnTypeRenameRanges(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult<{ + ranges: IRange[]; + wordPattern?: RegExp; + }>; } /** diff --git a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts index 0c84bbc7c63..48b171e1b76 100644 --- a/src/vs/platform/actions/browser/menuEntryActionViewItem.ts +++ b/src/vs/platform/actions/browser/menuEntryActionViewItem.ts @@ -110,11 +110,11 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; to.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/platform/contextkey/common/contextkey.ts b/src/vs/platform/contextkey/common/contextkey.ts index 55e154645ec..8cdc6954c0a 100644 --- a/src/vs/platform/contextkey/common/contextkey.ts +++ b/src/vs/platform/contextkey/common/contextkey.ts @@ -17,6 +17,8 @@ STATIC_VALUES.set('isWindows', isWindows); STATIC_VALUES.set('isWeb', isWeb); STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb); +const hasOwnProperty = Object.prototype.hasOwnProperty; + export const enum ContextKeyExprType { False = 0, True = 1, @@ -27,7 +29,9 @@ export const enum ContextKeyExprType { And = 6, Regex = 7, NotRegex = 8, - Or = 9 + Or = 9, + In = 10, + NotIn = 11, } export interface IContextKeyExprMapper { @@ -36,6 +40,7 @@ export interface IContextKeyExprMapper { mapEquals(key: string, value: any): ContextKeyExpression; mapNotEquals(key: string, value: any): ContextKeyExpression; mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr; + mapIn(key: string, valueKey: string): ContextKeyInExpr; } export interface IContextKeyExpression { @@ -52,7 +57,7 @@ export interface IContextKeyExpression { export type ContextKeyExpression = ( ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr | ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr - | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr + | ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr | ContextKeyNotInExpr ); export abstract class ContextKeyExpr { @@ -81,6 +86,10 @@ export abstract class ContextKeyExpr { return ContextKeyRegexExpr.create(key, value); } + public static in(key: string, value: string): ContextKeyExpression { + return ContextKeyInExpr.create(key, value); + } + public static not(key: string): ContextKeyExpression { return ContextKeyNotExpr.create(key); } @@ -129,6 +138,11 @@ export abstract class ContextKeyExpr { return ContextKeyRegexExpr.create(pieces[0].trim(), this._deserializeRegexValue(pieces[1], strict)); } + if (serializedOne.indexOf(' in ') >= 0) { + let pieces = serializedOne.split(' in '); + return ContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim()); + } + if (/^\!\s*/.test(serializedOne)) { return ContextKeyNotExpr.create(serializedOne.substr(1).trim()); } @@ -393,6 +407,122 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression { } } +export class ContextKeyInExpr implements IContextKeyExpression { + + public static create(key: string, valueKey: string): ContextKeyInExpr { + return new ContextKeyInExpr(key, valueKey); + } + + public readonly type = ContextKeyExprType.In; + + private constructor(private readonly key: string, private readonly valueKey: string) { + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + if (this.key < other.key) { + return -1; + } + if (this.key > other.key) { + return 1; + } + if (this.valueKey < other.valueKey) { + return -1; + } + if (this.valueKey > other.valueKey) { + return 1; + } + return 0; + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return (this.key === other.key && this.valueKey === other.valueKey); + } + return false; + } + + public evaluate(context: IContext): boolean { + const source = context.getValue(this.valueKey); + + const item = context.getValue(this.key); + + if (Array.isArray(source)) { + return (source.indexOf(item) >= 0); + } + + if (typeof item === 'string' && typeof source === 'object' && source !== null) { + return hasOwnProperty.call(source, item); + } + return false; + } + + public serialize(): string { + return this.key + ' in \'' + this.valueKey + '\''; + } + + public keys(): string[] { + return [this.key, this.valueKey]; + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyInExpr { + return mapFnc.mapIn(this.key, this.valueKey); + } + + public negate(): ContextKeyExpression { + return ContextKeyNotInExpr.create(this); + } +} + +export class ContextKeyNotInExpr implements IContextKeyExpression { + + public static create(actual: ContextKeyInExpr): ContextKeyNotInExpr { + return new ContextKeyNotInExpr(actual); + } + + public readonly type = ContextKeyExprType.NotIn; + + private constructor(private readonly _actual: ContextKeyInExpr) { + // + } + + public cmp(other: ContextKeyExpression): number { + if (other.type !== this.type) { + return this.type - other.type; + } + return this._actual.cmp(other._actual); + } + + public equals(other: ContextKeyExpression): boolean { + if (other.type === this.type) { + return this._actual.equals(other._actual); + } + return false; + } + + public evaluate(context: IContext): boolean { + return !this._actual.evaluate(context); + } + + public serialize(): string { + throw new Error('Method not implemented.'); + } + + public keys(): string[] { + return this._actual.keys(); + } + + public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression { + return new ContextKeyNotInExpr(this._actual.map(mapFnc)); + } + + public negate(): ContextKeyExpression { + return this._actual; + } +} + export class ContextKeyNotEqualsExpr implements IContextKeyExpression { public static create(key: string, value: any): ContextKeyExpression { diff --git a/src/vs/platform/contextkey/test/common/contextkey.test.ts b/src/vs/platform/contextkey/test/common/contextkey.test.ts index c7784c888e1..042012f1d33 100644 --- a/src/vs/platform/contextkey/test/common/contextkey.test.ts +++ b/src/vs/platform/contextkey/test/common/contextkey.test.ts @@ -150,4 +150,19 @@ suite('ContextKeyExpr', () => { t('a || b', 'c && d', 'a && c && d || b && c && d'); t('a || b', 'c && d || e', 'a && e || b && e || a && c && d || b && c && d'); }); + + test('ContextKeyInExpr', () => { + const ainb = ContextKeyExpr.deserialize('a in b')!; + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [3, 2, 1] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2, 3] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': [1, 2] })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 3 })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 3, 'b': null })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['x'] })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': ['y'] })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': {} })), false); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': false } })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'x', 'b': { 'x': true } })), true); + assert.equal(ainb.evaluate(createContext({ 'a': 'prototype', 'b': {} })), false); + }); }); diff --git a/src/vs/platform/credentials/common/credentials.ts b/src/vs/platform/credentials/common/credentials.ts index 2799abeed19..06af1a01af5 100644 --- a/src/vs/platform/credentials/common/credentials.ts +++ b/src/vs/platform/credentials/common/credentials.ts @@ -5,15 +5,16 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -export const ICredentialsService = createDecorator('ICredentialsService'); - -export interface ICredentialsService { - - readonly _serviceBrand: undefined; - +export interface ICredentialsProvider { getPassword(service: string, account: string): Promise; setPassword(service: string, account: string, password: string): Promise; deletePassword(service: string, account: string): Promise; findPassword(service: string): Promise; findCredentials(service: string): Promise>; } + +export const ICredentialsService = createDecorator('ICredentialsService'); + +export interface ICredentialsService extends ICredentialsProvider { + readonly _serviceBrand: undefined; +} diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 52949788147..513ce8e92a7 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -115,7 +115,8 @@ const PropertyType = { Dependency: 'Microsoft.VisualStudio.Code.ExtensionDependencies', ExtensionPack: 'Microsoft.VisualStudio.Code.ExtensionPack', Engine: 'Microsoft.VisualStudio.Code.Engine', - LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages' + LocalizedLanguages: 'Microsoft.VisualStudio.Code.LocalizedLanguages', + WebExtension: 'Microsoft.VisualStudio.Code.WebExtension' }; interface ICriterium { @@ -266,6 +267,11 @@ function getIsPreview(flags: string): boolean { return flags.indexOf('preview') !== -1; } +function getIsWebExtension(version: IRawGalleryExtensionVersion): boolean { + const webExtensionProperty = version.properties ? version.properties.find(p => p.key === PropertyType.WebExtension) : undefined; + return !!webExtensionProperty && webExtensionProperty.value === 'true'; +} + function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGalleryExtensionVersion, index: number, query: Query, querySource?: string): IGalleryExtension { const assets = { manifest: getVersionAsset(version, AssetType.Manifest), @@ -301,7 +307,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller dependencies: getExtensions(version, PropertyType.Dependency), extensionPack: getExtensions(version, PropertyType.ExtensionPack), engine: getEngine(version), - localizedLanguages: getLocalizedLanguages(version) + localizedLanguages: getLocalizedLanguages(version), + webExtension: getIsWebExtension(version) }, /* __GDPR__FRAGMENT__ "GalleryExtensionTelemetryData2" : { diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index 34811829b09..609e3a501e2 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -19,6 +19,7 @@ export interface IGalleryExtensionProperties { extensionPack?: string[]; engine?: string; localizedLanguages?: string[]; + webExtension?: boolean; } export interface IGalleryExtensionAsset { @@ -204,6 +205,7 @@ export interface IExtensionManagementService { unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; install(vsix: URI, isMachineScoped?: boolean): Promise; + canInstall(extension: IGalleryExtension): Promise; installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise; uninstall(extension: ILocalExtension, force?: boolean): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; @@ -239,6 +241,7 @@ export type IExecutableBasedExtensionTip = { readonly extensionId: string, readonly extensionName: string, readonly isExtensionPack: boolean, + readonly exeName: string, readonly exeFriendlyName: string, readonly windowsPath?: string, }; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index d497780449a..048aa90adf3 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -63,6 +63,7 @@ export class ExtensionManagementChannel implements IServerChannel { case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer)); case 'install': return this.service.install(transformIncomingURI(args[0], uriTransformer)); case 'getManifest': return this.service.getManifest(transformIncomingURI(args[0], uriTransformer)); + case 'canInstall': return this.service.canInstall(args[0]); case 'installFromGallery': return this.service.installFromGallery(args[0]); case 'uninstall': return this.service.uninstall(transformIncomingExtension(args[0], uriTransformer), args[1]); case 'reinstallFromGallery': return this.service.reinstallFromGallery(transformIncomingExtension(args[0], uriTransformer)); @@ -104,6 +105,10 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer return Promise.resolve(this.channel.call('getManifest', [vsix])); } + async canInstall(extension: IGalleryExtension): Promise { + return true; + } + installFromGallery(extension: IGalleryExtension): Promise { return Promise.resolve(this.channel.call('installFromGallery', [extension])).then(local => transformIncomingExtension(local, null)); } diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index bfd0fbd7877..badeef4aa5d 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -193,7 +193,7 @@ export class ExtensionManagementService extends Disposable implements IExtension this._onInstallExtension.fire({ identifier, zipPath }); return this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) .then( - metadata => this.installFromZipPath(identifierWithVersion, zipPath, { ...metadata, isMachineScoped }, operation, token), + metadata => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { ...metadata, isMachineScoped } : metadata, operation, token), () => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { isMachineScoped } : undefined, operation, token)) .then( local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, @@ -238,6 +238,10 @@ export class ExtensionManagementService extends Disposable implements IExtension )); } + async canInstall(extension: IGalleryExtension): Promise { + return true; + } + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { if (!this.galleryService.isEnabled()) { return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); diff --git a/src/vs/platform/extensionManagement/node/extensionTipsService.ts b/src/vs/platform/extensionManagement/node/extensionTipsService.ts index 5780216da37..3dd401c785b 100644 --- a/src/vs/platform/extensionManagement/node/extensionTipsService.ts +++ b/src/vs/platform/extensionManagement/node/extensionTipsService.ts @@ -105,6 +105,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService { extensionId, extensionName, isExtensionPack, + exeName, exeFriendlyName: extensionTip.exeFriendlyName, windowsPath: extensionTip.windowsPath, }); diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 3370a608b4b..bb33203d172 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -20,7 +20,7 @@ if (isWeb) { // Running out of sources if (Object.keys(product).length === 0) { Object.assign(product, { - version: '1.48.0-dev', + version: '1.49.0-dev', nameLong: 'Visual Studio Code Web Dev', nameShort: 'VSCode Web Dev', urlProtocol: 'code-oss', diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index fc98af96bcd..52dfacbc403 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -56,6 +56,14 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address: }; } +export function isLocalhost(host: string): boolean { + return host === 'localhost' || host === '127.0.0.1'; +} + +function getOtherLocalhost(host: string): string | undefined { + return (host === 'localhost') ? '127.0.0.1' : ((host === '127.0.0.1') ? 'localhost' : undefined); +} + export abstract class AbstractTunnelService implements ITunnelService { declare readonly _serviceBrand: undefined; @@ -105,7 +113,7 @@ export abstract class AbstractTunnelService implements ITunnelService { return undefined; } - if (!remoteHost || (remoteHost === '127.0.0.1')) { + if (!remoteHost) { remoteHost = 'localhost'; } @@ -172,13 +180,29 @@ export abstract class AbstractTunnelService implements ITunnelService { this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); } + protected getTunnelFromMap(remoteHost: string, remotePort: number): { refcount: number, readonly value: Promise } | undefined { + const otherLocalhost = getOtherLocalhost(remoteHost); + let portMap: Map }> | undefined; + if (otherLocalhost) { + const firstMap = this._tunnels.get(remoteHost); + const secondMap = this._tunnels.get(otherLocalhost); + if (firstMap && secondMap) { + portMap = new Map([...Array.from(firstMap.entries()), ...Array.from(secondMap.entries())]); + } else { + portMap = firstMap ?? secondMap; + } + } else { + portMap = this._tunnels.get(remoteHost); + } + return portMap ? portMap.get(remotePort) : undefined; + } + protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined; } export class TunnelService extends AbstractTunnelService { protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; + const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; diff --git a/src/vs/platform/remote/node/tunnelService.ts b/src/vs/platform/remote/node/tunnelService.ts index 983401be417..c8d84bdeff4 100644 --- a/src/vs/platform/remote/node/tunnelService.ts +++ b/src/vs/platform/remote/node/tunnelService.ts @@ -86,7 +86,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel { this.tunnelLocalPort = address.port; await this._barrier.wait(); - this.localAddress = 'localhost:' + address.port; + this.localAddress = `${this.tunnelRemoteHost === '127.0.0.1' ? '127.0.0.1' : 'localhost'}:${address.port}`; return this; } @@ -132,8 +132,7 @@ export class TunnelService extends AbstractTunnelService { } protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { - const portMap = this._tunnels.get(remoteHost); - const existing = portMap ? portMap.get(remotePort) : undefined; + const existing = this.getTunnelFromMap(remoteHost, remotePort); if (existing) { ++existing.refcount; return existing.value; diff --git a/src/vs/platform/severityIcon/common/severityIcon.ts b/src/vs/platform/severityIcon/common/severityIcon.ts index f09bc4b255b..8b4697e84b9 100644 --- a/src/vs/platform/severityIcon/common/severityIcon.ts +++ b/src/vs/platform/severityIcon/common/severityIcon.ts @@ -20,8 +20,9 @@ export namespace SeverityIcon { return Codicon.warning.classNames; case Severity.Error: return Codicon.error.classNames; + default: + return ''; } - return ''; } } diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index 59b1baf9120..ab3fd347b69 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -5,7 +5,7 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; -import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; +import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; @@ -20,8 +20,6 @@ export class BrowserStorageService extends Disposable implements IStorageService declare readonly _serviceBrand: undefined; - private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker'; - private readonly _onDidChangeStorage = this._register(new Emitter()); readonly onDidChangeStorage = this._onDidChangeStorage.event; @@ -82,12 +80,20 @@ export class BrowserStorageService extends Disposable implements IStorageService this.globalStorage.init() ]); - // Check to see if this is the first time we are "opening" this workspace - const firstOpen = this.workspaceStorage.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY); + // Check to see if this is the first time we are "opening" the application + const firstOpen = this.globalStorage.getBoolean(IS_NEW_KEY); if (firstOpen === undefined) { - this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, true); + this.globalStorage.set(IS_NEW_KEY, true); } else if (firstOpen) { - this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, false); + this.globalStorage.set(IS_NEW_KEY, false); + } + + // Check to see if this is the first time we are "opening" this workspace + const firstWorkspaceOpen = this.workspaceStorage.getBoolean(IS_NEW_KEY); + if (firstWorkspaceOpen === undefined) { + this.workspaceStorage.set(IS_NEW_KEY, true); + } else if (firstWorkspaceOpen) { + this.workspaceStorage.set(IS_NEW_KEY, false); } // In the browser we do not have support for long running unload sequences. As such, @@ -189,8 +195,8 @@ export class BrowserStorageService extends Disposable implements IStorageService this.dispose(); } - isNew(scope: StorageScope.WORKSPACE): boolean { - return this.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY, scope) === true; + isNew(scope: StorageScope): boolean { + return this.getBoolean(IS_NEW_KEY, scope) === true; } dispose(): void { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 1623957cb18..6611f1dae42 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -9,6 +9,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; +export const IS_NEW_KEY = '__$__isNewStorageMarker'; + export const IStorageService = createDecorator('storageService'); export enum WillSaveStateReason { @@ -104,12 +106,11 @@ export interface IStorageService { migrate(toWorkspace: IWorkspaceInitializationPayload): Promise; /** - * Wether the storage for the given scope was created during this session or + * Whether the storage for the given scope was created during this session or * existed before. * - * Note: currently only implemented for `WORKSPACE` scope. */ - isNew(scope: StorageScope.WORKSPACE): boolean; + isNew(scope: StorageScope): boolean; /** * Allows to flush state, e.g. in cases where a shutdown is @@ -239,6 +240,8 @@ export class InMemoryStorageService extends Disposable implements IStorageServic isNew(): boolean { return true; // always new when in-memory } + + async close(): Promise { } } export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index 1abfd328ce6..9ba93bb462a 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -12,6 +12,7 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; import { Storage, IStorage, InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage'; import { join } from 'vs/base/common/path'; +import { IS_NEW_KEY } from 'vs/platform/storage/common/storage'; export const IStorageMainService = createDecorator('storageMainService'); @@ -135,7 +136,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic return this.initializePromise; } - private doInitialize(): Promise { + private async doInitialize(): Promise { this.storage.dispose(); this.storage = new Storage(new SQLiteStorageDatabase(this.storagePath, { logging: this.createLogginOptions() @@ -143,7 +144,15 @@ export class StorageMainService extends Disposable implements IStorageMainServic this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); - return this.storage.init(); + await this.storage.init(); + + // Check to see if this is the first time we are "opening" the application + const firstOpen = this.storage.getBoolean(IS_NEW_KEY); + if (firstOpen === undefined) { + this.storage.set(IS_NEW_KEY, true); + } else if (firstOpen) { + this.storage.set(IS_NEW_KEY, false); + } } get(key: string, fallbackValue: string): string; diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index 75514fe5a4f..ac657056aa6 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -6,7 +6,7 @@ import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; -import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; +import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage'; import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage'; import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage'; import { mark } from 'vs/base/common/performance'; @@ -25,8 +25,6 @@ export class NativeStorageService extends Disposable implements IStorageService private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; private static readonly WORKSPACE_META_NAME = 'workspace.json'; - private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker'; - private readonly _onDidChangeStorage = this._register(new Emitter()); readonly onDidChangeStorage = this._onDidChangeStorage.event; @@ -108,11 +106,11 @@ export class NativeStorageService extends Disposable implements IStorageService await workspaceStorage.init(); // Check to see if this is the first time we are "opening" this workspace - const firstOpen = workspaceStorage.getBoolean(NativeStorageService.WORKSPACE_IS_NEW_KEY); - if (firstOpen === undefined) { - workspaceStorage.set(NativeStorageService.WORKSPACE_IS_NEW_KEY, result.wasCreated); - } else if (firstOpen) { - workspaceStorage.set(NativeStorageService.WORKSPACE_IS_NEW_KEY, false); + const firstWorkspaceOpen = workspaceStorage.getBoolean(IS_NEW_KEY); + if (firstWorkspaceOpen === undefined) { + workspaceStorage.set(IS_NEW_KEY, result.wasCreated); + } else if (firstWorkspaceOpen) { + workspaceStorage.set(IS_NEW_KEY, false); } } finally { mark('didInitWorkspaceStorage'); @@ -281,7 +279,7 @@ export class NativeStorageService extends Disposable implements IStorageService return this.createWorkspaceStorage(newWorkspaceStoragePath).init(); } - isNew(scope: StorageScope.WORKSPACE): boolean { - return this.getBoolean(NativeStorageService.WORKSPACE_IS_NEW_KEY, scope) === true; + isNew(scope: StorageScope): boolean { + return this.getBoolean(IS_NEW_KEY, scope) === true; } } diff --git a/src/vs/platform/theme/electron-main/themeMainService.ts b/src/vs/platform/theme/electron-main/themeMainService.ts index 0d16ddb9002..7bbacdb3d52 100644 --- a/src/vs/platform/theme/electron-main/themeMainService.ts +++ b/src/vs/platform/theme/electron-main/themeMainService.ts @@ -42,14 +42,14 @@ export class ThemeMainService implements IThemeMainService { } getBackgroundColor(): string { - if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { + if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { return DEFAULT_BG_HC_BLACK; } let background = this.stateService.getItem(THEME_BG_STORAGE_KEY, null); if (!background) { let baseTheme: string; - if (isWindows && nativeTheme.shouldUseInvertedColorScheme) { + if ((isWindows || isMacintosh) && nativeTheme.shouldUseInvertedColorScheme) { baseTheme = 'hc-black'; } else { baseTheme = this.stateService.getItem(THEME_STORAGE_KEY, 'vs-dark').split(' ')[0]; diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 3457eabe31e..4e654f548e6 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -53,6 +53,10 @@ function isSyncData(thing: any): thing is ISyncData { return false; } +function getLastSyncResourceUri(syncResource: SyncResource, environmentService: IEnvironmentService): URI { + return joinPath(environmentService.userDataSyncHome, syncResource, `lastSync${syncResource}.json`); +} + export interface IResourcePreview { readonly remoteResource: URI; @@ -133,7 +137,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.syncResourceLogLabel = uppercaseFirstLetter(this.resource); this.syncFolder = joinPath(environmentService.userDataSyncHome, resource); this.syncPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME); - this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`); + this.lastSyncResource = getLastSyncResourceUri(resource, environmentService); this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService); } @@ -324,6 +328,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize ${this.syncResourceLogLabel} as there is a new local version available. Synchronizing again...`); return this.performSync(remoteUserData, lastSyncUserData, apply); + case UserDataSyncErrorCode.Conflict: case UserDataSyncErrorCode.PreconditionFailed: // Rejected as there is a new remote version. Syncing again... this.logService.info(`${this.syncResourceLogLabel}: Failed to synchronize as there is a new remote version available. Synchronizing again...`); @@ -796,3 +801,62 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni } } + +export abstract class AbstractInitializer { + + private readonly lastSyncResource: URI; + + constructor( + readonly resource: SyncResource, + @IEnvironmentService protected readonly environmentService: IEnvironmentService, + @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, + @IFileService protected readonly fileService: IFileService, + ) { + this.lastSyncResource = getLastSyncResourceUri(this.resource, environmentService); + } + + async initialize({ ref, content }: IUserData): Promise { + if (!content) { + this.logService.info('Remote content does not exist.', this.resource); + return; + } + + const syncData = this.parseSyncData(content); + if (!syncData) { + return; + } + + const isPreviouslySynced = await this.fileService.exists(this.lastSyncResource); + if (isPreviouslySynced) { + this.logService.info('Remote content does not exist.', this.resource); + return; + } + + try { + await this.doInitialize({ ref, syncData }); + } catch (error) { + this.logService.error(error); + } + } + + private parseSyncData(content: string): ISyncData | undefined { + try { + const syncData: ISyncData = JSON.parse(content); + if (isSyncData(syncData)) { + return syncData; + } + } catch (error) { + this.logService.error(error); + } + this.logService.info('Cannot parse sync data as it is not compatible with the current version.', this.resource); + return undefined; + } + + protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { + const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps }; + await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); + } + + protected abstract doInitialize(remoteUserData: IRemoteUserData): Promise; + +} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index d0c714df48b..0fe85a4a9e8 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -15,7 +15,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; -import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources'; @@ -42,6 +42,34 @@ interface ILastSyncUserData extends IRemoteUserData { skippedExtensions: ISyncExtension[] | undefined; } +async function parseAndMigrateExtensions(syncData: ISyncData, extensionManagementService: IExtensionManagementService): Promise { + const extensions = JSON.parse(syncData.content); + if (syncData.version === 1 + || syncData.version === 2 + ) { + const systemExtensions = await extensionManagementService.getInstalled(ExtensionType.System); + for (const extension of extensions) { + // #region Migration from v1 (enabled -> disabled) + if (syncData.version === 1) { + if ((extension).enabled === false) { + extension.disabled = true; + } + delete (extension).enabled; + } + // #endregion + + // #region Migration from v2 (set installed property on extension) + if (syncData.version === 2) { + if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) { + extension.installed = true; + } + } + // #endregion + } + } + return extensions; +} + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/extensions.json` }); @@ -84,9 +112,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; + const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; - const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null; + const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await parseAndMigrateExtensions(lastSyncUserData.syncData!, this.extensionManagementService) : null; const installedExtensions = await this.extensionManagementService.getInstalled(); const localExtensions = this.getLocalExtensions(installedExtensions); @@ -385,34 +413,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private async parseAndMigrateExtensions(syncData: ISyncData): Promise { - const extensions = this.parseExtensions(syncData); - if (syncData.version === 1 - || syncData.version === 2 - ) { - const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System); - for (const extension of extensions) { - // #region Migration from v1 (enabled -> disabled) - if (syncData.version === 1) { - if ((extension).enabled === false) { - extension.disabled = true; - } - delete (extension).enabled; - } - // #endregion - - // #region Migration from v2 (set installed property on extension) - if (syncData.version === 2) { - if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) { - extension.installed = true; - } - } - // #endregion - } - } - return extensions; - } - private parseExtensions(syncData: ISyncData): ISyncExtension[] { return JSON.parse(syncData.content); } @@ -433,3 +433,68 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } } + +export class ExtensionsInitializer extends AbstractInitializer { + + constructor( + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, + @IGlobalExtensionEnablementService private readonly extensionEnablementService: IGlobalExtensionEnablementService, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Extensions, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null; + if (!remoteExtensions) { + this.logService.info('Skipping initializing extensions because remote extensions does not exist.'); + return; + } + + const installedExtensions = await this.extensionManagementService.getInstalled(); + const toInstall: { names: string[], uuids: string[] } = { names: [], uuids: [] }; + const toDisable: IExtensionIdentifier[] = []; + for (const extension of remoteExtensions) { + if (installedExtensions.some(i => areSameExtensions(i.identifier, extension.identifier))) { + if (extension.disabled) { + toDisable.push(extension.identifier); + } + } else { + if (extension.installed) { + if (extension.identifier.uuid) { + toInstall.uuids.push(extension.identifier.uuid); + } else { + toInstall.names.push(extension.identifier.id); + } + } + } + } + + if (toInstall.names.length || toInstall.uuids.length) { + const galleryExtensions = (await this.galleryService.query({ ids: toInstall.uuids, names: toInstall.names, pageSize: toInstall.uuids.length + toInstall.names.length }, CancellationToken.None)).firstPage; + for (const galleryExtension of galleryExtensions) { + try { + this.logService.trace(`Installing extension...`, galleryExtension.identifier.id); + await this.extensionManagementService.installFromGallery(galleryExtension); + this.logService.info(`Installed extension.`, galleryExtension.identifier.id); + } catch (error) { + this.logService.error(error); + } + } + } + + if (toDisable.length) { + for (const identifier of toDisable) { + this.logService.trace(`Enabling extension...`, identifier.id); + await this.extensionEnablementService.disableExtension(identifier); + this.logService.info(`Enabled extension`, identifier.id); + } + } + } + +} + + diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index 5929719d952..ab8209b396b 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -16,7 +16,7 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; -import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; @@ -341,3 +341,55 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => ({ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))]; } } + +export class GlobalStateInitializer extends AbstractInitializer { + + constructor( + @IStorageService private readonly storageService: IStorageService, + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.GlobalState, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + if (!remoteGlobalState) { + this.logService.info('Skipping initializing global state because remote global state does not exist.'); + return; + } + + const argv: IStringDictionary = {}; + const storage: IStringDictionary = {}; + for (const key of Object.keys(remoteGlobalState.storage)) { + if (key.startsWith(argvStoragePrefx)) { + argv[key.substring(argvStoragePrefx.length)] = remoteGlobalState.storage[key].value; + } else { + if (this.storageService.get(key, StorageScope.GLOBAL) === undefined) { + storage[key] = remoteGlobalState.storage[key].value; + } + } + } + + if (Object.keys(argv).length) { + let content = '{}'; + try { + const fileContent = await this.fileService.readFile(this.environmentService.argvResource); + content = fileContent.value.toString(); + } catch (error) { } + for (const argvProperty of Object.keys(argv)) { + content = edit(content, [argvProperty], argv[argvProperty], {}); + } + await this.fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(content)); + } + + if (Object.keys(storage).length) { + for (const key of Object.keys(storage)) { + this.storageService.store(key, storage[key], StorageScope.GLOBAL); + } + } + } + +} + diff --git a/src/vs/platform/userDataSync/common/keybindingsMerge.ts b/src/vs/platform/userDataSync/common/keybindingsMerge.ts index 3f994050bc7..3cbd0faf84d 100644 --- a/src/vs/platform/userDataSync/common/keybindingsMerge.ts +++ b/src/vs/platform/userDataSync/common/keybindingsMerge.ts @@ -28,10 +28,14 @@ interface IMergeResult { conflicts: Set; } +export function parseKeybindings(content: string): IUserFriendlyKeybinding[] { + return parse(content) || []; +} + export async function merge(localContent: string, remoteContent: string, baseContent: string | null, formattingOptions: FormattingOptions, userDataSyncUtilService: IUserDataSyncUtilService): Promise<{ mergeContent: string, hasChanges: boolean, hasConflicts: boolean }> { - const local = parse(localContent); - const remote = parse(remoteContent); - const base = baseContent ? parse(baseContent) : null; + const local = parseKeybindings(localContent); + const remote = parseKeybindings(remoteContent); + const base = baseContent ? parseKeybindings(baseContent) : null; const userbindings: string[] = [...local, ...remote, ...(base || [])].map(keybinding => keybinding.key); const normalizedKeys = await userDataSyncUtilService.resolveUserBindings(userbindings); @@ -331,7 +335,7 @@ function addKeybindings(content: string, keybindings: IUserFriendlyKeybinding[], } function removeKeybindings(content: string, command: string, formattingOptions: FormattingOptions): string { - const keybindings = parse(content); + const keybindings = parseKeybindings(content); for (let index = keybindings.length - 1; index >= 0; index--) { if (keybindings[index].command === command || keybindings[index].command === `-${command}`) { content = contentUtil.edit(content, [index], undefined, formattingOptions); @@ -341,7 +345,7 @@ function removeKeybindings(content: string, command: string, formattingOptions: } function updateKeybindings(content: string, command: string, keybindings: IUserFriendlyKeybinding[], formattingOptions: FormattingOptions): string { - const allKeybindings = parse(content); + const allKeybindings = parseKeybindings(content); const location = findFirstIndex(allKeybindings, keybinding => keybinding.command === command || keybinding.command === `-${command}`); // Remove all entries with this command for (let index = allKeybindings.length - 1; index >= 0; index--) { diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index cfafec8c3ae..3d9816563da 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -18,11 +18,12 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { VSBuffer } from 'vs/base/common/buffer'; interface ISyncContent { mac?: string; @@ -35,6 +36,21 @@ interface IKeybindingsResourcePreview extends IFileResourcePreview { previewResult: IMergeResult; } +export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean): string | null { + const parsed = JSON.parse(syncContent); + if (!platformSpecific) { + return isUndefined(parsed.all) ? null : parsed.all; + } + switch (OS) { + case OperatingSystem.Macintosh: + return isUndefined(parsed.mac) ? null : parsed.mac; + case OperatingSystem.Linux: + return isUndefined(parsed.linux) ? null : parsed.linux; + case OperatingSystem.Windows: + return isUndefined(parsed.windows) ? null : parsed.windows; + } +} + export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ @@ -209,7 +225,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); - const lastSyncContent = content !== null ? this.toSyncContent(content, null) : null; + const lastSyncContent = content !== null ? this.toSyncContent(content, null) : remoteUserData.syncData?.content; await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { @@ -266,20 +282,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return null; } - getKeybindingsContentFromSyncContent(syncContent: string): string | null { + private getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { - const parsed = JSON.parse(syncContent); - if (!this.syncKeybindingsPerPlatform()) { - return isUndefined(parsed.all) ? null : parsed.all; - } - switch (OS) { - case OperatingSystem.Macintosh: - return isUndefined(parsed.mac) ? null : parsed.mac; - case OperatingSystem.Linux: - return isUndefined(parsed.linux) ? null : parsed.linux; - case OperatingSystem.Windows: - return isUndefined(parsed.windows) ? null : parsed.windows; - } + return getKeybindingsContentFromSyncContent(syncContent, this.syncKeybindingsPerPlatform()); } catch (e) { this.logService.error(e); return null; @@ -325,3 +330,52 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } } + +export class KeybindingsInitializer extends AbstractInitializer { + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Keybindings, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const keybindingsContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; + if (!keybindingsContent) { + this.logService.info('Skipping initializing keybindings because remote keybindings does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing keybindings because local keybindings exist.'); + return; + } + + await this.fileService.writeFile(this.environmentService.keybindingsResource, VSBuffer.fromString(keybindingsContent)); + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + try { + const fileContent = await this.fileService.readFile(this.environmentService.settingsResource); + const keybindings = parse(fileContent.value.toString()); + return !isNonEmptyArray(keybindings); + } catch (error) { + return (error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + } + + private getKeybindingsContentFromSyncContent(syncContent: string): string | null { + try { + return getKeybindingsContentFromSyncContent(syncContent, true); + } catch (e) { + this.logService.error(e); + return null; + } + } + +} diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index ad6800c6cbb..f4333ac84aa 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -275,8 +275,11 @@ export function areSame(localContent: string, remoteContent: string, ignoredSett } export function isEmpty(content: string): boolean { - const nodes = parseSettings(content); - return nodes.length === 0; + if (content) { + const nodes = parseSettings(content); + return nodes.length === 0; + } + return true; } function compare(from: IStringDictionary | null, to: IStringDictionary, ignored: Set): { added: Set, removed: Set, updated: Set } { diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index cbba5c25c3d..f1734e5e838 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; @@ -40,6 +40,11 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent { && Object.keys(thing).length === 1; } +export function parseSettingsSyncContent(syncContent: string): ISettingsSyncContent { + const parsed = JSON.parse(syncContent); + return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; +} + export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser { /* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */ @@ -281,10 +286,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; } - parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { try { - const parsed = JSON.parse(syncContent); - return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; + return parseSettingsSyncContent(syncContent); } catch (e) { this.logService.error(e); } @@ -350,6 +354,54 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement } } +export class SettingsInitializer extends AbstractInitializer { + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Settings, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const settingsSyncContent = remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; + if (!settingsSyncContent) { + this.logService.info('Skipping initializing settings because remote settings does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing settings because local settings exist.'); + return; + } + + await this.fileService.writeFile(this.environmentService.settingsResource, VSBuffer.fromString(settingsSyncContent.settings)); + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + try { + const fileContent = await this.fileService.readFile(this.environmentService.settingsResource); + return isEmpty(fileContent.value.toString().trim()); + } catch (error) { + return (error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + } + + private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + try { + return parseSettingsSyncContent(syncContent); + } catch (e) { + this.logService.error(e); + } + return null; + } + +} + function isSyncData(thing: any): thing is ISyncData { if (thing && (thing.version !== undefined && typeof thing.version === 'number') diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index f00332c395a..c5b1029bd14 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -10,7 +10,7 @@ import { import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractInitializer, AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IStringDictionary } from 'vs/base/common/collections'; import { URI } from 'vs/base/common/uri'; @@ -499,3 +499,49 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD return snippets; } } + +export class SnippetsInitializer extends AbstractInitializer { + + constructor( + @IFileService fileService: IFileService, + @IEnvironmentService environmentService: IEnvironmentService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + ) { + super(SyncResource.Snippets, environmentService, logService, fileService); + } + + async doInitialize(remoteUserData: IRemoteUserData): Promise { + const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + if (!remoteSnippets) { + this.logService.info('Skipping initializing snippets because remote snippets does not exist.'); + return; + } + + const isEmpty = await this.isEmpty(); + if (!isEmpty) { + this.logService.info('Skipping initializing snippets because local snippets exist.'); + return; + } + + for (const key of Object.keys(remoteSnippets)) { + const content = remoteSnippets[key]; + if (content) { + const resource = joinPath(this.environmentService.snippetsHome, key); + await this.fileService.createFile(resource, VSBuffer.fromString(content)); + this.logService.info('Created snippet', basename(resource)); + } + } + + await this.updateLastSyncUserData(remoteUserData); + } + + private async isEmpty(): Promise { + try { + const stat = await this.fileService.resolve(this.environmentService.snippetsHome); + return !stat.children?.length; + } catch (error) { + return (error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND; + } + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 097be01f51d..9574d24e419 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -131,8 +131,8 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this._register(userDataSyncAccountService.onDidChangeAccount(() => this.updateAutoSync())); this._register(userDataSyncStoreService.onDidChangeDonotMakeRequestsUntil(() => this.updateAutoSync())); - this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false))); - this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false))); + this._register(Event.debounce(userDataSyncService.onDidChangeLocal, (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, false, false))); + this._register(Event.filter(this.userDataSyncResourceEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerSync(['resourceEnablement'], false, false))); } } @@ -320,7 +320,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i } private sources: string[] = []; - async triggerSync(sources: string[], skipIfSyncedRecently: boolean): Promise { + async triggerSync(sources: string[], skipIfSyncedRecently: boolean, disableCache: boolean): Promise { if (this.autoSync.value === undefined) { return this.syncTriggerDelayer.cancel(); } @@ -337,7 +337,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources }); this.sources = []; if (this.autoSync.value) { - await this.autoSync.value.sync('Activity'); + await this.autoSync.value.sync('Activity', disableCache); } }, this.successiveFailures ? this.getSyncTriggerDelayTime() * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */ @@ -393,14 +393,14 @@ class AutoSync extends Disposable { this.logService.info('Auto Sync: Stopped'); })); this.logService.info('Auto Sync: Started'); - this.sync(AutoSync.INTERVAL_SYNCING); + this.sync(AutoSync.INTERVAL_SYNCING, false); } private waitUntilNextIntervalAndSync(): void { - this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval); + this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING, false), this.interval); } - sync(reason: string): Promise { + sync(reason: string, disableCache: boolean): Promise { const syncPromise = createCancelablePromise(async token => { if (this.syncPromise) { try { @@ -414,7 +414,7 @@ class AutoSync extends Disposable { } } } - return this.doSync(reason, token); + return this.doSync(reason, disableCache, token); }); this.syncPromise = syncPromise; this.syncPromise.finally(() => this.syncPromise = undefined); @@ -435,12 +435,12 @@ class AutoSync extends Disposable { !isEqual(current.stableUrl, previous.stableUrl)); } - private async doSync(reason: string, token: CancellationToken): Promise { + private async doSync(reason: string, disableCache: boolean, token: CancellationToken): Promise { this.logService.info(`Auto Sync: Triggered by ${reason}`); this._onDidStartSync.fire(); let error: Error | undefined; try { - this.syncTask = await this.userDataSyncService.createSyncTask(); + this.syncTask = await this.userDataSyncService.createSyncTask(disableCache); if (token.isCancellationRequested) { return; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index dbc24ef949e..2381e7e7345 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -164,10 +164,7 @@ export interface IUserDataSyncStoreManagementService { getPreviousUserDataSyncStore(): Promise; } -export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); -export interface IUserDataSyncStoreService { - readonly _serviceBrand: undefined; - +export interface IUserDataSyncStoreClient { readonly onDidChangeDonotMakeRequestsUntil: Event; readonly donotMakeRequestsUntil: Date | undefined; @@ -186,6 +183,11 @@ export interface IUserDataSyncStoreService { resolveContent(resource: ServerResource, ref: string): Promise; } +export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); +export interface IUserDataSyncStoreService extends IUserDataSyncStoreClient { + readonly _serviceBrand: undefined; +} + export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); export interface IUserDataSyncBackupStoreService { readonly _serviceBrand: undefined; @@ -208,6 +210,7 @@ export const HEADER_EXECUTION_ID = 'X-Execution-Id'; export enum UserDataSyncErrorCode { // Client Errors (>= 400 ) Unauthorized = 'Unauthorized', /* 401 */ + Conflict = 'Conflict', /* 409 */ Gone = 'Gone', /* 410 */ PreconditionFailed = 'PreconditionFailed', /* 412 */ TooLarge = 'TooLarge', /* 413 */ @@ -434,7 +437,7 @@ export interface IUserDataSyncService { readonly onDidResetRemote: Event; readonly onDidResetLocal: Event; - createSyncTask(): Promise; + createSyncTask(disableCache?: boolean): Promise; createManualSyncTask(): Promise; replace(uri: URI): Promise; @@ -462,7 +465,7 @@ export interface IUserDataAutoSyncService { canToggleEnablement(): boolean; turnOn(): Promise; turnOff(everywhere: boolean): Promise; - triggerSync(sources: string[], hasToLimitSync: boolean): Promise; + triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise; } export const IUserDataSyncUtilService = createDecorator('IUserDataSyncUtilService'); diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 8d6296e8051..08a8243bb42 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -128,7 +128,7 @@ export class UserDataAutoSyncChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { - case 'triggerSync': return this.service.triggerSync(args[0], args[1]); + case 'triggerSync': return this.service.triggerSync(args[0], args[1], args[2]); case 'turnOn': return this.service.turnOn(); case 'turnOff': return this.service.turnOff(args[0]); } diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index f9c446c2124..54d42878bf5 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -106,13 +106,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource))); } - async createSyncTask(): Promise { + async createSyncTask(disableCache?: boolean): Promise { await this.checkEnablement(); const executionId = generateUuid(); let manifest: IUserDataManifest | null; try { - manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId)); + const syncHeaders = createSyncHeaders(executionId); + if (disableCache) { + syncHeaders['Cache-Control'] = 'no-cache'; + } + manifest = await this.userDataSyncStoreService.manifest(syncHeaders); } catch (error) { error = UserDataSyncError.toUserDataSyncError(error); this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() }); diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index d17ec3b39fc..0d9489ed8e9 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, } from 'vs/base/common/lifecycle'; -import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType, IUserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -125,9 +125,7 @@ export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStor } } -export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { - - _serviceBrand: any; +export class UserDataSyncStoreClient extends Disposable implements IUserDataSyncStoreClient { private readonly userDataSyncStoreUrl: URI | undefined; @@ -147,16 +145,16 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn readonly onDidChangeDonotMakeRequestsUntil = this._onDidChangeDonotMakeRequestsUntil.event; constructor( + userDataSyncStoreUrl: URI | undefined, @IProductService productService: IProductService, @IRequestService private readonly requestService: IRequestService, - @IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, @IStorageService private readonly storageService: IStorageService, ) { super(); - this.userDataSyncStoreUrl = this.userDataSyncStoreManagementService.userDataSyncStore ? joinPath(this.userDataSyncStoreManagementService.userDataSyncStore.url, 'v1') : undefined; + this.userDataSyncStoreUrl = userDataSyncStoreUrl ? joinPath(userDataSyncStoreUrl, 'v1') : undefined; this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService) .then(uuid => { const headers: IHeaders = { @@ -395,12 +393,16 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this._onTokenSucceed.fire(); + if (context.res.statusCode === 409) { + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Conflict (409). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.Conflict, operationId); + } + if (context.res.statusCode === 410) { throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because the requested resource is not longer available (410).`, UserDataSyncErrorCode.Gone, operationId); } if (context.res.statusCode === 412) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data exists for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of Precondition Failed (412). There is new data for this resource. Make the request again with latest data.`, UserDataSyncErrorCode.PreconditionFailed, operationId); } if (context.res.statusCode === 413) { @@ -444,6 +446,23 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } +export class UserDataSyncStoreService extends UserDataSyncStoreClient implements IUserDataSyncStoreService { + + _serviceBrand: any; + + constructor( + @IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService, + @IProductService productService: IProductService, + @IRequestService requestService: IRequestService, + @IUserDataSyncLogService logService: IUserDataSyncLogService, + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, + ) { + super(userDataSyncStoreManagementService.userDataSyncStore?.url, productService, requestService, logService, environmentService, fileService, storageService); + } +} + export class RequestsSession { private requests: string[] = []; @@ -463,7 +482,7 @@ export class RequestsSession { if (this.requests.length >= this.limit) { this.logService.info('Too many requests', ...this.requests); - throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined); + throw new UserDataSyncStoreError(`Too many requests. Only ${this.limit} requests allowed in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests, undefined); } this.startTime = this.startTime || new Date(); diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 81483659e9b..44ed7f9b7ea 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -33,7 +33,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { this._register(Event.debounce(Event.any( Event.map(electronService.onWindowFocus, () => 'windowFocus'), Event.map(electronService.onWindowOpen, () => 'windowOpen'), - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true))); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true, false))); } } diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts index 02b0154b627..9007dcdb544 100644 --- a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -10,7 +10,7 @@ import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { getKeybindingsContentFromSyncContent, KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; import { VSBuffer } from 'vs/base/common/buffer'; suite('KeybindingsSync', () => { @@ -70,8 +70,8 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); - assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), '[]'); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]'); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), '[]'); assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), ''); }); @@ -95,11 +95,75 @@ suite('KeybindingsSync', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), content); - assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), content); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content); }); + test('when keybindings file is empty with comment and remote has no changes', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + const expectedContent = '// Empty Keybindings'; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString(expectedContent)); + + await testObject.sync(await client.manifest()); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), expectedContent); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), expectedContent); + assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), expectedContent); + }); + + test('when keybindings file is empty and remote has keybindings', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = JSON.stringify([ + { + 'key': 'shift+cmd+w', + 'command': 'workbench.action.closeAllEditors', + } + ]); + await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString('// Empty Keybindings')); + + await testObject.sync(await client.manifest()); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content); + }); + + test('when keybindings file is empty and remote has empty array', async () => { + const client2 = disposableStore.add(new UserDataSyncClient(server)); + await client2.setUp(true); + const content = + `// Place your key bindings in this file to override the defaults +[ +]`; + await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content)); + await client2.sync(); + + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + const expectedLocalContent = '// Empty Keybindings'; + await fileService.writeFile(keybindingsResource, VSBuffer.fromString(expectedLocalContent)); + + await testObject.sync(await client.manifest()); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), content); + assert.equal(getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!, true), content); + assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), expectedLocalContent); + }); + test('when keybindings file is created after first sync', async () => { const fileService = client.instantiationService.get(IFileService); const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; @@ -119,7 +183,7 @@ suite('KeybindingsSync', () => { const remoteUserData = await testObject.getRemoteUserData(null); assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); + assert.equal(getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!, true), '[]'); }); test('test apply remote when keybindings file does not exist', async () => { diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index 6117c2b55e2..e83d8a6aa73 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { SettingsSynchroniser, ISettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync'; +import { SettingsSynchroniser, ISettingsSyncContent, parseSettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -88,8 +88,8 @@ suite('SettingsSync - Auto', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); - assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}'); + assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + assert.equal(parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}'); assert.equal((await fileService.readFile(settingsResource)).value.toString(), ''); }); @@ -129,8 +129,8 @@ suite('SettingsSync - Auto', () => { const lastSyncUserData = await testObject.getLastSyncUserData(); const remoteUserData = await testObject.getRemoteUserData(null); - assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content); - assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content); + assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content); + assert.equal(parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content); assert.equal((await fileService.readFile(settingsResource)).value.toString(), content); }); @@ -154,7 +154,7 @@ suite('SettingsSync - Auto', () => { const remoteUserData = await testObject.getRemoteUserData(null); assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); - assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + assert.equal(parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); }); test('sync for first time to the server', async () => { diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts index 310c69dd6cf..49df9ea7e0f 100644 --- a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -20,7 +20,7 @@ class TestUserDataAutoSyncService extends UserDataAutoSyncService { protected getSyncTriggerDelayTime(): number { return 50; } sync(): Promise { - return this.triggerSync(['sync'], false); + return this.triggerSync(['sync'], false, false); } } @@ -43,7 +43,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); // Trigger auto sync with settings change - await testObject.triggerSync([SyncResource.Settings], false); + await testObject.triggerSync([SyncResource.Settings], false, false); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -66,7 +66,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with settings change multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync([SyncResource.Settings], false); + await testObject.triggerSync([SyncResource.Settings], false, false); } // Filter out machine requests @@ -91,7 +91,7 @@ suite('UserDataAutoSyncService', () => { const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); // Trigger auto sync with window focus once - await testObject.triggerSync(['windowFocus'], true); + await testObject.triggerSync(['windowFocus'], true, false); // Filter out machine requests const actual = target.requests.filter(request => !request.url.startsWith(`${target.url}/v1/resource/machines`)); @@ -114,7 +114,7 @@ suite('UserDataAutoSyncService', () => { // Trigger auto sync with window focus multiple times for (let counter = 0; counter < 2; counter++) { - await testObject.triggerSync(['windowFocus'], true); + await testObject.triggerSync(['windowFocus'], true, false); } // Filter out machine requests @@ -401,4 +401,28 @@ suite('UserDataAutoSyncService', () => { assert.deepEqual(target.requests, []); }); + test('test cache control header with no cache is sent when triggered with disable cache option', async () => { + const target = new UserDataSyncTestServer(5, 1); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + + await testObject.triggerSync(['some reason'], true, true); + assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], 'no-cache'); + }); + + test('test cache control header is not sent when triggered without disable cache option', async () => { + const target = new UserDataSyncTestServer(5, 1); + + // Set up and sync from the test client + const testClient = disposableStore.add(new UserDataSyncClient(target)); + await testClient.setUp(); + const testObject: TestUserDataAutoSyncService = testClient.instantiationService.createInstance(TestUserDataAutoSyncService); + + await testObject.triggerSync(['some reason'], true, false); + assert.equal(target.requestsWithAllHeaders[0].headers!['Cache-Control'], undefined); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 767fbbf6cb5..aa427813097 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts @@ -6,7 +6,7 @@ import { IRequestService } from 'vs/platform/request/common/request'; import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService, registerConfiguration } from 'vs/platform/userDataSync/common/userDataSync'; import { bufferToStream, VSBuffer } from 'vs/base/common/buffer'; import { generateUuid } from 'vs/base/common/uuid'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; @@ -49,6 +49,7 @@ export class UserDataSyncClient extends Disposable { } async setUp(empty: boolean = false): Promise { + registerConfiguration(); const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory }); const userDataSyncHome = joinPath(userRoamingDataHome, '.sync'); const environmentService = this.instantiationService.stub(IEnvironmentService, >{ diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index 4933ec468bb..c6bd8a1c5d9 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -7,7 +7,6 @@ import { isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ThemeType } from 'vs/platform/theme/common/themeService'; import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export interface IBaseOpenWindowsOptions { @@ -182,7 +181,6 @@ export interface IWindowConfiguration { remoteAuthority?: string; highContrast?: boolean; - defaultThemeType?: ThemeType; filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index e441c8c4bb0..5005fe95c8b 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -212,8 +212,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private registerListeners(): void { - // React to HC color scheme changes (Windows) - if (isWindows) { + // React to HC color scheme changes (Windows, macOS) + if (isWindows || isMacintosh) { nativeTheme.on('updated', () => { if (nativeTheme.shouldUseInvertedColorScheme || nativeTheme.shouldUseHighContrastColors) { this.sendToAll('vscode:enterHighContrast'); @@ -1134,7 +1134,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic if (forceOpenWorkspaceAsFile) { return { fileUri: uri, remoteAuthority }; } - } else if (posix.extname(anyPath).length > 0) { + } else if (posix.basename(anyPath).indexOf('.') !== -1) { // file name starts with a dot or has an file extension return { fileUri: uri, remoteAuthority }; } } diff --git a/src/vs/platform/workspace/common/workspace.ts b/src/vs/platform/workspace/common/workspace.ts index 0f2e82d0e85..c0c34d74f57 100644 --- a/src/vs/platform/workspace/common/workspace.ts +++ b/src/vs/platform/workspace/common/workspace.ts @@ -82,9 +82,9 @@ export interface IWorkspaceFoldersChangeEvent { export namespace IWorkspace { export function isIWorkspace(thing: unknown): thing is IWorkspace { - return thing && typeof thing === 'object' + return !!(thing && typeof thing === 'object' && typeof (thing as IWorkspace).id === 'string' - && Array.isArray((thing as IWorkspace).folders); + && Array.isArray((thing as IWorkspace).folders)); } } @@ -127,10 +127,10 @@ export interface IWorkspaceFolderData { export namespace IWorkspaceFolder { export function isIWorkspaceFolder(thing: unknown): thing is IWorkspaceFolder { - return thing && typeof thing === 'object' + return !!(thing && typeof thing === 'object' && URI.isUri((thing as IWorkspaceFolder).uri) && typeof (thing as IWorkspaceFolder).name === 'string' - && typeof (thing as IWorkspaceFolder).toResource === 'function'; + && typeof (thing as IWorkspaceFolder).toResource === 'function'); } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ca87d5f7227..9a09ec73ed2 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -1112,7 +1112,7 @@ declare module 'vscode' { * isn't one of the main editors, e.g. an embedded editor, or when the editor * column is larger than three. */ - viewColumn?: ViewColumn; + readonly viewColumn?: ViewColumn; /** * Perform an edit on the document associated with this text editor. @@ -3086,6 +3086,8 @@ declare module 'vscode' { * @param uri Uri of the new file.. * @param options Defines if an existing file should be overwritten or be * ignored. When overwrite and ignoreIfExists are both set overwrite wins. + * When both are unset and when the file already exists then the edit cannot + * be applied successfully. * @param metadata Optional metadata for the entry. */ createFile(uri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; @@ -5629,6 +5631,20 @@ declare module 'vscode' { */ asAbsolutePath(relativePath: string): string; + /** + * The uri of a workspace specific directory in which the extension + * can store private state. The directory might not exist and creation is + * up to the extension. However, the parent directory is guaranteed to be existent. + * The value is `undefined` when no workspace nor folder has been opened. + * + * Use [`workspaceState`](#ExtensionContext.workspaceState) or + * [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * an uri. + */ + readonly storageUri: Uri | undefined; + /** * An absolute file path of a workspace specific directory in which the extension * can store private state. The directory might not exist on disk and creation is @@ -5636,22 +5652,50 @@ declare module 'vscode' { * * Use [`workspaceState`](#ExtensionContext.workspaceState) or * [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @deprecated Use [storagePath](#ExtensionContent.storageUri) instead. */ readonly storagePath: string | undefined; + /** + * The uri of a directory in which the extension can store global state. + * The directory might not exist on disk and creation is + * up to the extension. However, the parent directory is guaranteed to be existent. + * + * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * an uri. + */ + readonly globalStorageUri: Uri; + /** * An absolute file path in which the extension can store global state. * The directory might not exist on disk and creation is * up to the extension. However, the parent directory is guaranteed to be existent. * * Use [`globalState`](#ExtensionContext.globalState) to store key value data. + * + * @deprecated Use [globalStoragePath](#ExtensionContent.globalStorageUri) instead. */ readonly globalStoragePath: string; + /** + * The uri of a directory in which the extension can create log files. + * The directory might not exist on disk and creation is up to the extension. However, + * the parent directory is guaranteed to be existent. + * + * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from + * an uri. + */ + readonly logUri: Uri; + /** * An absolute file path of a directory in which the extension can create log files. * The directory might not exist on disk and creation is up to the extension. However, * the parent directory is guaranteed to be existent. + * + * @deprecated Use [logUri](#ExtensionContext.logUri) instead. */ readonly logPath: string; @@ -6081,9 +6125,10 @@ declare module 'vscode' { * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). * @param process The [Pseudoterminal](#Pseudoterminal) to be used by the task to display output. - * @param callback The callback that will be called when the task is started by a user. + * @param callback The callback that will be called when the task is started by a user. Any ${} style variables that + * were in the task definition will be resolved and passed into the callback. */ - constructor(callback: () => Thenable); + constructor(callback: (resolvedDefinition: TaskDefinition) => Thenable); } /** @@ -6160,6 +6205,11 @@ declare module 'vscode' { */ name: string; + /** + * A detail to show for the task on a second line in places where the task's name is displayed. + */ + detail?: string; + /** * The task's execution engine */ @@ -10654,6 +10704,27 @@ declare module 'vscode' { export function createSourceControl(id: string, label: string, rootUri?: Uri): SourceControl; } + /** + * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolMessage { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). + } + + /** + * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolSource { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). + } + + /** + * A DebugProtocolBreakpoint is an opaque stand-in type for the [Breakpoint](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolBreakpoint { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Breakpoint). + } + /** * Configuration for a debug session. */ @@ -10717,6 +10788,15 @@ declare module 'vscode' { * Send a custom request to the debug adapter. */ customRequest(command: string, args?: any): Thenable; + + /** + * Maps a VS Code breakpoint to the corresponding Debug Adapter Protocol (DAP) breakpoint that is managed by the debug adapter of the debug session. + * If no DAP breakpoint exists (either because the VS Code breakpoint was not yet registered or because the debug adapter is not interested in the breakpoint), the value `undefined` is returned. + * + * @param breakpoint A VS Code [breakpoint](#Breakpoint). + * @return A promise that resolves to the Debug Adapter Protocol breakpoint or `undefined`. + */ + getDebugProtocolBreakpoint(breakpoint: Breakpoint): Thenable; } /** @@ -10857,6 +10937,21 @@ declare module 'vscode' { constructor(port: number, host?: string); } + /** + * Represents a debug adapter running as a Named Pipe (on Windows)/UNIX Domain Socket (on non-Windows) based server. + */ + export class DebugAdapterNamedPipeServer { + /** + * The path to the NamedPipe/UNIX Domain Socket. + */ + readonly path: string; + + /** + * Create a description for a debug adapter running as a socket based server. + */ + constructor(path: string); + } + /** * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. */ @@ -10877,13 +10972,6 @@ declare module 'vscode' { handleMessage(message: DebugProtocolMessage): void; } - /** - * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolMessage { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). - } - /** * A debug adapter descriptor for an inline implementation. */ @@ -10895,7 +10983,7 @@ declare module 'vscode' { constructor(implementation: DebugAdapter); } - export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterInlineImplementation; + export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterNamedPipeServer | DebugAdapterInlineImplementation; export interface DebugAdapterDescriptorFactory { /** @@ -11090,13 +11178,19 @@ declare module 'vscode' { * Defaults to Separate. */ consoleMode?: DebugConsoleMode; - } - /** - * A DebugProtocolSource is an opaque stand-in type for the [Source](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolSource { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Types_Source). + /** + * Controls whether this session should run without debugging, thus ignoring breakpoints. + * When this property is not specified, the value from the parent session (if there is one) is used. + */ + noDebug?: boolean; + + /** + * Controls if the debug session's parent session is shown in the CALL STACK view even if it has only a single child. + * By default, the debug session will never hide its parent. + * If compact is true, debug sessions with a single child are hidden in the CALL STACK view to make the tree more compact. + */ + compact?: boolean; } /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index c753ef72f24..c898362a2e4 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -737,21 +737,6 @@ declare module 'vscode' { //#region debug - export interface DebugSessionOptions { - /** - * Controls whether this session should run without debugging, thus ignoring breakpoints. - * When this property is not specified, the value from the parent session (if there is one) is used. - */ - noDebug?: boolean; - - /** - * Controls if the debug session's parent session is shown in the CALL STACK view even if it has only a single child. - * By default, the debug session will never hide its parent. - * If compact is true, debug sessions with a single child are hidden in the CALL STACK view to make the tree more compact. - */ - compact?: boolean; - } - // deprecated debug API export interface DebugConfigurationProvider { @@ -978,27 +963,6 @@ declare module 'vscode' { } //#endregion - //#region CustomExecution: https://github.com/microsoft/vscode/issues/81007 - /** - * A task to execute - */ - export class Task2 extends Task { - detail?: string; - } - - export class CustomExecution2 extends CustomExecution { - /** - * Constructs a CustomExecution task object. The callback will be executed the task is run, at which point the - * extension should return the Pseudoterminal it will "run in". The task should wait to do further execution until - * [Pseudoterminal.open](#Pseudoterminal.open) is called. Task cancellation should be handled using - * [Pseudoterminal.close](#Pseudoterminal.close). When the task is complete fire - * [Pseudoterminal.onDidClose](#Pseudoterminal.onDidClose). - * @param callback The callback that will be called when the task is started by a user. - */ - constructor(callback: (resolvedDefinition?: TaskDefinition) => Thenable); - } - //#endregion - //#region Task presentation group: https://github.com/microsoft/vscode/issues/47265 export interface TaskPresentationOptions { /** @@ -1075,9 +1039,11 @@ declare module 'vscode' { * @param position The position at which the command was invoked. * @param token A cancellation token. * @return A list of ranges that can be live-renamed togehter. The ranges must have - * identical length and contain identical text content. The ranges cannot overlap. + * identical length and contain identical text content. The ranges cannot overlap. Optional a word pattern + * that overrides the word pattern defined when registering the provider. Live rename stops as soon as the renamed content + * no longer matches the word pattern. */ - provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + provideOnTypeRenameRanges(document: TextDocument, position: Position, token: CancellationToken): ProviderResult<{ ranges: Range[]; wordPattern?: RegExp; }>; } namespace languages { @@ -1090,10 +1056,10 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider An on type rename provider. - * @param stopPattern Stop on type renaming when input text matches the regular expression. Defaults to `^\s`. + * @param wordPattern Word pattern for this provider. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ - export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, stopPattern?: RegExp): Disposable; + export function registerOnTypeRenameProvider(selector: DocumentSelector, provider: OnTypeRenameProvider, wordPattern?: RegExp): Disposable; } //#endregion @@ -1281,7 +1247,7 @@ declare module 'vscode' { readonly uri: Uri; readonly cellKind: CellKind; readonly document: TextDocument; - language: string; + readonly language: string; outputs: CellOutput[]; metadata: NotebookCellMetadata; } @@ -1332,11 +1298,12 @@ declare module 'vscode' { export interface NotebookDocument { readonly uri: Uri; + readonly version: number; readonly fileName: string; readonly viewType: string; readonly isDirty: boolean; readonly isUntitled: boolean; - readonly cells: NotebookCell[]; + readonly cells: ReadonlyArray; languages: string[]; displayOrder?: GlobPattern[]; metadata: NotebookDocumentMetadata; @@ -1361,8 +1328,21 @@ declare module 'vscode' { contains(uri: Uri): boolean } + export interface WorkspaceEdit { + replaceCells(uri: Uri, start: number, end: number, cells: NotebookCellData[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellOutput(uri: Uri, index: number, outputs: CellOutput[], metadata?: WorkspaceEditEntryMetadata): void; + replaceCellMetadata(uri: Uri, index: number, cellMetadata: NotebookCellMetadata, metadata?: WorkspaceEditEntryMetadata): void; + } + export interface NotebookEditorCellEdit { + + replaceCells(start: number, end: number, cells: NotebookCellData[]): void; + replaceOutput(index: number, outputs: CellOutput[]): void; + replaceMetadata(index: number, metadata: NotebookCellMetadata): void; + + /** @deprecated */ insert(index: number, content: string | string[], language: string, type: CellKind, outputs: CellOutput[], metadata: NotebookCellMetadata | undefined): void; + /** @deprecated */ delete(index: number): void; } @@ -1380,7 +1360,7 @@ declare module 'vscode' { /** * The column in which this editor shows. */ - viewColumn?: ViewColumn; + readonly viewColumn?: ViewColumn; /** * Whether the panel is active (focused by the user). @@ -1433,31 +1413,6 @@ declare module 'vscode' { outputId: string; } - export interface NotebookOutputRenderer { - /** - * - * @returns HTML fragment. We can probably return `CellOutput` instead of string ? - * - */ - render(document: NotebookDocument, request: NotebookRenderRequest): string; - - /** - * Call before HTML from the renderer is executed, and will be called for - * every editor associated with notebook documents where the renderer - * is or was used. - * - * The communication object will only send and receive messages to the - * render API, retrieved via `acquireNotebookRendererApi`, acquired with - * this specific renderer's ID. - * - * If you need to keep an association between the communication object - * and the document for use in the `render()` method, you can use a WeakMap. - */ - resolveNotebook?(document: NotebookDocument, communication: NotebookCommunication): void; - - readonly preloads?: Uri[]; - } - export interface NotebookCellsChangeData { readonly start: number; readonly deletedCount: number; @@ -1511,9 +1466,9 @@ declare module 'vscode' { export interface NotebookCellData { readonly cellKind: CellKind; readonly source: string; - language: string; - outputs: CellOutput[]; - metadata: NotebookCellMetadata; + readonly language: string; + readonly outputs: CellOutput[]; + readonly metadata: NotebookCellMetadata | undefined; } export interface NotebookData { @@ -1631,8 +1586,6 @@ declare module 'vscode' { saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; readonly onDidChangeNotebook: Event; backupNotebook(document: NotebookDocument, context: NotebookDocumentBackupContext, cancellation: CancellationToken): Promise; - - kernel?: NotebookKernel; } export interface NotebookKernel { @@ -1659,10 +1612,51 @@ declare module 'vscode' { resolveKernel?(kernel: T, document: NotebookDocument, webview: NotebookCommunication, token: CancellationToken): ProviderResult; } + /** + * Represents the alignment of status bar items. + */ + export enum NotebookCellStatusBarAlignment { + + /** + * Aligned to the left side. + */ + Left = 1, + + /** + * Aligned to the right side. + */ + Right = 2 + } + + export interface NotebookCellStatusBarItem { + readonly cell: NotebookCell; + readonly alignment: NotebookCellStatusBarAlignment; + readonly priority?: number; + text: string; + tooltip: string | undefined; + command: string | Command | undefined; + accessibilityInformation?: AccessibilityInformation; + show(): void; + hide(): void; + dispose(): void; + } + export namespace notebook { export function registerNotebookContentProvider( notebookType: string, - provider: NotebookContentProvider + provider: NotebookContentProvider, + options?: { + /** + * Controls if outputs change will trigger notebook document content change and if it will be used in the diff editor + * Default to false. If the content provider doesn't persisit the outputs in the file document, this should be set to true. + */ + 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. + */ + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean } + } ): Disposable; export function registerNotebookKernelProvider( @@ -1676,12 +1670,6 @@ declare module 'vscode' { kernel: NotebookKernel ): Disposable; - export function registerNotebookOutputRenderer( - id: string, - outputSelector: NotebookOutputSelector, - renderer: NotebookOutputRenderer - ): Disposable; - export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; export const onDidSaveNotebookDocument: Event; @@ -1710,6 +1698,17 @@ declare module 'vscode' { export function createConcatTextDocument(notebook: NotebookDocument, selector?: DocumentSelector): NotebookConcatTextDocument; export const onDidChangeActiveNotebookKernel: Event<{ document: NotebookDocument, kernel: NotebookKernel | undefined }>; + + /** + * Creates a notebook cell status bar [item](#NotebookCellStatusBarItem). + * It will be disposed automatically when the notebook document is closed or the cell is deleted. + * + * @param cell The cell on which this item should be shown. + * @param alignment The alignment of the item. + * @param priority The priority of the item. Higher values mean the item should be shown more to the left. + * @return A new status bar item. + */ + export function createCellStatusBarItem(cell: NotebookCell, alignment?: NotebookCellStatusBarAlignment, priority?: number): NotebookCellStatusBarItem; } //#endregion @@ -1949,61 +1948,6 @@ declare module 'vscode' { readonly contextValue?: string; } - //#endregion - //#region https://github.com/microsoft/vscode/issues/101857 - - export interface ExtensionContext { - - /** - * The uri of a directory in which the extension can create log files. - * The directory might not exist on disk and creation is up to the extension. However, - * the parent directory is guaranteed to be existent. - * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from - * an uri. - */ - readonly logUri: Uri; - - /** - * The uri of a workspace specific directory in which the extension - * can store private state. The directory might not exist and creation is - * up to the extension. However, the parent directory is guaranteed to be existent. - * The value is `undefined` when no workspace nor folder has been opened. - * - * Use [`workspaceState`](#ExtensionContext.workspaceState) or - * [`globalState`](#ExtensionContext.globalState) to store key value data. - * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from - * an uri. - */ - readonly storageUri: Uri | undefined; - - /** - * The uri of a directory in which the extension can store global state. - * The directory might not exist on disk and creation is - * up to the extension. However, the parent directory is guaranteed to be existent. - * - * Use [`globalState`](#ExtensionContext.globalState) to store key value data. - * - * @see [`workspace.fs`](#FileSystem) for how to read and write files and folders from - * an uri. - */ - readonly globalStorageUri: Uri; - - /** - * @deprecated Use [logUri](#ExtensionContext.logUri) instead. - */ - readonly logPath: string; - /** - * @deprecated Use [storagePath](#ExtensionContent.storageUri) instead. - */ - readonly storagePath: string | undefined; - /** - * @deprecated Use [globalStoragePath](#ExtensionContent.globalStorageUri) instead. - */ - readonly globalStoragePath: string; - } - //#endregion //#region https://github.com/microsoft/vscode/issues/104436 @@ -2037,4 +1981,141 @@ declare module 'vscode' { notebook: NotebookDocument | undefined; } //#endregion + + + //#region https://github.com/microsoft/vscode/issues/46585 + + /** + * A webview based view. + */ + export interface WebviewView { + /** + * Identifies the type of the webview view, such as `'hexEditor.dataView'`. + */ + readonly viewType: string; + + /** + * The underlying webview for the view. + */ + readonly webview: Webview; + + /** + * View title displayed in the UI. + * + * The view title is initially taken from the extension `package.json` contribution. + */ + title?: string; + + /** + * Event fired when the view is disposed. + * + * Views are disposed of in a few cases: + * + * - When a view is collapsed and `retainContextWhenHidden` has not been set. + * - When a view is hidden by a user. + * + * Trying to use the view after it has been disposed throws an exception. + */ + readonly onDidDispose: Event; + + /** + * Tracks if the webview is currently visible. + * + * Views are visible when they are on the screen and expanded. + */ + readonly visible: boolean; + + /** + * Event fired when the visibility of the view changes + */ + readonly onDidChangeVisibility: Event; + } + + interface WebviewViewResolveContext { + /** + * Persisted state from the webview content. + * + * To save resources, VS Code normally deallocates webview views that are not visible. For example, if the user + * collapse a view or switching to another top level activity, the underlying webview document is deallocates. + * + * You can prevent this behavior by setting `retainContextWhenHidden` in the `WebviewOptions`. However this + * increases resource usage and should be avoided wherever possible. Instead, you can use persisted state to + * save off a webview's state so that it can be quickly recreated as needed. + * + * To save off a persisted state, inside the webview call `acquireVsCodeApi().setState()` with + * any json serializable object. To restore the state again, call `getState()`. For example: + * + * ```js + * // Within the webview + * const vscode = acquireVsCodeApi(); + * + * // Get existing state + * const oldState = vscode.getState() || { value: 0 }; + * + * // Update state + * setState({ value: oldState.value + 1 }) + * ``` + * + * VS Code ensures that the persisted state is saved correctly when a webview is hidden and across + * editor restarts. + */ + readonly state: T | undefined; + } + + /** + * Provider for creating `WebviewView` elements. + */ + export interface WebviewViewProvider { + /** + * Revolves a webview view. + * + * `resolveWebviewView` is called when a view first becomes visible. This may happen when the view is + * first loaded or when the user hides and then shows a view again. + * + * @param webviewView Webview panel to restore. The serializer should take ownership of this panel. The + * provider must set the webview's `.html` and hook up all webview events it is interested in. + * @param context Additional metadata about the view being resolved. + * @param token Cancellation token indicating that the view being provided is no longer needed. + * + * @return Optional thenable indicating that the view has been fully resolved. + */ + resolveWebviewView(webviewView: WebviewView, context: WebviewViewResolveContext, token: CancellationToken): Thenable | void; + } + + namespace window { + /** + * Register a new provider for webview views. + * + * @param viewId Unique id of the view. This should match the `id` from the + * `views` contribution in the package.json. + * @param provider Provider for the webview views. + * + * @return Disposable that unregisters the provider. + */ + export function registerWebviewViewProvider(viewId: string, provider: WebviewViewProvider, options?: { + /** + * Content settings for the webview created for this view. + */ + readonly webviewOptions?: { + /** + * Controls if the webview panel's content (iframe) is kept around even when the panel + * is no longer visible. + * + * Normally the webview's html context is created when the panel becomes visible + * and destroyed when it is hidden. Extensions that have complex state + * or UI can set the `retainContextWhenHidden` to make VS Code keep the webview + * context around, even when the webview moves to a background tab. When a webview using + * `retainContextWhenHidden` becomes hidden, its scripts and other dynamic content are suspended. + * When the panel becomes visible again, the context is automatically restored + * in the exact same state it was in originally. You cannot send messages to a + * hidden webview, even with `retainContextWhenHidden` enabled. + * + * `retainContextWhenHidden` has a high memory overhead and should only be used if + * your panel's context cannot be quickly saved and restored. + */ + readonly retainContextWhenHidden?: boolean; + }; + }): Disposable; + } + //#endregion } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 3d77009b908..bfabf000891 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -54,7 +54,7 @@ import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; import './mainThreadWindow'; -import './mainThreadWebview'; +import './mainThreadWebviewManager'; import './mainThreadWorkspace'; import './mainThreadComments'; import './mainThreadNotebook'; diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 7a9e0fc6a64..fb2e5ddfeda 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -17,7 +17,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { fromNow } from 'vs/base/common/date'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { Platform, platform } from 'vs/base/common/platform'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser']; @@ -249,7 +249,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu } $ensureProvider(id: string): Promise { - return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id)); + return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id), ActivationKind.Immediate); } $sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts similarity index 53% rename from src/vs/workbench/api/browser/mainThreadWebview.ts rename to src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 0d21c18efe8..9bd40eeb40d 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -7,170 +7,61 @@ import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async import { CancellationToken } from 'vs/base/common/cancellation'; import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; -import { isWeb } from 'vs/base/common/platform'; import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources'; -import { escape } from 'vs/base/common/strings'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { localize } from 'vs/nls'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILabelService } from 'vs/platform/label/common/label'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IProductService } from 'vs/platform/product/common/productService'; -import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels'; +import { MainThreadWebviews, reviveWebviewExtension } from 'vs/workbench/api/browser/mainThreadWebviews'; import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; -import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; -import { IEditorInput, IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; -import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { editorGroupToViewColumn } from 'vs/workbench/api/common/shared/editor'; +import { IRevertOptions, ISaveOptions } from 'vs/workbench/common/editor'; import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; import { CustomDocumentBackupData } from 'vs/workbench/contrib/customEditor/browser/customEditorInputFactory'; import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomTextEditorModel } from 'vs/workbench/contrib/customEditor/common/customTextEditorModel'; -import { WebviewExtensionDescription, WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; -import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewWorkbenchService } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; -import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +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 { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -import { extHostNamedCustomer } from '../common/extHostCustomers'; -/** - * Bi-directional map between webview handles and inputs. - */ -class WebviewInputStore { - private readonly _handlesToInputs = new Map(); - private readonly _inputsToHandles = new Map(); - - public add(handle: string, input: WebviewInput): void { - this._handlesToInputs.set(handle, input); - this._inputsToHandles.set(input, handle); - } - - public getHandleForInput(input: WebviewInput): string | undefined { - return this._inputsToHandles.get(input); - } - - public getInputForHandle(handle: string): WebviewInput | undefined { - return this._handlesToInputs.get(handle); - } - - public delete(handle: string): void { - const input = this.getInputForHandle(handle); - this._handlesToInputs.delete(handle); - if (input) { - this._inputsToHandles.delete(input); - } - } - - public get size(): number { - return this._handlesToInputs.size; - } - - [Symbol.iterator](): Iterator { - return this._handlesToInputs.values(); - } -} - -class WebviewViewTypeTransformer { - public constructor( - public readonly prefix: string, - ) { } - - public fromExternal(viewType: string): string { - return this.prefix + viewType; - } - - public toExternal(viewType: string): string | undefined { - return viewType.startsWith(this.prefix) - ? viewType.substr(this.prefix.length) - : undefined; - } -} - -const enum ModelType { +const enum CustomEditorModelType { Custom, Text, } -const webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); +export class MainThreadCustomEditors extends Disposable implements extHostProtocol.MainThreadCustomEditorsShape { -@extHostNamedCustomer(extHostProtocol.MainContext.MainThreadWebviews) -export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { + private readonly _proxyCustomEditors: extHostProtocol.ExtHostCustomEditorsShape; - private static readonly standardSupportedLinkSchemes = new Set([ - Schemas.http, - Schemas.https, - Schemas.mailto, - Schemas.vscode, - 'vscode-insider', - ]); - - private readonly _proxy: extHostProtocol.ExtHostWebviewsShape; - private readonly _webviewInputs = new WebviewInputStore(); - private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); - private readonly _webviewFromDiffEditorHandles = new Set(); constructor( + private readonly mainThreadWebview: MainThreadWebviews, + private readonly mainThreadWebviewPanels: MainThreadWebviewPanels, context: extHostProtocol.IExtHostContext, - @IExtensionService extensionService: IExtensionService, @IWorkingCopyService workingCopyService: IWorkingCopyService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, @ICustomEditorService private readonly _customEditorService: ICustomEditorService, @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @IEditorService private readonly _editorService: IEditorService, - @IOpenerService private readonly _openerService: IOpenerService, - @IProductService private readonly _productService: IProductService, - @ITelemetryService private readonly _telemetryService: ITelemetryService, @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IBackupFileService private readonly _backupService: IBackupFileService, ) { super(); - this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); - - this._register(_editorService.onDidActiveEditorChange(() => { - const activeInput = this._editorService.activeEditor; - if (activeInput instanceof DiffEditorInput && activeInput.primary instanceof WebviewInput && activeInput.secondary instanceof WebviewInput) { - this.registerWebviewFromDiffEditorListeners(activeInput); - } - - this.updateWebviewViewStates(activeInput); - })); - - this._register(_editorService.onDidVisibleEditorsChange(() => { - this.updateWebviewViewStates(this._editorService.activeEditor); - })); - - // This reviver's only job is to activate extensions. - // This should trigger the real reviver to be registered from the extension host side. - this._register(_webviewWorkbenchService.registerResolver({ - canResolve: (webview: WebviewInput) => { - if (webview instanceof CustomEditorInput) { - extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); - return false; - } - - const viewType = webviewPanelViewType.toExternal(webview.viewType); - if (typeof viewType === 'string') { - extensionService.activateByEvent(`onWebviewPanel:${viewType}`); - } - return false; - }, - resolveWebview: () => { throw new Error('not implemented'); } - })); + this._proxyCustomEditors = context.getProxy(extHostProtocol.ExtHostContext.ExtHostCustomEditors); workingCopyFileService.registerWorkingCopyProvider((editorResource) => { const matchedWorkingCopies: IWorkingCopy[] = []; @@ -183,7 +74,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } } return matchedWorkingCopies; - }); } @@ -193,140 +83,21 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma for (const disposable of this._editorProviders.values()) { disposable.dispose(); } + this._editorProviders.clear(); } - public $createWebviewPanel( - extensionData: extHostProtocol.WebviewExtensionDescription, - handle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; }, - options: WebviewInputOptions - ): void { - const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null); - if (showOptions) { - mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus; - mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); - } - - const extension = reviveWebviewExtension(extensionData); - const webview = this._webviewWorkbenchService.createWebview(handle, webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension); - this.hookupWebviewEventDelegate(handle, webview); - - this._webviewInputs.add(handle, webview); - - /* __GDPR__ - "webviews:createWebviewPanel" : { - "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extension.id.value }); - } - - public $disposeWebview(handle: extHostProtocol.WebviewPanelHandle): void { - const webview = this.getWebviewInput(handle); - webview.dispose(); - } - - public $setTitle(handle: extHostProtocol.WebviewPanelHandle, value: string): void { - const webview = this.getWebviewInput(handle); - webview.setName(value); - } - - public $setIconPath(handle: extHostProtocol.WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { - const webview = this.getWebviewInput(handle); - webview.iconPath = reviveWebviewIcon(value); - } - - public $setHtml(handle: extHostProtocol.WebviewPanelHandle, value: string): void { - const webview = this.getWebviewInput(handle); - webview.webview.html = value; - } - - public $setOptions(handle: extHostProtocol.WebviewPanelHandle, options: modes.IWebviewOptions): void { - const webview = this.getWebviewInput(handle); - webview.webview.contentOptions = reviveWebviewOptions(options); - } - - public $reveal(handle: extHostProtocol.WebviewPanelHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void { - const webview = this.getWebviewInput(handle); - if (webview.isDisposed()) { - return; - } - - const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0); - if (targetGroup) { - this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus); - } - } - - public async $postMessage(handle: extHostProtocol.WebviewPanelHandle, message: any): Promise { - const webview = this.getWebviewInput(handle); - webview.webview.postMessage(message); - return true; - } - - public $registerSerializer(viewType: string): void { - if (this._revivers.has(viewType)) { - throw new Error(`Reviver for ${viewType} already registered`); - } - - this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({ - canResolve: (webviewInput) => { - return webviewInput.viewType === webviewPanelViewType.fromExternal(viewType); - }, - resolveWebview: async (webviewInput): Promise => { - const viewType = webviewPanelViewType.toExternal(webviewInput.viewType); - if (!viewType) { - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType); - return; - } - - const handle = webviewInput.id; - this._webviewInputs.add(handle, webviewInput); - this.hookupWebviewEventDelegate(handle, webviewInput); - - let state = undefined; - if (webviewInput.webview.state) { - try { - state = JSON.parse(webviewInput.webview.state); - } catch (e) { - console.error('Could not load webview state', e, webviewInput.webview.state); - } - } - - try { - await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); - } catch (error) { - onUnexpectedError(error); - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType); - } - } - })); - } - - public $unregisterSerializer(viewType: string): void { - const reviver = this._revivers.get(viewType); - if (!reviver) { - throw new Error(`No reviver for ${viewType} registered`); - } - - reviver.dispose(); - this._revivers.delete(viewType); - } - public $registerTextEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities): void { - this.registerEditorProvider(ModelType.Text, extensionData, viewType, options, capabilities, true); + this.registerEditorProvider(CustomEditorModelType.Text, reviveWebviewExtension(extensionData), viewType, options, capabilities, true); } public $registerCustomEditorProvider(extensionData: extHostProtocol.WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void { - this.registerEditorProvider(ModelType.Custom, extensionData, viewType, options, {}, supportsMultipleEditorsPerDocument); + this.registerEditorProvider(CustomEditorModelType.Custom, reviveWebviewExtension(extensionData), viewType, options, {}, supportsMultipleEditorsPerDocument); } private registerEditorProvider( - modelType: ModelType, - extensionData: extHostProtocol.WebviewExtensionDescription, + modelType: CustomEditorModelType, + extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: extHostProtocol.CustomTextEditorCapabilities, @@ -336,8 +107,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma throw new Error(`Provider for ${viewType} already registered`); } - const extension = reviveWebviewExtension(extensionData); - const disposables = new DisposableStore(); disposables.add(this._customEditorService.registerCustomEditorCapabilities(viewType, { @@ -352,8 +121,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma const handle = webviewInput.id; const resource = webviewInput.resource; - this._webviewInputs.add(handle, webviewInput); - this.hookupWebviewEventDelegate(handle, webviewInput); + this.mainThreadWebviewPanels.addWebviewInput(handle, webviewInput); webviewInput.webview.options = options; webviewInput.webview.extension = extension; @@ -362,7 +130,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma modelRef = await this.getOrCreateCustomEditorModel(modelType, resource, viewType, { backupId: webviewInput.backupId }, cancellation); } catch (error) { onUnexpectedError(error); - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType); + webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); return; } @@ -390,16 +158,16 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.onMove(async (newResource: URI) => { const oldModel = modelRef; modelRef = await this.getOrCreateCustomEditorModel(modelType, newResource, viewType, {}, CancellationToken.None); - this._proxy.$onMoveCustomEditor(handle, newResource, viewType); + this._proxyCustomEditors.$onMoveCustomEditor(handle, newResource, viewType); oldModel.dispose(); }); } try { - await this._proxy.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation); + await this._proxyCustomEditors.$resolveWebviewEditor(resource, handle, viewType, webviewInput.getTitle(), editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options, cancellation); } catch (error) { onUnexpectedError(error); - webviewInput.webview.html = MainThreadWebviews.getWebviewResolvedFailedContent(viewType); + webviewInput.webview.html = this.mainThreadWebview.getWebviewResolvedFailedContent(viewType); modelRef.dispose(); return; } @@ -422,7 +190,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } private async getOrCreateCustomEditorModel( - modelType: ModelType, + modelType: CustomEditorModelType, resource: URI, viewType: string, options: { backupId?: string }, @@ -434,15 +202,15 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } switch (modelType) { - case ModelType.Text: + case CustomEditorModelType.Text: { const model = CustomTextEditorModel.create(this._instantiationService, viewType, resource); return this._customEditorService.models.add(resource, viewType, model); } - case ModelType.Custom: + case CustomEditorModelType.Custom: { - const model = MainThreadCustomEditorModel.create(this._instantiationService, this._proxy, viewType, resource, options, () => { - return Array.from(this._webviewInputs) + 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); return this._customEditorService.models.add(resource, viewType, model); @@ -460,112 +228,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma model.changeContent(); } - private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewPanelHandle, input: WebviewInput) { - const disposables = new DisposableStore(); - - disposables.add(input.webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); - disposables.add(input.webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); - disposables.add(input.webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); - - disposables.add(input.webview.onDispose(() => { - disposables.dispose(); - - this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { - this._webviewInputs.delete(handle); - }); - })); - } - - private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void { - const primary = diffEditorInput.primary as WebviewInput; - const secondary = diffEditorInput.secondary as WebviewInput; - - if (this._webviewFromDiffEditorHandles.has(primary.id) || this._webviewFromDiffEditorHandles.has(secondary.id)) { - return; - } - - this._webviewFromDiffEditorHandles.add(primary.id); - this._webviewFromDiffEditorHandles.add(secondary.id); - - const disposables = new DisposableStore(); - disposables.add(primary.webview.onDidFocus(() => this.updateWebviewViewStates(primary))); - disposables.add(secondary.webview.onDidFocus(() => this.updateWebviewViewStates(secondary))); - disposables.add(diffEditorInput.onDispose(() => { - this._webviewFromDiffEditorHandles.delete(primary.id); - this._webviewFromDiffEditorHandles.delete(secondary.id); - dispose(disposables); - })); - } - - private updateWebviewViewStates(activeEditorInput: IEditorInput | undefined) { - if (!this._webviewInputs.size) { - return; - } - - const viewStates: extHostProtocol.WebviewPanelViewStateData = {}; - - const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => { - if (!(editorInput instanceof WebviewInput)) { - return; - } - - editorInput.updateGroup(group.id); - - const handle = this._webviewInputs.getHandleForInput(editorInput); - if (handle) { - viewStates[handle] = { - visible: topLevelInput === group.activeEditor, - active: editorInput === activeEditorInput, - position: editorGroupToViewColumn(this._editorGroupService, group.id), - }; - } - }; - - for (const group of this._editorGroupService.groups) { - for (const input of group.editors) { - if (input instanceof DiffEditorInput) { - updateViewStatesForInput(group, input, input.primary); - updateViewStatesForInput(group, input, input.secondary); - } else { - updateViewStatesForInput(group, input, input); - } - } - } - - if (Object.keys(viewStates).length) { - this._proxy.$onDidChangeWebviewPanelViewStates(viewStates); - } - } - - private onDidClickLink(handle: extHostProtocol.WebviewPanelHandle, link: string): void { - const webview = this.getWebviewInput(handle); - if (this.isSupportedLink(webview, URI.parse(link))) { - this._openerService.open(link, { fromUserGesture: true }); - } - } - - private isSupportedLink(webview: WebviewInput, link: URI): boolean { - if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { - return true; - } - if (!isWeb && this._productService.urlProtocol === link.scheme) { - return true; - } - return !!webview.webview.contentOptions.enableCommandUris && link.scheme === Schemas.command; - } - - private getWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput { - const webview = this.tryGetWebviewInput(handle); - if (!webview) { - throw new Error(`Unknown webview handle:${handle}`); - } - return webview; - } - - private tryGetWebviewInput(handle: extHostProtocol.WebviewPanelHandle): WebviewInput | undefined { - return this._webviewInputs.getInputForHandle(handle); - } - private async getCustomEditorModel(resourceComponents: UriComponents, viewType: string) { const resource = URI.revive(resourceComponents); const model = await this._customEditorService.models.get(resource, viewType); @@ -574,37 +236,6 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } return model; } - - private static getWebviewResolvedFailedContent(viewType: string) { - return ` - - - - - - ${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))} - `; - } -} - -function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription { - return { id: extensionData.id, location: URI.revive(extensionData.location) }; -} - -function reviveWebviewOptions(options: modes.IWebviewOptions): WebviewInputOptions { - return { - ...options, - allowScripts: options.enableScripts, - localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, - }; -} - -function reviveWebviewIcon( - value: { light: UriComponents, dark: UriComponents; } | undefined -): WebviewIcons | undefined { - return value - ? { light: URI.revive(value.light), dark: URI.revive(value.dark) } - : undefined; } namespace HotExitState { @@ -644,7 +275,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod public static async create( instantiationService: IInstantiationService, - proxy: extHostProtocol.ExtHostWebviewsShape, + proxy: extHostProtocol.ExtHostCustomEditorsShape, viewType: string, resource: URI, options: { backupId?: string }, @@ -657,7 +288,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } constructor( - private readonly _proxy: extHostProtocol.ExtHostWebviewsShape, + private readonly _proxy: extHostProtocol.ExtHostCustomEditorsShape, private readonly _viewType: string, private readonly _editorResource: URI, fromBackup: boolean, @@ -712,7 +343,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } public get capabilities(): WorkingCopyCapabilities { - return 0; + return WorkingCopyCapabilities.None; } public isDirty(): boolean { @@ -890,10 +521,9 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod } const remoteAuthority = this._environmentService.configuration.remoteAuthority; - const localResrouce = toLocalResource(this._editorResource, remoteAuthority); + const localResource = toLocalResource(this._editorResource, remoteAuthority); - - return this._fileDialogService.pickFileToSave(localResrouce, options?.availableFileSystems); + return this._fileDialogService.pickFileToSave(localResource, options?.availableFileSystems); } public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 907f2886311..a723b1a38f1 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -264,6 +264,14 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.reject(new Error('debug session not found')); } + public $getDebugProtocolBreakpoint(sessionId: DebugSessionUUID, breakpoinId: string): Promise { + const session = this.debugService.getModel().getSession(sessionId, true); + if (session) { + return Promise.resolve(session.getDebugProtocolBreakpoint(breakpoinId)); + } + return Promise.reject(new Error('debug session not found')); + } + public $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise { if (sessionId) { const session = this.debugService.getModel().getSession(sessionId, true); diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index ed1f0d0efc4..d8e199ac0ad 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -8,7 +8,7 @@ import { disposed } from 'vs/base/common/errors'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { equals as objectEquals } from 'vs/base/common/objects'; import { URI, UriComponents } from 'vs/base/common/uri'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { IRange } from 'vs/editor/common/core/range'; import { ISelection } from 'vs/editor/common/core/selection'; @@ -20,7 +20,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { IOpenerService } from 'vs/platform/opener/common/opener'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, reviveWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, IExtHostContext, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { EditorViewColumn, editorGroupToViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -29,6 +29,26 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { revive } from 'vs/base/common/marshalling'; +import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; + +function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] { + if (!data?.edits) { + return []; + } + + const result: ResourceEdit[] = []; + for (let edit of revive(data).edits) { + if (edit._type === WorkspaceEditType.File) { + result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Text) { + result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } else if (edit._type === WorkspaceEditType.Cell) { + result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata)); + } + } + return result; +} export class MainThreadTextEditors implements MainThreadTextEditorsShape { @@ -222,8 +242,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } $tryApplyWorkspaceEdit(dto: IWorkspaceEditDto): Promise { - const { edits } = reviveWorkspaceEditDto(dto)!; - return this._bulkEditService.apply({ edits }).then(() => true, _err => false); + const edits = reviveWorkspaceEditDto2(dto); + return this._bulkEditService.apply(edits).then(() => true, _err => false); } $tryInsertSnippet(id: string, template: string, ranges: readonly IRange[], opts: IUndoStopOptions): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index f7195eaa219..b3133ba44c9 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -263,12 +263,19 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha // --- on type rename - $registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], stopPattern?: IRegExpDto): void { - const revivedStopPattern = stopPattern ? MainThreadLanguageFeatures._reviveRegExp(stopPattern) : undefined; + $registerOnTypeRenameProvider(handle: number, selector: IDocumentFilterDto[], wordPattern?: IRegExpDto): void { + const revivedWordPattern = wordPattern ? MainThreadLanguageFeatures._reviveRegExp(wordPattern) : undefined; this._registrations.set(handle, modes.OnTypeRenameProviderRegistry.register(selector, { - stopPattern: revivedStopPattern, - provideOnTypeRenameRanges: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { - return this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token); + wordPattern: revivedWordPattern, + provideOnTypeRenameRanges: async (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | undefined> => { + const res = await this._proxy.$provideOnTypeRenameRanges(handle, model.uri, position, token); + if (res) { + return { + ranges: res.ranges, + wordPattern: res.wordPattern ? MainThreadLanguageFeatures._reviveRegExp(res.wordPattern) : undefined + }; + } + return undefined; } })); } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 7fdf21501ff..91d4c528fde 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,61 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta } from '../common/extHost.protocol'; -import { Disposable, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { IRelativePattern } from 'vs/base/common/glob'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { Emitter } from 'vs/base/common/event'; +import { IRelativePattern } from 'vs/base/common/glob'; +import { combinedDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; - -export class MainThreadNotebookDocument extends Disposable { - private _textModel: NotebookTextModel; - - get textModel() { - return this._textModel; - } - - constructor( - private readonly _proxy: ExtHostNotebookShape, - public handle: number, - public viewType: string, - public supportBackup: boolean, - public uri: URI, - @INotebookService readonly notebookService: INotebookService, - @IUndoRedoService readonly undoRedoService: IUndoRedoService, - @ITextModelService modelService: ITextModelService - - ) { - super(); - - this._textModel = new NotebookTextModel(handle, viewType, supportBackup, uri, undoRedoService, modelService); - this._register(this._textModel.onDidModelChangeProxy(e => { - this._proxy.$acceptModelChanged(this.uri, e); - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: { selections: this._textModel.selections }, metadata: null }); - })); - this._register(this._textModel.onDidSelectionChange(e => { - const selectionsChange = e ? { selections: e } : null; - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange, metadata: null }); - })); - } - - dispose() { - // this._textModel.dispose(); - super.dispose(); - } -} +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, DisplayOrderKey, ICellEditOperation, IEditor, INotebookDocumentFilter, INotebookKernelInfo, INotebookKernelInfoDto, NotebookCellMetadata, NotebookCellOutputsSplice, NotebookDocumentMetadata, NOTEBOOK_DISPLAY_ORDER, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { ExtHostContext, ExtHostNotebookShape, IExtHostContext, INotebookCellStatusBarEntryDto, INotebookDocumentsAndEditorsDelta, MainContext, MainThreadNotebookShape, NotebookExtensionDescription } from '../common/extHost.protocol'; class DocumentAndEditorState { static ofSets(before: Set, after: Set): { removed: T[], added: T[] } { @@ -171,11 +133,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo private readonly _notebookProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map, provider: IDisposable }>(); - private readonly _notebookRenderers = new Map(); private readonly _proxy: ExtHostNotebookShape; private _toDisposeOnEditorRemove = new Map(); private _currentState?: DocumentAndEditorState; private _editorEventListenersMapping: Map = new Map(); + private readonly _cellStatusBarEntries: Map = new Map(); constructor( extHostContext: IExtHostContext, @@ -183,19 +145,19 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @ILogService private readonly logService: ILogService - + @ILogService private readonly logService: ILogService, + @INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook); this.registerListeners(); } - async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise { + async $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise { const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - await this._notebookService.transformEditsOutputs(textModel, edits); - return textModel.$applyEdit(modelVersionId, edits, true); + this._notebookService.transformEditsOutputs(textModel, edits); + return textModel.applyEdit(modelVersionId, edits, true); } return false; @@ -312,7 +274,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo })); const updateOrder = () => { - let userOrder = this.configurationService.getValue('notebook.displayOrder'); + let userOrder = this.configurationService.getValue(DisplayOrderKey); this._proxy.$acceptDisplayOrder({ defaultOrder: this.accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, userOrder: userOrder @@ -322,7 +284,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo updateOrder(); this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) { + if (e.affectedKeys.indexOf(DisplayOrderKey) >= 0) { updateOrder(); } })); @@ -414,26 +376,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo // } } - async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise { - const staticContribution = this._notebookService.getContributedNotebookOutputRenderers(type); - - if (!staticContribution) { - throw new Error(`Notebook renderer for '${type}' is not statically registered.`); - } - - const renderer = new MainThreadNotebookRenderer(this._proxy, type, staticContribution.displayName, extension.id, URI.revive(extension.location), selectors, preloads.map(uri => URI.revive(uri))); - this._notebookRenderers.set(type, renderer); - this._notebookService.registerNotebookRenderer(type, renderer); - } - - async $unregisterNotebookRenderer(id: string): Promise { - this._notebookService.unregisterNotebookRenderer(id); - } - - async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined): Promise { + async $registerNotebookProvider(_extension: NotebookExtensionDescription, _viewType: string, _supportBackup: boolean, _kernel: INotebookKernelInfoDto | undefined, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise { const controller: IMainNotebookController = { kernel: _kernel, supportBackup: _supportBackup, + options: options, reloadNotebook: async (mainthreadTextModel: NotebookTextModel) => { const data = await this._proxy.$resolveNotebookData(_viewType, mainthreadTextModel.uri); if (!data) { @@ -442,16 +389,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo mainthreadTextModel.languages = data.languages; mainthreadTextModel.metadata = data.metadata; + mainthreadTextModel.transientOptions = options; const edits: ICellEditOperation[] = [ - { editType: CellEditType.Delete, count: mainthreadTextModel.cells.length, index: 0 }, - { editType: CellEditType.Insert, index: 0, cells: data.cells } + { editType: CellEditType.Replace, index: 0, count: mainthreadTextModel.cells.length, cells: data.cells } ]; - await this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); + this._notebookService.transformEditsOutputs(mainthreadTextModel, edits); await new Promise(resolve => { DOM.scheduleAtNextAnimationFrame(() => { - const ret = mainthreadTextModel!.$applyEdit(mainthreadTextModel!.versionId, edits, true); + const ret = mainthreadTextModel!.applyEdit(mainthreadTextModel!.versionId, edits, true); resolve(ret); }); }); @@ -465,11 +412,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo textModel.languages = data.languages; textModel.metadata = data.metadata; + textModel.transientOptions = options; if (data.cells.length) { textModel.initialize(data!.cells); } else { - const mainCell = textModel.createCellTextModel([''], textModel.languages.length ? textModel.languages[0] : '', CellKind.Code, [], undefined); + const mainCell = textModel.createCellTextModel('', textModel.languages.length ? textModel.languages[0] : '', CellKind.Code, [], undefined); textModel.insertTemplateCell(mainCell); } @@ -606,16 +554,16 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata): Promise { this.logService.debug('MainThreadNotebooks#updateNotebookCellMetadata', resource.path, handle, metadata); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); - textModel?.updateNotebookCellMetadata(handle, metadata); + textModel?.changeCellMetadata(handle, metadata, true); } - async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { + async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise { this.logService.debug('MainThreadNotebooks#spliceNotebookCellOutputs', resource.path, cellHandle); const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - await this._notebookService.transformSpliceOutputs(textModel, splices); - textModel.$spliceNotebookCellOutputs(cellHandle, splices); + this._notebookService.transformSpliceOutputs(textModel, splices); + textModel.spliceNotebookCellOutputs(cellHandle, splices); } } @@ -643,7 +591,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); if (textModel) { - textModel.$handleEdit(label, () => { + textModel.handleEdit(label, () => { return this._proxy.$undoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); }, () => { return this._proxy.$redoNotebook(textModel.viewType, textModel.uri, editId, textModel.isDirty); @@ -655,6 +603,24 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo const textModel = this._notebookService.getNotebookTextModel(URI.from(resource)); textModel?.handleUnknownChange(); } + + async $setStatusBarEntry(id: number, rawStatusBarEntry: INotebookCellStatusBarEntryDto): Promise { + const statusBarEntry = { + ...rawStatusBarEntry, + ...{ cellResource: URI.revive(rawStatusBarEntry.cellResource) } + }; + + const existingEntry = this._cellStatusBarEntries.get(id); + if (existingEntry) { + existingEntry.dispose(); + } + + if (statusBarEntry.visible) { + this._cellStatusBarEntries.set( + id, + this.cellStatusBarService.addEntry(statusBarEntry)); + } + } } export class MainThreadNotebookKernel implements INotebookKernelInfo { @@ -675,25 +641,3 @@ export class MainThreadNotebookKernel implements INotebookKernelInfo { return this._proxy.$executeNotebook2(this.id, viewType, uri, handle); } } - -export class MainThreadNotebookRenderer implements INotebookRendererInfo { - constructor( - private readonly _proxy: ExtHostNotebookShape, - readonly id: string, - public displayName: string, - readonly extensionId: ExtensionIdentifier, - readonly extensionLocation: URI, - readonly selectors: INotebookMimeTypeSelector, - readonly preloads: URI[], - ) { - - } - - render(uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return this._proxy.$renderOutputs(uri, this.id, request); - } - - render2(uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return this._proxy.$renderOutputs2(uri, this.id, request); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 586acc3ab8d..350ce8e8753 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -414,10 +414,18 @@ export class MainThreadTask implements MainThreadTaskShape { ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTask); this._providers = new Map(); - this._taskService.onDidStateChange((event: TaskEvent) => { + this._taskService.onDidStateChange(async (event: TaskEvent) => { const task = event.__task!; if (event.kind === TaskEventKind.Start) { - this._proxy.$onDidStartTask(TaskExecutionDTO.from(task.getTaskExecution()), event.terminalId!); + const execution = TaskExecutionDTO.from(task.getTaskExecution()); + let resolvedDefinition: TaskDefinitionDTO = execution.task!.definition; + if (execution.task?.execution && CustomExecutionDTO.is(execution.task.execution) && event.resolvedVariables) { + const dictionary: IStringDictionary = {}; + Array.from(event.resolvedVariables.entries()).forEach(entry => dictionary[entry[0]] = entry[1]); + resolvedDefinition = await this._configurationResolverService.resolveAny(task.getWorkspaceFolder(), + execution.task.definition, dictionary); + } + this._proxy.$onDidStartTask(execution, event.terminalId!, resolvedDefinition); } else if (event.kind === TaskEventKind.ProcessStarted) { this._proxy.$onDidStartTaskProcess(TaskProcessStartedDTO.from(task.getTaskExecution(), event.processId!)); } else if (event.kind === TaskEventKind.ProcessEnded) { diff --git a/src/vs/workbench/api/browser/mainThreadWebviewManager.ts b/src/vs/workbench/api/browser/mainThreadWebviewManager.ts new file mode 100644 index 00000000000..abe953dd010 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewManager.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { MainThreadCustomEditors } from 'vs/workbench/api/browser/mainThreadCustomEditors'; +import { MainThreadWebviewPanels } from 'vs/workbench/api/browser/mainThreadWebviewPanels'; +import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; +import { MainThreadWebviewsViews } from 'vs/workbench/api/browser/mainThreadWebviewViews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { extHostCustomer } from '../common/extHostCustomers'; + +@extHostCustomer +export class MainThreadWebviewManager extends Disposable { + constructor( + context: extHostProtocol.IExtHostContext, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + + const webviews = this._register(instantiationService.createInstance(MainThreadWebviews, context)); + context.set(extHostProtocol.MainContext.MainThreadWebviews, webviews); + + const webviewPanels = this._register(instantiationService.createInstance(MainThreadWebviewPanels, webviews, context)); + context.set(extHostProtocol.MainContext.MainThreadWebviewPanels, webviewPanels); + + const customEditors = this._register(instantiationService.createInstance(MainThreadCustomEditors, webviews, webviewPanels, context)); + context.set(extHostProtocol.MainContext.MainThreadCustomEditors, customEditors); + + const webviewViews = this._register(instantiationService.createInstance(MainThreadWebviewsViews, webviews, context)); + context.set(extHostProtocol.MainContext.MainThreadWebviewViews, webviewViews); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts new file mode 100644 index 00000000000..b089b04b6dd --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewPanels.ts @@ -0,0 +1,344 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable, DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { MainThreadWebviews, reviveWebviewExtension, reviveWebviewOptions } from 'vs/workbench/api/browser/mainThreadWebviews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { IEditorInput } from 'vs/workbench/common/editor'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { CustomEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; +import { WebviewIcons } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; +import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; +import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +/** + * Bi-directional map between webview handles and inputs. + */ +class WebviewInputStore { + private readonly _handlesToInputs = new Map(); + private readonly _inputsToHandles = new Map(); + + public add(handle: string, input: WebviewInput): void { + this._handlesToInputs.set(handle, input); + this._inputsToHandles.set(input, handle); + } + + public getHandleForInput(input: WebviewInput): string | undefined { + return this._inputsToHandles.get(input); + } + + public getInputForHandle(handle: string): WebviewInput | undefined { + return this._handlesToInputs.get(handle); + } + + public delete(handle: string): void { + const input = this.getInputForHandle(handle); + this._handlesToInputs.delete(handle); + if (input) { + this._inputsToHandles.delete(input); + } + } + + public get size(): number { + return this._handlesToInputs.size; + } + + [Symbol.iterator](): Iterator { + return this._handlesToInputs.values(); + } +} + +class WebviewViewTypeTransformer { + public constructor( + public readonly prefix: string, + ) { } + + public fromExternal(viewType: string): string { + return this.prefix + viewType; + } + + public toExternal(viewType: string): string | undefined { + return viewType.startsWith(this.prefix) + ? viewType.substr(this.prefix.length) + : undefined; + } +} + +export class MainThreadWebviewPanels extends Disposable implements extHostProtocol.MainThreadWebviewPanelsShape { + + private readonly webviewPanelViewType = new WebviewViewTypeTransformer('mainThreadWebview-'); + + private readonly _proxy: extHostProtocol.ExtHostWebviewPanelsShape; + + private readonly _webviewInputs = new WebviewInputStore(); + + private readonly _editorProviders = new Map(); + private readonly _webviewFromDiffEditorHandles = new Set(); + + private readonly _revivers = new Map(); + + constructor( + private readonly _mainThreadWebviews: MainThreadWebviews, + context: extHostProtocol.IExtHostContext, + @IExtensionService extensionService: IExtensionService, + @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, + @IEditorService private readonly _editorService: IEditorService, + @ITelemetryService private readonly _telemetryService: ITelemetryService, + @IWebviewWorkbenchService private readonly _webviewWorkbenchService: IWebviewWorkbenchService, + ) { + super(); + + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewPanels); + + this._register(_editorService.onDidActiveEditorChange(() => { + const activeInput = this._editorService.activeEditor; + if (activeInput instanceof DiffEditorInput && activeInput.primary instanceof WebviewInput && activeInput.secondary instanceof WebviewInput) { + this.registerWebviewFromDiffEditorListeners(activeInput); + } + + this.updateWebviewViewStates(activeInput); + })); + + this._register(_editorService.onDidVisibleEditorsChange(() => { + this.updateWebviewViewStates(this._editorService.activeEditor); + })); + + // This reviver's only job is to activate extensions. + // This should trigger the real reviver to be registered from the extension host side. + this._register(_webviewWorkbenchService.registerResolver({ + canResolve: (webview: WebviewInput) => { + if (webview instanceof CustomEditorInput) { + extensionService.activateByEvent(`onCustomEditor:${webview.viewType}`); + return false; + } + + const viewType = this.webviewPanelViewType.toExternal(webview.viewType); + if (typeof viewType === 'string') { + extensionService.activateByEvent(`onWebviewPanel:${viewType}`); + } + return false; + }, + resolveWebview: () => { throw new Error('not implemented'); } + })); + } + + dispose() { + super.dispose(); + + for (const disposable of this._editorProviders.values()) { + disposable.dispose(); + } + this._editorProviders.clear(); + } + + public get webviewInputs(): Iterable { return this._webviewInputs; } + + public addWebviewInput(handle: extHostProtocol.WebviewHandle, input: WebviewInput): void { + this._webviewInputs.add(handle, input); + this._mainThreadWebviews.addWebview(handle, input.webview); + + input.webview.onDispose(() => { + this._proxy.$onDidDisposeWebviewPanel(handle).finally(() => { + this._webviewInputs.delete(handle); + }); + }); + } + + public $createWebviewPanel( + extensionData: extHostProtocol.WebviewExtensionDescription, + handle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + showOptions: { viewColumn?: EditorViewColumn, preserveFocus?: boolean; }, + options: WebviewInputOptions + ): void { + const mainThreadShowOptions: ICreateWebViewShowOptions = Object.create(null); + if (showOptions) { + mainThreadShowOptions.preserveFocus = !!showOptions.preserveFocus; + mainThreadShowOptions.group = viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn); + } + + const extension = reviveWebviewExtension(extensionData); + + const webview = this._webviewWorkbenchService.createWebview(handle, this.webviewPanelViewType.fromExternal(viewType), title, mainThreadShowOptions, reviveWebviewOptions(options), extension); + this.addWebviewInput(handle, webview); + + /* __GDPR__ + "webviews:createWebviewPanel" : { + "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._telemetryService.publicLog('webviews:createWebviewPanel', { extensionId: extension.id.value }); + } + + public $disposeWebview(handle: extHostProtocol.WebviewHandle): void { + const webview = this.getWebviewInput(handle); + webview.dispose(); + } + + public $setTitle(handle: extHostProtocol.WebviewHandle, value: string): void { + const webview = this.getWebviewInput(handle); + webview.setName(value); + } + + + public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void { + const webview = this.getWebviewInput(handle); + webview.iconPath = reviveWebviewIcon(value); + } + + public $reveal(handle: extHostProtocol.WebviewHandle, showOptions: extHostProtocol.WebviewPanelShowOptions): void { + const webview = this.getWebviewInput(handle); + if (webview.isDisposed()) { + return; + } + + const targetGroup = this._editorGroupService.getGroup(viewColumnToEditorGroup(this._editorGroupService, showOptions.viewColumn)) || this._editorGroupService.getGroup(webview.group || 0); + if (targetGroup) { + this._webviewWorkbenchService.revealWebview(webview, targetGroup, !!showOptions.preserveFocus); + } + } + + public $registerSerializer(viewType: string) + : void { + if (this._revivers.has(viewType)) { + throw new Error(`Reviver for ${viewType} already registered`); + } + + this._revivers.set(viewType, this._webviewWorkbenchService.registerResolver({ + canResolve: (webviewInput) => { + return webviewInput.viewType === this.webviewPanelViewType.fromExternal(viewType); + }, + resolveWebview: async (webviewInput): Promise => { + const viewType = this.webviewPanelViewType.toExternal(webviewInput.viewType); + if (!viewType) { + webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(webviewInput.viewType); + return; + } + + + const handle = webviewInput.id; + + this.addWebviewInput(handle, webviewInput); + + let state = undefined; + if (webviewInput.webview.state) { + try { + state = JSON.parse(webviewInput.webview.state); + } catch (e) { + console.error('Could not load webview state', e, webviewInput.webview.state); + } + } + + try { + await this._proxy.$deserializeWebviewPanel(handle, viewType, webviewInput.getTitle(), state, editorGroupToViewColumn(this._editorGroupService, webviewInput.group || 0), webviewInput.webview.options); + } catch (error) { + onUnexpectedError(error); + webviewInput.webview.html = this._mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + } + } + })); + } + + public $unregisterSerializer(viewType: string): void { + const reviver = this._revivers.get(viewType); + if (!reviver) { + throw new Error(`No reviver for ${viewType} registered`); + } + + reviver.dispose(); + this._revivers.delete(viewType); + } + + private registerWebviewFromDiffEditorListeners(diffEditorInput: DiffEditorInput): void { + const primary = diffEditorInput.primary as WebviewInput; + const secondary = diffEditorInput.secondary as WebviewInput; + + if (this._webviewFromDiffEditorHandles.has(primary.id) || this._webviewFromDiffEditorHandles.has(secondary.id)) { + return; + } + + this._webviewFromDiffEditorHandles.add(primary.id); + this._webviewFromDiffEditorHandles.add(secondary.id); + + const disposables = new DisposableStore(); + disposables.add(primary.webview.onDidFocus(() => this.updateWebviewViewStates(primary))); + disposables.add(secondary.webview.onDidFocus(() => this.updateWebviewViewStates(secondary))); + disposables.add(diffEditorInput.onDispose(() => { + this._webviewFromDiffEditorHandles.delete(primary.id); + this._webviewFromDiffEditorHandles.delete(secondary.id); + dispose(disposables); + })); + } + + private updateWebviewViewStates(activeEditorInput: IEditorInput | undefined) { + if (!this._webviewInputs.size) { + return; + } + + const viewStates: extHostProtocol.WebviewPanelViewStateData = {}; + + const updateViewStatesForInput = (group: IEditorGroup, topLevelInput: IEditorInput, editorInput: IEditorInput) => { + if (!(editorInput instanceof WebviewInput)) { + return; + } + + editorInput.updateGroup(group.id); + + const handle = this._webviewInputs.getHandleForInput(editorInput); + if (handle) { + viewStates[handle] = { + visible: topLevelInput === group.activeEditor, + active: editorInput === activeEditorInput, + position: editorGroupToViewColumn(this._editorGroupService, group.id), + }; + } + }; + + for (const group of this._editorGroupService.groups) { + for (const input of group.editors) { + if (input instanceof DiffEditorInput) { + updateViewStatesForInput(group, input, input.primary); + updateViewStatesForInput(group, input, input.secondary); + } else { + updateViewStatesForInput(group, input, input); + } + } + } + + if (Object.keys(viewStates).length) { + this._proxy.$onDidChangeWebviewPanelViewStates(viewStates); + } + } + + private getWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput { + const webview = this.tryGetWebviewInput(handle); + if (!webview) { + throw new Error(`Unknown webview handle:${handle}`); + } + return webview; + } + + private tryGetWebviewInput(handle: extHostProtocol.WebviewHandle): WebviewInput | undefined { + return this._webviewInputs.getInputForHandle(handle); + } +} + + +function reviveWebviewIcon( + value: { light: UriComponents, dark: UriComponents; } | undefined +): WebviewIcons | undefined { + return value + ? { light: URI.revive(value.light), dark: URI.revive(value.dark) } + : undefined; +} + diff --git a/src/vs/workbench/api/browser/mainThreadWebviewViews.ts b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts new file mode 100644 index 00000000000..df46e3c6e09 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviewViews.ts @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebviews'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { IWebviewViewService, WebviewView } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; + + +export class MainThreadWebviewsViews extends Disposable implements extHostProtocol.MainThreadWebviewViewsShape { + + private readonly _proxyViews: extHostProtocol.ExtHostWebviewViewsShape; + + private readonly _webviewViews = new Map(); + private readonly _webviewViewProviders = new Map(); + + constructor( + private readonly mainThreadWebviews: MainThreadWebviews, + context: extHostProtocol.IExtHostContext, + @IWebviewViewService private readonly _webviewViewService: IWebviewViewService, + ) { + super(); + + this._proxyViews = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviewViews); + } + + public $setWebviewViewTitle(handle: extHostProtocol.WebviewHandle, value: string | undefined): void { + const webviewView = this._webviewViews.get(handle); + if (!webviewView) { + throw new Error('unknown webview view'); + } + webviewView.title = value; + } + + public $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void { + if (this._webviewViewProviders.has(viewType)) { + throw new Error(`View provider for ${viewType} already registered`); + } + + this._webviewViewService.register(viewType, { + resolve: async (webviewView: WebviewView, cancellation: CancellationToken) => { + const handle = webviewView.webview.id; + + this._webviewViews.set(handle, webviewView); + this.mainThreadWebviews.addWebview(handle, webviewView.webview); + + let state = undefined; + if (webviewView.webview.state) { + try { + state = JSON.parse(webviewView.webview.state); + } catch (e) { + console.error('Could not load webview state', e, webviewView.webview.state); + } + } + + if (options) { + webviewView.webview.options = options; + } + + webviewView.onDidChangeVisibility(visible => { + this._proxyViews.$onDidChangeWebviewViewVisibility(handle, visible); + }); + + webviewView.onDispose(() => { + this._proxyViews.$disposeWebviewView(handle); + this._webviewViews.delete(handle); + }); + + try { + await this._proxyViews.$resolveWebviewView(handle, viewType, state, cancellation); + } catch (error) { + onUnexpectedError(error); + webviewView.webview.html = this.mainThreadWebviews.getWebviewResolvedFailedContent(viewType); + } + } + }); + } + + public $unregisterWebviewViewProvider(viewType: string): void { + const provider = this._webviewViewProviders.get(viewType); + if (!provider) { + throw new Error(`No view provider for ${viewType} registered`); + } + + provider.dispose(); + this._webviewViewProviders.delete(viewType); + } +} + diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts new file mode 100644 index 00000000000..8180b9972ee --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -0,0 +1,125 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isWeb } from 'vs/base/common/platform'; +import { escape } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { IWebviewOptions } from 'vs/editor/common/modes'; +import { localize } from 'vs/nls'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProductService } from 'vs/platform/product/common/productService'; +import * as extHostProtocol from 'vs/workbench/api/common/extHost.protocol'; +import { Webview, WebviewExtensionDescription, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; + +export class MainThreadWebviews extends Disposable implements extHostProtocol.MainThreadWebviewsShape { + + private static readonly standardSupportedLinkSchemes = new Set([ + Schemas.http, + Schemas.https, + Schemas.mailto, + Schemas.vscode, + 'vscode-insider', + ]); + + private readonly _proxy: extHostProtocol.ExtHostWebviewsShape; + + private readonly _webviews = new Map(); + + constructor( + context: extHostProtocol.IExtHostContext, + @IOpenerService private readonly _openerService: IOpenerService, + @IProductService private readonly _productService: IProductService, + ) { + super(); + + this._proxy = context.getProxy(extHostProtocol.ExtHostContext.ExtHostWebviews); + } + + public addWebview(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay): void { + this._webviews.set(handle, webview); + this.hookupWebviewEventDelegate(handle, webview); + } + + public $setHtml(handle: extHostProtocol.WebviewHandle, value: string): void { + const webview = this.getWebview(handle); + webview.html = value; + } + + public $setOptions(handle: extHostProtocol.WebviewHandle, options: IWebviewOptions): void { + const webview = this.getWebview(handle); + webview.contentOptions = reviveWebviewOptions(options); + } + + public async $postMessage(handle: extHostProtocol.WebviewHandle, message: any): Promise { + const webview = this.getWebview(handle); + webview.postMessage(message); + return true; + } + + private hookupWebviewEventDelegate(handle: extHostProtocol.WebviewHandle, webview: WebviewOverlay) { + const disposables = new DisposableStore(); + + disposables.add(webview.onDidClickLink((uri) => this.onDidClickLink(handle, uri))); + disposables.add(webview.onMessage((message: any) => { this._proxy.$onMessage(handle, message); })); + disposables.add(webview.onMissingCsp((extension: ExtensionIdentifier) => this._proxy.$onMissingCsp(handle, extension.value))); + + disposables.add(webview.onDispose(() => { + disposables.dispose(); + this._webviews.delete(handle); + })); + } + + private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void { + const webview = this.getWebview(handle); + if (this.isSupportedLink(webview, URI.parse(link))) { + this._openerService.open(link, { fromUserGesture: true }); + } + } + + private isSupportedLink(webview: Webview, link: URI): boolean { + if (MainThreadWebviews.standardSupportedLinkSchemes.has(link.scheme)) { + return true; + } + if (!isWeb && this._productService.urlProtocol === link.scheme) { + return true; + } + return !!webview.contentOptions.enableCommandUris && link.scheme === Schemas.command; + } + + private getWebview(handle: extHostProtocol.WebviewHandle): Webview { + const webview = this._webviews.get(handle); + if (!webview) { + throw new Error(`Unknown webview handle:${handle}`); + } + return webview; + } + + public getWebviewResolvedFailedContent(viewType: string) { + return ` + + + + + + ${localize('errorMessage', "An error occurred while loading view: {0}", escape(viewType))} + `; + } +} + +export function reviveWebviewExtension(extensionData: extHostProtocol.WebviewExtensionDescription): WebviewExtensionDescription { + return { id: extensionData.id, location: URI.revive(extensionData.location) }; +} + +export function reviveWebviewOptions(options: IWebviewOptions): WebviewInputOptions { + return { + ...options, + allowScripts: options.enableScripts, + localResourceRoots: Array.isArray(options.localResourceRoots) ? options.localResourceRoots.map(r => URI.revive(r)) : undefined, + }; +} diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 25dadd40aab..73ca03673bc 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -32,6 +32,7 @@ import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneCont import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Codicon } from 'vs/base/common/codicons'; import { CustomTreeView } from 'vs/workbench/contrib/views/browser/treeView'; +import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane'; export interface IUserFriendlyViewsContainerDescriptor { id: string; @@ -76,7 +77,15 @@ export const viewsContainersContribution: IJSONSchema = { } }; +enum ViewType { + Tree = 'tree', + Webview = 'webview' +} + + interface IUserFriendlyViewDescriptor { + type?: ViewType; + id: string; name: string; when?: string; @@ -208,11 +217,18 @@ const viewsContribution: IJSONSchema = { } }; -export interface ICustomViewDescriptor extends ITreeViewDescriptor { +export interface ICustomTreeViewDescriptor extends ITreeViewDescriptor { readonly extensionId: ExtensionIdentifier; readonly originalContainerId: string; } +export interface ICustomWebviewViewDescriptor extends IViewDescriptor { + readonly extensionId: ExtensionIdentifier; + readonly originalContainerId: string; +} + +export type ICustomViewDescriptor = ICustomTreeViewDescriptor | ICustomWebviewViewDescriptor; + type ViewContainerExtensionPointType = { [loc: string]: IUserFriendlyViewsContainerDescriptor[] }; const viewsContainersExtensionPoint: IExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ extensionPoint: 'viewsContainers', @@ -442,16 +458,24 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined; const initialVisibility = this.convertInitialVisibility(item.visibility); - const viewDescriptor = { + + const type = this.getViewType(item.type); + if (!type) { + collector.error(localize('unknownViewType', "Unknown view type `{0}`.", item.type)); + return null; + } + + const viewDescriptor = { + type: type, + ctorDescriptor: type === ViewType.Tree ? new SyncDescriptor(TreeViewPane) : new SyncDescriptor(WebviewViewPane), id: item.id, name: item.name, - ctorDescriptor: new SyncDescriptor(TreeViewPane), when: ContextKeyExpr.deserialize(item.when), containerIcon: icon || viewContainer?.icon, containerTitle: item.contextualTitle || viewContainer?.name, canToggleVisibility: true, canMoveView: true, - treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name), + treeView: type === ViewType.Tree ? this.instantiationService.createInstance(CustomTreeView, item.id, item.name) : undefined, collapsed: this.showCollapsed(container) || initialVisibility === InitialVisibility.Collapsed, order: order, extensionId: extension.description.identifier, @@ -461,6 +485,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution { hideByDefault: initialVisibility === InitialVisibility.Hidden }; + viewIds.add(viewDescriptor.id); return viewDescriptor; })); @@ -473,6 +498,16 @@ class ViewsExtensionHandler implements IWorkbenchContribution { this.viewsRegistry.registerViews2(allViewDescriptors); } + private getViewType(type: string | undefined): ViewType | undefined { + if (type === ViewType.Webview) { + return ViewType.Webview; + } + if (!type || type === ViewType.Tree) { + return ViewType.Tree; + } + return undefined; + } + private getDefaultViewContainer(): ViewContainer { return this.viewContainersRegistry.get(EXPLORER)!; } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 667fa4c6fa9..1a7edca9370 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -76,6 +76,9 @@ import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline'; import { ExtHostNotebookConcatDocument } from 'vs/workbench/api/common/extHostNotebookConcatDocument'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer'; +import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView'; +import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; export interface IExtensionApiFactory { (extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode; @@ -125,7 +128,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors)); const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService)); const extHostDocumentSaveParticipant = rpcProtocol.set(ExtHostContext.ExtHostDocumentSaveParticipant, new ExtHostDocumentSaveParticipant(extHostLogService, extHostDocuments, rpcProtocol.getProxy(MainContext.MainThreadTextEditors))); - const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService, extensionStoragePaths)); + const extHostEditors = rpcProtocol.set(ExtHostContext.ExtHostEditors, new ExtHostEditors(rpcProtocol, extHostDocumentsAndEditors, extHostNotebook)); const extHostTreeViews = rpcProtocol.set(ExtHostContext.ExtHostTreeViews, new ExtHostTreeViews(rpcProtocol.getProxy(MainContext.MainThreadTreeViews), extHostCommands, extHostLogService)); const extHostEditorInsets = rpcProtocol.set(ExtHostContext.ExtHostEditorInsets, new ExtHostEditorInsets(rpcProtocol.getProxy(MainContext.MainThreadEditorInsets), extHostEditors, initData.environment)); const extHostDiagnostics = rpcProtocol.set(ExtHostContext.ExtHostDiagnostics, new ExtHostDiagnostics(rpcProtocol, extHostLogService)); @@ -137,11 +141,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); - const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, initData.uiKind === UIKind.Web ? new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService) : new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment, extHostLogService, extensionStoragePaths)); const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); const extHostAuthentication = rpcProtocol.set(ExtHostContext.ExtHostAuthentication, new ExtHostAuthentication(rpcProtocol)); const extHostTimeline = rpcProtocol.set(ExtHostContext.ExtHostTimeline, new ExtHostTimeline(rpcProtocol, extHostCommands)); - const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation, extHostDocuments, extensionStoragePaths)); + const extHostWebviews = rpcProtocol.set(ExtHostContext.ExtHostWebviews, new ExtHostWebviews(rpcProtocol, initData.environment, extHostWorkspace, extHostLogService, extHostApiDeprecation)); + const extHostWebviewPanels = rpcProtocol.set(ExtHostContext.ExtHostWebviewPanels, new ExtHostWebviewPanels(rpcProtocol, extHostWebviews, extHostWorkspace)); + const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels)); + const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -564,7 +570,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostOutputService.createOutputChannel(name); }, createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options?: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { - return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); + return extHostWebviewPanels.createWebviewPanel(extension, viewType, title, showOptions, options); }, createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset { checkProposedApiEnabled(extension); @@ -589,10 +595,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I return extHostTreeViews.createTreeView(viewId, options, extension); }, registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { - return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); + return extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializer); }, registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => { - return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); + return extHostCustomEditors.registerCustomEditorProvider(extension, viewType, provider, options); }, registerDecorationProvider(provider: vscode.DecorationProvider) { checkProposedApiEnabled(extension); @@ -612,6 +618,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, onDidChangeActiveColorTheme(listener, thisArg?, disposables?) { return extHostTheming.onDidChangeActiveColorTheme(listener, thisArg, disposables); + }, + registerWebviewViewProvider(viewId: string, provider: vscode.WebviewViewProvider, options?: { + webviewOptions?: { + retainContextWhenHidden?: boolean + } + }) { + checkProposedApiEnabled(extension); + return extHostWebviewViews.registerWebviewViewProvider(extension, viewId, provider, options?.webviewOptions); } }; @@ -922,7 +936,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, get notebookDocuments(): vscode.NotebookDocument[] { checkProposedApiEnabled(extension); - return extHostNotebook.notebookDocuments; + return extHostNotebook.notebookDocuments.map(d => d.notebookDocument); }, get visibleNotebookEditors() { checkProposedApiEnabled(extension); @@ -936,9 +950,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidChangeActiveNotebookKernel; }, - registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { + registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider, options?: { + transientOutputs: boolean; + transientMetadata: { [K in keyof vscode.NotebookCellMetadata]?: boolean } + }) => { checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); + return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider, options); }, registerNotebookKernel: (id: string, selector: vscode.GlobPattern[], kernel: vscode.NotebookKernel) => { checkProposedApiEnabled(extension); @@ -948,10 +965,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookKernelProvider(extension, selector, provider); }, - registerNotebookOutputRenderer: (type: string, outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => { - checkProposedApiEnabled(extension); - return extHostNotebook.registerNotebookOutputRenderer(type, extension, outputFilter, renderer); - }, get activeNotebookEditor(): vscode.NotebookEditor | undefined { checkProposedApiEnabled(extension); return extHostNotebook.activeNotebookEditor; @@ -979,6 +992,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I createConcatTextDocument(notebook, selector) { checkProposedApiEnabled(extension); return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector); + }, + createCellStatusBarItem(cell: vscode.NotebookCell, alignment?: vscode.NotebookCellStatusBarAlignment, priority?: number): vscode.NotebookCellStatusBarItem { + checkProposedApiEnabled(extension); + return extHostNotebook.createNotebookCellStatusBarItemInternal(cell, alignment, priority); } }; @@ -1019,6 +1036,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ConfigurationTarget: extHostTypes.ConfigurationTarget, DebugAdapterExecutable: extHostTypes.DebugAdapterExecutable, DebugAdapterServer: extHostTypes.DebugAdapterServer, + DebugAdapterNamedPipeServer: extHostTypes.DebugAdapterNamedPipeServer, DebugAdapterInlineImplementation: extHostTypes.DebugAdapterInlineImplementation, DecorationRangeBehavior: extHostTypes.DecorationRangeBehavior, Diagnostic: extHostTypes.Diagnostic, @@ -1038,7 +1056,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ExtensionMode: extHostTypes.ExtensionMode, ExtensionRuntime: extHostTypes.ExtensionRuntime, CustomExecution: extHostTypes.CustomExecution, - CustomExecution2: extHostTypes.CustomExecution, FileChangeType: extHostTypes.FileChangeType, FileSystemError: extHostTypes.FileSystemError, FileType: files.FileType, @@ -1081,7 +1098,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SymbolKind: extHostTypes.SymbolKind, SymbolTag: extHostTypes.SymbolTag, Task: extHostTypes.Task, - Task2: extHostTypes.Task, TaskGroup: extHostTypes.TaskGroup, TaskPanelKind: extHostTypes.TaskPanelKind, TaskRevealKind: extHostTypes.TaskRevealKind, @@ -1113,7 +1129,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I CellKind: extHostTypes.CellKind, CellOutputKind: extHostTypes.CellOutputKind, NotebookCellRunState: extHostTypes.NotebookCellRunState, - NotebookRunState: extHostTypes.NotebookRunState + NotebookRunState: extHostTypes.NotebookRunState, + NotebookCellStatusBarAlignment: extHostTypes.NotebookCellStatusBarAlignment }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 1c7e49f5c97..6b001f1df78 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -42,7 +42,7 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views'; import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder'; import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal'; -import { ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions'; import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import * as search from 'vs/workbench/services/search/common/search'; import { SaveReason } from 'vs/workbench/common/editor'; @@ -51,7 +51,7 @@ import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { TunnelOptions } 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 { INotebookMimeTypeSelector, IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, IOutputRenderRequest, IOutputRenderResponse, INotebookDocumentFilter, INotebookKernelInfoDto2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy'; import { Dto } from 'vs/base/common/types'; import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable'; @@ -581,7 +581,7 @@ export interface ExtHostEditorInsetsShape { $onDidReceiveMessage(handle: number, message: any): void; } -export type WebviewPanelHandle = string; +export type WebviewHandle = string; export interface WebviewPanelShowOptions { readonly viewColumn?: EditorViewColumn; @@ -609,20 +609,23 @@ export interface CustomTextEditorCapabilities { } export interface MainThreadWebviewsShape extends IDisposable { - $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewPanelHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; - $disposeWebview(handle: WebviewPanelHandle): void; - $reveal(handle: WebviewPanelHandle, showOptions: WebviewPanelShowOptions): void; - $setTitle(handle: WebviewPanelHandle, value: string): void; - $setIconPath(handle: WebviewPanelHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; + $setHtml(handle: WebviewHandle, value: string): void; + $setOptions(handle: WebviewHandle, options: modes.IWebviewOptions): void; + $postMessage(handle: WebviewHandle, value: any): Promise +} - $setHtml(handle: WebviewPanelHandle, value: string): void; - $setOptions(handle: WebviewPanelHandle, options: modes.IWebviewOptions): void; - - $postMessage(handle: WebviewPanelHandle, value: any): Promise; +export interface MainThreadWebviewPanelsShape extends IDisposable { + $createWebviewPanel(extension: WebviewExtensionDescription, handle: WebviewHandle, viewType: string, title: string, showOptions: WebviewPanelShowOptions, options: modes.IWebviewPanelOptions & modes.IWebviewOptions): void; + $disposeWebview(handle: WebviewHandle): void; + $reveal(handle: WebviewHandle, showOptions: WebviewPanelShowOptions): void; + $setTitle(handle: WebviewHandle, value: string): void; + $setIconPath(handle: WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void; $registerSerializer(viewType: string): void; $unregisterSerializer(viewType: string): void; +} +export interface MainThreadCustomEditorsShape extends IDisposable { $registerTextEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void; $registerCustomEditorProvider(extension: WebviewExtensionDescription, viewType: string, options: modes.IWebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void; $unregisterEditorProvider(viewType: string): void; @@ -631,6 +634,13 @@ export interface MainThreadWebviewsShape extends IDisposable { $onContentChange(resource: UriComponents, viewType: string): void; } +export interface MainThreadWebviewViewsShape extends IDisposable { + $registerWebviewViewProvider(viewType: string, options?: { retainContextWhenHidden?: boolean }): void; + $unregisterWebviewViewProvider(viewType: string): void; + + $setWebviewViewTitle(handle: WebviewHandle, value: string | undefined): void; +} + export interface WebviewPanelViewStateData { [handle: string]: { readonly active: boolean; @@ -640,14 +650,18 @@ export interface WebviewPanelViewStateData { } export interface ExtHostWebviewsShape { - $onMessage(handle: WebviewPanelHandle, message: any): void; - $onMissingCsp(handle: WebviewPanelHandle, extensionId: string): void; + $onMessage(handle: WebviewHandle, message: any): void; + $onMissingCsp(handle: WebviewHandle, extensionId: string): void; +} + +export interface ExtHostWebviewPanelsShape { $onDidChangeWebviewPanelViewStates(newState: WebviewPanelViewStateData): void; - $onDidDisposeWebviewPanel(handle: WebviewPanelHandle): Promise; + $onDidDisposeWebviewPanel(handle: WebviewHandle): Promise; + $deserializeWebviewPanel(newWebviewHandle: WebviewHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; +} - $deserializeWebviewPanel(newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, state: any, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions): Promise; - - $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewPanelHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise; +export interface ExtHostCustomEditorsShape { + $resolveWebviewEditor(resource: UriComponents, newWebviewHandle: WebviewHandle, viewType: string, title: string, position: EditorViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, cancellation: CancellationToken): Promise; $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken): Promise<{ editable: boolean }>; $disposeCustomDocument(resource: UriComponents, viewType: string): Promise; @@ -661,7 +675,15 @@ export interface ExtHostWebviewsShape { $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise; - $onMoveCustomEditor(handle: WebviewPanelHandle, newResource: UriComponents, viewType: string): Promise; + $onMoveCustomEditor(handle: WebviewHandle, newResource: UriComponents, viewType: string): Promise; +} + +export interface ExtHostWebviewViewsShape { + $resolveWebviewView(webviewHandle: WebviewHandle, viewType: string, state: any, cancellation: CancellationToken): Promise; + + $onDidChangeWebviewViewVisibility(webviewHandle: WebviewHandle, visible: boolean): void; + + $disposeWebviewView(webviewHandle: WebviewHandle): void; } export enum CellKind { @@ -697,23 +719,24 @@ export type NotebookCellOutputsSplice = [ IProcessedOutput[] ]; +export type INotebookCellStatusBarEntryDto = Dto; + export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, supportBackup: boolean, kernelInfoDto: INotebookKernelInfoDto | undefined, options: { transientOutputs: boolean; transientMetadata: TransientMetadata }): Promise; $onNotebookChange(viewType: string, resource: UriComponents): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise; - $unregisterNotebookRenderer(id: string): Promise; $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise; $registerNotebookKernelProvider(extension: NotebookExtensionDescription, handle: number, documentFilter: INotebookDocumentFilter): Promise; $unregisterNotebookKernelProvider(handle: number): Promise; $onNotebookKernelChange(handle: number): void; $unregisterNotebookKernel(id: string): Promise; - $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; + $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[]): Promise; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; $updateNotebookCellMetadata(viewType: string, resource: UriComponents, handle: number, metadata: NotebookCellMetadata | undefined): Promise; - $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise; + $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[]): Promise; $postMessage(editorId: string, forRendererId: string | undefined, value: any): Promise; + $setStatusBarEntry(id: number, statusBarEntry: INotebookCellStatusBarEntryDto): Promise; $onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void; $onContentChange(resource: UriComponents, viewType: string): void; @@ -879,6 +902,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $stopDebugging(sessionId: DebugSessionUUID | undefined): Promise; $setDebugSessionName(id: DebugSessionUUID, name: string): void; $customDebugAdapterRequest(id: DebugSessionUUID, command: string, args: any): Promise; + $getDebugProtocolBreakpoint(id: DebugSessionUUID, breakpoinId: string): Promise; $appendDebugConsole(value: string): void; $startBreakpointEvents(): void; $registerBreakpoints(breakpoints: Array): Promise; @@ -1058,7 +1082,7 @@ export type IResolveAuthorityResult = IResolveAuthorityErrorResult | IResolveAut export interface ExtHostExtensionServiceShape { $resolveAuthority(remoteAuthority: string, resolveAttempt: number): Promise; $startExtensionHost(enabledExtensionIds: ExtensionIdentifier[]): Promise; - $activateByEvent(activationEvent: string): Promise; + $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise; $activate(extensionId: ExtensionIdentifier, reason: ExtensionActivationReason): Promise; $setRemoteEnvironment(env: { [key: string]: string | null; }): Promise; $updateRemoteConnectionData(connectionData: IRemoteConnectionData): Promise; @@ -1216,7 +1240,14 @@ export interface IWorkspaceEditEntryMetadataDto { iconPath?: { id: string } | UriComponents | { light: UriComponents, dark: UriComponents }; } +export const enum WorkspaceEditType { + File = 1, + Text = 2, + Cell = 3, +} + export interface IWorkspaceFileEditDto { + _type: WorkspaceEditType.File; oldUri?: UriComponents; newUri?: UriComponents; options?: modes.WorkspaceFileEditOptions @@ -1224,14 +1255,23 @@ export interface IWorkspaceFileEditDto { } export interface IWorkspaceTextEditDto { + _type: WorkspaceEditType.Text; resource: UriComponents; edit: modes.TextEdit; modelVersionId?: number; metadata?: IWorkspaceEditEntryMetadataDto; } +export interface IWorkspaceCellEditDto { + _type: WorkspaceEditType.Cell; + resource: UriComponents; + edit: ICellEditOperation; + modelVersionId?: number; + metadata?: IWorkspaceEditEntryMetadataDto; +} + export interface IWorkspaceEditDto { - edits: Array; + edits: Array; // todo@joh reject should go into rename rejectReason?: string; @@ -1332,7 +1372,7 @@ export interface ExtHostLanguageFeaturesShape { $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: IRegExpDto; } | undefined>; $provideReferences(handle: number, resource: UriComponents, position: IPosition, context: modes.ReferenceContext, token: CancellationToken): Promise; $provideCodeActions(handle: number, resource: UriComponents, rangeOrSelection: IRange | ISelection, context: modes.CodeActionContext, token: CancellationToken): Promise; $releaseCodeActions(handle: number, cacheId: number): void; @@ -1448,7 +1488,7 @@ export interface ExtHostSCMShape { export interface ExtHostTaskShape { $provideTasks(handle: number, validTypes: { [key: string]: boolean; }): Thenable; $resolveTask(handle: number, taskDTO: tasks.TaskDTO): Thenable; - $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number): void; + $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): void; $onDidStartTaskProcess(value: tasks.TaskProcessStartedDTO): void; $onDidEndTaskProcess(value: tasks.TaskProcessEndedDTO): void; $OnDidEndTask(execution: tasks.TaskExecutionDTO): void; @@ -1627,8 +1667,6 @@ export interface ExtHostNotebookShape { $backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; $acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void; - $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; - $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; $onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptModelSaved(uriComponents: UriComponents): void; @@ -1695,6 +1733,9 @@ export const MainContext = { MainThreadTelemetry: createMainId('MainThreadTelemetry'), MainThreadTerminalService: createMainId('MainThreadTerminalService'), MainThreadWebviews: createMainId('MainThreadWebviews'), + MainThreadWebviewPanels: createMainId('MainThreadWebviewPanels'), + MainThreadWebviewViews: createMainId('MainThreadWebviewViews'), + MainThreadCustomEditors: createMainId('MainThreadCustomEditors'), MainThreadUrls: createMainId('MainThreadUrls'), MainThreadWorkspace: createMainId('MainThreadWorkspace'), MainThreadFileSystem: createMainId('MainThreadFileSystem'), @@ -1735,6 +1776,9 @@ export const ExtHostContext = { ExtHostWorkspace: createExtId('ExtHostWorkspace'), ExtHostWindow: createExtId('ExtHostWindow'), ExtHostWebviews: createExtId('ExtHostWebviews'), + ExtHostWebviewPanels: createExtId('ExtHostWebviewPanels'), + ExtHostCustomEditors: createExtId('ExtHostCustomEditors'), + ExtHostWebviewViews: createExtId('ExtHostWebviewViews'), ExtHostEditorInsets: createExtId('ExtHostEditorInsets'), ExtHostProgress: createMainId('ExtHostProgress'), ExtHostComments: createMainId('ExtHostComments'), diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 83fc7bc31ac..8dc69cb7abd 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -141,7 +141,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { try { const result = await this._proxy.$executeCommand(id, toArgs, retry); - return revive(result); + return revive(result); } catch (e) { // Rerun the command when it wasn't known, had arguments, and when retry // is enabled. We do this because the command might be registered inside diff --git a/src/vs/workbench/api/common/extHostCustomEditors.ts b/src/vs/workbench/api/common/extHostCustomEditors.ts new file mode 100644 index 00000000000..f3b3cbd8438 --- /dev/null +++ b/src/vs/workbench/api/common/extHostCustomEditors.ts @@ -0,0 +1,388 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { hash } from 'vs/base/common/hash'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { joinPath } from 'vs/base/common/resources'; +import { URI, UriComponents } from 'vs/base/common/uri'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import { Cache } from './cache'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + + +class CustomDocumentStoreEntry { + + private _backupCounter = 1; + + constructor( + public readonly document: vscode.CustomDocument, + private readonly _storagePath: URI | undefined, + ) { } + + private readonly _edits = new Cache('custom documents'); + + private _backup?: vscode.CustomDocumentBackup; + + addEdit(item: vscode.CustomDocumentEditEvent): number { + return this._edits.add([item]); + } + + async undo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).undo(); + if (!isDirty) { + this.disposeBackup(); + } + } + + async redo(editId: number, isDirty: boolean): Promise { + await this.getEdit(editId).redo(); + if (!isDirty) { + this.disposeBackup(); + } + } + + disposeEdits(editIds: number[]): void { + for (const id of editIds) { + this._edits.delete(id); + } + } + + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.document.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); + } + + updateBackup(backup: vscode.CustomDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } + + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + + private getEdit(editId: number): vscode.CustomDocumentEditEvent { + const edit = this._edits.get(editId, 0); + if (!edit) { + throw new Error('No edit found'); + } + return edit; + } +} + +class CustomDocumentStore { + private readonly _documents = new Map(); + + public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined { + return this._documents.get(this.key(viewType, resource)); + } + + public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry { + const key = this.key(viewType, document.uri); + if (this._documents.has(key)) { + throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`); + } + const entry = new CustomDocumentStoreEntry(document, storagePath); + this._documents.set(key, entry); + return entry; + } + + public delete(viewType: string, document: vscode.CustomDocument) { + const key = this.key(viewType, document.uri); + this._documents.delete(key); + } + + private key(viewType: string, resource: vscode.Uri): string { + return `${viewType}@@@${resource}`; + } + +} + +const enum WebviewEditorType { + Text, + Custom +} + +type ProviderEntry = { + readonly extension: IExtensionDescription; + readonly type: WebviewEditorType.Text; + readonly provider: vscode.CustomTextEditorProvider; +} | { + readonly extension: IExtensionDescription; + readonly type: WebviewEditorType.Custom; + readonly provider: vscode.CustomReadonlyEditorProvider; +}; + +class EditorProviderStore { + private readonly _providers = new Map(); + + public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable { + return this.add(WebviewEditorType.Text, viewType, extension, provider); + } + + public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable { + return this.add(WebviewEditorType.Custom, viewType, extension, provider); + } + + public get(viewType: string): ProviderEntry | undefined { + return this._providers.get(viewType); + } + + private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): vscode.Disposable { + if (this._providers.has(viewType)) { + throw new Error(`Provider for viewType:${viewType} already registered`); + } + this._providers.set(viewType, { type, extension, provider } as ProviderEntry); + return new extHostTypes.Disposable(() => this._providers.delete(viewType)); + } +} + +export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditorsShape { + + private readonly _proxy: extHostProtocol.MainThreadCustomEditorsShape; + + private readonly _editorProviders = new EditorProviderStore(); + + private readonly _documents = new CustomDocumentStore(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly _extHostDocuments: ExtHostDocuments, + private readonly _extensionStoragePaths: IExtensionStoragePaths | undefined, + private readonly _extHostWebview: ExtHostWebviews, + private readonly _extHostWebviewPanels: ExtHostWebviewPanels, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadCustomEditors); + } + + public registerCustomEditorProvider( + extension: IExtensionDescription, + viewType: string, + provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider, + options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, + ): vscode.Disposable { + const disposables = new DisposableStore(); + if ('resolveCustomTextEditor' in provider) { + disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); + this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { + supportsMove: !!provider.moveCustomTextEditor, + }); + } else { + disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); + + if (this.supportEditing(provider)) { + disposables.add(provider.onDidChangeCustomDocument(e => { + const entry = this.getCustomDocumentEntry(viewType, e.document.uri); + if (isEditEvent(e)) { + const editId = entry.addEdit(e); + this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label); + } else { + this._proxy.$onContentChange(e.document.uri, viewType); + } + })); + } + + this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument); + } + + return extHostTypes.Disposable.from( + disposables, + new extHostTypes.Disposable(() => { + this._proxy.$unregisterEditorProvider(viewType); + })); + } + + + async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (entry.type !== WebviewEditorType.Custom) { + throw new Error(`Invalid provide type for '${viewType}'`); + } + + const revivedResource = URI.revive(resource); + const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation); + + let storageRoot: URI | undefined; + if (this.supportEditing(entry.provider) && this._extensionStoragePaths) { + storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); + } + this._documents.add(viewType, document, storageRoot); + + return { editable: this.supportEditing(entry.provider) }; + } + + async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (entry.type !== WebviewEditorType.Custom) { + throw new Error(`Invalid provider type for '${viewType}'`); + } + + const revivedResource = URI.revive(resource); + const { document } = this.getCustomDocumentEntry(viewType, revivedResource); + this._documents.delete(viewType, document); + document.dispose(); + } + + async $resolveWebviewEditor( + resource: UriComponents, + handle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions, + cancellation: CancellationToken, + ): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension); + const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview); + + const revivedResource = URI.revive(resource); + + switch (entry.type) { + case WebviewEditorType.Custom: + { + const { document } = this.getCustomDocumentEntry(viewType, revivedResource); + return entry.provider.resolveCustomEditor(document, panel, cancellation); + } + case WebviewEditorType.Text: + { + const document = this._extHostDocuments.getDocument(revivedResource); + return entry.provider.resolveCustomTextEditor(document, panel, cancellation); + } + default: + { + throw new Error('Unknown webview provider type'); + } + } + } + + $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void { + const document = this.getCustomDocumentEntry(viewType, resourceComponents); + document.disposeEdits(editIds); + } + + async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise { + const entry = this._editorProviders.get(viewType); + if (!entry) { + throw new Error(`No provider found for '${viewType}'`); + } + + if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) { + throw new Error(`Provider does not implement move '${viewType}'`); + } + + const webview = this._extHostWebviewPanels.getWebviewPanel(handle); + if (!webview) { + throw new Error(`No webview found`); + } + + const resource = URI.revive(newResourceComponents); + const document = this._extHostDocuments.getDocument(resource); + await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None); + } + + async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + return entry.undo(editId, isDirty); + } + + async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + return entry.redo(editId, isDirty); + } + + async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + await provider.revertCustomDocument(entry.document, cancellation); + entry.disposeBackup(); + } + + async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + await provider.saveCustomDocument(entry.document, cancellation); + entry.disposeBackup(); + } + + async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation); + } + + async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { + const entry = this.getCustomDocumentEntry(viewType, resourceComponents); + const provider = this.getCustomEditorProvider(viewType); + + const backup = await provider.backupCustomDocument(entry.document, { + destination: entry.getNewBackupUri(), + }, cancellation); + entry.updateBackup(backup); + return backup.id; + } + + + private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { + const entry = this._documents.get(viewType, URI.revive(resource)); + if (!entry) { + throw new Error('No custom document found'); + } + return entry; + } + + private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider { + const entry = this._editorProviders.get(viewType); + const provider = entry?.provider; + if (!provider || !this.supportEditing(provider)) { + throw new Error('Custom document is not editable'); + } + return provider; + } + + private supportEditing( + provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider + ): provider is vscode.CustomEditorProvider { + return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument; + } +} + + +function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { + return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' + && typeof (e as vscode.CustomDocumentEditEvent).redo === 'function'; +} + +function hashPath(resource: URI): string { + const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); + return hash(str) + ''; +} + diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index 82121bcb2c6..1e1643ee414 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -11,12 +11,12 @@ import { MainContext, MainThreadDebugServiceShape, ExtHostDebugServiceShape, DebugSessionUUID, IBreakpointsDeltaDto, ISourceMultiBreakpointDto, IFunctionBreakpointDto, IDebugSessionDto } from 'vs/workbench/api/common/extHost.protocol'; -import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation } from 'vs/workbench/api/common/extHostTypes'; +import { Disposable, Position, Location, SourceBreakpoint, FunctionBreakpoint, DebugAdapterServer, DebugAdapterExecutable, DataBreakpoint, DebugConsoleMode, DebugAdapterInlineImplementation, DebugAdapterNamedPipeServer } from 'vs/workbench/api/common/extHostTypes'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterImpl } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebuggerContribution, IConfig, IDebugAdapter, IDebugAdapterServer, IDebugAdapterExecutable, IAdapterDescriptor, IDebugAdapterImpl, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { AbstractVariableResolverService } from 'vs/workbench/services/configurationResolver/common/variableResolver'; import { ExtHostConfigProvider, IExtHostConfiguration } from '../common/extHostConfiguration'; @@ -737,6 +737,11 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E port: x.port, host: x.host }; + } else if (x instanceof DebugAdapterNamedPipeServer) { + return { + type: 'pipeServer', + path: x.path + }; } else if (x instanceof DebugAdapterInlineImplementation) { return { type: 'implementation', @@ -957,6 +962,10 @@ export class ExtHostDebugSession implements vscode.DebugSession { public customRequest(command: string, args: any): Promise { return this._debugServiceProxy.$customDebugAdapterRequest(this._id, command, args); } + + public getDebugProtocolBreakpoint(breakpoint: vscode.Breakpoint): Promise { + return this._debugServiceProxy.$getDebugProtocolBreakpoint(this._id, breakpoint.id); + } } export class ExtHostDebugConsole implements vscode.DebugConsole { diff --git a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts index 1eb5b7bc391..5613e66db18 100644 --- a/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/common/extHostDocumentSaveParticipant.ts @@ -6,7 +6,7 @@ import { Event } from 'vs/base/common/event'; import { URI, UriComponents } from 'vs/base/common/uri'; import { illegalState } from 'vs/base/common/errors'; -import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentSaveParticipantShape, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/common/extHostTypes'; import { Range, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/common/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; @@ -146,6 +146,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic if (Array.isArray(value) && (value).every(e => e instanceof TextEdit)) { for (const { newText, newEol, range } of value) { dto.edits.push({ + _type: WorkspaceEditType.Text, resource: document.uri, edit: { range: range && Range.from(range), diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index 18600b6fff5..6d9e1a525e0 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -17,7 +17,7 @@ import { ExtHostConfiguration, IExtHostConfiguration } from 'vs/workbench/api/co import { ActivatedExtension, EmptyExtension, ExtensionActivationReason, ExtensionActivationTimes, ExtensionActivationTimesBuilder, ExtensionsActivator, IExtensionAPI, IExtensionModule, HostExtension, ExtensionActivationTimesFragment } from 'vs/workbench/api/common/extHostExtensionActivator'; import { ExtHostStorage, IExtHostStorage } from 'vs/workbench/api/common/extHostStorage'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { ExtensionActivationError, checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; +import { ExtensionActivationError, checkProposedApiEnabled, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import * as errors from 'vs/base/common/errors'; import type * as vscode from 'vscode'; @@ -396,18 +396,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; }, get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; }, get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); }, - get logUri() { - checkProposedApiEnabled(extensionDescription); - return URI.joinPath(that._initData.logsLocation, extensionDescription.identifier.value); - }, - get storageUri() { - checkProposedApiEnabled(extensionDescription); - return that._storagePath.workspaceValue(extensionDescription); - }, - get globalStorageUri() { - checkProposedApiEnabled(extensionDescription); - return that._storagePath.globalValue(extensionDescription); - }, + get logUri() { return URI.joinPath(that._initData.logsLocation, extensionDescription.identifier.value); }, + get storageUri() { return that._storagePath.workspaceValue(extensionDescription); }, + get globalStorageUri() { return that._storagePath.globalValue(extensionDescription); }, get extensionMode() { return extensionMode; }, get extensionRuntime() { checkProposedApiEnabled(extensionDescription); @@ -695,7 +686,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme return this._startExtensionHost(); } - public $activateByEvent(activationEvent: string): Promise { + public $activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + if (activationKind === ActivationKind.Immediate) { + return this._activateByEvent(activationEvent, false); + } + return ( this._readyToRunExtensions.wait() .then(_ => this._activateByEvent(activationEvent, false)) diff --git a/src/vs/workbench/api/common/extHostFileSystemEventService.ts b/src/vs/workbench/api/common/extHostFileSystemEventService.ts index 41606d40393..76a5f2f81e9 100644 --- a/src/vs/workbench/api/common/extHostFileSystemEventService.ts +++ b/src/vs/workbench/api/common/extHostFileSystemEventService.ts @@ -8,12 +8,11 @@ import { IRelativePattern, parse } from 'vs/base/common/glob'; import { URI } from 'vs/base/common/uri'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import type * as vscode from 'vscode'; -import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, IWorkspaceFileEditDto, IWorkspaceTextEditDto, SourceTargetPair } from './extHost.protocol'; +import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, MainThreadTextEditorsShape, SourceTargetPair, IWorkspaceEditDto } from './extHost.protocol'; import * as typeConverter from './extHostTypeConverters'; import { Disposable, WorkspaceEdit } from './extHostTypes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { FileOperation } from 'vs/platform/files/common/files'; -import { flatten } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ILogService } from 'vs/platform/log/common/log'; @@ -217,14 +216,13 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ } if (edits.length > 0) { - // flatten all WorkspaceEdits collected via waitUntil-call - // and apply them in one go. - const allEdits = new Array>(); + // concat all WorkspaceEdits collected via waitUntil-call and apply them in one go. + const dto: IWorkspaceEditDto = { edits: [] }; for (let edit of edits) { let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); - allEdits.push(edits); + dto.edits = dto.edits.concat(edits); } - return this._mainThreadTextEditors.$tryApplyWorkspaceEdit({ edits: flatten(allEdits) }); + return this._mainThreadTextEditors.$tryApplyWorkspaceEdit(dto); } } } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index cbe85a2675e..c912e48141d 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -324,14 +324,17 @@ class OnTypeRenameAdapter { private readonly _provider: vscode.OnTypeRenameProvider ) { } - provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise { + provideOnTypeRenameRanges(resource: URI, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: RegExp; } | undefined> { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); return asPromise(() => this._provider.provideOnTypeRenameRanges(doc, pos, token)).then(value => { - if (Array.isArray(value)) { - return coalesce(value.map(typeConvert.Range.from)); + if (value && Array.isArray(value.ranges)) { + return { + ranges: coalesce(value.ranges.map(typeConvert.Range.from)), + wordPattern: value.wordPattern + }; } return undefined; }); @@ -1117,7 +1120,7 @@ class ColorProviderAdapter { provideColors(resource: URI, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); return asPromise(() => this._provider.provideDocumentColors(doc, token)).then(colors => { - if (!Array.isArray(colors)) { + if (!Array.isArray(colors)) { return []; } @@ -1549,15 +1552,24 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- on type rename - registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, stopPattern?: RegExp): vscode.Disposable { + registerOnTypeRenameProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.OnTypeRenameProvider, wordPattern?: RegExp): vscode.Disposable { const handle = this._addNewAdapter(new OnTypeRenameAdapter(this._documents, provider), extension); - const serializedStopPattern = stopPattern ? ExtHostLanguageFeatures._serializeRegExp(stopPattern) : undefined; - this._proxy.$registerOnTypeRenameProvider(handle, this._transformDocumentSelector(selector), serializedStopPattern); + const serializedWordPattern = wordPattern ? ExtHostLanguageFeatures._serializeRegExp(wordPattern) : undefined; + this._proxy.$registerOnTypeRenameProvider(handle, this._transformDocumentSelector(selector), serializedWordPattern); return this._createDisposable(handle); } - $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { - return this._withAdapter(handle, OnTypeRenameAdapter, adapter => adapter.provideOnTypeRenameRanges(URI.revive(resource), position, token), undefined); + $provideOnTypeRenameRanges(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise<{ ranges: IRange[]; wordPattern?: extHostProtocol.IRegExpDto; } | undefined> { + return this._withAdapter(handle, OnTypeRenameAdapter, async adapter => { + const res = await adapter.provideOnTypeRenameRanges(URI.revive(resource), position, token); + if (res) { + return { + ranges: res.ranges, + wordPattern: res.wordPattern ? ExtHostLanguageFeatures._serializeRegExp(res.wordPattern) : undefined + }; + } + return undefined; + }, undefined); } // --- references diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 037b0c71e63..17db2365cf1 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -14,20 +14,19 @@ import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, ExtHostNotebookShape, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, ExtHostNotebookShape, ICommandDto, IMainContext, IModelAddedData, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol'; import { ILogService } from 'vs/platform/log/common/log'; -import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; +import { CommandsConverter, ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors, IExtHostModelAddedData } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; -import { CellEditType, CellOutputKind, diff, ICellDeleteEdit, ICellEditOperation, ICellInsertEdit, IMainCellDto, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseCellInfo, IOutputRenderResponseOutputInfo, IProcessedOutput, IRawOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { addIdToOutput, CellEditType, CellOutputKind, CellStatusbarAlignment, CellUri, diff, ICellEditOperation, ICellReplaceEdit, IMainCellDto, INotebookCellStatusBarEntry, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IProcessedOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import * as vscode from 'vscode'; import { Cache } from './cache'; import { ResourceMap } from 'vs/base/common/map'; - interface IObservable { proxy: T; onDidChange: Event; @@ -56,12 +55,9 @@ interface INotebookEventEmitter { emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void; } -const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich - ? ({ ...output, outputId: id }) : output; +export class ExtHostCell extends Disposable { -export class ExtHostCell extends Disposable implements vscode.NotebookCell { - - public static asModelAddData(notebook: ExtHostNotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { + public static asModelAddData(notebook: vscode.NotebookDocument, cell: IMainCellDto): IExtHostModelAddedData { return { EOL: cell.eol, lines: cell.source, @@ -73,6 +69,9 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { }; } + private _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; + private _onDidChangeOutputs = new Emitter[]>(); readonly onDidChangeOutputs: Event[]> = this._onDidChangeOutputs.event; @@ -86,44 +85,61 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { readonly uri: URI; readonly cellKind: CellKind; + private _cell: vscode.NotebookCell | undefined; + constructor( - private _proxy: MainThreadNotebookShape, - readonly notebook: ExtHostNotebookDocument, - private _extHostDocument: ExtHostDocumentsAndEditors, - cell: IMainCellDto, + private readonly _proxy: MainThreadNotebookShape, + private readonly _notebook: ExtHostNotebookDocument, + private readonly _extHostDocument: ExtHostDocumentsAndEditors, + private readonly _cellData: IMainCellDto, ) { super(); - this.handle = cell.handle; - this.uri = URI.revive(cell.uri); - this.cellKind = cell.cellKind; + this.handle = _cellData.handle; + this.uri = URI.revive(_cellData.uri); + this.cellKind = _cellData.cellKind; - this._outputs = cell.outputs; + this._outputs = _cellData.outputs; for (const output of this._outputs) { this._outputMapping.set(output, output.outputId); delete output.outputId; } - const observableMetadata = getObservable(cell.metadata ?? {}); + const observableMetadata = getObservable(_cellData.metadata ?? {}); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { this._updateMetadata(); })); } - get document(): vscode.TextDocument { - return this._extHostDocument.getDocument(this.uri)!.document; + get cell(): vscode.NotebookCell { + if (!this._cell) { + const that = this; + const document = this._extHostDocument.getDocument(this.uri)!.document; + this._cell = Object.freeze({ + notebook: that._notebook.notebookDocument, + uri: that.uri, + cellKind: this._cellData.cellKind, + document, + language: document.languageId, + get outputs() { return that._outputs; }, + set outputs(value) { that._updateOutputs(value); }, + get metadata() { return that._metadata; }, + set metadata(value) { + that.setMetadata(value); + that._updateMetadata(); + }, + }); + } + return this._cell; } - get language(): string { - return this.document.languageId; + dispose() { + super.dispose(); + this._onDidDispose.fire(); } - get outputs() { - return this._outputs; - } - - set outputs(newOutputs: vscode.CellOutput[]) { + private _updateOutputs(newOutputs: vscode.CellOutput[]) { const rawDiffs = diff(this._outputs || [], newOutputs || [], (a) => { return this._outputMapping.has(a); }); @@ -153,15 +169,6 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { this._onDidChangeOutputs.fire(transformedDiffs); } - get metadata() { - return this._metadata; - } - - set metadata(newMetadata: vscode.NotebookCellMetadata) { - this.setMetadata(newMetadata); - this._updateMetadata(); - } - setMetadata(newMetadata: vscode.NotebookCellMetadata): void { // Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata' this._metadataChangeListener.dispose(); @@ -173,11 +180,26 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { } private _updateMetadata(): Promise { - return this._proxy.$updateNotebookCellMetadata(this.notebook.viewType, this.notebook.uri, this.handle, this._metadata); + return this._proxy.$updateNotebookCellMetadata(this._notebook.notebookDocument.viewType, this._notebook.uri, this.handle, this._metadata); } } -export class ExtHostNotebookDocument extends Disposable implements vscode.NotebookDocument { +class RawContentChangeEvent { + + constructor(readonly start: number, readonly deletedCount: number, readonly deletedItems: ExtHostCell[], readonly items: ExtHostCell[]) { } + + static asApiEvent(event: RawContentChangeEvent): vscode.NotebookCellsChangeData { + return Object.freeze({ + start: event.start, + deletedCount: event.deletedCount, + deletedItems: event.deletedItems.map(data => data.cell), + items: event.items.map(data => data.cell) + }); + } +} + +export class ExtHostNotebookDocument extends Disposable { + private static _handlePool: number = 0; readonly handle = ExtHostNotebookDocument._handlePool++; @@ -185,33 +207,44 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo private _cellDisposableMapping = new Map(); - get cells() { - return this._cells; - } - - private _languages: string[] = []; - - get languages() { - return this._languages = []; - } - - set languages(newLanguages: string[]) { - this._languages = newLanguages; - this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages); - } - - get isUntitled() { - return this.uri.scheme === Schemas.untitled; - } + private _notebook: vscode.NotebookDocument | undefined; private _metadata: Required = notebookDocumentMetadataDefaults; private _metadataChangeListener: IDisposable; + private _displayOrder: string[] = []; + private _versionId = 0; + private _backupCounter = 1; + private _backup?: vscode.NotebookDocumentBackup; + private _disposed = false; + private _languages: string[] = []; - get metadata() { - return this._metadata; + private readonly _edits = new Cache('notebook documents'); + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _documentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _emitter: INotebookEventEmitter, + private readonly _viewType: string, + public readonly uri: URI, + public readonly renderingHandler: ExtHostNotebookOutputRenderingHandler, + private readonly _storagePath: URI | undefined + ) { + super(); + + const observableMetadata = getObservable(notebookDocumentMetadataDefaults); + this._metadata = observableMetadata.proxy; + this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { + this._tryUpdateMetadata(); + })); } - set metadata(newMetadata: Required) { + dispose() { + this._disposed = true; + super.dispose(); + dispose(this._cellDisposableMapping.values()); + } + + private _updateMetadata(newMetadata: Required) { this._metadataChangeListener.dispose(); newMetadata = { ...notebookDocumentMetadataDefaults, @@ -224,34 +257,207 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo const observableMetadata = getObservable(newMetadata); this._metadata = observableMetadata.proxy; this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this.updateMetadata(); + this._tryUpdateMetadata(); })); - this.updateMetadata(); + this._tryUpdateMetadata(); } - private _displayOrder: string[] = []; - - get displayOrder() { - return this._displayOrder; + private _tryUpdateMetadata() { + this._proxy.$updateNotebookMetadata(this._viewType, this.uri, this._metadata); + } + get notebookDocument(): vscode.NotebookDocument { + if (!this._notebook) { + const that = this; + this._notebook = Object.freeze({ + get uri() { return that.uri; }, + get version() { return that._versionId; }, + get fileName() { return that.uri.fsPath; }, + get viewType() { return that._viewType; }, + get isDirty() { return false; }, + get isUntitled() { return that.uri.scheme === Schemas.untitled; }, + get cells(): ReadonlyArray { return that._cells.map(cell => cell.cell); }, + get languages() { return that._languages; }, + set languages(value: string[]) { that._trySetLanguages(value); }, + get displayOrder() { return that._displayOrder; }, + set displayOrder(value: string[]) { that._displayOrder = value; }, + get metadata() { return that._metadata; }, + set metadata(value: Required) { that._updateMetadata(value); }, + }); + } + return this._notebook; } - set displayOrder(newOrder: string[]) { - this._displayOrder = newOrder; + private _trySetLanguages(newLanguages: string[]) { + this._languages = newLanguages; + this._proxy.$updateNotebookLanguages(this._viewType, this.uri, this._languages); } - private _versionId = 0; - - get versionId() { - return this._versionId; + getNewBackupUri(): URI { + if (!this._storagePath) { + throw new Error('Backup requires a valid storage path'); + } + const fileName = hashPath(this.uri) + (this._backupCounter++); + return joinPath(this._storagePath, fileName); } - private _backupCounter = 1; + updateBackup(backup: vscode.NotebookDocumentBackup): void { + this._backup?.delete(); + this._backup = backup; + } - private _backup?: vscode.NotebookDocumentBackup; + disposeBackup(): void { + this._backup?.delete(); + this._backup = undefined; + } + acceptModelChanged(event: NotebookCellsChangedEvent): void { + this._versionId = event.versionId; + if (event.kind === NotebookCellsChangeType.Initialize) { + this._spliceNotebookCells(event.changes, true); + } if (event.kind === NotebookCellsChangeType.ModelChange) { + this._spliceNotebookCells(event.changes, false); + } else if (event.kind === NotebookCellsChangeType.Move) { + this._moveCell(event.index, event.newIdx); + } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { + this._clearCellOutputs(event.index); + } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { + this._clearAllCellOutputs(); + } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { + this._changeCellLanguage(event.index, event.language); + } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { + this._changeCellMetadata(event.index, event.metadata); + } + } - private readonly _edits = new Cache('notebook documents'); + private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { + if (this._disposed) { + return; + } + + const contentChangeEvents: RawContentChangeEvent[] = []; + const addedCellDocuments: IExtHostModelAddedData[] = []; + const removedCellDocuments: URI[] = []; + + splices.reverse().forEach(splice => { + const cellDtos = splice[2]; + const newCells = cellDtos.map(cell => { + + const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); + + if (!initialization) { + addedCellDocuments.push(ExtHostCell.asModelAddData(this.notebookDocument, cell)); + } + + if (!this._cellDisposableMapping.has(extCell.handle)) { + const store = new DisposableStore(); + store.add(extCell); + this._cellDisposableMapping.set(extCell.handle, store); + } + + const store = this._cellDisposableMapping.get(extCell.handle)!; + + store.add(extCell.onDidChangeOutputs((diffs) => { + this.eventuallyUpdateCellOutputs(extCell, diffs); + })); + + return extCell; + }); + + for (let j = splice[0]; j < splice[0] + splice[1]; j++) { + this._cellDisposableMapping.get(this._cells[j].handle)?.dispose(); + this._cellDisposableMapping.delete(this._cells[j].handle); + } + + const deletedItems = this._cells.splice(splice[0], splice[1], ...newCells); + for (let cell of deletedItems) { + removedCellDocuments.push(cell.uri); + } + + contentChangeEvents.push(new RawContentChangeEvent(splice[0], splice[1], deletedItems, newCells)); + }); + + this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ + addedDocuments: addedCellDocuments, + removedDocuments: removedCellDocuments + }); + + if (!initialization) { + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes: contentChangeEvents.map(RawContentChangeEvent.asApiEvent) + }); + } + } + + private _moveCell(index: number, newIdx: number): void { + const cells = this._cells.splice(index, 1); + this._cells.splice(newIdx, 0, ...cells); + const changes: vscode.NotebookCellsChangeData[] = [{ + start: index, + deletedCount: 1, + deletedItems: cells.map(data => data.cell), + items: [] + }, { + start: newIdx, + deletedCount: 0, + deletedItems: [], + items: cells.map(data => data.cell) + }]; + this._emitter.emitModelChange({ + document: this.notebookDocument, + changes + }); + } + + private _clearCellOutputs(index: number): void { + const cell = this._cells[index].cell; + cell.outputs = []; + const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: [cell] }; + this._emitter.emitCellOutputsChange(event); + } + + private _clearAllCellOutputs(): void { + const modifedCells: vscode.NotebookCell[] = []; + this._cells.forEach(({ cell }) => { + if (cell.outputs.length !== 0) { + cell.outputs = []; + modifedCells.push(cell); + } + }); + const event: vscode.NotebookCellOutputsChangeEvent = { document: this.notebookDocument, cells: modifedCells }; + this._emitter.emitCellOutputsChange(event); + } + + private _changeCellLanguage(index: number, language: string): void { + const cell = this._cells[index]; + const event: vscode.NotebookCellLanguageChangeEvent = { document: this.notebookDocument, cell: cell.cell, language }; + this._emitter.emitCellLanguageChange(event); + } + + private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata | undefined): void { + const cell = this._cells[index]; + cell.setMetadata(newMetadata || {}); + const event: vscode.NotebookCellMetadataChangeEvent = { document: this.notebookDocument, cell: cell.cell }; + this._emitter.emitCellMetadataChange(event); + } + + async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { + const outputs = diff.toInsert; + return [diff.start, diff.deleteCount, outputs]; + }); + + await this._proxy.$spliceNotebookCellOutputs(this._viewType, this.uri, cell.handle, outputDtos); + this._emitter.emitCellOutputsChange({ + document: this.notebookDocument, + cells: [cell.cell] + }); + } + + getCell(cellHandle: number): ExtHostCell | undefined { + return this._cells.find(cell => cell.handle === cellHandle); + } addEdit(item: vscode.NotebookDocumentEditEvent): number { @@ -286,221 +492,16 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo this._edits.delete(id); } } - - private _disposed = false; - - constructor( - private readonly _proxy: MainThreadNotebookShape, - private _documentsAndEditors: ExtHostDocumentsAndEditors, - private _emitter: INotebookEventEmitter, - public viewType: string, - public uri: URI, - public renderingHandler: ExtHostNotebookOutputRenderingHandler, - private readonly _storagePath: URI | undefined - ) { - super(); - - const observableMetadata = getObservable(notebookDocumentMetadataDefaults); - this._metadata = observableMetadata.proxy; - this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => { - this.updateMetadata(); - })); - } - - private updateMetadata() { - this._proxy.$updateNotebookMetadata(this.viewType, this.uri, this._metadata); - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.NotebookDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - dispose() { - this._disposed = true; - super.dispose(); - dispose(this._cellDisposableMapping.values()); - } - - get fileName() { return this.uri.fsPath; } - - get isDirty() { return false; } - - acceptModelChanged(event: NotebookCellsChangedEvent): void { - this._versionId = event.versionId; - if (event.kind === NotebookCellsChangeType.Initialize) { - this._spliceNotebookCells(event.changes, true); - } if (event.kind === NotebookCellsChangeType.ModelChange) { - this._spliceNotebookCells(event.changes, false); - } else if (event.kind === NotebookCellsChangeType.Move) { - this._moveCell(event.index, event.newIdx); - } else if (event.kind === NotebookCellsChangeType.CellClearOutput) { - this._clearCellOutputs(event.index); - } else if (event.kind === NotebookCellsChangeType.CellsClearOutput) { - this._clearAllCellOutputs(); - } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { - this._changeCellLanguage(event.index, event.language); - } else if (event.kind === NotebookCellsChangeType.ChangeMetadata) { - this._changeCellMetadata(event.index, event.metadata); - } - } - - private _spliceNotebookCells(splices: NotebookCellsSplice2[], initialization: boolean): void { - if (this._disposed) { - return; - } - - const contentChangeEvents: vscode.NotebookCellsChangeData[] = []; - const addedCellDocuments: IExtHostModelAddedData[] = []; - - splices.reverse().forEach(splice => { - const cellDtos = splice[2]; - const newCells = cellDtos.map(cell => { - - const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell); - - if (!initialization) { - addedCellDocuments.push(ExtHostCell.asModelAddData(this, cell)); - } - - if (!this._cellDisposableMapping.has(extCell.handle)) { - this._cellDisposableMapping.set(extCell.handle, new DisposableStore()); - } - - const store = this._cellDisposableMapping.get(extCell.handle)!; - - store.add(extCell.onDidChangeOutputs((diffs) => { - this.eventuallyUpdateCellOutputs(extCell, diffs); - })); - - return extCell; - }); - - for (let j = splice[0]; j < splice[0] + splice[1]; j++) { - this._cellDisposableMapping.get(this.cells[j].handle)?.dispose(); - this._cellDisposableMapping.delete(this.cells[j].handle); - } - - const deletedItems = this.cells.splice(splice[0], splice[1], ...newCells); - - contentChangeEvents.push({ - start: splice[0], - deletedCount: splice[1], - deletedItems, - items: newCells - }); - }); - - if (addedCellDocuments) { - this._documentsAndEditors.acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); - } - - if (!initialization) { - this._emitter.emitModelChange({ - document: this, - changes: contentChangeEvents - }); - } - } - - private _moveCell(index: number, newIdx: number): void { - const cells = this.cells.splice(index, 1); - this.cells.splice(newIdx, 0, ...cells); - const changes: vscode.NotebookCellsChangeData[] = [{ - start: index, - deletedCount: 1, - deletedItems: cells, - items: [] - }, { - start: newIdx, - deletedCount: 0, - deletedItems: [], - items: cells - }]; - this._emitter.emitModelChange({ - document: this, - changes - }); - } - - private _clearCellOutputs(index: number): void { - const cell = this.cells[index]; - cell.outputs = []; - const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: [cell] }; - this._emitter.emitCellOutputsChange(event); - } - - private _clearAllCellOutputs(): void { - const modifedCells: vscode.NotebookCell[] = []; - this.cells.forEach(cell => { - if (cell.outputs.length !== 0) { - cell.outputs = []; - modifedCells.push(cell); - } - }); - const event: vscode.NotebookCellOutputsChangeEvent = { document: this, cells: modifedCells }; - this._emitter.emitCellOutputsChange(event); - } - - private _changeCellLanguage(index: number, language: string): void { - const cell = this.cells[index]; - const event: vscode.NotebookCellLanguageChangeEvent = { document: this, cell, language }; - this._emitter.emitCellLanguageChange(event); - } - - private _changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void { - const cell = this.cells[index]; - cell.setMetadata(newMetadata); - const event: vscode.NotebookCellMetadataChangeEvent = { document: this, cell }; - this._emitter.emitCellMetadataChange(event); - } - - async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { - const renderers = new Set(); - const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { - const outputs = diff.toInsert; - return [diff.start, diff.deleteCount, outputs]; - }); - - await this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers)); - this._emitter.emitCellOutputsChange({ - document: this, - cells: [cell] - }); - } - - getCell(cellHandle: number) { - return this.cells.find(cell => cell.handle === cellHandle); - } - - getCell2(cellUri: UriComponents) { - return this.cells.find(cell => cell.uri.fragment === cellUri.fragment); - } } export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellEdit { - private _finalized: boolean = false; - private readonly _documentVersionId: number; - private _collectedEdits: ICellEditOperation[] = []; - private _renderers = new Set(); - constructor( - readonly editor: ExtHostNotebookEditor - ) { - this._documentVersionId = editor.document.versionId; + private readonly _documentVersionId: number; + private readonly _collectedEdits: ICellEditOperation[] = []; + private _finalized: boolean = false; + + constructor(documentVersionId: number) { + this._documentVersionId = documentVersionId; } finalize(): INotebookEditData { @@ -508,7 +509,6 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE return { documentVersionId: this._documentVersionId, edits: this._collectedEdits, - renderers: Array.from(this._renderers) }; } @@ -518,33 +518,54 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE } } - insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + replaceMetadata(index: number, metadata: vscode.NotebookCellMetadata): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Metadata, + index, + metadata + }); + } + + replaceOutput(index: number, outputs: vscode.CellOutput[]): void { + this._throwIfFinalized(); + this._collectedEdits.push({ + editType: CellEditType.Output, + index, + outputs: outputs.map(output => addIdToOutput(output)) + }); + } + + replaceCells(from: number, to: number, cells: vscode.NotebookCellData[]): void { this._throwIfFinalized(); - const sourceArr = Array.isArray(content) ? content : content.split(/\r|\n|\r\n/g); - const cell = { - source: sourceArr, - language, - cellKind: type, - outputs: outputs.map(o => addIdToOutput(o)), - metadata, - }; - this._collectedEdits.push({ - editType: CellEditType.Insert, - index, - cells: [cell] + editType: CellEditType.Replace, + index: from, + count: to - from, + cells: cells.map(data => { + return { + ...data, + outputs: data.outputs.map(output => addIdToOutput(output)), + }; + }) }); } + insert(index: number, content: string | string[], language: string, type: CellKind, outputs: vscode.CellOutput[], metadata: vscode.NotebookCellMetadata | undefined): void { + this._throwIfFinalized(); + this.replaceCells(index, index, [{ + language, + outputs, + metadata, + cellKind: type, + source: Array.isArray(content) ? content.join('\n') : content, + }]); + } + delete(index: number): void { this._throwIfFinalized(); - - this._collectedEdits.push({ - editType: CellEditType.Delete, - index, - count: 1 - }); + this.replaceCells(index, 1, []); } } @@ -596,7 +617,7 @@ class ExtHostWebviewCommWrapper extends Disposable { export class ExtHostNotebookEditor extends Disposable implements vscode.NotebookEditor { private _viewColumn: vscode.ViewColumn | undefined; - selection?: ExtHostCell = undefined; + selection?: vscode.NotebookCell; private _active: boolean = false; get active(): boolean { @@ -645,7 +666,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook public uri: URI, private _proxy: MainThreadNotebookShape, private _webComm: vscode.NotebookCommunication, - public document: ExtHostNotebookDocument, + public readonly notebookData: ExtHostNotebookDocument, ) { super(); this._register(this._webComm.onDidReceiveMessage(e => { @@ -653,14 +674,17 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook })); } - edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { - const edit = new NotebookEditorCellEditBuilder(this); - callback(edit); - return this._applyEdit(edit); + get document(): vscode.NotebookDocument { + return this.notebookData.notebookDocument; } - private _applyEdit(editBuilder: NotebookEditorCellEditBuilder): Promise { - const editData = editBuilder.finalize(); + edit(callback: (editBuilder: NotebookEditorCellEditBuilder) => void): Thenable { + const edit = new NotebookEditorCellEditBuilder(this.document.version); + callback(edit); + return this._applyEdit(edit.finalize()); + } + + private _applyEdit(editData: INotebookEditData): Promise { // return when there is nothing to do if (editData.edits.length === 0) { @@ -680,16 +704,10 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook const prevIndex = compressedEditsIndex; const prev = compressedEdits[prevIndex]; - if (prev.editType === CellEditType.Insert && editData.edits[i].editType === CellEditType.Insert) { + if (prev.editType === CellEditType.Replace && editData.edits[i].editType === CellEditType.Replace) { if (prev.index === editData.edits[i].index) { - prev.cells.push(...(editData.edits[i] as ICellInsertEdit).cells); - continue; - } - } - - if (prev.editType === CellEditType.Delete && editData.edits[i].editType === CellEditType.Delete) { - if (prev.index === editData.edits[i].index) { - prev.count += (editData.edits[i] as ICellDeleteEdit).count; + prev.cells.push(...(editData.edits[i] as ICellReplaceEdit).cells); + prev.count += (editData.edits[i] as ICellReplaceEdit).count; continue; } } @@ -698,7 +716,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook compressedEditsIndex++; } - return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits, editData.renderers); + return this._proxy.$tryApplyEdits(this.viewType, this.uri, editData.documentVersionId, compressedEdits); } get viewColumn(): vscode.ViewColumn | undefined { @@ -725,44 +743,8 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook } } -export class ExtHostNotebookOutputRenderer { - private static _handlePool: number = 0; - private resolvedComms = new WeakSet(); - readonly handle = ExtHostNotebookOutputRenderer._handlePool++; - - constructor( - public type: string, - public filter: vscode.NotebookOutputSelector, - public renderer: vscode.NotebookOutputRenderer - ) { - - } - - matches(mimeType: string): boolean { - if (this.filter.mimeTypes) { - if (this.filter.mimeTypes.indexOf(mimeType) >= 0) { - return true; - } - } - return false; - } - - resolveNotebook(document: ExtHostNotebookDocument, comm: ExtHostWebviewCommWrapper) { - if (!this.resolvedComms.has(comm) && this.renderer.resolveNotebook) { - this.renderer.resolveNotebook(document, comm.getRendererComm(this.type)); - this.resolvedComms.add(comm); - } - } - - render(document: ExtHostNotebookDocument, output: vscode.CellDisplayOutput, outputId: string, mimeType: string): string { - const html = this.renderer.render(document, { output, outputId, mimeType }); - - return html; - } -} export interface ExtHostNotebookOutputRenderingHandler { outputDisplayOrder: INotebookDisplayOrder | undefined; - findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[]; } export class ExtHostNotebookKernelProviderAdapter extends Disposable { @@ -784,7 +766,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } async provideKernels(document: ExtHostNotebookDocument, token: vscode.CancellationToken): Promise { - const data = await this._provider.provideKernels(document, token) || []; + const data = await this._provider.provideKernels(document.notebookDocument, token) || []; const newMap = new Map(); let kernel_unique_pool = 0; @@ -833,7 +815,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { const kernel = this._idToKernel.get(kernelId); if (kernel && this._provider.resolveKernel) { - return this._provider.resolveKernel(kernel, document, webview, token); + return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token); } } @@ -845,9 +827,9 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return withToken(token => (kernel.executeCell as any)(document, cell, token)); + return withToken(token => (kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); } else { - return withToken(token => (kernel.executeAllCells as any)(document, token)); + return withToken(token => (kernel.executeAllCells as any)(document.notebookDocument, token)); } } @@ -859,9 +841,9 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable { } if (cell) { - return kernel.cancelCellExecution(document, cell); + return kernel.cancelCellExecution(document.notebookDocument, cell.cell); } else { - return kernel.cancelAllCellsExecution(document); + return kernel.cancelAllCellsExecution(document.notebookDocument); } } } @@ -880,15 +862,14 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private static _notebookKernelProviderHandlePool: number = 0; private readonly _proxy: MainThreadNotebookShape; - private readonly _notebookContentProviders = new Map(); + private readonly _notebookContentProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _notebookKernelProviders = new Map(); private readonly _documents = new ResourceMap(); private readonly _unInitializedDocuments = new ResourceMap(); private readonly _editors = new Map(); private readonly _webviewComm = new Map(); - private readonly _notebookOutputRenderers = new Map(); - private readonly _renderersUsedInNotebooks = new WeakMap>(); + private readonly _commandsConverter: CommandsConverter; private readonly _onDidChangeNotebookCells = new Emitter(); readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; private readonly _onDidChangeCellOutputs = new Emitter(); @@ -912,10 +893,6 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return this._activeNotebookEditor; } - get notebookDocuments() { - return [...this._documents.values()]; - } - private _onDidOpenNotebookDocument = new Emitter(); onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); @@ -923,7 +900,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private _onDidSaveNotebookDocument = new Emitter(); onDidSaveNotebookDocument: Event = this._onDidCloseNotebookDocument.event; visibleNotebookEditors: ExtHostNotebookEditor[] = []; - private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); + private _onDidChangeActiveNotebookKernel = new Emitter<{ document: vscode.NotebookDocument, kernel: vscode.NotebookKernel | undefined; }>(); onDidChangeActiveNotebookKernel = this._onDidChangeActiveNotebookKernel.event; private _onDidChangeVisibleNotebookEditors = new Emitter(); onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; @@ -934,21 +911,23 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN private _documentsAndEditors: ExtHostDocumentsAndEditors, private readonly _webviewInitData: WebviewInitData, private readonly logService: ILogService, - private readonly _extensionStoragePaths?: IExtensionStoragePaths, + private readonly _extensionStoragePaths: IExtensionStoragePaths, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); + this._commandsConverter = commands.converter; commands.registerArgumentProcessor({ - processArgument: arg => { + // Serialized INotebookCellActionContext + processArgument: (arg) => { if (arg && arg.$mid === 12) { const documentHandle = arg.notebookEditor?.notebookHandle; const cellHandle = arg.cell.handle; for (const value of this._editors) { - if (value[1].editor.document.handle === documentHandle) { - const cell = value[1].editor.document.getCell(cellHandle); + if (value[1].editor.notebookData.handle === documentHandle) { + const cell = value[1].editor.notebookData.getCell(cellHandle); if (cell) { - return cell; + return cell.cell; } } } @@ -958,112 +937,22 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); } - registerNotebookOutputRenderer( - type: string, - extension: IExtensionDescription, - filter: vscode.NotebookOutputSelector, - renderer: vscode.NotebookOutputRenderer - ): vscode.Disposable { - if (this._notebookOutputRenderers.has(type)) { - throw new Error(`Notebook renderer for '${type}' already registered`); - } - - const extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); - this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer); - this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, type, filter, renderer.preloads || []); - return new extHostTypes.Disposable(() => { - this._notebookOutputRenderers.delete(extHostRenderer.type); - this._proxy.$unregisterNotebookRenderer(extHostRenderer.type); - }); + get notebookDocuments() { + return [...this._documents.values()]; } - async $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined> { - if (!this._notebookOutputRenderers.has(id)) { - throw new Error(`Notebook renderer for '${id}' is not registered`); - } - - const document = this._documents.get(URI.revive(uriComponents)); - - if (!document) { - return; - } - - const renderer = this._notebookOutputRenderers.get(id)!; - this.provideCommToNotebookRenderers(document, renderer); - - const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { - const cell = document.getCell2(cellInfo.key)!; - const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { - return { - index: output.index, - outputId: output.outputId, - mimeType: output.mimeType, - handlerId: id, - transformedOutput: renderer.render(document, cell.outputs[output.index] as vscode.CellDisplayOutput, output.outputId, output.mimeType) - }; - }); - - return { - key: cellInfo.key, - outputs: outputResponse - }; - }); - - return { items: cellsResponse }; - } - - /** - * The request carry the raw data for outputs so we don't look up in the existing document - */ - async $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined> { - if (!this._notebookOutputRenderers.has(id)) { - throw new Error(`Notebook renderer for '${id}' is not registered`); - } - - const document = this._documents.get(URI.revive(uriComponents)); - - if (!document) { - return; - } - - const renderer = this._notebookOutputRenderers.get(id)!; - this.provideCommToNotebookRenderers(document, renderer); - - const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { - const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { - return { - index: output.index, - outputId: output.outputId, - mimeType: output.mimeType, - handlerId: id, - transformedOutput: renderer.render(document, output.output! as vscode.CellDisplayOutput, output.outputId, output.mimeType) - }; - }); - - return { - key: cellInfo.key, - outputs: outputResponse - }; - }); - - return { items: cellsResponse }; - } - - findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { - const matches: ExtHostNotebookOutputRenderer[] = []; - for (const renderer of this._notebookOutputRenderers) { - if (renderer[1].matches(mimeType)) { - matches.push(renderer[1]); - } - } - - return matches; + lookupNotebookDocument(uri: URI): ExtHostNotebookDocument | undefined { + return this._documents.get(uri); } registerNotebookContentProvider( extension: IExtensionDescription, viewType: string, - provider: vscode.NotebookContentProvider, + provider: vscode.NotebookContentProvider & { kernel?: vscode.NotebookKernel }, + options?: { + transientOutputs: boolean; + transientMetadata: { [K in keyof NotebookCellMetadata]?: boolean }; + } ): vscode.Disposable { if (this._notebookContentProviders.has(viewType)) { @@ -1095,7 +984,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const supportBackup = !!provider.backupNotebook; - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, viewType, supportBackup, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined, { transientOutputs: options?.transientOutputs || false, transientMetadata: options?.transientMetadata || {} }); return new extHostTypes.Disposable(() => { listener.dispose(); @@ -1175,11 +1064,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return; } - let storageRoot: URI | undefined; - if (this._extensionStoragePaths) { - storageRoot = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); - } - + const storageRoot = this._extensionStoragePaths.workspaceValue(provider.extension) ?? this._extensionStoragePaths.globalValue(provider.extension); let document = this._documents.get(revivedUri); if (!document) { @@ -1235,27 +1120,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return; } - await provider.provider.resolveNotebook(document, webComm.contentProviderComm); - } - - private provideCommToNotebookRenderers(document: ExtHostNotebookDocument, renderer: ExtHostNotebookOutputRenderer) { - let alreadyRegistered = this._renderersUsedInNotebooks.get(document); - if (!alreadyRegistered) { - alreadyRegistered = new Set(); - this._renderersUsedInNotebooks.set(document, alreadyRegistered); - } - - if (alreadyRegistered.has(renderer)) { - return; - } - - alreadyRegistered.add(renderer); - for (const editorId of this._editors.keys()) { - const comm = this._webviewComm.get(editorId); - if (comm) { - renderer.resolveNotebook(document, comm); - } - } + await provider.provider.resolveNotebook(document.notebookDocument, webComm.contentProviderComm); } async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { @@ -1292,9 +1157,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (provider.kernel) { if (cell) { - return provider.kernel.cancelCellExecution(document, cell); + return provider.kernel.cancelCellExecution(document.notebookDocument, cell.cell); } else { - return provider.kernel.cancelAllCellsExecution(document); + return provider.kernel.cancelAllCellsExecution(document.notebookDocument); } } } @@ -1319,7 +1184,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise { const document = this._documents.get(URI.revive(uri)); - if (!document || document.viewType !== viewType) { + if (!document || document.notebookDocument.viewType !== viewType) { return; } @@ -1332,9 +1197,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; if (cell) { - return withToken(token => (kernelInfo!.kernel.executeCell as any)(document, cell, token)); + return withToken(token => (kernelInfo!.kernel.executeCell as any)(document.notebookDocument, cell.cell, token)); } else { - return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document, token)); + return withToken(token => (kernelInfo!.kernel.executeAllCells as any)(document.notebookDocument, token)); } } @@ -1345,7 +1210,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (this._notebookContentProviders.has(viewType)) { - await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document, token); + await this._notebookContentProviders.get(viewType)!.provider.saveNotebook(document.notebookDocument, token); return true; } @@ -1359,7 +1224,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (this._notebookContentProviders.has(viewType)) { - await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document, token); + await this._notebookContentProviders.get(viewType)!.provider.saveNotebookAs(URI.revive(target), document.notebookDocument, token); return true; } @@ -1391,7 +1256,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const provider = this._notebookContentProviders.get(viewType); if (document && provider && provider.provider.backupNotebook) { - const backup = await provider.provider.backupNotebook(document, { destination: document.getNewBackupUri() }, cancellation); + const backup = await provider.provider.backupNotebook(document.notebookDocument, { destination: document.getNewBackupUri() }, cancellation); document.updateBackup(backup); return backup.id; } @@ -1408,11 +1273,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => { const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined; this._editors.forEach(editor => { - if (editor.editor.document === document) { + if (editor.editor.notebookData === document) { editor.editor.updateActiveKernel(kernel); } }); - this._onDidChangeActiveNotebookKernel.fire({ document, kernel }); + this._onDidChangeActiveNotebookKernel.fire({ document: document.notebookDocument, kernel }); }); } } @@ -1447,7 +1312,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const document = this._documents.get(URI.revive(uriComponents)); if (document) { // this.$acceptDirtyStateChanged(uriComponents, false); - this._onDidSaveNotebookDocument.fire(document); + this._onDidSaveNotebookDocument.fire(document.notebookDocument); } } @@ -1460,18 +1325,16 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (data.selections) { - const cells = editor.editor.document.cells; - if (data.selections.selections.length) { const firstCell = data.selections.selections[0]; - editor.editor.selection = cells.find(cell => cell.handle === firstCell); + editor.editor.selection = editor.editor.notebookData.getCell(firstCell)?.cell; } else { editor.editor.selection = undefined; } } if (data.metadata) { - editor.editor.document.metadata = { + editor.editor.notebookData.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, ...data.metadata }; @@ -1488,7 +1351,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } const editor = new ExtHostNotebookEditor( - document.viewType, + document.notebookDocument.viewType, editorId, revivedUri, this._proxy, @@ -1496,21 +1359,14 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN document ); - const cells = editor.document.cells; - if (selections.length) { const firstCell = selections[0]; - editor.selection = cells.find(cell => cell.handle === firstCell); + editor.selection = editor.notebookData.getCell(firstCell)?.cell; } else { editor.selection = undefined; } this._editors.get(editorId)?.editor.dispose(); - - for (const renderer of this._renderersUsedInNotebooks.get(document) ?? []) { - renderer.resolveNotebook(document, webComm); - } - this._editors.set(editorId, { editor }); } @@ -1525,8 +1381,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN if (document) { document.dispose(); this._documents.delete(revivedUri); - this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: document.cells.map(cell => cell.uri) }); - this._onDidCloseNotebookDocument.fire(document); + this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: document.notebookDocument.cells.map(cell => cell.uri) }); + this._onDidCloseNotebookDocument.fire(document.notebookDocument); } for (const e of this._editors.values()) { @@ -1547,10 +1403,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const revivedUri = URI.revive(modelData.uri); const viewType = modelData.viewType; const entry = this._notebookContentProviders.get(viewType); - let storageRoot: URI | undefined; - if (entry && this._extensionStoragePaths) { - storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); - } + const storageRoot = entry && (this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension)); + if (!this._documents.has(revivedUri)) { const that = this; @@ -1572,7 +1426,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._unInitializedDocuments.delete(revivedUri); if (modelData.metadata) { - document.metadata = { + document.notebookDocument.metadata = { ...notebookDocumentMetadataDefaults, ...modelData.metadata }; @@ -1589,7 +1443,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }); // add cell document as vscode.TextDocument - addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document, cell))); + addedCellDocuments.push(...modelData.cells.map(cell => ExtHostCell.asModelAddData(document.notebookDocument, cell))); this._documents.get(revivedUri)?.dispose(); this._documents.set(revivedUri, document); @@ -1604,7 +1458,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._documentsAndEditors.$acceptDocumentsAndEditorsDelta({ addedDocuments: addedCellDocuments }); const document = this._documents.get(revivedUri)!; - this._onDidOpenNotebookDocument.fire(document); + this._onDidOpenNotebookDocument.fire(document.notebookDocument); } } @@ -1683,6 +1537,24 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor); } } + + createNotebookCellStatusBarItemInternal(cell: vscode.NotebookCell, alignment: extHostTypes.NotebookCellStatusBarAlignment | undefined, priority: number | undefined) { + const statusBarItem = new NotebookCellStatusBarItemInternal(this._proxy, this._commandsConverter, cell, alignment, priority); + + // Look up the ExtHostCell for this NotebookCell URI, bind to its disposable lifecycle + const parsedUri = CellUri.parse(cell.uri); + if (parsedUri) { + const document = this._documents.get(parsedUri.notebook); + if (document) { + const cell = document.getCell(parsedUri.handle); + if (cell) { + Event.once(cell.onDidDispose)(() => statusBarItem.dispose()); + } + } + } + + return statusBarItem; + } } function hashPath(resource: URI): string { @@ -1694,3 +1566,178 @@ function isEditEvent(e: vscode.NotebookDocumentEditEvent | vscode.NotebookDocume return typeof (e as vscode.NotebookDocumentEditEvent).undo === 'function' && typeof (e as vscode.NotebookDocumentEditEvent).redo === 'function'; } + +export class NotebookCellStatusBarItemInternal extends Disposable { + private static NEXT_ID = 0; + + private readonly _id = NotebookCellStatusBarItemInternal.NEXT_ID++; + private readonly _internalCommandRegistration: DisposableStore; + + private _isDisposed = false; + private _alignment: extHostTypes.NotebookCellStatusBarAlignment; + + constructor( + private readonly _proxy: MainThreadNotebookShape, + private readonly _commands: CommandsConverter, + private readonly _cell: vscode.NotebookCell, + alignment: extHostTypes.NotebookCellStatusBarAlignment | undefined, + private _priority: number | undefined) { + super(); + this._internalCommandRegistration = this._register(new DisposableStore()); + this._alignment = alignment ?? extHostTypes.NotebookCellStatusBarAlignment.Left; + } + + private _apiItem: vscode.NotebookCellStatusBarItem | undefined; + get apiItem(): vscode.NotebookCellStatusBarItem { + if (!this._apiItem) { + this._apiItem = createNotebookCellStatusBarApiItem(this); + } + + return this._apiItem; + } + + get cell(): vscode.NotebookCell { + return this._cell; + } + + get alignment(): extHostTypes.NotebookCellStatusBarAlignment { + return this._alignment; + } + + set alignment(v: extHostTypes.NotebookCellStatusBarAlignment) { + this._alignment = v; + this.update(); + } + + get priority(): number | undefined { + return this._priority; + } + + set priority(v: number | undefined) { + this._priority = v; + this.update(); + } + + private _text: string = ''; + get text(): string { + return this._text; + } + + set text(v: string) { + this._text = v; + this.update(); + } + + private _tooltip: string | undefined; + get tooltip(): string | undefined { + return this._tooltip; + } + + set tooltip(v: string | undefined) { + this._tooltip = v; + this.update(); + } + + private _command?: { + readonly fromApi: string | vscode.Command, + readonly internal: ICommandDto, + }; + get command(): string | vscode.Command | undefined { + return this._command?.fromApi; + } + + set command(command: string | vscode.Command | undefined) { + if (this._command?.fromApi === command) { + return; + } + + this._internalCommandRegistration.clear(); + if (typeof command === 'string') { + this._command = { + fromApi: command, + internal: this._commands.toInternal({ title: '', command }, this._internalCommandRegistration), + }; + } else if (command) { + this._command = { + fromApi: command, + internal: this._commands.toInternal(command, this._internalCommandRegistration), + }; + } else { + this._command = undefined; + } + this.update(); + } + + private _accessibilityInformation: vscode.AccessibilityInformation | undefined; + get accessibilityInformation(): vscode.AccessibilityInformation | undefined { + return this._accessibilityInformation; + } + + set accessibilityInformation(v: vscode.AccessibilityInformation | undefined) { + this._accessibilityInformation = v; + this.update(); + } + + private _visible: boolean = false; + show(): void { + this._visible = true; + this.update(); + } + + hide(): void { + this._visible = false; + this.update(); + } + + dispose(): void { + this.hide(); + this._isDisposed = true; + this._internalCommandRegistration.dispose(); + } + + private update(): void { + if (this._isDisposed) { + return; + } + + const entry: INotebookCellStatusBarEntry = { + alignment: this.alignment === extHostTypes.NotebookCellStatusBarAlignment.Left ? CellStatusbarAlignment.LEFT : CellStatusbarAlignment.RIGHT, + cellResource: this.cell.uri, + command: this._command?.internal, + text: this.text, + tooltip: this.tooltip, + accessibilityInformation: this.accessibilityInformation, + priority: this.priority, + visible: this._visible + }; + + this._proxy.$setStatusBarEntry(this._id, entry); + } +} + +function createNotebookCellStatusBarApiItem(internalItem: NotebookCellStatusBarItemInternal): vscode.NotebookCellStatusBarItem { + return Object.freeze({ + cell: internalItem.cell, + get alignment() { return internalItem.alignment; }, + set alignment(v: NotebookCellStatusBarItemInternal['alignment']) { internalItem.alignment = v; }, + + get priority() { return internalItem.priority; }, + set priority(v: NotebookCellStatusBarItemInternal['priority']) { internalItem.priority = v; }, + + get text() { return internalItem.text; }, + set text(v: NotebookCellStatusBarItemInternal['text']) { internalItem.text = v; }, + + get tooltip() { return internalItem.tooltip; }, + set tooltip(v: NotebookCellStatusBarItemInternal['tooltip']) { internalItem.tooltip = v; }, + + get command() { return internalItem.command; }, + set command(v: NotebookCellStatusBarItemInternal['command']) { internalItem.command = v; }, + + get accessibilityInformation() { return internalItem.accessibilityInformation; }, + set accessibilityInformation(v: NotebookCellStatusBarItemInternal['accessibilityInformation']) { internalItem.accessibilityInformation = v; }, + + show() { internalItem.show(); }, + hide() { internalItem.hide(); }, + dispose() { internalItem.dispose(); } + }); +} diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index 8ffeda97be3..4702e1c8165 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -6,7 +6,7 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import * as vscode from 'vscode'; import { Event, Emitter } from 'vs/base/common/event'; -import { ExtHostNotebookController, ExtHostCell } from 'vs/workbench/api/common/extHostNotebook'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -21,7 +21,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD private _disposables = new DisposableStore(); private _isClosed = false; - private _cells!: ExtHostCell[]; + private _cells!: vscode.NotebookCell[]; private _cellUris!: ResourceMap; private _cellLengths!: PrefixSumComputer; private _cellLines!: PrefixSumComputer; @@ -78,7 +78,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD for (const cell of this._notebook.cells) { if (cell.cellKind === CellKind.Code && (!this._selector || score(this._selector, cell.uri, cell.language, true))) { this._cellUris.set(cell.uri, this._cells.length); - this._cells.push(cell); + this._cells.push(cell); cellLengths.push(cell.document.getText().length + 1); cellLineCounts.push(cell.document.lineCount); } diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 610e10ec65e..3fb57723234 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -269,8 +269,8 @@ export namespace TaskDTO { presentationOptions: TaskPresentationOptionsDTO.from(value.presentationOptions), problemMatchers: value.problemMatchers, hasDefinedMatchers: (value as types.Task).hasDefinedMatchers, - runOptions: (value).runOptions ? (value).runOptions : { reevaluateOnRerun: true }, - detail: (value).detail + runOptions: value.runOptions ? value.runOptions : { reevaluateOnRerun: true }, + detail: value.detail }; return result; } @@ -475,11 +475,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask return this._onDidExecuteTask.event; } - protected async resolveDefinition(uri: number | UriComponents | undefined, definition: vscode.TaskDefinition | undefined): Promise { - return definition; - } - - public async $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number): Promise { + public async $onDidStartTask(execution: tasks.TaskExecutionDTO, terminalId: number, resolvedDefinition: tasks.TaskDefinitionDTO): Promise { const customExecution: types.CustomExecution | undefined = this._providedCustomExecutions2.get(execution.id); if (customExecution) { if (this._activeCustomExecutions2.get(execution.id) !== undefined) { @@ -488,7 +484,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape, IExtHostTask // Clone the custom execution to keep the original untouched. This is important for multiple runs of the same task. this._activeCustomExecutions2.set(execution.id, customExecution); - this._terminalService.attachPtyToTerminal(terminalId, await customExecution.callback(await this.resolveDefinition(execution.task?.source.scope, execution.task?.definition))); + this._terminalService.attachPtyToTerminal(terminalId, await customExecution.callback(resolvedDefinition)); } this._lastStartedTask = execution.id; diff --git a/src/vs/workbench/api/common/extHostTextEditors.ts b/src/vs/workbench/api/common/extHostTextEditors.ts index e99f513e940..a2ba800bf5c 100644 --- a/src/vs/workbench/api/common/extHostTextEditors.ts +++ b/src/vs/workbench/api/common/extHostTextEditors.ts @@ -7,6 +7,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import * as arrays from 'vs/base/common/arrays'; import { ExtHostEditorsShape, IEditorPropertiesChangeData, IMainContext, ITextDocumentShowOptions, ITextEditorPositionData, MainContext, MainThreadTextEditorsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; import { ExtHostTextEditor, TextEditorDecorationType } from 'vs/workbench/api/common/extHostTextEditor'; import * as TypeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { TextEditorSelectionChangeKind } from 'vs/workbench/api/common/extHostTypes'; @@ -28,16 +29,15 @@ export class ExtHostEditors implements ExtHostEditorsShape { readonly onDidChangeActiveTextEditor: Event = this._onDidChangeActiveTextEditor.event; readonly onDidChangeVisibleTextEditors: Event = this._onDidChangeVisibleTextEditors.event; - - private _proxy: MainThreadTextEditorsShape; - private _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors; + private readonly _proxy: MainThreadTextEditorsShape; constructor( mainContext: IMainContext, - extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors, + private readonly _extHostNotebooks: ExtHostNotebookController, ) { this._proxy = mainContext.getProxy(MainContext.MainThreadTextEditors); - this._extHostDocumentsAndEditors = extHostDocumentsAndEditors; + this._extHostDocumentsAndEditors.onDidChangeVisibleTextEditors(e => this._onDidChangeVisibleTextEditors.fire(e)); this._extHostDocumentsAndEditors.onDidChangeActiveTextEditor(e => this._onDidChangeActiveTextEditor.fire(e)); @@ -93,7 +93,7 @@ export class ExtHostEditors implements ExtHostEditorsShape { } applyWorkspaceEdit(edit: vscode.WorkspaceEdit): Promise { - const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors); + const dto = TypeConverters.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors, this._extHostNotebooks); return this._proxy.$tryApplyWorkspaceEdit(dto); } diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 6a7993f9ae7..b27490d45a1 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -31,6 +31,7 @@ import { LogLevel as _MainLogLevel } from 'vs/platform/log/common/log'; import { coalesce, isNonEmptyArray } from 'vs/base/common/arrays'; import { RenderLineNumbersType } from 'vs/editor/common/config/editorOptions'; import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands'; +import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook'; export interface PositionLike { line: number; @@ -131,8 +132,9 @@ export namespace DiagnosticTag { return types.DiagnosticTag.Unnecessary; case MarkerTag.Deprecated: return types.DiagnosticTag.Deprecated; + default: + return undefined; } - return undefined; } } @@ -210,8 +212,9 @@ export namespace DiagnosticSeverity { return types.DiagnosticSeverity.Error; case MarkerSeverity.Hint: return types.DiagnosticSeverity.Hint; + default: + return types.DiagnosticSeverity.Error; } - return types.DiagnosticSeverity.Error; } } @@ -503,32 +506,42 @@ export namespace TextEdit { } export namespace WorkspaceEdit { - export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors): extHostProtocol.IWorkspaceEditDto { + export function from(value: vscode.WorkspaceEdit, documents?: ExtHostDocumentsAndEditors, notebooks?: ExtHostNotebookController): extHostProtocol.IWorkspaceEditDto { const result: extHostProtocol.IWorkspaceEditDto = { edits: [] }; if (value instanceof types.WorkspaceEdit) { - for (let entry of value.allEntries()) { + for (let entry of value._allEntries()) { - if (entry._type === 1) { + if (entry._type === types.FileEditType.File) { // file operation result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.File, oldUri: entry.from, newUri: entry.to, options: entry.options, metadata: entry.metadata }); - } else { + } else if (entry._type === types.FileEditType.Text) { // text edits const doc = documents?.getDocument(entry.uri); result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.Text, resource: entry.uri, edit: TextEdit.from(entry.edit), modelVersionId: doc?.version, metadata: entry.metadata }); + } else if (entry._type === types.FileEditType.Cell) { + result.edits.push({ + _type: extHostProtocol.WorkspaceEditType.Cell, + resource: entry.uri, + edit: entry.edit, + metadata: entry.metadata, + modelVersionId: notebooks?.lookupNotebookDocument(entry.uri)?.notebookDocument.version + }); } } } @@ -1253,9 +1266,9 @@ export namespace LogLevel { return _MainLogLevel.Critical; case types.LogLevel.Off: return _MainLogLevel.Off; + default: + return _MainLogLevel.Info; } - - return _MainLogLevel.Info; } export function to(mainLevel: _MainLogLevel): types.LogLevel { @@ -1274,8 +1287,8 @@ export namespace LogLevel { return types.LogLevel.Critical; case _MainLogLevel.Off: return types.LogLevel.Off; + default: + return types.LogLevel.Info; } - - return types.LogLevel.Info; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4ba62a170c5..bb8a0e560d3 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -3,17 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, equals } from 'vs/base/common/arrays'; +import { coalesceInPlace, equals } from 'vs/base/common/arrays'; import { escapeCodicons } from 'vs/base/common/codicons'; import { illegalArgument } from 'vs/base/common/errors'; import { IRelativePattern } from 'vs/base/common/glob'; import { isMarkdownString } from 'vs/base/common/htmlContent'; +import { ResourceMap } from 'vs/base/common/map'; import { startsWith } from 'vs/base/common/strings'; 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 { addIdToOutput, CellEditType, ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import type * as vscode from 'vscode'; function es5ClassCompat(target: Function): any { @@ -575,8 +577,14 @@ export interface IFileOperationOptions { recursive?: boolean; } +export const enum FileEditType { + File = 1, + Text = 2, + Cell = 3 +} + export interface IFileOperation { - _type: 1; + _type: FileEditType.File; from?: URI; to?: URI; options?: IFileOperationOptions; @@ -584,31 +592,61 @@ export interface IFileOperation { } export interface IFileTextEdit { - _type: 2; + _type: FileEditType.Text; uri: URI; edit: TextEdit; metadata?: vscode.WorkspaceEditEntryMetadata; } +export interface IFileCellEdit { + _type: FileEditType.Cell; + uri: URI; + edit: ICellEditOperation; + metadata?: vscode.WorkspaceEditEntryMetadata; +} + @es5ClassCompat export class WorkspaceEdit implements vscode.WorkspaceEdit { - private _edits = new Array(); + private readonly _edits = new Array(); + + + _allEntries(): ReadonlyArray { + return this._edits; + } + + // --- file renameFile(from: vscode.Uri, to: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from, to, options, metadata }); + this._edits.push({ _type: FileEditType.File, from, to, options, metadata }); } createFile(uri: vscode.Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from: undefined, to: uri, options, metadata }); + this._edits.push({ _type: FileEditType.File, from: undefined, to: uri, options, metadata }); } deleteFile(uri: vscode.Uri, options?: { recursive?: boolean, ignoreIfNotExists?: boolean; }, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 1, from: uri, to: undefined, options, metadata }); + this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }); } + // --- cell + + replaceCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Replace, index: start, count: end - start, cells: cells.map(cell => ({ ...cell, outputs: cell.outputs.map(output => addIdToOutput(output)) })) } }); + } + + replaceCellOutput(uri: URI, index: number, outputs: vscode.CellOutput[], metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Output, index, outputs: outputs.map(output => addIdToOutput(output)) } }); + } + + replaceCellMetadata(uri: URI, index: number, cellMetadata: vscode.NotebookCellMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.Metadata, index, metadata: cellMetadata } }); + } + + // --- text + replace(uri: URI, range: Range, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { - this._edits.push({ _type: 2, uri, edit: new TextEdit(range, newText), metadata }); + this._edits.push({ _type: FileEditType.Text, uri, edit: new TextEdit(range, newText), metadata }); } insert(resource: URI, position: Position, newText: string, metadata?: vscode.WorkspaceEditEntryMetadata): void { @@ -619,8 +657,10 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { this.replace(resource, range, '', metadata); } + // --- text (Maplike) + has(uri: URI): boolean { - return this._edits.some(edit => edit._type === 2 && edit.uri.toString() === uri.toString()); + return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()); } set(uri: URI, edits: TextEdit[]): void { @@ -628,16 +668,16 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { // remove all text edits for `uri` for (let i = 0; i < this._edits.length; i++) { const element = this._edits[i]; - if (element._type === 2 && element.uri.toString() === uri.toString()) { + if (element._type === FileEditType.Text && element.uri.toString() === uri.toString()) { this._edits[i] = undefined!; // will be coalesced down below } } - this._edits = coalesce(this._edits); + coalesceInPlace(this._edits); } else { // append edit to the end for (const edit of edits) { if (edit) { - this._edits.push({ _type: 2, uri, edit }); + this._edits.push({ _type: FileEditType.Text, uri, edit }); } } } @@ -646,7 +686,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { get(uri: URI): TextEdit[] { const res: TextEdit[] = []; for (let candidate of this._edits) { - if (candidate._type === 2 && candidate.uri.toString() === uri.toString()) { + if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { res.push(candidate.edit); } } @@ -654,13 +694,13 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { } entries(): [URI, TextEdit[]][] { - const textEdits = new Map(); + const textEdits = new ResourceMap<[URI, TextEdit[]]>(); for (let candidate of this._edits) { - if (candidate._type === 2) { - let textEdit = textEdits.get(candidate.uri.toString()); + if (candidate._type === FileEditType.Text) { + let textEdit = textEdits.get(candidate.uri); if (!textEdit) { textEdit = [candidate.uri, []]; - textEdits.set(candidate.uri.toString(), textEdit); + textEdits.set(candidate.uri, textEdit); } textEdit[1].push(candidate.edit); } @@ -668,22 +708,6 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit { return [...textEdits.values()]; } - allEntries(): ReadonlyArray { - return this._edits; - } - - // _allEntries(): ([URI, TextEdit] | [URI?, URI?, IFileOperationOptions?])[] { - // const res: ([URI, TextEdit] | [URI?, URI?, IFileOperationOptions?])[] = []; - // for (let edit of this._edits) { - // if (edit._type === 1) { - // res.push([edit.from, edit.to, edit.options]); - // } else { - // res.push([edit.uri, edit.edit]); - // } - // } - // return res; - // } - get size(): number { return this.entries().length; } @@ -1831,26 +1855,26 @@ export enum TaskScope { Workspace = 2 } -export class CustomExecution implements vscode.CustomExecution2 { - private _callback: (resolvedDefintion?: vscode.TaskDefinition) => Thenable; - constructor(callback: (resolvedDefintion?: vscode.TaskDefinition) => Thenable) { +export class CustomExecution implements vscode.CustomExecution { + private _callback: (resolvedDefintion: vscode.TaskDefinition) => Thenable; + constructor(callback: (resolvedDefintion: vscode.TaskDefinition) => Thenable) { this._callback = callback; } public computeId(): string { return 'customExecution' + generateUuid(); } - public set callback(value: (resolvedDefintion?: vscode.TaskDefinition) => Thenable) { + public set callback(value: (resolvedDefintion: vscode.TaskDefinition) => Thenable) { this._callback = value; } - public get callback(): ((resolvedDefintion?: vscode.TaskDefinition) => Thenable) { + public get callback(): ((resolvedDefintion: vscode.TaskDefinition) => Thenable) { return this._callback; } } @es5ClassCompat -export class Task implements vscode.Task2 { +export class Task implements vscode.Task { private static ExtensionCallbackType: string = 'customExecution'; private static ProcessType: string = 'process'; @@ -2294,6 +2318,12 @@ export class DebugAdapterServer implements vscode.DebugAdapterServer { } } +@es5ClassCompat +export class DebugAdapterNamedPipeServer implements vscode.DebugAdapterNamedPipeServer { + constructor(public readonly path: string) { + } +} + @es5ClassCompat export class DebugAdapterInlineImplementation implements vscode.DebugAdapterInlineImplementation { readonly implementation: vscode.DebugAdapter; @@ -2750,6 +2780,12 @@ export enum NotebookRunState { Idle = 2 } +export enum NotebookCellStatusBarAlignment { + Left = 1, + Right = 2 +} + + //#endregion //#region Timeline diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index f005de99781..e5dc3c670d5 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -3,34 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { hash } from 'vs/base/common/hash'; -import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { Schemas } from 'vs/base/common/network'; -import { joinPath } from 'vs/base/common/resources'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { generateUuid } from 'vs/base/common/uuid'; +import { URI } from 'vs/base/common/uri'; import * as modes from 'vs/editor/common/modes'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; -import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; -import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; import type * as vscode from 'vscode'; -import { Cache } from './cache'; import * as extHostProtocol from './extHost.protocol'; -import * as extHostTypes from './extHostTypes'; - -type IconPath = URI | { light: URI, dark: URI }; export class ExtHostWebview implements vscode.Webview { - readonly #handle: extHostProtocol.WebviewPanelHandle; + readonly #handle: extHostProtocol.WebviewHandle; readonly #proxy: extHostProtocol.MainThreadWebviewsShape; readonly #deprecationService: IExtHostApiDeprecationService; @@ -44,7 +30,7 @@ export class ExtHostWebview implements vscode.Webview { #hasCalledAsWebviewUri = false; constructor( - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, proxy: extHostProtocol.MainThreadWebviewsShape, options: vscode.WebviewOptions, initData: WebviewInitData, @@ -64,7 +50,13 @@ export class ExtHostWebview implements vscode.Webview { /* internal */ readonly _onMessageEmitter = new Emitter(); public readonly onDidReceiveMessage: Event = this._onMessageEmitter.event; + readonly #onDidDisposeEmitter = new Emitter(); + /* internal */ readonly _onDidDispose: Event = this.#onDidDisposeEmitter.event; + public dispose() { + this.#onDidDisposeEmitter.fire(); + + this.#onDidDisposeEmitter.dispose(); this._onMessageEmitter.dispose(); } @@ -119,302 +111,11 @@ export class ExtHostWebview implements vscode.Webview { } } -export class ExtHostWebviewEditor extends Disposable implements vscode.WebviewPanel { - - readonly #handle: extHostProtocol.WebviewPanelHandle; - readonly #proxy: extHostProtocol.MainThreadWebviewsShape; - readonly #viewType: string; - - readonly #webview: ExtHostWebview; - readonly #options: vscode.WebviewPanelOptions; - - #title: string; - #iconPath?: IconPath; - #viewColumn: vscode.ViewColumn | undefined = undefined; - #visible: boolean = true; - #active: boolean = true; - #isDisposed: boolean = false; - - readonly #onDidDispose = this._register(new Emitter()); - public readonly onDidDispose = this.#onDidDispose.event; - - readonly #onDidChangeViewState = this._register(new Emitter()); - public readonly onDidChangeViewState = this.#onDidChangeViewState.event; - - constructor( - handle: extHostProtocol.WebviewPanelHandle, - proxy: extHostProtocol.MainThreadWebviewsShape, - viewType: string, - title: string, - viewColumn: vscode.ViewColumn | undefined, - editorOptions: vscode.WebviewPanelOptions, - webview: ExtHostWebview - ) { - super(); - this.#handle = handle; - this.#proxy = proxy; - this.#viewType = viewType; - this.#options = editorOptions; - this.#viewColumn = viewColumn; - this.#title = title; - this.#webview = webview; - } - - public dispose() { - if (this.#isDisposed) { - return; - } - - this.#isDisposed = true; - this.#onDidDispose.fire(); - this.#proxy.$disposeWebview(this.#handle); - this.#webview.dispose(); - - super.dispose(); - } - - get webview() { - this.assertNotDisposed(); - return this.#webview; - } - - get viewType(): string { - this.assertNotDisposed(); - return this.#viewType; - } - - get title(): string { - this.assertNotDisposed(); - return this.#title; - } - - set title(value: string) { - this.assertNotDisposed(); - if (this.#title !== value) { - this.#title = value; - this.#proxy.$setTitle(this.#handle, value); - } - } - - get iconPath(): IconPath | undefined { - this.assertNotDisposed(); - return this.#iconPath; - } - - set iconPath(value: IconPath | undefined) { - this.assertNotDisposed(); - if (this.#iconPath !== value) { - this.#iconPath = value; - - this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); - } - } - - get options() { - return this.#options; - } - - get viewColumn(): vscode.ViewColumn | undefined { - this.assertNotDisposed(); - if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { - // We are using a symbolic view column - // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. - return undefined; - } - return this.#viewColumn; - } - - public get active(): boolean { - this.assertNotDisposed(); - return this.#active; - } - - public get visible(): boolean { - this.assertNotDisposed(); - return this.#visible; - } - - _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { - if (this.#isDisposed) { - return; - } - - if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { - this.#active = newState.active; - this.#visible = newState.visible; - this.#viewColumn = newState.viewColumn; - this.#onDidChangeViewState.fire({ webviewPanel: this }); - } - } - - public postMessage(message: any): Promise { - this.assertNotDisposed(); - return this.#proxy.$postMessage(this.#handle, message); - } - - public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { - this.assertNotDisposed(); - this.#proxy.$reveal(this.#handle, { - viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, - preserveFocus: !!preserveFocus - }); - } - - private assertNotDisposed() { - if (this.#isDisposed) { - throw new Error('Webview is disposed'); - } - } -} - -class CustomDocumentStoreEntry { - - private _backupCounter = 1; - - constructor( - public readonly document: vscode.CustomDocument, - private readonly _storagePath: URI | undefined, - ) { } - - private readonly _edits = new Cache('custom documents'); - - private _backup?: vscode.CustomDocumentBackup; - - addEdit(item: vscode.CustomDocumentEditEvent): number { - return this._edits.add([item]); - } - - async undo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).undo(); - if (!isDirty) { - this.disposeBackup(); - } - } - - async redo(editId: number, isDirty: boolean): Promise { - await this.getEdit(editId).redo(); - if (!isDirty) { - this.disposeBackup(); - } - } - - disposeEdits(editIds: number[]): void { - for (const id of editIds) { - this._edits.delete(id); - } - } - - getNewBackupUri(): URI { - if (!this._storagePath) { - throw new Error('Backup requires a valid storage path'); - } - const fileName = hashPath(this.document.uri) + (this._backupCounter++); - return joinPath(this._storagePath, fileName); - } - - updateBackup(backup: vscode.CustomDocumentBackup): void { - this._backup?.delete(); - this._backup = backup; - } - - disposeBackup(): void { - this._backup?.delete(); - this._backup = undefined; - } - - private getEdit(editId: number): vscode.CustomDocumentEditEvent { - const edit = this._edits.get(editId, 0); - if (!edit) { - throw new Error('No edit found'); - } - return edit; - } -} - -class CustomDocumentStore { - private readonly _documents = new Map(); - - public get(viewType: string, resource: vscode.Uri): CustomDocumentStoreEntry | undefined { - return this._documents.get(this.key(viewType, resource)); - } - - public add(viewType: string, document: vscode.CustomDocument, storagePath: URI | undefined): CustomDocumentStoreEntry { - const key = this.key(viewType, document.uri); - if (this._documents.has(key)) { - throw new Error(`Document already exists for viewType:${viewType} resource:${document.uri}`); - } - const entry = new CustomDocumentStoreEntry(document, storagePath); - this._documents.set(key, entry); - return entry; - } - - public delete(viewType: string, document: vscode.CustomDocument) { - const key = this.key(viewType, document.uri); - this._documents.delete(key); - } - - private key(viewType: string, resource: vscode.Uri): string { - return `${viewType}@@@${resource}`; - } - -} - -const enum WebviewEditorType { - Text, - Custom -} - -type ProviderEntry = { - readonly extension: IExtensionDescription; - readonly type: WebviewEditorType.Text; - readonly provider: vscode.CustomTextEditorProvider; -} | { - readonly extension: IExtensionDescription; - readonly type: WebviewEditorType.Custom; - readonly provider: vscode.CustomReadonlyEditorProvider; -}; - -class EditorProviderStore { - private readonly _providers = new Map(); - - public addTextProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider): vscode.Disposable { - return this.add(WebviewEditorType.Text, viewType, extension, provider); - } - - public addCustomProvider(viewType: string, extension: IExtensionDescription, provider: vscode.CustomReadonlyEditorProvider): vscode.Disposable { - return this.add(WebviewEditorType.Custom, viewType, extension, provider); - } - - public get(viewType: string): ProviderEntry | undefined { - return this._providers.get(viewType); - } - - private add(type: WebviewEditorType, viewType: string, extension: IExtensionDescription, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider): vscode.Disposable { - if (this._providers.has(viewType)) { - throw new Error(`Provider for viewType:${viewType} already registered`); - } - this._providers.set(viewType, { type, extension, provider } as ProviderEntry); - return new extHostTypes.Disposable(() => this._providers.delete(viewType)); - } -} - export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { - private static newHandle(): extHostProtocol.WebviewPanelHandle { - return generateUuid(); - } + private readonly _webviewProxy: extHostProtocol.MainThreadWebviewsShape; - private readonly _proxy: extHostProtocol.MainThreadWebviewsShape; - private readonly _webviewPanels = new Map(); - - private readonly _serializers = new Map(); - - private readonly _editorProviders = new EditorProviderStore(); - - private readonly _documents = new CustomDocumentStore(); + private readonly _webviews = new Map(); constructor( mainContext: extHostProtocol.IMainContext, @@ -422,342 +123,50 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { private readonly workspace: IExtHostWorkspace | undefined, private readonly _logService: ILogService, private readonly _deprecationService: IExtHostApiDeprecationService, - private readonly _extHostDocuments: ExtHostDocuments, - private readonly _extensionStoragePaths?: IExtensionStoragePaths, ) { - this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); - } - - public createWebviewPanel( - extension: IExtensionDescription, - viewType: string, - title: string, - showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, - options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, - ): vscode.WebviewPanel { - const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; - const webviewShowOptions = { - viewColumn: typeConverters.ViewColumn.from(viewColumn), - preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus - }; - - const handle = ExtHostWebviews.newHandle(); - this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); - - const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService); - const panel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, viewColumn, options, webview); - this._webviewPanels.set(handle, panel); - return panel; - } - - public registerWebviewPanelSerializer( - extension: IExtensionDescription, - viewType: string, - serializer: vscode.WebviewPanelSerializer - ): vscode.Disposable { - if (this._serializers.has(viewType)) { - throw new Error(`Serializer for '${viewType}' already registered`); - } - - this._serializers.set(viewType, { serializer, extension }); - this._proxy.$registerSerializer(viewType); - - return new extHostTypes.Disposable(() => { - this._serializers.delete(viewType); - this._proxy.$unregisterSerializer(viewType); - }); - } - - public registerCustomEditorProvider( - extension: IExtensionDescription, - viewType: string, - provider: vscode.CustomReadonlyEditorProvider | vscode.CustomTextEditorProvider, - options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean }, - ): vscode.Disposable { - const disposables = new DisposableStore(); - if ('resolveCustomTextEditor' in provider) { - disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider)); - this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, { - supportsMove: !!provider.moveCustomTextEditor, - }); - } else { - disposables.add(this._editorProviders.addCustomProvider(viewType, extension, provider)); - - if (this.supportEditing(provider)) { - disposables.add(provider.onDidChangeCustomDocument(e => { - const entry = this.getCustomDocumentEntry(viewType, e.document.uri); - if (isEditEvent(e)) { - const editId = entry.addEdit(e); - this._proxy.$onDidEdit(e.document.uri, viewType, editId, e.label); - } else { - this._proxy.$onContentChange(e.document.uri, viewType); - } - })); - } - - this._proxy.$registerCustomEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, !!options.supportsMultipleEditorsPerDocument); - } - - return extHostTypes.Disposable.from( - disposables, - new extHostTypes.Disposable(() => { - this._proxy.$unregisterEditorProvider(viewType); - })); + this._webviewProxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviews); } public $onMessage( - handle: extHostProtocol.WebviewPanelHandle, + handle: extHostProtocol.WebviewHandle, message: any ): void { - const panel = this.getWebviewPanel(handle); - if (panel) { - panel.webview._onMessageEmitter.fire(message); + const webview = this.getWebview(handle); + if (webview) { + webview._onMessageEmitter.fire(message); } } public $onMissingCsp( - _handle: extHostProtocol.WebviewPanelHandle, + _handle: extHostProtocol.WebviewHandle, extensionId: string ): void { this._logService.warn(`${extensionId} created a webview without a content security policy: https://aka.ms/vscode-webview-missing-csp`); } - public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { - const handles = Object.keys(newStates); - // Notify webviews of state changes in the following order: - // - Non-visible - // - Visible - // - Active - handles.sort((a, b) => { - const stateA = newStates[a]; - const stateB = newStates[b]; - if (stateA.active) { - return 1; - } - if (stateB.active) { - return -1; - } - return (+stateA.visible) - (+stateB.visible); - }); + public createNewWebview(handle: string, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, extension: IExtensionDescription): ExtHostWebview { + const webview = new ExtHostWebview(handle, this._webviewProxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); + this._webviews.set(handle, webview); - for (const handle of handles) { - const panel = this.getWebviewPanel(handle); - if (!panel) { - continue; - } + webview._onDidDispose(() => { this._webviews.delete(handle); }); - const newState = newStates[handle]; - panel._updateViewState({ - active: newState.active, - visible: newState.visible, - viewColumn: typeConverters.ViewColumn.to(newState.position), - }); - } + return webview; } - async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): Promise { - const panel = this.getWebviewPanel(handle); - if (panel) { - panel.dispose(); - this._webviewPanels.delete(handle); - } + public deleteWebview(handle: string) { + this._webviews.delete(handle); } - async $deserializeWebviewPanel( - webviewHandle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - state: any, - position: EditorViewColumn, - options: modes.IWebviewOptions & modes.IWebviewPanelOptions - ): Promise { - const entry = this._serializers.get(viewType); - if (!entry) { - throw new Error(`No serializer found for '${viewType}'`); - } - const { serializer, extension } = entry; - - const webview = new ExtHostWebview(webviewHandle, this._proxy, reviveOptions(options), this.initData, this.workspace, extension, this._deprecationService); - const revivedPanel = new ExtHostWebviewEditor(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(webviewHandle, revivedPanel); - await serializer.deserializeWebviewPanel(revivedPanel, state); - } - - async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - if (entry.type !== WebviewEditorType.Custom) { - throw new Error(`Invalid provide type for '${viewType}'`); - } - - const revivedResource = URI.revive(resource); - const document = await entry.provider.openCustomDocument(revivedResource, { backupId }, cancellation); - - let storageRoot: URI | undefined; - if (this.supportEditing(entry.provider) && this._extensionStoragePaths) { - storageRoot = this._extensionStoragePaths.workspaceValue(entry.extension) ?? this._extensionStoragePaths.globalValue(entry.extension); - } - this._documents.add(viewType, document, storageRoot); - - return { editable: this.supportEditing(entry.provider) }; - } - - async $disposeCustomDocument(resource: UriComponents, viewType: string): Promise { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - if (entry.type !== WebviewEditorType.Custom) { - throw new Error(`Invalid provider type for '${viewType}'`); - } - - const revivedResource = URI.revive(resource); - const { document } = this.getCustomDocumentEntry(viewType, revivedResource); - this._documents.delete(viewType, document); - document.dispose(); - } - - async $resolveWebviewEditor( - resource: UriComponents, - handle: extHostProtocol.WebviewPanelHandle, - viewType: string, - title: string, - position: EditorViewColumn, - options: modes.IWebviewOptions & modes.IWebviewPanelOptions, - cancellation: CancellationToken, - ): Promise { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - const webview = new ExtHostWebview(handle, this._proxy, reviveOptions(options), this.initData, this.workspace, entry.extension, this._deprecationService); - const revivedPanel = new ExtHostWebviewEditor(handle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); - this._webviewPanels.set(handle, revivedPanel); - - const revivedResource = URI.revive(resource); - - switch (entry.type) { - case WebviewEditorType.Custom: - { - const { document } = this.getCustomDocumentEntry(viewType, revivedResource); - return entry.provider.resolveCustomEditor(document, revivedPanel, cancellation); - } - case WebviewEditorType.Text: - { - const document = this._extHostDocuments.getDocument(revivedResource); - return entry.provider.resolveCustomTextEditor(document, revivedPanel, cancellation); - } - default: - { - throw new Error('Unknown webview provider type'); - } - } - } - - $disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void { - const document = this.getCustomDocumentEntry(viewType, resourceComponents); - document.disposeEdits(editIds); - } - - async $onMoveCustomEditor(handle: string, newResourceComponents: UriComponents, viewType: string): Promise { - const entry = this._editorProviders.get(viewType); - if (!entry) { - throw new Error(`No provider found for '${viewType}'`); - } - - if (!(entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor) { - throw new Error(`Provider does not implement move '${viewType}'`); - } - - const webview = this.getWebviewPanel(handle); - if (!webview) { - throw new Error(`No webview found`); - } - - const resource = URI.revive(newResourceComponents); - const document = this._extHostDocuments.getDocument(resource); - await (entry.provider as vscode.CustomTextEditorProvider).moveCustomTextEditor!(document, webview, CancellationToken.None); - } - - async $undo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - return entry.undo(editId, isDirty); - } - - async $redo(resourceComponents: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - return entry.redo(editId, isDirty); - } - - async $revert(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - await provider.revertCustomDocument(entry.document, cancellation); - entry.disposeBackup(); - } - - async $onSave(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - await provider.saveCustomDocument(entry.document, cancellation); - entry.disposeBackup(); - } - - async $onSaveAs(resourceComponents: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - return provider.saveCustomDocumentAs(entry.document, URI.revive(targetResource), cancellation); - } - - async $backup(resourceComponents: UriComponents, viewType: string, cancellation: CancellationToken): Promise { - const entry = this.getCustomDocumentEntry(viewType, resourceComponents); - const provider = this.getCustomEditorProvider(viewType); - - const backup = await provider.backupCustomDocument(entry.document, { - destination: entry.getNewBackupUri(), - }, cancellation); - entry.updateBackup(backup); - return backup.id; - } - - private getWebviewPanel(handle: extHostProtocol.WebviewPanelHandle): ExtHostWebviewEditor | undefined { - return this._webviewPanels.get(handle); - } - - private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry { - const entry = this._documents.get(viewType, URI.revive(resource)); - if (!entry) { - throw new Error('No custom document found'); - } - return entry; - } - - private getCustomEditorProvider(viewType: string): vscode.CustomEditorProvider { - const entry = this._editorProviders.get(viewType); - const provider = entry?.provider; - if (!provider || !this.supportEditing(provider)) { - throw new Error('Custom document is not editable'); - } - return provider; - } - - private supportEditing( - provider: vscode.CustomTextEditorProvider | vscode.CustomEditorProvider | vscode.CustomReadonlyEditorProvider - ): provider is vscode.CustomEditorProvider { - return !!(provider as vscode.CustomEditorProvider).onDidChangeCustomDocument; + private getWebview(handle: extHostProtocol.WebviewHandle): ExtHostWebview | undefined { + return this._webviews.get(handle); } } -function toExtensionData(extension: IExtensionDescription): extHostProtocol.WebviewExtensionDescription { +export function toExtensionData(extension: IExtensionDescription): extHostProtocol.WebviewExtensionDescription { return { id: extension.identifier, location: extension.extensionLocation }; } -function convertWebviewOptions( +export function convertWebviewOptions( extension: IExtensionDescription, workspace: IExtHostWorkspace | undefined, options: vscode.WebviewPanelOptions & vscode.WebviewOptions, @@ -786,13 +195,3 @@ function getDefaultLocalResourceRoots( extension.extensionLocation, ]; } - -function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent { - return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function' - && typeof (e as vscode.CustomDocumentEditEvent).redo === 'function'; -} - -function hashPath(resource: URI): string { - const str = resource.scheme === Schemas.file || resource.scheme === Schemas.untitled ? resource.fsPath : resource.toString(); - return hash(str) + ''; -} diff --git a/src/vs/workbench/api/common/extHostWebviewPanels.ts b/src/vs/workbench/api/common/extHostWebviewPanels.ts new file mode 100644 index 00000000000..d29fdc631fa --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewPanels.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { generateUuid } from 'vs/base/common/uuid'; +import * as modes from 'vs/editor/common/modes'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { convertWebviewOptions, ExtHostWebview, ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview'; +import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + + +type IconPath = URI | { light: URI, dark: URI }; + +class ExtHostWebviewPanel extends Disposable implements vscode.WebviewPanel { + + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewPanelsShape; + readonly #viewType: string; + + readonly #webview: ExtHostWebview; + readonly #options: vscode.WebviewPanelOptions; + + #title: string; + #iconPath?: IconPath; + #viewColumn: vscode.ViewColumn | undefined = undefined; + #visible: boolean = true; + #active: boolean = true; + #isDisposed: boolean = false; + + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + readonly #onDidChangeViewState = this._register(new Emitter()); + public readonly onDidChangeViewState = this.#onDidChangeViewState.event; + + constructor( + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewPanelsShape, + viewType: string, + title: string, + viewColumn: vscode.ViewColumn | undefined, + editorOptions: vscode.WebviewPanelOptions, + webview: ExtHostWebview + ) { + super(); + this.#handle = handle; + this.#proxy = proxy; + this.#viewType = viewType; + this.#options = editorOptions; + this.#viewColumn = viewColumn; + this.#title = title; + this.#webview = webview; + } + + public dispose() { + if (this.#isDisposed) { + return; + } + + this.#isDisposed = true; + this.#onDidDispose.fire(); + + this.#proxy.$disposeWebview(this.#handle); + this.#webview.dispose(); + + super.dispose(); + } + + get webview() { + this.assertNotDisposed(); + return this.#webview; + } + + get viewType(): string { + this.assertNotDisposed(); + return this.#viewType; + } + + get title(): string { + this.assertNotDisposed(); + return this.#title; + } + + set title(value: string) { + this.assertNotDisposed(); + if (this.#title !== value) { + this.#title = value; + this.#proxy.$setTitle(this.#handle, value); + } + } + + get iconPath(): IconPath | undefined { + this.assertNotDisposed(); + return this.#iconPath; + } + + set iconPath(value: IconPath | undefined) { + this.assertNotDisposed(); + if (this.#iconPath !== value) { + this.#iconPath = value; + + this.#proxy.$setIconPath(this.#handle, URI.isUri(value) ? { light: value, dark: value } : value); + } + } + + get options() { + return this.#options; + } + + get viewColumn(): vscode.ViewColumn | undefined { + this.assertNotDisposed(); + if (typeof this.#viewColumn === 'number' && this.#viewColumn < 0) { + // We are using a symbolic view column + // Return undefined instead to indicate that the real view column is currently unknown but will be resolved. + return undefined; + } + return this.#viewColumn; + } + + public get active(): boolean { + this.assertNotDisposed(); + return this.#active; + } + + public get visible(): boolean { + this.assertNotDisposed(); + return this.#visible; + } + + _updateViewState(newState: { active: boolean; visible: boolean; viewColumn: vscode.ViewColumn; }) { + if (this.#isDisposed) { + return; + } + + if (this.active !== newState.active || this.visible !== newState.visible || this.viewColumn !== newState.viewColumn) { + this.#active = newState.active; + this.#visible = newState.visible; + this.#viewColumn = newState.viewColumn; + this.#onDidChangeViewState.fire({ webviewPanel: this }); + } + } + + public reveal(viewColumn?: vscode.ViewColumn, preserveFocus?: boolean): void { + this.assertNotDisposed(); + this.#proxy.$reveal(this.#handle, { + viewColumn: viewColumn ? typeConverters.ViewColumn.from(viewColumn) : undefined, + preserveFocus: !!preserveFocus + }); + } + + private assertNotDisposed() { + if (this.#isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanelsShape { + + private static newHandle(): extHostProtocol.WebviewHandle { + return generateUuid(); + } + + private readonly _proxy: extHostProtocol.MainThreadWebviewPanelsShape; + + private readonly _webviewPanels = new Map(); + + private readonly _serializers = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly webviews: ExtHostWebviews, + private readonly workspace: IExtHostWorkspace | undefined, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewPanels); + } + + public createWebviewPanel( + extension: IExtensionDescription, + viewType: string, + title: string, + showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, + options: (vscode.WebviewPanelOptions & vscode.WebviewOptions) = {}, + ): vscode.WebviewPanel { + const viewColumn = typeof showOptions === 'object' ? showOptions.viewColumn : showOptions; + const webviewShowOptions = { + viewColumn: typeConverters.ViewColumn.from(viewColumn), + preserveFocus: typeof showOptions === 'object' && !!showOptions.preserveFocus + }; + + const handle = ExtHostWebviewPanels.newHandle(); + this._proxy.$createWebviewPanel(toExtensionData(extension), handle, viewType, title, webviewShowOptions, convertWebviewOptions(extension, this.workspace, options)); + + const webview = this.webviews.createNewWebview(handle, options, extension); + const panel = this.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview); + + return panel; + } + + public $onDidChangeWebviewPanelViewStates(newStates: extHostProtocol.WebviewPanelViewStateData): void { + const handles = Object.keys(newStates); + // Notify webviews of state changes in the following order: + // - Non-visible + // - Visible + // - Active + handles.sort((a, b) => { + const stateA = newStates[a]; + const stateB = newStates[b]; + if (stateA.active) { + return 1; + } + if (stateB.active) { + return -1; + } + return (+stateA.visible) - (+stateB.visible); + }); + + for (const handle of handles) { + const panel = this.getWebviewPanel(handle); + if (!panel) { + continue; + } + + const newState = newStates[handle]; + panel._updateViewState({ + active: newState.active, + visible: newState.visible, + viewColumn: typeConverters.ViewColumn.to(newState.position), + }); + } + } + + async $onDidDisposeWebviewPanel(handle: extHostProtocol.WebviewHandle): Promise { + const panel = this.getWebviewPanel(handle); + panel?.dispose(); + + this._webviewPanels.delete(handle); + this.webviews.deleteWebview(handle); + } + + public registerWebviewPanelSerializer( + extension: IExtensionDescription, + viewType: string, + serializer: vscode.WebviewPanelSerializer + ): vscode.Disposable { + if (this._serializers.has(viewType)) { + throw new Error(`Serializer for '${viewType}' already registered`); + } + + this._serializers.set(viewType, { serializer, extension }); + this._proxy.$registerSerializer(viewType); + + return new extHostTypes.Disposable(() => { + this._serializers.delete(viewType); + this._proxy.$unregisterSerializer(viewType); + }); + } + + async $deserializeWebviewPanel( + webviewHandle: extHostProtocol.WebviewHandle, + viewType: string, + title: string, + state: any, + position: EditorViewColumn, + options: modes.IWebviewOptions & modes.IWebviewPanelOptions + ): Promise { + const entry = this._serializers.get(viewType); + if (!entry) { + throw new Error(`No serializer found for '${viewType}'`); + } + const { serializer, extension } = entry; + + const webview = this.webviews.createNewWebview(webviewHandle, options, extension); + const revivedPanel = this.createNewWebviewPanel(webviewHandle, viewType, title, position, options, webview); + await serializer.deserializeWebviewPanel(revivedPanel, state); + } + + public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) { + const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview); + this._webviewPanels.set(webviewHandle, panel); + return panel; + } + + public getWebviewPanel(handle: extHostProtocol.WebviewHandle): ExtHostWebviewPanel | undefined { + return this._webviewPanels.get(handle); + } +} diff --git a/src/vs/workbench/api/common/extHostWebviewView.ts b/src/vs/workbench/api/common/extHostWebviewView.ts new file mode 100644 index 00000000000..cc23d2a1fcd --- /dev/null +++ b/src/vs/workbench/api/common/extHostWebviewView.ts @@ -0,0 +1,176 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { ExtHostWebview, ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol'; +import * as extHostTypes from './extHostTypes'; + +class ExtHostWebviewView extends Disposable implements vscode.WebviewView { + + readonly #handle: extHostProtocol.WebviewHandle; + readonly #proxy: extHostProtocol.MainThreadWebviewViewsShape; + + readonly #viewType: string; + readonly #webview: ExtHostWebview; + + #isDisposed = false; + #isVisible: boolean; + #title: string | undefined; + + constructor( + handle: extHostProtocol.WebviewHandle, + proxy: extHostProtocol.MainThreadWebviewViewsShape, + viewType: string, + webview: ExtHostWebview, + isVisible: boolean, + ) { + super(); + + this.#viewType = viewType; + this.#handle = handle; + this.#proxy = proxy; + this.#webview = webview; + this.#isVisible = isVisible; + } + + public dispose() { + if (this.#isDisposed) { + return; + } + + this.#isDisposed = true; + this.#onDidDispose.fire(); + + super.dispose(); + } + + readonly #onDidChangeVisibility = this._register(new Emitter()); + public readonly onDidChangeVisibility = this.#onDidChangeVisibility.event; + + readonly #onDidDispose = this._register(new Emitter()); + public readonly onDidDispose = this.#onDidDispose.event; + + public get title(): string | undefined { + this.assertNotDisposed(); + return this.#title; + } + + public set title(value: string | undefined) { + this.assertNotDisposed(); + if (this.#title !== value) { + this.#title = value; + this.#proxy.$setWebviewViewTitle(this.#handle, value); + } + } + + public get visible(): boolean { return this.#isVisible; } + + public get webview(): vscode.Webview { return this.#webview; } + + public get viewType(): string { return this.#viewType; } + + /* internal */ _setVisible(visible: boolean) { + if (visible === this.#isVisible) { + return; + } + + this.#isVisible = visible; + this.#onDidChangeVisibility.fire(); + } + + private assertNotDisposed() { + if (this.#isDisposed) { + throw new Error('Webview is disposed'); + } + } +} + +export class ExtHostWebviewViews implements extHostProtocol.ExtHostWebviewViewsShape { + + private readonly _proxy: extHostProtocol.MainThreadWebviewViewsShape; + + private readonly _viewProviders = new Map(); + + private readonly _webviewViews = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext, + private readonly _extHostWebview: ExtHostWebviews, + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadWebviewViews); + } + + public registerWebviewViewProvider( + extension: IExtensionDescription, + viewType: string, + provider: vscode.WebviewViewProvider, + webviewOptions?: { + retainContextWhenHidden?: boolean + }, + ): vscode.Disposable { + if (this._viewProviders.has(viewType)) { + throw new Error(`View provider for '${viewType}' already registered`); + } + + this._viewProviders.set(viewType, { provider, extension }); + this._proxy.$registerWebviewViewProvider(viewType, webviewOptions); + + return new extHostTypes.Disposable(() => { + this._viewProviders.delete(viewType); + this._proxy.$unregisterWebviewViewProvider(viewType); + }); + } + + async $resolveWebviewView( + webviewHandle: string, + viewType: string, + state: any, + cancellation: CancellationToken, + ): Promise { + const entry = this._viewProviders.get(viewType); + if (!entry) { + throw new Error(`No view provider found for '${viewType}'`); + } + + const { provider, extension } = entry; + + const webview = this._extHostWebview.createNewWebview(webviewHandle, { /* todo */ }, extension); + const revivedView = new ExtHostWebviewView(webviewHandle, this._proxy, viewType, webview, true); + + this._webviewViews.set(webviewHandle, revivedView); + + await provider.resolveWebviewView(revivedView, { state }, cancellation); + } + + async $onDidChangeWebviewViewVisibility( + webviewHandle: string, + visible: boolean + ) { + const webviewView = this.getWebviewView(webviewHandle); + webviewView._setVisible(visible); + } + + async $disposeWebviewView(webviewHandle: string) { + const webviewView = this.getWebviewView(webviewHandle); + this._webviewViews.delete(webviewHandle); + webviewView.dispose(); + } + + private getWebviewView(handle: string): ExtHostWebviewView { + const entry = this._webviewViews.get(handle); + if (!entry) { + throw new Error('No webview found'); + } + return entry; + } +} diff --git a/src/vs/workbench/api/node/extHostDebugService.ts b/src/vs/workbench/api/node/extHostDebugService.ts index 7d3cc3b33c3..4206b06776a 100644 --- a/src/vs/workbench/api/node/extHostDebugService.ts +++ b/src/vs/workbench/api/node/extHostDebugService.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import type * as vscode from 'vscode'; import * as env from 'vs/base/common/platform'; import { DebugAdapterExecutable } from 'vs/workbench/api/common/extHostTypes'; -import { ExecutableDebugAdapter, SocketDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; +import { ExecutableDebugAdapter, SocketDebugAdapter, NamedPipeDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; import { AbstractDebugAdapter } from 'vs/workbench/contrib/debug/common/abstractDebugAdapter'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService'; @@ -49,6 +49,8 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase { switch (adapter.type) { case 'server': return new SocketDebugAdapter(adapter); + case 'pipeServer': + return new NamedPipeDebugAdapter(adapter); case 'executable': return new ExecutableDebugAdapter(adapter, session.type); } diff --git a/src/vs/workbench/api/node/extHostTask.ts b/src/vs/workbench/api/node/extHostTask.ts index 311a85fa2b2..01fc86aeccf 100644 --- a/src/vs/workbench/api/node/extHostTask.ts +++ b/src/vs/workbench/api/node/extHostTask.ts @@ -11,7 +11,6 @@ import * as types from 'vs/workbench/api/common/extHostTypes'; import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import type * as vscode from 'vscode'; import * as tasks from '../common/shared/tasks'; -import * as Objects from 'vs/base/common/objects'; import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService'; import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration'; @@ -100,7 +99,7 @@ export class ExtHostTask extends ExtHostTaskBase { // The ID is calculated on the main thread task side, so, let's call into it here. // We need the task id's pre-computed for custom task executions because when OnDidStartTask // is invoked, we have to be able to map it back to our data. - taskIdPromises.push(this.addCustomExecution(taskDTO, task, true)); + taskIdPromises.push(this.addCustomExecution(taskDTO, task, true)); } } } @@ -123,32 +122,6 @@ export class ExtHostTask extends ExtHostTaskBase { return this._variableResolver; } - protected async resolveDefinition(uri: number | UriComponents | undefined, definition: vscode.TaskDefinition | undefined): Promise { - if (!uri || (typeof uri === 'number') || !definition) { - return definition; - } - const workspaceFolder = await this._workspaceProvider.resolveWorkspaceFolder(URI.revive(uri)); - const workspaceFolders = await this._workspaceProvider.getWorkspaceFolders2(); - if (!workspaceFolders || !workspaceFolder) { - return definition; - } - const resolver = await this.getVariableResolver(workspaceFolders); - const ws: IWorkspaceFolder = { - uri: workspaceFolder.uri, - name: workspaceFolder.name, - index: workspaceFolder.index, - toResource: () => { - throw new Error('Not implemented'); - } - }; - const resolvedDefinition = Objects.deepClone(definition); - for (const key in resolvedDefinition) { - resolvedDefinition[key] = resolver.resolve(ws, resolvedDefinition[key]); - } - - return resolvedDefinition; - } - public async $resolveVariables(uriComponents: UriComponents, toResolve: { process?: { name: string; cwd?: string; path?: string }, variables: string[] }): Promise<{ process?: string, variables: { [key: string]: string; } }> { const uri: URI = URI.revive(uriComponents); const result = { diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 118699c1986..1de01f036f3 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -21,8 +21,6 @@ import { PanelPositionContext } from 'vs/workbench/common/panel'; import { getRemoteName } from 'vs/platform/remote/common/remoteHosts'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; -export const Deprecated_RemoteAuthorityContext = new RawContextKey('remoteAuthority', ''); - export const RemoteNameContext = new RawContextKey('remoteName', ''); export const RemoteConnectionState = new RawContextKey<'' | 'initializing' | 'disconnected' | 'connected'>('remoteConnectionState', ''); diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index 8e0aec443eb..5df3e69fd8d 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -6,17 +6,18 @@ import { EditorInput } from 'vs/workbench/common/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { insert } from 'vs/base/common/arrays'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { - instantiate(instantiationService: IInstantiationService): BaseEditor; getId(): string; getName(): string; + instantiate(instantiationService: IInstantiationService): EditorPane; + describes(obj: unknown): boolean; } @@ -56,20 +57,20 @@ export interface IEditorRegistry { export class EditorDescriptor implements IEditorDescriptor { static create( - ctor: { new(...services: Services): BaseEditor }, + ctor: { new(...services: Services): EditorPane }, id: string, name: string ): EditorDescriptor { - return new EditorDescriptor(ctor as IConstructorSignature0, id, name); + return new EditorDescriptor(ctor as IConstructorSignature0, id, name); } constructor( - private readonly ctor: IConstructorSignature0, + private readonly ctor: IConstructorSignature0, private readonly id: string, private readonly name: string ) { } - instantiate(instantiationService: IInstantiationService): BaseEditor { + instantiate(instantiationService: IInstantiationService): EditorPane { return instantiationService.createInstance(this.ctor); } @@ -82,7 +83,7 @@ export class EditorDescriptor implements IEditorDescriptor { } describes(obj: unknown): boolean { - return obj instanceof BaseEditor && obj.getId() === this.id; + return obj instanceof EditorPane && obj.getId() === this.id; } } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 2385e99eac7..dd1e2146344 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1061,9 +1061,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return !this.state.activityBar.hidden; case Parts.EDITOR_PART: return !this.state.editor.hidden; + default: + return true; // any other part cannot be hidden } - - return true; // any other part cannot be hidden } focus(): void { diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index ff18883e95e..241d3bced2d 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -28,12 +28,13 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { Codicon } from 'vs/base/common/codicons'; import { isMacintosh } from 'vs/base/common/platform'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { AuthenticationSession } from 'vs/editor/common/modes'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IProductService } from 'vs/platform/product/common/productService'; export class ViewContainerActivityAction extends ActivityAction { @@ -125,7 +126,8 @@ export class AccountsActionViewItem extends ActivityActionViewItem { @IContextKeyService private readonly contextKeyService: IContextKeyService, @IAuthenticationService private readonly authenticationService: IAuthenticationService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, ) { super(action, { draggable: false, colors, icon: true }, themeService); } @@ -178,10 +180,11 @@ export class AccountsActionViewItem extends ActivityActionViewItem { const result = await Promise.all(allSessions); let menus: IAction[] = []; + const authenticationSession = this.environmentService.options?.credentialsProvider ? await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService) : undefined; result.forEach(sessionInfo => { const providerDisplayName = this.authenticationService.getLabel(sessionInfo.providerId); Object.keys(sessionInfo.sessions).forEach(accountName => { - const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === this.environmentService.options?.authenticationSessionId); + const hasEmbedderAccountSession = sessionInfo.sessions[accountName].some(session => session.id === (authenticationSession?.id || this.environmentService.options?.authenticationSessionId)); const manageExtensionsAction = new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, _ => { return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName); }); diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index ca270875dfb..bed01dd0687 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -6,8 +6,8 @@ import 'vs/css!./media/binaryeditor'; import * as nls from 'vs/nls'; import { Emitter } from 'vs/base/common/event'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; @@ -30,7 +30,7 @@ export interface IOpenCallbacks { /* * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseBinaryResourceEditor extends BaseEditor { +export abstract class BaseBinaryResourceEditor extends EditorPane { private readonly _onMetadataChanged = this._register(new Emitter()); readonly onMetadataChanged = this._onMetadataChanged.event; @@ -74,8 +74,8 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { parent.appendChild(this.scrollbar.getDomNode()); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); const model = await input.resolve(); // Check for cancellation diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index b3a49e5d197..5c27e74e8d2 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -4,12 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorInput, EditorOptions, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext, IVisibleEditorPane } from 'vs/workbench/common/editor'; import { Dimension, show, hide, addClass } from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorRegistry, Extensions as EditorExtensions, IEditorDescriptor } from 'vs/workbench/browser/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IEditorProgressService, LongRunningOperation } from 'vs/platform/progress/common/progress'; import { IEditorGroupView, DEFAULT_EDITOR_MIN_DIMENSIONS, DEFAULT_EDITOR_MAX_DIMENSIONS } from 'vs/workbench/browser/parts/editor/editor'; @@ -17,7 +17,7 @@ import { Emitter } from 'vs/base/common/event'; import { assertIsDefined } from 'vs/base/common/types'; export interface IOpenEditorResult { - readonly editorPane: BaseEditor; + readonly editorPane: EditorPane; readonly editorChanged: boolean; } @@ -34,10 +34,10 @@ export class EditorControl extends Disposable { private _onDidSizeConstraintsChange = this._register(new Emitter<{ width: number; height: number; } | undefined>()); readonly onDidSizeConstraintsChange = this._onDidSizeConstraintsChange.event; - private _activeEditorPane: BaseEditor | null = null; + private _activeEditorPane: EditorPane | null = null; get activeEditorPane(): IVisibleEditorPane | null { return this._activeEditorPane as IVisibleEditorPane | null; } - private readonly editorPanes: BaseEditor[] = []; + private readonly editorPanes: EditorPane[] = []; private readonly activeEditorPaneDisposables = this._register(new DisposableStore()); private dimension: Dimension | undefined; @@ -53,7 +53,7 @@ export class EditorControl extends Disposable { super(); } - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { // Editor pane const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editor); @@ -63,11 +63,11 @@ export class EditorControl extends Disposable { const editorPane = this.doShowEditorPane(descriptor); // Set input - const editorChanged = await this.doSetInput(editorPane, editor, options); + const editorChanged = await this.doSetInput(editorPane, editor, options, context); return { editorPane, editorChanged }; } - private doShowEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doShowEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if the currently active editor pane can handle the input if (this._activeEditorPane && descriptor.describes(this._activeEditorPane)) { @@ -99,7 +99,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doCreateEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doCreateEditorPane(descriptor: IEditorDescriptor): EditorPane { // Instantiate editor const editorPane = this.doInstantiateEditorPane(descriptor); @@ -116,7 +116,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doInstantiateEditorPane(descriptor: IEditorDescriptor): BaseEditor { + private doInstantiateEditorPane(descriptor: IEditorDescriptor): EditorPane { // Return early if already instantiated const existingEditorPane = this.editorPanes.find(editorPane => descriptor.describes(editorPane)); @@ -131,7 +131,7 @@ export class EditorControl extends Disposable { return editorPane; } - private doSetActiveEditorPane(editorPane: BaseEditor | null) { + private doSetActiveEditorPane(editorPane: EditorPane | null) { this._activeEditorPane = editorPane; // Clear out previous active editor pane listeners @@ -147,7 +147,7 @@ export class EditorControl extends Disposable { this._onDidSizeConstraintsChange.fire(undefined); } - private async doSetInput(editorPane: BaseEditor, editor: EditorInput, options: EditorOptions | undefined): Promise { + private async doSetInput(editorPane: EditorPane, editor: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext): Promise { // If the input did not change, return early and only apply the options // unless the options instruct us to force open it even if it is the same @@ -174,7 +174,7 @@ export class EditorControl extends Disposable { // Call into editor pane const editorWillChange = !inputMatches; try { - await editorPane.setInput(editor, options, operation.token); + await editorPane.setInput(editor, options, context, operation.token); // Focus (unless prevented or another operation is running) if (operation.isCurrent()) { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 0bf99cb44ea..0fcb9999b13 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -459,7 +459,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const activeElement = document.activeElement; // Show active editor - await this.doShowEditor(activeEditor, true, options); + await this.doShowEditor(activeEditor, { active: true, isNew: false /* restored */ }, options); // Set focused now if this is the active group and focus has // not changed meanwhile. This prevents focus from being @@ -954,10 +954,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update model and make sure to continue to use the editor we get from // the model. It is possible that the editor was already opened and we // want to ensure that we use the existing instance in that case. - const openedEditor = this._group.openEditor(editor, openEditorOptions); + const { editor: openedEditor, isNew } = this._group.openEditor(editor, openEditorOptions); // Show editor - const showEditorResult = this.doShowEditor(openedEditor, !!openEditorOptions.active, options); + const showEditorResult = this.doShowEditor(openedEditor, { active: !!openEditorOptions.active, isNew }, options); // Finally make sure the group is active or restored as instructed if (activateGroup) { @@ -969,14 +969,14 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return showEditorResult; } - private async doShowEditor(editor: EditorInput, active: boolean, options?: EditorOptions): Promise { + private async doShowEditor(editor: EditorInput, context: { active: boolean, isNew: boolean }, options?: EditorOptions): Promise { // Show in editor control if the active editor changed let openEditorPromise: Promise | undefined; - if (active) { + if (context.active) { openEditorPromise = (async () => { try { - const result = await this.editorControl.openEditor(editor, options); + const result = await this.editorControl.openEditor(editor, options, { newInGroup: context.isNew }); // Editor change event if (result.editorChanged) { diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/editorPane.ts similarity index 96% rename from src/vs/workbench/browser/parts/editor/baseEditor.ts rename to src/vs/workbench/browser/parts/editor/editorPane.ts index cf48687419b..e5e48349b04 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/editorPane.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Composite } from 'vs/workbench/browser/composite'; -import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorPane, GroupIdentifier, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -41,7 +41,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; * * This class is only intended to be subclassed and not instantiated. */ -export abstract class BaseEditor extends Composite implements IEditorPane { +export abstract class EditorPane extends Composite implements IEditorPane { private static readonly EDITOR_MEMENTOS = new Map>(); @@ -91,10 +91,12 @@ export abstract class BaseEditor extends Composite implements IEditorPane { * to be different from the previous input that was set using the `input.matches()` * method. * + * The provided context gives more information around how the editor was opened. + * * The provided cancellation token should be used to test if the operation * was cancelled. */ - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this._input = input; this._options = options; } @@ -146,10 +148,10 @@ export abstract class BaseEditor extends Composite implements IEditorPane { protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { const mementoKey = `${this.getId()}${key}`; - let editorMemento = BaseEditor.EDITOR_MEMENTOS.get(mementoKey); + let editorMemento = EditorPane.EDITOR_MEMENTOS.get(mementoKey); if (!editorMemento) { editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); - BaseEditor.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + EditorPane.EDITOR_MEMENTOS.set(mementoKey, editorMemento); } return editorMemento; @@ -158,7 +160,7 @@ export abstract class BaseEditor extends Composite implements IEditorPane { protected saveState(): void { // Save all editor memento for this editor type - BaseEditor.EDITOR_MEMENTOS.forEach(editorMemento => { + EditorPane.EDITOR_MEMENTOS.forEach(editorMemento => { if (editorMemento.id === this.getId()) { editorMemento.saveState(); } diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 5f3a4cf23c5..d62cc3b59d5 100644 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts @@ -10,7 +10,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IRange } from 'vs/editor/common/core/range'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { ModelDecorationOptions } from 'vs/editor/common/model/textModel'; -import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser'; import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; export interface IRangeHighlightDecoration { @@ -44,9 +44,11 @@ export class RangeHighlightDecorations extends Disposable { } highlightRange(range: IRangeHighlightDecoration, editor?: any) { - editor = editor ? editor : this.getEditor(range); + editor = editor ?? this.getEditor(range); if (isCodeEditor(editor)) { this.doHighlightRange(editor, range); + } else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) { + this.doHighlightRange(editor.activeCodeEditor, range); } } diff --git a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts index aff171a997f..ed99e7e0567 100644 --- a/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts +++ b/src/vs/workbench/browser/parts/editor/sideBySideEditor.ts @@ -5,8 +5,8 @@ import * as DOM from 'vs/base/browser/dom'; import { Registry } from 'vs/platform/registry/common/platform'; -import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, SideBySideEditorInput, IEditorControl, IEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; @@ -19,7 +19,7 @@ import { Event, Relay, Emitter } from 'vs/base/common/event'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { assertIsDefined } from 'vs/base/common/types'; -export class SideBySideEditor extends BaseEditor { +export class SideBySideEditor extends EditorPane { static readonly ID: string = 'workbench.editor.sidebysideEditor'; @@ -33,7 +33,7 @@ export class SideBySideEditor extends BaseEditor { private get minimumSecondaryHeight() { return this.secondaryEditorPane ? this.secondaryEditorPane.minimumHeight : 0; } private get maximumSecondaryHeight() { return this.secondaryEditorPane ? this.secondaryEditorPane.maximumHeight : Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /* noop */ } set maximumWidth(value: number) { /* noop */ } set minimumHeight(value: number) { /* noop */ } @@ -44,8 +44,8 @@ export class SideBySideEditor extends BaseEditor { get minimumHeight() { return this.minimumPrimaryHeight + this.minimumSecondaryHeight; } get maximumHeight() { return this.maximumPrimaryHeight + this.maximumSecondaryHeight; } - protected primaryEditorPane?: BaseEditor; - protected secondaryEditorPane?: BaseEditor; + protected primaryEditorPane?: EditorPane; + protected secondaryEditorPane?: EditorPane; private primaryEditorContainer: HTMLElement | undefined; private secondaryEditorContainer: HTMLElement | undefined; @@ -94,11 +94,11 @@ export class SideBySideEditor extends BaseEditor { this.updateStyles(); } - async setInput(newInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(newInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const oldInput = this.input as SideBySideEditorInput; - await super.setInput(newInput, options, token); + await super.setInput(newInput, options, context, token); - return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, token); + return this.updateInput(oldInput, (newInput as SideBySideEditorInput), options, context, token); } setOptions(options: EditorOptions | undefined): void { @@ -162,13 +162,13 @@ export class SideBySideEditor extends BaseEditor { return this.secondaryEditorPane; } - private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private async updateInput(oldInput: SideBySideEditorInput, newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (!newInput.matches(oldInput)) { if (oldInput) { this.disposeEditors(); } - return this.setNewInput(newInput, options, token); + return this.setNewInput(newInput, options, context, token); } if (!this.secondaryEditorPane || !this.primaryEditorPane) { @@ -176,19 +176,19 @@ export class SideBySideEditor extends BaseEditor { } await Promise.all([ - this.secondaryEditorPane.setInput(newInput.secondary, undefined, token), - this.primaryEditorPane.setInput(newInput.primary, options, token) + this.secondaryEditorPane.setInput(newInput.secondary, undefined, context, token), + this.primaryEditorPane.setInput(newInput.primary, options, context, token) ]); } - private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private setNewInput(newInput: SideBySideEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const secondaryEditor = this.doCreateEditor(newInput.secondary, assertIsDefined(this.secondaryEditorContainer)); const primaryEditor = this.doCreateEditor(newInput.primary, assertIsDefined(this.primaryEditorContainer)); - return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, token); + return this.onEditorsCreated(secondaryEditor, primaryEditor, newInput.secondary, newInput.primary, options, context, token); } - private doCreateEditor(editorInput: EditorInput, container: HTMLElement): BaseEditor { + private doCreateEditor(editorInput: EditorInput, container: HTMLElement): EditorPane { const descriptor = Registry.as(EditorExtensions.Editors).getEditor(editorInput); if (!descriptor) { throw new Error('No descriptor for editor found'); @@ -201,7 +201,7 @@ export class SideBySideEditor extends BaseEditor { return editor; } - private async onEditorsCreated(secondary: BaseEditor, primary: BaseEditor, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + private async onEditorsCreated(secondary: EditorPane, primary: EditorPane, secondaryInput: EditorInput, primaryInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.secondaryEditorPane = secondary; this.primaryEditorPane = primary; @@ -213,8 +213,8 @@ export class SideBySideEditor extends BaseEditor { this.onDidCreateEditors.fire(undefined); await Promise.all([ - this.secondaryEditorPane.setInput(secondaryInput, undefined, token), - this.primaryEditorPane.setInput(primaryInput, options, token)] + this.secondaryEditorPane.setInput(secondaryInput, undefined, context, token), + this.primaryEditorPane.setInput(primaryInput, options, context, token)] ); } diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index e03a4159dc4..22f107c6c88 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -9,7 +9,7 @@ import { isFunction, isObject, isArray, assertIsDefined } from 'vs/base/common/t import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { BaseTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditorPane, IEditorInput } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, TEXT_DIFF_EDITOR_ID, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, ITextDiffEditorPane, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { DiffNavigator } from 'vs/editor/browser/widget/diffNavigator'; import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; @@ -72,7 +72,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return this.instantiationService.createInstance(DiffEditorWidget, parent, configuration); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -81,7 +81,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan this.doSaveOrClearTextDiffEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); try { const resolvedModel = await input.resolve(); @@ -107,9 +107,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan optionsGotApplied = (options).apply(diffEditor, ScrollType.Immediate); } - // Otherwise restore View State + // Otherwise restore View State unless disabled via settings let hasPreviousViewState = false; - if (!optionsGotApplied) { + if (!optionsGotApplied && this.shouldRestoreTextEditorViewState(input, context)) { hasPreviousViewState = this.restoreTextDiffEditorViewState(input, diffEditor); } @@ -288,7 +288,7 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan } // Clear view state if input is disposed or we are configured to not storing any state - if (input.isDisposed() || (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input)))) { + if (input.isDisposed() || (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input)))) { super.clearTextEditorViewState([resource], this.group); } diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index d3e594728ed..f420982dd27 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -10,8 +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 } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorInput, EditorOptions, IEditorMemento, ITextEditorPane, TextEditorOptions, IEditorCloseEvent, IEditorInput, computeEditorAriaLabel, IEditorOpenContext, toResource, SideBySideEditor } from 'vs/workbench/common/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'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -35,7 +35,7 @@ export interface IEditorConfiguration { * The base class of editors that leverage the text editor for the editing experience. This class is only intended to * be subclassed and not instantiated. */ -export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPane { +export abstract class BaseTextEditor extends EditorPane implements ITextEditorPane { static readonly TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; @@ -47,9 +47,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa private readonly groupListener = this._register(new MutableDisposable()); - private _shouldRestoreViewState: boolean | undefined; - protected get shouldRestoreViewState(): boolean | undefined { return this._shouldRestoreViewState; } - private _instantiationService: IInstantiationService; protected get instantiationService(): IInstantiationService { return this._instantiationService; } protected set instantiationService(value: IInstantiationService) { this._instantiationService = value; } @@ -69,7 +66,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); - this._register(this.textResourceConfigurationService.onDidChangeConfiguration(e => { + this._register(this.textResourceConfigurationService.onDidChangeConfiguration(() => { const resource = this.getActiveResource(); const value = resource ? this.textResourceConfigurationService.getValue(resource) : undefined; @@ -84,13 +81,9 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorContainer?.setAttribute('aria-label', ariaLabel); this.editorControl?.updateOptions({ ariaLabel }); })); - - this.updateRestoreViewStateConfiguration(); } protected handleConfigurationChangeEvent(configuration?: IEditorConfiguration): void { - this.updateRestoreViewStateConfiguration(); - if (this.isVisible()) { this.updateEditorConfiguration(configuration); } else { @@ -98,10 +91,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa } } - private updateRestoreViewStateConfiguration(): void { - this._shouldRestoreViewState = this.textResourceConfigurationService.getValue(undefined, 'workbench.editor.restoreViewState') ?? true /* default */; - } - private consumePendingConfigurationChangeEvent(): void { if (this.hasPendingConfigurationChange) { this.updateEditorConfiguration(); @@ -163,8 +152,8 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa return this.instantiationService.createInstance(CodeEditorWidget, parent, configuration, {}); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); // Update editor options after having set the input. We do this because there can be // editor input specific options (e.g. an ARIA label depending on the input showing) @@ -238,6 +227,17 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa this.editorMemento.saveEditorState(this.group, resource, editorViewState); } + protected shouldRestoreTextEditorViewState(editor: IEditorInput, context?: IEditorOpenContext): boolean { + + // new editor: check with workbench.editor.restoreViewState setting + if (context?.newInGroup) { + return this.textResourceConfigurationService.getValue(toResource(editor, { supportSideBySide: SideBySideEditor.PRIMARY }), 'workbench.editor.restoreViewState') === false ? false : true /* restore by default */; + } + + // existing editor: always restore viewstate + return true; + } + getViewState(): IEditorViewState | undefined { const resource = this.input?.resource; if (resource) { diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index bf60f8d5e3e..a99323c2ed9 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { assertIsDefined, isFunction, withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditor, getCodeEditor, IPasteEvent } from 'vs/editor/browser/editorBrowser'; -import { TextEditorOptions, EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { TextEditorOptions, EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; @@ -54,13 +54,13 @@ export class AbstractTextResourceEditor extends BaseTextEditor { return nls.localize('textEditor', "Text Editor"); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Remember view settings if input changes this.saveTextResourceEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); const resolvedModel = await input.resolve(); // Check for cancellation @@ -85,8 +85,8 @@ export class AbstractTextResourceEditor extends BaseTextEditor { optionsGotApplied = textOptions.apply(textEditor, ScrollType.Immediate); } - // Otherwise restore View State - if (!optionsGotApplied) { + // Otherwise restore View State unless disabled via settings + if (!optionsGotApplied && this.shouldRestoreTextEditorViewState(input, context)) { this.restoreTextResourceEditorViewState(input, textEditor); } diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index c693429a87d..96ba8faeb84 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -28,7 +28,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { listActiveSelectionBackground, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceDataTransfers, LocalSelectionTransfer } from 'vs/workbench/browser/dnd'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; @@ -163,7 +163,7 @@ export abstract class TitleControl extends Themable { const activeEditorPane = this.group.activeEditorPane; // Check Active Editor - if (activeEditorPane instanceof BaseEditor) { + if (activeEditorPane instanceof EditorPane) { const result = activeEditorPane.getActionViewItem(action); if (result) { @@ -236,7 +236,7 @@ export abstract class TitleControl extends Themable { // Editor actions require the editor control to be there, so we retrieve it via service const activeEditorPane = this.group.activeEditorPane; - if (activeEditorPane instanceof BaseEditor) { + if (activeEditorPane instanceof EditorPane) { const codeEditor = getCodeEditor(activeEditorPane.getControl()); const scopedContextKeyService = codeEditor?.invokeWithinContext(accessor => accessor.get(IContextKeyService)) || this.contextKeyService; const titleBarMenu = this.menuService.createMenu(MenuId.EditorTitle, scopedContextKeyService); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts index e41c8692e57..a935e757aad 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCommands.ts @@ -221,7 +221,7 @@ export function registerNotificationCommands(center: INotificationsCenterControl // Commands for Command Palette const category = { value: localize('notifications', "Notifications"), original: 'Notifications' }; - MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category }, when: NotificationsCenterVisibleContext.toNegated() }); + MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: SHOW_NOTIFICATIONS_CENTER, title: { value: localize('showNotifications', "Show Notifications"), original: 'Show Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: HIDE_NOTIFICATIONS_CENTER, title: { value: localize('hideNotifications', "Hide Notifications"), original: 'Hide Notifications' }, category }, when: NotificationsCenterVisibleContext }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: CLEAR_ALL_NOTIFICATIONS, title: { value: localize('clearAllNotifications', "Clear All Notifications"), original: 'Clear All Notifications' }, category } }); MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: FOCUS_NOTIFICATION_TOAST, title: { value: localize('focusNotificationToasts', "Focus Notification Toast"), original: 'Focus Notification Toast' }, category }, when: NotificationsToastsVisibleContext }); diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 0462617196b..55f0f189714 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -50,6 +50,9 @@ import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFil import { WebResourceIdentityService, IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IndexedDB, INDEXEDDB_LOGS_OBJECT_STORE, INDEXEDDB_USERDATA_OBJECT_STORE } from 'vs/platform/files/browser/indexedDBFileSystemProvider'; +import { BrowserRequestService } from 'vs/workbench/services/request/browser/requestService'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; class BrowserMain extends Disposable { @@ -180,7 +183,7 @@ class BrowserMain extends Disposable { await this.registerFileSystemProviders(environmentService, fileService, remoteAgentService, logService, logsPath); // Long running services (workspace, config, storage) - const services = await Promise.all([ + const [configurationService, storageService] = await Promise.all([ this.createWorkspaceService(payload, environmentService, fileService, remoteAgentService, logService).then(service => { // Workspace @@ -201,7 +204,16 @@ class BrowserMain extends Disposable { }) ]); - return { serviceCollection, logService, storageService: services[1] }; + // Request Service + const requestService = new BrowserRequestService(remoteAgentService, configurationService, logService); + serviceCollection.set(IRequestService, requestService); + + // initialize user data + const userDataInitializationService = new UserDataInitializationService(environmentService, fileService, storageService, productService, requestService, logService); + serviceCollection.set(IUserDataInitializationService, userDataInitializationService); + await userDataInitializationService.initializeRequiredResources(); + + return { serviceCollection, logService, storageService }; } private async registerFileSystemProviders(environmentService: IWorkbenchEnvironmentService, fileService: IFileService, remoteAgentService: IRemoteAgentService, logService: BufferLogService, logsPath: URI): Promise { diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 7612428bc95..7656a699f3c 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -150,8 +150,9 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.restoreViewState': { 'type': 'boolean', - 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening files after they have been closed."), + 'description': nls.localize('restoreViewState', "Restores the last view state (e.g. scroll position) when re-opening textual editors after they have been closed."), 'default': true, + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE }, 'workbench.editor.centeredLayoutAutoResize': { 'type': 'boolean', @@ -309,7 +310,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio const base = '${dirty}${activeEditorShort}${separator}${rootName}${separator}${appName}'; if (isWeb) { - return base + '${separator}${remoteName}'; // Web: always show remote indicator + return base + '${separator}${remoteName}'; // Web: always show remote name } return base; diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index 202f03996a9..d698bbfe734 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1137,6 +1137,22 @@ export class TextEditorOptions extends EditorOptions implements ITextEditorOptio } } +/** + * Context passed into `EditorPane#setInput` to give additional + * context information around why the editor was opened. + */ +export interface IEditorOpenContext { + + /** + * An indicator if the editor input is new for the group the editor is in. + * An editor is new for a group if it was not part of the group before and + * otherwise was already opened in the group and just became the active editor. + * + * This hint can e.g. be used to decide wether to restore view state or not. + */ + newInGroup?: boolean; +} + export interface IEditorIdentifier { groupId: GroupIdentifier; editor: IEditorInput; diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 99d46391837..97c2058102e 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -19,38 +19,43 @@ const EditorOpenPositioning = { }; export interface EditorCloseEvent extends IEditorCloseEvent { - editor: EditorInput; + readonly editor: EditorInput; } export interface EditorIdentifier extends IEditorIdentifier { - groupId: GroupIdentifier; - editor: EditorInput; + readonly groupId: GroupIdentifier; + readonly editor: EditorInput; } export interface IEditorOpenOptions { - pinned?: boolean; + readonly pinned?: boolean; sticky?: boolean; active?: boolean; - index?: number; + readonly index?: number; +} + +export interface IEditorOpenResult { + readonly editor: EditorInput; + readonly isNew: boolean; } export interface ISerializedEditorInput { - id: string; - value: string; + readonly id: string; + readonly value: string; } export interface ISerializedEditorGroup { - id: number; - editors: ISerializedEditorInput[]; - mru: number[]; - preview?: number; + readonly id: number; + readonly editors: ISerializedEditorInput[]; + readonly mru: number[]; + readonly preview?: number; sticky?: number; } export function isSerializedEditorGroup(obj?: unknown): obj is ISerializedEditorGroup { const group = obj as ISerializedEditorGroup; - return obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru); + return !!(obj && typeof obj === 'object' && Array.isArray(group.editors) && Array.isArray(group.mru)); } export class EditorGroup extends Disposable { @@ -174,7 +179,7 @@ export class EditorGroup extends Disposable { return this.preview; } - openEditor(candidate: EditorInput, options?: IEditorOpenOptions): EditorInput { + openEditor(candidate: EditorInput, options?: IEditorOpenOptions): IEditorOpenResult { const makeSticky = options?.sticky || (typeof options?.index === 'number' && this.isSticky(options.index)); const makePinned = options?.pinned || options?.sticky; const makeActive = options?.active || !this.activeEditor || (!makePinned && this.matches(this.preview, this.activeEditor)); @@ -274,7 +279,10 @@ export class EditorGroup extends Disposable { this.doSetActive(newEditor); } - return newEditor; + return { + editor: newEditor, + isNew: true + }; } // Existing editor @@ -302,7 +310,10 @@ export class EditorGroup extends Disposable { this.doStick(existingEditor, this.indexOf(existingEditor)); } - return existingEditor; + return { + editor: existingEditor, + isNew: false + }; } } diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 3d327ef6f64..e50bd99f313 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -196,6 +196,8 @@ Registry.add(Extensions.ViewContainersRegistry, new ViewContainersRegistryImpl() export interface IViewDescriptor { + readonly type?: string; + readonly id: string; readonly name: string; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts new file mode 100644 index 00000000000..6697646978b --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkCellEdits.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { groupBy } from 'vs/base/common/arrays'; +import { compare } from 'vs/base/common/strings'; +import { URI } from 'vs/base/common/uri'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; +import { IProgress } from 'vs/platform/progress/common/progress'; +import { ICellEditOperation } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; + +export class ResourceNotebookCellEdit extends ResourceEdit { + + constructor( + readonly resource: URI, + readonly cellEdit: ICellEditOperation, + readonly versionId?: number, + readonly metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class BulkCellEdits { + + constructor( + private readonly _progress: IProgress, + private readonly _edits: ResourceNotebookCellEdit[], + @INotebookEditorModelResolverService private readonly _notebookModelService: INotebookEditorModelResolverService, + ) { } + + async apply(): Promise { + + const editsByNotebook = groupBy(this._edits, (a, b) => compare(a.resource.toString(), b.resource.toString())); + + for (let group of editsByNotebook) { + const [first] = group; + const ref = await this._notebookModelService.resolve(first.resource); + + // check state + if (typeof first.versionId === 'number' && ref.object.notebook.versionId !== first.versionId) { + ref.dispose(); + throw new Error(`Notebook '${first.resource}' has changed in the meantime`); + } + + // apply edits + const cellEdits = group.map(edit => edit.cellEdit); + ref.object.notebook.applyEdit(ref.object.notebook.versionId, cellEdits, true); + ref.dispose(); + + this._progress.report(undefined); + } + } +} diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts similarity index 59% rename from src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index ca37d322d6c..b1207b2323c 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -6,39 +6,28 @@ import { localize } from 'vs/nls'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceFileEdit, WorkspaceTextEdit, WorkspaceEdit } from 'vs/editor/common/modes'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService, IBulkEditPreviewHandler, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { IProgress, IProgressStep, Progress } from 'vs/platform/progress/common/progress'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { BulkTextEdits } from 'vs/workbench/services/bulkEdit/browser/bulkTextEdits'; -import { BulkFileEdits } from 'vs/workbench/services/bulkEdit/browser/bulkFileEdits'; -import { ResourceMap } from 'vs/base/common/map'; - -type Edit = WorkspaceFileEdit | WorkspaceTextEdit; +import { BulkTextEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkTextEdits'; +import { BulkFileEdits } from 'vs/workbench/contrib/bulkEdit/browser/bulkFileEdits'; +import { BulkCellEdits, ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits'; class BulkEdit { - private readonly _label: string | undefined; - private readonly _edits: Edit[] = []; - private readonly _editor: ICodeEditor | undefined; - private readonly _progress: IProgress; - constructor( - label: string | undefined, - editor: ICodeEditor | undefined, - progress: IProgress | undefined, - edits: Edit[], + private readonly _label: string | undefined, + private readonly _editor: ICodeEditor | undefined, + private readonly _progress: IProgress, + private readonly _edits: ResourceEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @ILogService private readonly _logService: ILogService, ) { - this._label = label; - this._editor = editor; - this._progress = progress || Progress.None; - this._edits = edits; + } ariaMessage(): string { @@ -55,56 +44,56 @@ class BulkEdit { async perform(): Promise { - let seen = new ResourceMap(); - let total = 0; + if (this._edits.length === 0) { + return; + } - const groups: Edit[][] = []; - let group: Edit[] | undefined; - for (const edit of this._edits) { - if (!group - || (WorkspaceFileEdit.is(group[0]) && !WorkspaceFileEdit.is(edit)) - || (WorkspaceTextEdit.is(group[0]) && !WorkspaceTextEdit.is(edit)) - ) { - group = []; - groups.push(group); - } - group.push(edit); - - if (WorkspaceFileEdit.is(edit)) { - total += 1; - } else if (!seen.has(edit.resource)) { - seen.set(edit.resource, true); - total += 2; + const ranges: number[] = [1]; + for (let i = 1; i < this._edits.length; i++) { + if (Object.getPrototypeOf(this._edits[i - 1]) === Object.getPrototypeOf(this._edits[i])) { + ranges[ranges.length - 1]++; + } else { + ranges.push(1); } } - // define total work and progress callback - // for child operations - this._progress.report({ total }); - + this._progress.report({ total: this._edits.length }); const progress: IProgress = { report: _ => this._progress.report({ increment: 1 }) }; - // do it. - for (const group of groups) { - if (WorkspaceFileEdit.is(group[0])) { - await this._performFileEdits(group, progress); + + let index = 0; + for (let range of ranges) { + const group = this._edits.slice(index, index + range); + if (group[0] instanceof ResourceFileEdit) { + await this._performFileEdits(group, progress); + } else if (group[0] instanceof ResourceTextEdit) { + await this._performTextEdits(group, progress); + } else if (group[0] instanceof ResourceNotebookCellEdit) { + await this._performCellEdits(group, progress); } else { - await this._performTextEdits(group, progress); + console.log('UNKNOWN EDIT'); } + index = index + range; } } - private async _performFileEdits(edits: WorkspaceFileEdit[], progress: IProgress) { + private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { this._logService.debug('_performFileEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkFileEdits, this._label || localize('workspaceEdit', "Workspace Edit"), progress, edits); await model.apply(); } - private async _performTextEdits(edits: WorkspaceTextEdit[], progress: IProgress): Promise { + private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): Promise { this._logService.debug('_performTextEdits', JSON.stringify(edits)); const model = this._instaService.createInstance(BulkTextEdits, this._label || localize('workspaceEdit', "Workspace Edit"), this._editor, progress, edits); await model.apply(); } + + private async _performCellEdits(edits: ResourceNotebookCellEdit[], progress: IProgress): Promise { + this._logService.debug('_performCellEdits', JSON.stringify(edits)); + const model = this._instaService.createInstance(BulkCellEdits, progress, edits); + await model.apply(); + } } export class BulkEditService implements IBulkEditService { @@ -132,17 +121,16 @@ export class BulkEditService implements IBulkEditService { return Boolean(this._previewHandler); } - async apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise { + async apply(edits: ResourceEdit[], options?: IBulkEditOptions): Promise { - if (edit.edits.length === 0) { + if (edits.length === 0) { return { ariaSummary: localize('nothing', "Made no edits") }; } - if (this._previewHandler && (options?.showPreview || edit.edits.some(value => value.metadata?.needsConfirmation))) { - edit = await this._previewHandler(edit, options); + if (this._previewHandler && (options?.showPreview || edits.some(value => value.metadata?.needsConfirmation))) { + edits = await this._previewHandler(edits, options); } - const { edits } = edit; let codeEditor = options?.editor; // try to find code editor if (!codeEditor) { @@ -156,15 +144,23 @@ export class BulkEditService implements IBulkEditService { // If the code editor is readonly still allow bulk edits to be applied #68549 codeEditor = undefined; } - const bulkEdit = this._instaService.createInstance(BulkEdit, options?.quotableLabel || options?.label, codeEditor, options?.progress, edits); - return bulkEdit.perform().then(() => { + + const bulkEdit = this._instaService.createInstance( + BulkEdit, + options?.quotableLabel || options?.label, + codeEditor, options?.progress ?? Progress.None, + edits + ); + + try { + await bulkEdit.perform(); return { ariaSummary: bulkEdit.ariaMessage() }; - }).catch(err => { + } catch (err) { // console.log('apply FAILED'); // console.log(err); this._logService.error(err); throw err; - }); + } } } diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts similarity index 93% rename from src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts index b5e768349cb..2a6e2bc7d54 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkFileEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkFileEdits.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { WorkspaceFileEdit, WorkspaceFileEditOptions } from 'vs/editor/common/modes'; +import { WorkspaceFileEditOptions } from 'vs/editor/common/modes'; import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -14,6 +14,7 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; interface IFileOperation { uris: URI[]; @@ -147,7 +148,7 @@ export class BulkFileEdits { constructor( private readonly _label: string, private readonly _progress: IProgress, - private readonly _edits: WorkspaceFileEdit[], + private readonly _edits: ResourceFileEdit[], @IInstantiationService private readonly _instaService: IInstantiationService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, ) { } @@ -159,15 +160,15 @@ export class BulkFileEdits { const options = edit.options || {}; let op: IFileOperation | undefined; - if (edit.newUri && edit.oldUri) { + if (edit.newResource && edit.oldResource) { // rename - op = this._instaService.createInstance(RenameOperation, edit.newUri, edit.oldUri, options); - } else if (!edit.newUri && edit.oldUri) { + op = this._instaService.createInstance(RenameOperation, edit.newResource, edit.oldResource, options); + } else if (!edit.newResource && edit.oldResource) { // delete file - op = this._instaService.createInstance(DeleteOperation, edit.oldUri, options); - } else if (edit.newUri && !edit.oldUri) { + op = this._instaService.createInstance(DeleteOperation, edit.oldResource, options); + } else if (edit.newResource && !edit.oldResource) { // create file - op = this._instaService.createInstance(CreateOperation, edit.newUri, options, undefined); + op = this._instaService.createInstance(CreateOperation, edit.newResource, options, undefined); } if (op) { const undoOp = await op.perform(); diff --git a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts similarity index 88% rename from src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts rename to src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts index ca9dfa7739c..1877843f01a 100644 --- a/src/vs/workbench/services/bulkEdit/browser/bulkTextEdits.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkTextEdits.ts @@ -11,7 +11,6 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IProgress } from 'vs/platform/progress/common/progress'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; @@ -19,6 +18,7 @@ import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { SingleModelEditStackElement, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; import { ResourceMap } from 'vs/base/common/map'; import { IModelService } from 'vs/editor/common/services/modelService'; +import { ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; type ValidationResult = { canApply: true } | { canApply: false, reason: URI }; @@ -39,31 +39,31 @@ class ModelEditTask implements IDisposable { this._modelReference.dispose(); } - addEdit(resourceEdit: WorkspaceTextEdit): void { - this._expectedModelVersionId = resourceEdit.modelVersionId; - const { edit } = resourceEdit; + addEdit(resourceEdit: ResourceTextEdit): void { + this._expectedModelVersionId = resourceEdit.versionId; + const { textEdit } = resourceEdit; - if (typeof edit.eol === 'number') { + if (typeof textEdit.eol === 'number') { // honor eol-change - this._newEol = edit.eol; + this._newEol = textEdit.eol; } - if (!edit.range && !edit.text) { + if (!textEdit.range && !textEdit.text) { // lacks both a range and the text return; } - if (Range.isEmpty(edit.range) && !edit.text) { + if (Range.isEmpty(textEdit.range) && !textEdit.text) { // no-op edit (replace empty range with empty text) return; } // create edit operation let range: Range; - if (!edit.range) { + if (!textEdit.range) { range = this.model.getFullModelRange(); } else { - range = Range.lift(edit.range); + range = Range.lift(textEdit.range); } - this._edits.push(EditOperation.replaceMove(range, edit.text)); + this._edits.push(EditOperation.replaceMove(range, textEdit.text)); } validate(): ValidationResult { @@ -116,13 +116,13 @@ class EditorEditTask extends ModelEditTask { export class BulkTextEdits { - private readonly _edits = new ResourceMap(); + private readonly _edits = new ResourceMap(); constructor( private readonly _label: string, private readonly _editor: ICodeEditor | undefined, private readonly _progress: IProgress, - edits: WorkspaceTextEdit[], + edits: ResourceTextEdit[], @IEditorWorkerService private readonly _editorWorker: IEditorWorkerService, @IModelService private readonly _modelService: IModelService, @ITextModelService private readonly _textModelResolverService: ITextModelService, @@ -143,9 +143,9 @@ export class BulkTextEdits { // First check if loaded models were not changed in the meantime for (const array of this._edits.values()) { for (let edit of array) { - if (typeof edit.modelVersionId === 'number') { + if (typeof edit.versionId === 'number') { let model = this._modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { + if (model && model.getVersionId() !== edit.versionId) { // model changed in the meantime throw new Error(`${model.uri.toString()} has changed in the meantime`); } @@ -172,12 +172,12 @@ export class BulkTextEdits { for (const edit of value) { if (makeMinimal) { - const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.edit]); + const newEdits = await this._editorWorker.computeMoreMinimalEdits(edit.resource, [edit.textEdit]); if (!newEdits) { task.addEdit(edit); } else { for (let moreMinialEdit of newEdits) { - task.addEdit({ ...edit, edit: moreMinialEdit }); + task.addEdit(new ResourceTextEdit(edit.resource, moreMinialEdit, edit.versionId, edit.metadata)); } } } else { @@ -186,7 +186,6 @@ export class BulkTextEdits { } tasks.push(task); - this._progress.report(undefined); }); promises.push(promise); } diff --git a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts similarity index 80% rename from src/vs/workbench/services/bulkEdit/browser/conflicts.ts rename to src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts index 341353d2adb..2a3ed128b4a 100644 --- a/src/vs/workbench/services/bulkEdit/browser/conflicts.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/conflicts.ts @@ -5,12 +5,12 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; -import { WorkspaceEdit, WorkspaceTextEdit } from 'vs/editor/common/modes'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ResourceMap } from 'vs/base/common/map'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { ITextModel } from 'vs/editor/common/model'; +import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; export class ConflictDetector { @@ -21,31 +21,35 @@ export class ConflictDetector { readonly onDidConflict: Event = this._onDidConflict.event; constructor( - workspaceEdit: WorkspaceEdit, + edits: ResourceEdit[], @IFileService fileService: IFileService, @IModelService modelService: IModelService, ) { const _workspaceEditResources = new ResourceMap(); - for (let edit of workspaceEdit.edits) { - if (WorkspaceTextEdit.is(edit)) { - + for (let edit of edits) { + if (edit instanceof ResourceTextEdit) { _workspaceEditResources.set(edit.resource, true); - - if (typeof edit.modelVersionId === 'number') { + if (typeof edit.versionId === 'number') { const model = modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { + if (model && model.getVersionId() !== edit.versionId) { this._conflicts.set(edit.resource, true); this._onDidConflict.fire(this); } } - } else if (edit.newUri) { - _workspaceEditResources.set(edit.newUri, true); + } else if (edit instanceof ResourceFileEdit) { + if (edit.newResource) { + _workspaceEditResources.set(edit.newResource, true); - } else if (edit.oldUri) { - _workspaceEditResources.set(edit.oldUri, true); + } else if (edit.oldResource) { + _workspaceEditResources.set(edit.oldResource, true); + } + + } else { + //todo@jrieken + console.log('UNKNOWN EDIT TYPE'); } } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts similarity index 95% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts index cfc08528e27..de1fcca2f2d 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution.ts @@ -7,16 +7,15 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPane'; +import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; +import { BulkEditPane } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane'; import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry, FocusedViewContext, IViewsService } from 'vs/workbench/common/views'; import { localize } from 'vs/nls'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { RawContextKey, IContextKeyService, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; -import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkEditPreviewProvider } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; @@ -105,18 +104,18 @@ class BulkEditPreviewContribution { @IBulkEditService bulkEditService: IBulkEditService, @IContextKeyService contextKeyService: IContextKeyService, ) { - bulkEditService.setPreviewHandler((edit) => this._previewEdit(edit)); + bulkEditService.setPreviewHandler(edits => this._previewEdit(edits)); this._ctxEnabled = BulkEditPreviewContribution.ctxEnabled.bindTo(contextKeyService); } - private async _previewEdit(edit: WorkspaceEdit) { + private async _previewEdit(edits: ResourceEdit[]): Promise { this._ctxEnabled.set(true); const uxState = this._activeSession?.uxState ?? new UXState(this._panelService, this._editorGroupsService); const view = await getBulkEditPane(this._viewsService); if (!view) { this._ctxEnabled.set(false); - return edit; + return edits; } // check for active preview session and let the user decide @@ -130,7 +129,7 @@ class BulkEditPreviewContribution { if (choice.choice === 0) { // this refactoring is being cancelled - return { edits: [] }; + return []; } } @@ -147,12 +146,7 @@ class BulkEditPreviewContribution { // the actual work... try { - const newEditOrUndefined = await view.setInput(edit, session.cts.token); - if (!newEditOrUndefined) { - return { edits: [] }; - } - - return newEditOrUndefined; + return await view.setInput(edits, session.cts.token); } finally { // restore UX state @@ -366,4 +360,3 @@ Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews ctorDescriptor: new SyncDescriptor(BulkEditPane), containerIcon: Codicon.lightbulb.classNames, }], container); - diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css similarity index 100% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.css diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts similarity index 96% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts index b0c6afd8861..64b406d9e07 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPane.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPane.ts @@ -5,8 +5,7 @@ import 'vs/css!./bulkEdit'; import { WorkbenchAsyncDataTree, IOpenEvent } from 'vs/platform/list/browser/listService'; -import { WorkspaceEdit } from 'vs/editor/common/modes'; -import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditTree'; +import { BulkEditElement, BulkEditDelegate, TextEditElementRenderer, FileElementRenderer, BulkEditDataSource, BulkEditIdentityProvider, FileElement, TextEditElement, BulkEditAccessibilityProvider, CategoryElementRenderer, BulkEditNaviLabelProvider, CategoryElement, BulkEditSorter } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerThemingParticipant, IColorTheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; @@ -14,7 +13,7 @@ import { diffInserted, diffRemoved } from 'vs/platform/theme/common/colorRegistr import { localize } from 'vs/nls'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkEditPreviewProvider, BulkFileOperations, BulkFileOperationType } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { ILabelService } from 'vs/platform/label/common/label'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { URI } from 'vs/base/common/uri'; @@ -39,6 +38,7 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { ResourceEdit } from 'vs/editor/browser/services/bulkEditService'; const enum State { Data = 'data', @@ -66,7 +66,7 @@ export class BulkEditPane extends ViewPane { private readonly _disposables = new DisposableStore(); private readonly _sessionDisposables = new DisposableStore(); - private _currentResolve?: (edit?: WorkspaceEdit) => void; + private _currentResolve?: (edit?: ResourceEdit[]) => void; private _currentInput?: BulkFileOperations; @@ -163,7 +163,7 @@ export class BulkEditPane extends ViewPane { this.element.dataset['state'] = state; } - async setInput(edit: WorkspaceEdit, token: CancellationToken): Promise { + async setInput(edit: ResourceEdit[], token: CancellationToken): Promise { this._setState(State.Data); this._sessionDisposables.clear(); this._treeViewStates.clear(); @@ -307,11 +307,11 @@ export class BulkEditPane extends ViewPane { let fileElement: FileElement; if (e.element instanceof TextEditElement) { fileElement = e.element.parent; - options.selection = e.element.edit.textEdit.edit.range; + options.selection = e.element.edit.textEdit.textEdit.range; } else if (e.element instanceof FileElement) { fileElement = e.element; - options.selection = e.element.edit.textEdits[0]?.textEdit.edit.range; + options.selection = e.element.edit.textEdits[0]?.textEdit.textEdit.range; } else { // invalid event diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts similarity index 81% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts index d6d0d1510d3..4aecbf746cf 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview.ts @@ -8,7 +8,7 @@ import { URI } from 'vs/base/common/uri'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; -import { WorkspaceEdit, WorkspaceTextEdit, WorkspaceFileEdit, WorkspaceEditMetadata } from 'vs/editor/common/modes'; +import { WorkspaceEditMetadata } from 'vs/editor/common/modes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { mergeSort, coalesceInPlace } from 'vs/base/common/arrays'; import { Range } from 'vs/editor/common/core/range'; @@ -17,10 +17,11 @@ import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiati import { IFileService } from 'vs/platform/files/common/files'; import { Emitter, Event } from 'vs/base/common/event'; import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts'; +import { ConflictDetector } from 'vs/workbench/contrib/bulkEdit/browser/conflicts'; import { ResourceMap } from 'vs/base/common/map'; import { localize } from 'vs/nls'; import { extUri } from 'vs/base/common/resources'; +import { ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; export class CheckedStates { @@ -67,7 +68,7 @@ export class BulkTextEdit { constructor( readonly parent: BulkFileOperation, - readonly textEdit: WorkspaceTextEdit + readonly textEdit: ResourceTextEdit ) { } } @@ -82,7 +83,7 @@ export class BulkFileOperation { type: BulkFileOperationType = 0; textEdits: BulkTextEdit[] = []; - originalEdits = new Map(); + originalEdits = new Map(); newUri?: URI; constructor( @@ -90,14 +91,14 @@ export class BulkFileOperation { readonly parent: BulkFileOperations ) { } - addEdit(index: number, type: BulkFileOperationType, edit: WorkspaceTextEdit | WorkspaceFileEdit) { + addEdit(index: number, type: BulkFileOperationType, edit: ResourceTextEdit | ResourceFileEdit) { this.type |= type; this.originalEdits.set(index, edit); - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { this.textEdits.push(new BulkTextEdit(this, edit)); } else if (type === BulkFileOperationType.Rename) { - this.newUri = edit.newUri; + this.newUri = edit.newResource; } } @@ -134,19 +135,19 @@ export class BulkCategory { export class BulkFileOperations { - static async create(accessor: ServicesAccessor, bulkEdit: WorkspaceEdit): Promise { + static async create(accessor: ServicesAccessor, bulkEdit: ResourceEdit[]): Promise { const result = accessor.get(IInstantiationService).createInstance(BulkFileOperations, bulkEdit); return await result._init(); } - readonly checked = new CheckedStates(); + readonly checked = new CheckedStates(); readonly fileOperations: BulkFileOperation[] = []; readonly categories: BulkCategory[] = []; readonly conflicts: ConflictDetector; constructor( - private readonly _bulkEdit: WorkspaceEdit, + private readonly _bulkEdit: ResourceEdit[], @IFileService private readonly _fileService: IFileService, @IInstantiationService instaService: IInstantiationService, ) { @@ -164,8 +165,8 @@ export class BulkFileOperations { const newToOldUri = new ResourceMap(); - for (let idx = 0; idx < this._bulkEdit.edits.length; idx++) { - const edit = this._bulkEdit.edits[idx]; + for (let idx = 0; idx < this._bulkEdit.length; idx++) { + const edit = this._bulkEdit[idx]; let uri: URI; let type: BulkFileOperationType; @@ -173,39 +174,45 @@ export class BulkFileOperations { // store inital checked state this.checked.updateChecked(edit, !edit.metadata?.needsConfirmation); - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { type = BulkFileOperationType.TextEdit; uri = edit.resource; - } else if (edit.newUri && edit.oldUri) { - type = BulkFileOperationType.Rename; - uri = edit.oldUri; - if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { - // noop -> "soft" rename to something that already exists - continue; - } - // map newUri onto oldUri so that text-edit appear for - // the same file element - newToOldUri.set(edit.newUri, uri); + } else if (edit instanceof ResourceFileEdit) { + if (edit.newResource && edit.oldResource) { + type = BulkFileOperationType.Rename; + uri = edit.oldResource; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" rename to something that already exists + continue; + } + // map newResource onto oldResource so that text-edit appear for + // the same file element + newToOldUri.set(edit.newResource, uri); - } else if (edit.oldUri) { - type = BulkFileOperationType.Delete; - uri = edit.oldUri; - if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { - // noop -> "soft" delete something that doesn't exist - continue; - } + } else if (edit.oldResource) { + type = BulkFileOperationType.Delete; + uri = edit.oldResource; + if (edit.options?.ignoreIfNotExists && !await this._fileService.exists(uri)) { + // noop -> "soft" delete something that doesn't exist + continue; + } - } else if (edit.newUri) { - type = BulkFileOperationType.Create; - uri = edit.newUri; - if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { - // noop -> "soft" create something that already exists + } else if (edit.newResource) { + type = BulkFileOperationType.Create; + uri = edit.newResource; + if (edit.options?.overwrite === undefined && edit.options?.ignoreIfExists && await this._fileService.exists(uri)) { + // noop -> "soft" create something that already exists + continue; + } + + } else { + // invalid edit -> skip continue; } } else { - // invalid edit -> skip + // unsupported edit continue; } @@ -249,7 +256,7 @@ export class BulkFileOperations { if (file.type !== BulkFileOperationType.TextEdit) { let checked = true; for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && this.checked.isChecked(edit); } } @@ -275,14 +282,14 @@ export class BulkFileOperations { return this; } - getWorkspaceEdit(): WorkspaceEdit { - const result: WorkspaceEdit = { edits: [] }; + getWorkspaceEdit(): ResourceEdit[] { + const result: ResourceEdit[] = []; let allAccepted = true; - for (let i = 0; i < this._bulkEdit.edits.length; i++) { - const edit = this._bulkEdit.edits[i]; + for (let i = 0; i < this._bulkEdit.length; i++) { + const edit = this._bulkEdit[i]; if (this.checked.isChecked(edit)) { - result.edits[i] = edit; + result[i] = edit; continue; } allAccepted = false; @@ -293,7 +300,7 @@ export class BulkFileOperations { } // not all edits have been accepted - coalesceInPlace(result.edits); + coalesceInPlace(result); return result; } @@ -306,9 +313,9 @@ export class BulkFileOperations { let ignoreAll = false; for (const edit of file.originalEdits.values()) { - if (WorkspaceTextEdit.is(edit)) { + if (edit instanceof ResourceTextEdit) { if (this.checked.isChecked(edit)) { - result.push(EditOperation.replaceMove(Range.lift(edit.edit.range), edit.edit.text)); + result.push(EditOperation.replaceMove(Range.lift(edit.textEdit.range), edit.textEdit.text)); } } else if (!this.checked.isChecked(edit)) { @@ -330,7 +337,7 @@ export class BulkFileOperations { return []; } - getUriOfEdit(edit: WorkspaceFileEdit | WorkspaceTextEdit): URI { + getUriOfEdit(edit: ResourceEdit): URI { for (let file of this.fileOperations) { for (const value of file.originalEdits.values()) { if (value === edit) { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts similarity index 96% rename from src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts rename to src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts index 52a59197356..4db2acf28b7 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditTree.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/preview/bulkEditTree.ts @@ -14,7 +14,7 @@ import * as dom from 'vs/base/browser/dom'; import { ITextModel } from 'vs/editor/common/model'; import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkFileOperations, BulkFileOperation, BulkFileOperationType, BulkTextEdit, BulkCategory } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { FileKind } from 'vs/platform/files/common/files'; import { localize } from 'vs/nls'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -22,11 +22,11 @@ import type { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWid import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { basename } from 'vs/base/common/resources'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { WorkspaceFileEdit } from 'vs/editor/common/modes'; import { compare } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { Iterable } from 'vs/base/common/iterator'; +import { ResourceFileEdit } from 'vs/editor/browser/services/bulkEditService'; // --- VIEW MODEL @@ -62,7 +62,7 @@ export class FileElement implements ICheckable { // multiple file edits -> reflect single state for (let edit of this.edit.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -73,7 +73,7 @@ export class FileElement implements ICheckable { for (let file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -113,7 +113,7 @@ export class FileElement implements ICheckable { for (let file of category.fileOperations) { if (file.uri.toString() === this.edit.uri.toString()) { for (const edit of file.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { checked = checked && model.checked.isChecked(edit); } } @@ -155,7 +155,7 @@ export class TextEditElement implements ICheckable { // make sure parent is checked when this element is checked... if (value) { for (const edit of this.parent.edit.originalEdits.values()) { - if (WorkspaceFileEdit.is(edit)) { + if (edit instanceof ResourceFileEdit) { (model).checked.updateChecked(edit, value); } } @@ -219,7 +219,7 @@ export class BulkEditDataSource implements IAsyncDataSource { - const range = Range.lift(edit.textEdit.edit.range); + const range = Range.lift(edit.textEdit.textEdit.range); //prefix-math let startTokens = textModel.getLineTokens(range.startLineNumber); @@ -241,7 +241,7 @@ export class BulkEditDataSource implements IAsyncDataSource { } if (a instanceof TextEditElement && b instanceof TextEditElement) { - return Range.compareRangesUsingStarts(a.edit.textEdit.edit.range, b.edit.textEdit.edit.range); + return Range.compareRangesUsingStarts(a.edit.textEdit.textEdit.range, b.edit.textEdit.textEdit.range); } return 0; @@ -336,13 +336,13 @@ export class BulkEditAccessibilityProvider implements IListAccessibilityProvider if (element instanceof TextEditElement) { if (element.selecting.length > 0 && element.inserting.length > 0) { // edit: replace - return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.edit.range.startLineNumber, element.selecting, element.inserting); + return localize('aria.replace', "line {0}, replacing {1} with {2}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting, element.inserting); } else if (element.selecting.length > 0 && element.inserting.length === 0) { // edit: delete - return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting); + return localize('aria.del', "line {0}, removing {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting); } else if (element.selecting.length === 0 && element.inserting.length > 0) { // edit: insert - return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.edit.range.startLineNumber, element.selecting); + return localize('aria.insert', "line {0}, inserting {1}", element.edit.textEdit.textEdit.range.startLineNumber, element.selecting); } } diff --git a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts index 768bb672eaa..0aac694ea89 100644 --- a/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts +++ b/src/vs/workbench/contrib/bulkEdit/test/browser/bulkEditPreview.test.ts @@ -11,10 +11,10 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IModelService } from 'vs/editor/common/services/modelService'; -import type { WorkspaceEdit } from 'vs/editor/common/modes'; import { URI } from 'vs/base/common/uri'; -import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditPreview'; +import { BulkFileOperations } from 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEditPreview'; import { Range } from 'vs/editor/common/core/range'; +import { ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; suite('BulkEditPreview', function () { @@ -47,28 +47,25 @@ suite('BulkEditPreview', function () { test('one needsConfirmation unchecks all of file', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'cat1', needsConfirmation: true } }, - { oldUri: URI.parse('some:///uri1'), newUri: URI.parse('some:///uri2'), metadata: { label: 'cat2', needsConfirmation: false } }, - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'cat1', needsConfirmation: true }), + new ResourceFileEdit(URI.parse('some:///uri1'), URI.parse('some:///uri2'), undefined, { label: 'cat2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.fileOperations.length, 1); - assert.equal(ops.checked.isChecked(edit.edits[0]), false); + assert.equal(ops.checked.isChecked(edits[0]), false); }); test('has categories', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } }, - { newUri: URI.parse('some:///uri2'), metadata: { label: 'uri2', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }), + new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.categories.length, 2); assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.equal(ops.categories[1].metadata.label, 'uri2'); @@ -76,14 +73,12 @@ suite('BulkEditPreview', function () { test('has not categories', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'uri1', needsConfirmation: true } }, - { newUri: URI.parse('some:///uri2'), metadata: { label: 'uri1', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'uri1', needsConfirmation: true }), + new ResourceFileEdit(undefined, URI.parse('some:///uri2'), undefined, { label: 'uri1', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); assert.equal(ops.categories.length, 1); assert.equal(ops.categories[0].metadata.label, 'uri1'); // unconfirmed! assert.equal(ops.categories[0].metadata.label, 'uri1'); @@ -91,43 +86,41 @@ suite('BulkEditPreview', function () { test('category selection', async function () { - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: false } }, - { resource: URI.parse('some:///uri2'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } } - ] - }; + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: false }), + new ResourceTextEdit(URI.parse('some:///uri2'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }), + ]; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); - assert.equal(ops.checked.isChecked(edit.edits[0]), true); - assert.equal(ops.checked.isChecked(edit.edits[1]), true); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); - assert.ok(edit === ops.getWorkspaceEdit()); + assert.equal(ops.checked.isChecked(edits[0]), true); + assert.equal(ops.checked.isChecked(edits[1]), true); + + assert.ok(edits === ops.getWorkspaceEdit()); // NOT taking to create, but the invalid text edit will // go through - ops.checked.updateChecked(edit.edits[0], false); - const newEdit = ops.getWorkspaceEdit(); - assert.ok(edit !== newEdit); + ops.checked.updateChecked(edits[0], false); + const newEdits = ops.getWorkspaceEdit(); + assert.ok(edits !== newEdits); - assert.equal(edit.edits.length, 2); - assert.equal(newEdit.edits.length, 1); + assert.equal(edits.length, 2); + assert.equal(newEdits.length, 1); }); test('fix bad metadata', async function () { // bogous edit that wants creation to be confirmed, but not it's textedit-child... - const edit: WorkspaceEdit = { - edits: [ - { newUri: URI.parse('some:///uri1'), metadata: { label: 'C1', needsConfirmation: true } }, - { resource: URI.parse('some:///uri1'), edit: { text: 'foo', range: new Range(1, 1, 1, 1) }, metadata: { label: 'C2', needsConfirmation: false } } - ] - }; - const ops = await instaService.invokeFunction(BulkFileOperations.create, edit); + const edits = [ + new ResourceFileEdit(undefined, URI.parse('some:///uri1'), undefined, { label: 'C1', needsConfirmation: true }), + new ResourceTextEdit(URI.parse('some:///uri1'), { text: 'foo', range: new Range(1, 1, 1, 1) }, undefined, { label: 'C2', needsConfirmation: false }) + ]; - assert.equal(ops.checked.isChecked(edit.edits[0]), false); - assert.equal(ops.checked.isChecked(edit.edits[1]), false); + const ops = await instaService.invokeFunction(BulkFileOperations.create, edits); + + assert.equal(ops.checked.isChecked(edits[0]), false); + assert.equal(ops.checked.isChecked(edits[1]), false); }); }); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index c32da9ad715..32c4c2f8b74 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -178,7 +178,11 @@ registerAction2(class extends EditorAction2 { menu: { id: MenuId.EditorContextPeek, group: 'navigation', - order: 1000 + order: 1000, + when: ContextKeyExpr.and( + _ctxHasCallHierarchyProvider, + PeekContext.notInPeekEditor + ), }, keybinding: { when: EditorContextKeys.editorTextFocus, diff --git a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts index 069b114a77a..d4ca7479c7f 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/inspectEditorTokens/inspectEditorTokens.ts @@ -425,8 +425,8 @@ class InspectEditorTokensWidget extends Disposable implements IContentWidget { case StandardTokenType.Comment: return 'Comment'; case StandardTokenType.String: return 'String'; case StandardTokenType.RegEx: return 'RegEx'; + default: return '??'; } - return '??'; } private _getTokensAtPosition(grammar: IGrammar, position: Position): ITextMateTokenInfo { diff --git a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts index 473166ff599..83a504c0135 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/saveParticipants.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import * as strings from 'vs/base/common/strings'; -import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand'; import { EditOperation } from 'vs/editor/common/core/editOperation'; @@ -13,11 +13,11 @@ import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { ITextModel } from 'vs/editor/common/model'; -import { CodeActionTriggerType, DocumentFormattingEditProvider, CodeActionProvider } from 'vs/editor/common/modes'; +import { CodeActionTriggerType, CodeActionProvider } from 'vs/editor/common/modes'; import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction'; import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands'; import { CodeActionKind } from 'vs/editor/contrib/codeAction/types'; -import { formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import { formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -29,6 +29,8 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution, Extensions as WorkbenchContributionsExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { getModifiedRanges } from 'vs/workbench/contrib/format/browser/formatModified'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class TrimWhitespaceParticipant implements ITextFileSaveParticipant { @@ -221,15 +223,14 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { if (!model.textEditorModel) { return; } + if (env.reason === SaveReason.AUTO) { + return undefined; + } const textEditorModel = model.textEditorModel; const overrides = { overrideIdentifier: textEditorModel.getLanguageIdentifier().language, resource: textEditorModel.uri }; - if (env.reason === SaveReason.AUTO || !this.configurationService.getValue('editor.formatOnSave', overrides)) { - return undefined; - } - - const nestedProgress = new Progress(provider => { + const nestedProgress = new Progress<{ displayName?: string, extensionId?: ExtensionIdentifier }>(provider => { progress.report({ message: localize( 'formatting', @@ -238,8 +239,24 @@ class FormatOnSaveParticipant implements ITextFileSaveParticipant { ) }); }); + + const enabled = this.configurationService.getValue('editor.formatOnSave', overrides); + if (!enabled) { + return undefined; + } + const editorOrModel = findEditor(textEditorModel, this.codeEditorService) || textEditorModel; - await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); + const mode = this.configurationService.getValue<'file' | 'modifications'>('editor.formatOnSaveMode', overrides); + if (mode === 'modifications') { + // format modifications + const ranges = await this.instantiationService.invokeFunction(getModifiedRanges, isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel); + if (ranges) { + await this.instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, editorOrModel, ranges, FormattingMode.Silent, nestedProgress, token); + } + } else { + // format the whole file + await this.instantiationService.invokeFunction(formatDocumentWithSelectedProvider, editorOrModel, FormattingMode.Silent, nestedProgress, token); + } } } diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index ec11e213ac4..490a1a53d87 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -535,11 +535,11 @@ function fillInActions(groups: [string, Array(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; to.unshift(...actions); } else { - const to = Array.isArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts index 41b877f6723..71aecb597c2 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointEditorContribution.ts @@ -35,7 +35,6 @@ import { isSafari } from 'vs/base/browser/browser'; import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService'; import { registerColor } from 'vs/platform/theme/common/colorRegistry'; import { ILabelService } from 'vs/platform/label/common/label'; -import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; const $ = dom.$; @@ -169,14 +168,16 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi ) { this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService); this.setDecorationsScheduler = new RunOnceScheduler(() => this.setDecorations(), 30); - debugAdapterRegisteredEmitter.event(() => { - this.registerListeners(); - this.setDecorationsScheduler.schedule(); - }); + this.registerListeners(); + this.setDecorationsScheduler.schedule(); } private registerListeners(): void { this.toDispose.push(this.editor.onMouseDown(async (e: IEditorMouseEvent) => { + if (!this.debugService.getConfigurationManager().hasDebuggers()) { + return; + } + const data = e.target.detail as IMarginData; const model = this.editor.getModel(); if (!e.target.position || !model || e.target.type !== MouseTargetType.GUTTER_GLYPH_MARGIN || data.isAfterLines || !this.marginFreeFromNonDebugDecorations(e.target.position.lineNumber)) { @@ -251,6 +252,10 @@ export class BreakpointEditorContribution implements IBreakpointEditorContributi * 2. When users click on line numbers, the breakpoint hint displays immediately, however it doesn't create the breakpoint unless users click on the left gutter. On a touch screen, it's hard to click on that small area. */ this.toDispose.push(this.editor.onMouseMove((e: IEditorMouseEvent) => { + if (!this.debugService.getConfigurationManager().hasDebuggers()) { + return; + } + let showBreakpointHintAtLineNumber = -1; const model = this.editor.getModel(); if (model && e.target.position && (e.target.type === MouseTargetType.GUTTER_GLYPH_MARGIN || e.target.type === MouseTargetType.GUTTER_LINE_NUMBERS) && this.debugService.getConfigurationManager().canSetBreakpointsIn(model) && diff --git a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts index 023fa7edd57..b0c25ff46dd 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackEditorContribution.ts @@ -40,7 +40,7 @@ const FOCUSED_STACK_FRAME_DECORATION: IModelDecorationOptions = { stickiness }; -export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStackFrameRange: IRange | undefined): IModelDeltaDecoration[] { +export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStackFrameRange: IRange | undefined, isFocusedSession: boolean): IModelDeltaDecoration[] { // only show decorations for the currently focused thread. const result: IModelDeltaDecoration[] = []; const columnUntilEOLRange = new Range(stackFrame.range.startLineNumber, stackFrame.range.startColumn, stackFrame.range.startLineNumber, Constants.MAX_SAFE_SMALL_INTEGER); @@ -50,10 +50,12 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStack // an exception or a stack frame that did not change the line number (we only decorate the columns, not the whole line). const topStackFrame = stackFrame.thread.getTopStackFrame(); if (stackFrame.getId() === topStackFrame?.getId()) { - result.push({ - options: TOP_STACK_FRAME_MARGIN, - range - }); + if (isFocusedSession) { + result.push({ + options: TOP_STACK_FRAME_MARGIN, + range + }); + } result.push({ options: TOP_STACK_FRAME_DECORATION, @@ -68,10 +70,12 @@ export function createDecorationsForStackFrame(stackFrame: IStackFrame, topStack } topStackFrameRange = columnUntilEOLRange; } else { - result.push({ - options: FOCUSED_STACK_FRAME_MARGIN, - range - }); + if (isFocusedSession) { + result.push({ + options: FOCUSED_STACK_FRAME_MARGIN, + range + }); + } result.push({ options: FOCUSED_STACK_FRAME_DECORATION, @@ -106,6 +110,7 @@ export class CallStackEditorContribution implements IEditorContribution { const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; const decorations: IModelDeltaDecoration[] = []; this.debugService.getModel().getSessions().forEach(s => { + const isSessionFocused = s === focusedStackFrame?.thread.session; s.getAllThreads().forEach(t => { if (t.stopped) { let candidateStackFrame = t === focusedStackFrame?.thread ? focusedStackFrame : undefined; @@ -117,7 +122,7 @@ export class CallStackEditorContribution implements IEditorContribution { } if (candidateStackFrame && candidateStackFrame.source.uri.toString() === this.editor.getModel()?.uri.toString()) { - decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange)); + decorations.push(...createDecorationsForStackFrame(candidateStackFrame, this.topStackFrameRange, isSessionFocused)); } } }); diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 7f0411f6483..1700d0f93c5 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -17,7 +17,7 @@ import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView' import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, DEBUG_PANEL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, BREAKPOINT_EDITOR_CONTRIBUTION_ID, REPL_VIEW_ID, CONTEXT_BREAKPOINTS_EXIST, EDITOR_CONTRIBUTION_ID, CONTEXT_DEBUGGERS_AVAILABLE, } from 'vs/workbench/contrib/debug/common/debug'; import { StartAction, AddFunctionBreakpointAction, ConfigureAction, DisableAllBreakpointsAction, EnableAllBreakpointsAction, RemoveAllBreakpointsAction, RunAction, ReapplyBreakpointsAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { DebugToolBar } from 'vs/workbench/contrib/debug/browser/debugToolBar'; @@ -51,25 +51,20 @@ import { DebugProgressContribution } from 'vs/workbench/contrib/debug/browser/de import { DebugTitleContribution } from 'vs/workbench/contrib/debug/browser/debugTitle'; import { Codicon } from 'vs/base/common/codicons'; import { registerColors } from 'vs/workbench/contrib/debug/browser/debugColors'; -import { debugAdapterRegisteredEmitter } from 'vs/workbench/contrib/debug/browser/debugConfigurationManager'; import { DebugEditorContribution } from 'vs/workbench/contrib/debug/browser/debugEditorContribution'; const registry = Registry.as(WorkbenchActionRegistryExtensions.WorkbenchActions); -// register service -debugAdapterRegisteredEmitter.event(() => { - // Register these contributions lazily only once a debug adapter extension has been registered - registerWorkbenchContributions(); - registerColors(); - registerCommandsAndActions(); - registerDebugMenu(); -}); +const debugCategory = nls.localize('debugCategory', "Debug"); +const runCategroy = nls.localize('runCategory', "Run"); +registerWorkbenchContributions(); +registerColors(); +registerCommandsAndActions(); +registerDebugMenu(); registerEditorActions(); registerCommands(); registerDebugPanel(); -const debugCategory = nls.localize('debugCategory', "Debug"); -const runCategroy = nls.localize('runCategory', "Run"); -registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory); -registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy); +registry.registerWorkbenchAction(SyncActionDescriptor.from(StartAction, { primary: KeyCode.F5 }, CONTEXT_IN_DEBUG_MODE.toNegated()), 'Debug: Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); +registry.registerWorkbenchAction(SyncActionDescriptor.from(RunAction, { primary: KeyMod.CtrlCmd | KeyCode.F5, mac: { primary: KeyMod.WinCtrl | KeyCode.F5 } }), 'Run: Start Without Debugging', runCategroy, CONTEXT_DEBUGGERS_AVAILABLE); registerSingleton(IDebugService, DebugService, true); registerDebugView(); @@ -106,18 +101,18 @@ function regsiterEditorContributions(): void { function registerCommandsAndActions(): void { - registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory); - registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ConfigureAction), 'Debug: Open launch.json', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(AddFunctionBreakpointAction), 'Debug: Add Function Breakpoint', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ReapplyBreakpointsAction), 'Debug: Reapply All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(RemoveAllBreakpointsAction), 'Debug: Remove All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(EnableAllBreakpointsAction), 'Debug: Enable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(DisableAllBreakpointsAction), 'Debug: Disable All Breakpoints', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(SelectAndStartAction), 'Debug: Select and Start Debugging', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); + registry.registerWorkbenchAction(SyncActionDescriptor.from(ClearReplAction), 'Debug: Clear Console', debugCategory, CONTEXT_DEBUGGERS_AVAILABLE); const registerDebugCommandPaletteItem = (id: string, title: string, when?: ContextKeyExpression, precondition?: ContextKeyExpression) => { MenuRegistry.appendMenuItem(MenuId.CommandPalette, { - when, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), command: { id, title: `Debug: ${title}`, @@ -158,7 +153,7 @@ function registerCommandsAndActions(): void { }; registerDebugToolBarItem(CONTINUE_ID, CONTINUE_LABEL, 10, { id: 'codicon/debug-continue' }, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); - registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, { id: 'codicon/debug-pause' }, CONTEXT_DEBUG_STATE.notEqualsTo('stopped')); + registerDebugToolBarItem(PAUSE_ID, PAUSE_LABEL, 10, { id: 'codicon/debug-pause' }, CONTEXT_DEBUG_STATE.notEqualsTo('stopped'), CONTEXT_DEBUG_STATE.isEqualTo('running')); registerDebugToolBarItem(STOP_ID, STOP_LABEL, 70, { id: 'codicon/debug-stop' }, CONTEXT_FOCUSED_SESSION_IS_ATTACH.toNegated()); registerDebugToolBarItem(DISCONNECT_ID, DISCONNECT_LABEL, 70, { id: 'codicon/debug-disconnect' }, CONTEXT_FOCUSED_SESSION_IS_ATTACH); registerDebugToolBarItem(STEP_OVER_ID, STEP_OVER_LABEL, 20, { id: 'codicon/debug-step-over' }, undefined, CONTEXT_DEBUG_STATE.isEqualTo('stopped')); @@ -202,7 +197,7 @@ function registerCommandsAndActions(): void { title, icon: { dark: iconUri } }, - when, + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, when), group: '9_debug', order }); @@ -453,10 +448,11 @@ function registerDebugPanel(): void { containerIcon: Codicon.debugConsole.classNames, canToggleVisibility: false, canMoveView: true, + when: CONTEXT_DEBUGGERS_AVAILABLE, ctorDescriptor: new SyncDescriptor(Repl), }], VIEW_CONTAINER); - registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View")); + registry.registerWorkbenchAction(SyncActionDescriptor.from(OpenDebugConsoleAction, { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Y }), 'View: Debug Console', nls.localize('view', "View"), CONTEXT_DEBUGGERS_AVAILABLE); } function registerDebugView(): void { diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 987674fa14c..b1d483a7755 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -49,8 +49,6 @@ const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; interface IDynamicPickItem { label: string, launch: ILaunch, config: IConfig } -export const debugAdapterRegisteredEmitter = new Emitter(); - export class ConfigurationManager implements IConfigurationManager { private debuggers: Debugger[]; private breakpointModeIdsSet = new Set(); @@ -64,6 +62,7 @@ export class ConfigurationManager implements IConfigurationManager { private adapterDescriptorFactories: IDebugAdapterDescriptorFactory[]; private debugAdapterFactories = new Map(); private debugConfigurationTypeContext: IContextKey; + private debuggersAvailable: IContextKey; private readonly _onDidRegisterDebugger = new Emitter(); constructor( @@ -87,6 +86,7 @@ export class ConfigurationManager implements IConfigurationManager { const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot); this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService); + this.debuggersAvailable = CONTEXT_DEBUGGERS_AVAILABLE.bindTo(contextKeyService); if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } else if (this.launches.length > 0) { @@ -97,11 +97,8 @@ export class ConfigurationManager implements IConfigurationManager { // debuggers registerDebugAdapterFactory(debugTypes: string[], debugAdapterLauncher: IDebugAdapterFactory): IDisposable { - const firstTimeRegistration = debugTypes.length && this.debugAdapterFactories.size === 0; debugTypes.forEach(debugType => this.debugAdapterFactories.set(debugType, debugAdapterLauncher)); - if (firstTimeRegistration) { - debugAdapterRegisteredEmitter.fire(); - } + this.debuggersAvailable.set(this.debugAdapterFactories.size > 0); return { dispose: () => { @@ -110,6 +107,10 @@ export class ConfigurationManager implements IConfigurationManager { }; } + hasDebuggers(): boolean { + return this.debugAdapterFactories.size > 0; + } + createDebugAdapter(session: IDebugSession): IDebugAdapter | undefined { let factory = this.debugAdapterFactories.get(session.configuration.type); if (factory) { @@ -319,6 +320,7 @@ export class ConfigurationManager implements IConfigurationManager { if (launch.workspace && provider) { picks.push(provider.provideDebugConfigurations!(launch.workspace.uri, token.token).then(configurations => configurations.map(config => ({ label: config.name, + description: launch.name, config, buttons: [{ iconClass: 'codicon-gear', diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 3d699d77e69..f8350cb13d5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED, WATCH_VIEW_ID, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpointsView'; @@ -27,7 +27,7 @@ class ToggleBreakpointAction extends EditorAction { id: TOGGLE_BREAKPOINT_ID, label: nls.localize('toggleBreakpointAction', "Debug: Toggle Breakpoint"), alias: 'Debug: Toggle Breakpoint', - precondition: undefined, + precondition: CONTEXT_DEBUGGERS_AVAILABLE, kbOpts: { kbExpr: EditorContextKeys.editorTextFocus, primary: KeyCode.F9, @@ -66,7 +66,7 @@ class ConditionalBreakpointAction extends EditorAction { id: TOGGLE_CONDITIONAL_BREAKPOINT_ID, label: nls.localize('conditionalBreakpointEditorAction', "Debug: Add Conditional Breakpoint..."), alias: 'Debug: Add Conditional Breakpoint...', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } @@ -88,7 +88,7 @@ class LogPointAction extends EditorAction { id: ADD_LOG_POINT_ID, label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), alias: 'Debug: Add Logpoint...', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } @@ -339,7 +339,7 @@ class GoToNextBreakpointAction extends GoToBreakpointAction { id: 'editor.debug.action.goToNextBreakpoint', label: nls.localize('goToNextBreakpoint', "Debug: Go To Next Breakpoint"), alias: 'Debug: Go To Next Breakpoint', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } } @@ -350,7 +350,7 @@ class GoToPreviousBreakpointAction extends GoToBreakpointAction { id: 'editor.debug.action.goToPreviousBreakpoint', label: nls.localize('goToPreviousBreakpoint', "Debug: Go To Previous Breakpoint"), alias: 'Debug: Go To Previous Breakpoint', - precondition: undefined + precondition: CONTEXT_DEBUGGERS_AVAILABLE }); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index fa3df30dc4d..898d4f0ab82 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -47,6 +47,7 @@ import { DebugStorage } from 'vs/workbench/contrib/debug/common/debugStorage'; import { DebugTelemetry } from 'vs/workbench/contrib/debug/common/debugTelemetry'; import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; import { ICommandService } from 'vs/platform/commands/common/commands'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export class DebugService implements IDebugService { declare readonly _serviceBrand: undefined; @@ -90,7 +91,8 @@ export class DebugService implements IDebugService { @IConfigurationService private readonly configurationService: IConfigurationService, @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, @IActivityService private readonly activityService: IActivityService, - @ICommandService private readonly commandService: ICommandService + @ICommandService private readonly commandService: ICommandService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { this.toDispose = []; @@ -732,7 +734,7 @@ export class DebugService implements IDebugService { }); } - stopSession(session: IDebugSession | undefined): Promise { + async stopSession(session: IDebugSession | undefined): Promise { if (session) { return session.terminate(); } @@ -740,6 +742,8 @@ export class DebugService implements IDebugService { const sessions = this.model.getSessions(); if (sessions.length === 0) { this.taskRunner.cancel(); + // User might have cancelled starting of a debug session, and in some cases the quick pick is left open + await this.quickInputService.cancel(); this.endInitializingState(); this.cancelTokens(undefined); } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 19c471f74bd..22373d30863 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -440,6 +440,10 @@ export class DebugSession implements IDebugSession { return distinct(positions, p => `${p.lineNumber}:${p.column}`); } + getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined { + return this.model.getDebugProtocolBreakpoint(breakpointId, this.getId()); + } + customRequest(request: string, args: any): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", request)); diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index b9599c28628..adfebd7645e 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -87,3 +87,40 @@ .monaco-workbench .repl .repl-tree .output.expression .code-bold { font-weight: bold; } .monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; } .monaco-workbench .repl .repl-tree .output.expression .code-underline { text-decoration: underline; } + +.monaco-action-bar .action-item.panel-action-tree-filter-container { + cursor: default; + display: flex; +} + +.monaco-action-bar .panel-action-tree-filter{ + display: flex; + align-items: center; + flex: 1; +} + +.monaco-action-bar .panel-action-tree-filter .monaco-inputbox { + height: 24px; + font-size: 12px; + flex: 1; +} + +.pane-header .monaco-action-bar .panel-action-tree-filter .monaco-inputbox { + height: 20px; + line-height: 18px; +} + +.monaco-workbench.vs .monaco-action-bar .panel-action-tree-filter .monaco-inputbox { + height: 25px; +} + +.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container { + max-width: 400px; + min-width: 300px; + margin-right: 10px; +} + +.monaco-action-bar .action-item.panel-action-tree-filter-container, +.panel > .title .monaco-action-bar .action-item.panel-action-tree-filter-container.grow { + flex: 1; +} diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index a5e01950a9a..85f739834b5 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -59,12 +59,13 @@ import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; +import { ReplFilter, TreeFilterState, TreeFilterPanelActionViewItem } from 'vs/workbench/contrib/debug/browser/replFilter'; const $ = dom.$; const HISTORY_STORAGE_KEY = 'debug.repl.history'; const DECORATION_KEY = 'replinputdecoration'; - +const FILTER_ACTION_ID = `workbench.actions.treeView.repl.filter`; function revealLastElement(tree: WorkbenchAsyncDataTree) { tree.scrollTop = tree.scrollHeight - tree.renderHeight; @@ -93,6 +94,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private styleElement: HTMLStyleElement | undefined; private completionItemProvider: IDisposable | undefined; private modelChangeListener: IDisposable = Disposable.None; + private filter: ReplFilter; + private filterState: TreeFilterState; constructor( options: IViewPaneOptions, @@ -116,6 +119,13 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); + this.filter = new ReplFilter(); + this.filterState = this._register(new TreeFilterState({ + filterText: '', + filterHistory: [], + layout: new dom.Dimension(0, 0), + })); + codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); } @@ -237,6 +247,15 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this._register(this.editorService.onDidActiveEditorChange(() => { this.setMode(); })); + + this._register(this.filterState.onDidChange((e) => { + if (e.filterText) { + this.filter.filterQuery = this.filterState.filterText; + if (this.tree) { + this.tree.refilter(); + } + } + })); } get isReadonly(): boolean { @@ -428,6 +447,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { this.replInputContainer.style.height = `${replInputHeight}px`; this.replInput.layout({ width: width - 30, height: replInputHeight }); + this.filterState.layout = new dom.Dimension(width, height); } focus(): void { @@ -437,6 +457,8 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { getActionViewItem(action: IAction): IActionViewItem | undefined { if (action.id === SelectReplAction.ID) { return this.instantiationService.createInstance(SelectReplActionViewItem, this.selectReplAction); + } else if (action.id === FILTER_ACTION_ID) { + return this.instantiationService.createInstance(TreeFilterPanelActionViewItem, action, localize('workbench.debug.filter.placeholder', "Filter. E.g.: text, !exclude"), this.filterState); } return super.getActionViewItem(action); @@ -444,6 +466,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { getActions(): IAction[] { const result: IAction[] = []; + result.push(new Action(FILTER_ACTION_ID)); if (this.debugService.getModel().getSessions(true).filter(s => s.hasSeparateRepl() && !sessionsToIgnore.has(s)).length > 1) { result.push(this.selectReplAction); } @@ -532,6 +555,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { // https://github.com/microsoft/TypeScript/issues/32526 new ReplDataSource() as IAsyncDataSource, { + filter: this.filter, accessibilityProvider: new ReplAccessibilityProvider(), identityProvider: { getId: (element: IReplElement) => element.getId() }, mouseSupport: false, diff --git a/src/vs/workbench/contrib/debug/browser/replFilter.ts b/src/vs/workbench/contrib/debug/browser/replFilter.ts new file mode 100644 index 00000000000..e5675cf2bab --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/replFilter.ts @@ -0,0 +1,246 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { matchesFuzzy } from 'vs/base/common/filters'; +import { splitGlobAware } from 'vs/base/common/glob'; +import * as strings from 'vs/base/common/strings'; +import { ITreeFilter, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree'; +import { IReplElement } from 'vs/workbench/contrib/debug/common/debug'; +import * as DOM from 'vs/base/browser/dom'; +import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems'; +import { Delayer } from 'vs/base/common/async'; +import { IAction } from 'vs/base/common/actions'; +import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { ContextScopedHistoryInputBox } from 'vs/platform/browser/contextScopedHistoryWidget'; +import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + + +type ParsedQuery = { + type: 'include' | 'exclude', + query: string, +}; + +export class ReplFilter implements ITreeFilter { + + static matchQuery = matchesFuzzy; + + private _parsedQueries: ParsedQuery[] = []; + set filterQuery(query: string) { + this._parsedQueries = []; + query = query.trim(); + + if (query && query !== '') { + const filters = splitGlobAware(query, ',').map(s => s.trim()).filter(s => !!s.length); + for (const f of filters) { + if (strings.startsWith(f, '!')) { + this._parsedQueries.push({ type: 'exclude', query: f.slice(1) }); + } else { + this._parsedQueries.push({ type: 'include', query: f }); + } + } + } + } + + filter(element: IReplElement, parentVisibility: TreeVisibility): TreeFilterResult { + if (this._parsedQueries.length === 0) { + return parentVisibility; + } + + let includeQueryPresent = false; + let includeQueryMatched = false; + + const text = element.toString(); + + for (let { type, query } of this._parsedQueries) { + if (type === 'exclude' && ReplFilter.matchQuery(query, text)) { + // If exclude query matches, ignore all other queries and hide + return false; + } else if (type === 'include') { + includeQueryPresent = true; + if (ReplFilter.matchQuery(query, text)) { + includeQueryMatched = true; + } + } + } + + return includeQueryPresent ? includeQueryMatched : parentVisibility; + } +} + +export interface IReplFiltersChangeEvent { + filterText?: boolean; + layout?: boolean; +} + +export interface IReplFiltersOptions { + filterText: string; + filterHistory: string[]; + layout: DOM.Dimension; +} + +export class TreeFilterState extends Disposable { + + private readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor(options: IReplFiltersOptions) { + super(); + this._filterText = options.filterText; + this.filterHistory = options.filterHistory; + this._layout = options.layout; + } + + private _filterText: string; + get filterText(): string { + return this._filterText; + } + set filterText(filterText: string) { + if (this._filterText !== filterText) { + this._filterText = filterText; + this._onDidChange.fire({ filterText: true }); + } + } + + filterHistory: string[]; + + private _layout: DOM.Dimension = new DOM.Dimension(0, 0); + get layout(): DOM.Dimension { + return this._layout; + } + set layout(layout: DOM.Dimension) { + if (this._layout.width !== layout.width || this._layout.height !== layout.height) { + this._layout = layout; + this._onDidChange.fire({ layout: true }); + } + } +} + +export class TreeFilterPanelActionViewItem extends BaseActionViewItem { + + private delayedFilterUpdate: Delayer; + private container: HTMLElement | undefined; + private filterInputBox: HistoryInputBox | undefined; + + constructor( + action: IAction, + private placeholder: string, + private filters: TreeFilterState, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IThemeService private readonly themeService: IThemeService, + @IContextViewService private readonly contextViewService: IContextViewService) { + super(null, action); + this.delayedFilterUpdate = new Delayer(200); + this._register(toDisposable(() => this.delayedFilterUpdate.cancel())); + } + + render(container: HTMLElement): void { + this.container = container; + DOM.addClass(this.container, 'panel-action-tree-filter-container'); + + this.element = DOM.append(this.container, DOM.$('')); + this.element.className = this.class; + this.createInput(this.element); + this.updateClass(); + + this.adjustInputBox(); + } + + focus(): void { + if (this.filterInputBox) { + this.filterInputBox.focus(); + } + } + + private clearFilterText(): void { + if (this.filterInputBox) { + this.filterInputBox.value = ''; + } + } + + private createInput(container: HTMLElement): void { + this.filterInputBox = this._register(this.instantiationService.createInstance(ContextScopedHistoryInputBox, container, this.contextViewService, { + placeholder: this.placeholder, + history: this.filters.filterHistory + })); + this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); + this.filterInputBox.value = this.filters.filterText; + this._register(this.filterInputBox.onDidChange(() => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); + this._register(this.filters.onDidChange((event: IReplFiltersChangeEvent) => { + if (event.filterText) { + this.filterInputBox!.value = this.filters.filterText; + } + })); + this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.KEY_DOWN, (e: any) => this.onInputKeyDown(e))); + this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_DOWN, this.handleKeyboardEvent)); + this._register(DOM.addStandardDisposableListener(container, DOM.EventType.KEY_UP, this.handleKeyboardEvent)); + this._register(DOM.addStandardDisposableListener(this.filterInputBox.inputElement, DOM.EventType.CLICK, (e) => { + e.stopPropagation(); + e.preventDefault(); + })); + this._register(this.filters.onDidChange(e => this.onDidFiltersChange(e))); + } + + private onDidFiltersChange(e: IReplFiltersChangeEvent): void { + if (e.layout) { + this.updateClass(); + } + } + + private onDidInputChange(inputbox: HistoryInputBox) { + inputbox.addToHistory(); + this.filters.filterText = inputbox.value; + this.filters.filterHistory = inputbox.getHistory(); + } + + // Action toolbar is swallowing some keys for action items which should not be for an input box + private handleKeyboardEvent(event: StandardKeyboardEvent) { + if (event.equals(KeyCode.Space) + || event.equals(KeyCode.LeftArrow) + || event.equals(KeyCode.RightArrow) + || event.equals(KeyCode.Escape) + ) { + event.stopPropagation(); + } + } + + private onInputKeyDown(event: StandardKeyboardEvent) { + if (event.equals(KeyCode.Escape)) { + this.clearFilterText(); + event.stopPropagation(); + event.preventDefault(); + } + } + + private adjustInputBox(): void { + if (this.element && this.filterInputBox) { + this.filterInputBox.inputElement.style.paddingRight = DOM.hasClass(this.element, 'small') ? '25px' : '150px'; + } + } + + protected updateClass(): void { + if (this.element && this.container) { + this.element.className = this.class; + DOM.toggleClass(this.container, 'grow', DOM.hasClass(this.element, 'grow')); + this.adjustInputBox(); + } + } + + protected get class(): string { + if (this.filters.layout.width > 800) { + return 'panel-action-tree-filter grow'; + } else if (this.filters.layout.width < 600) { + return 'panel-action-tree-filter small'; + } else { + return 'panel-action-tree-filter'; + } + } +} diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index ea99871de6d..99b071635d4 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -181,7 +181,7 @@ export class ReplSimpleElementsRenderer implements ITreeRenderer element.sourceData; } diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index d9238fcd9e7..b0bad1df2f2 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -8,10 +8,10 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey, IContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; -import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, CONTEXT_DEBUGGERS_AVAILABLE } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -109,30 +109,32 @@ const viewsRegistry = Registry.as(Extensions.ViewsRegistry); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'openAFileWhichCanBeDebugged', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Open a file](command:{0}) which can be debugged or run.", isMacintosh ? OpenFileFolderAction.ID : OpenFileAction.ID), - when: CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated() + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR.toNegated()) }); let debugKeybindingLabel = ''; viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'runAndDebugAction', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Run and Debug{0}](command:{1})", debugKeybindingLabel, StartAction.ID), - preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] + preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR], + when: CONTEXT_DEBUGGERS_AVAILABLE }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), - priority: ViewContentPriority.Lowest + priority: ViewContentPriority.Lowest, + when: CONTEXT_DEBUGGERS_AVAILABLE }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), - when: WorkbenchStateContext.notEqualsTo('empty') + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.notEqualsTo('empty')) }); viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebugOpenFolder', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "To customize Run and Debug, [open a folder](command:{0}) and create a launch.json file.", isMacintosh ? OpenFileFolderAction.ID : OpenFolderAction.ID), - when: WorkbenchStateContext.isEqualTo('empty') + when: ContextKeyExpr.and(CONTEXT_DEBUGGERS_AVAILABLE, WorkbenchStateContext.isEqualTo('empty')) }); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 8353ae1ab17..6467c93b443 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -59,6 +59,7 @@ export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey('resta export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey('jumpToCursorSupported', false); export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey('stepIntoTargetsSupported', false); export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey('breakpointsExist', false); +export const CONTEXT_DEBUGGERS_AVAILABLE = new RawContextKey('debuggersAvailable', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; export const BREAKPOINT_EDITOR_CONTRIBUTION_ID = 'editor.contrib.breakpoint'; @@ -224,6 +225,7 @@ export interface IDebugSession extends ITreeElement { sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; breakpointsLocations(uri: uri, lineNumber: number): Promise; + getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined; stackTrace(threadId: number, startFrame: number, levels: number, token: CancellationToken): Promise; exceptionInfo(threadId: number): Promise; @@ -572,6 +574,11 @@ export interface IDebugAdapterServer { readonly host?: string; } +export interface IDebugAdapterNamedPipeServer { + readonly type: 'pipeServer'; + readonly path: string; +} + export interface IDebugAdapterInlineImpl extends IDisposable { readonly onDidSendMessage: Event; handleMessage(message: DebugProtocol.Message): void; @@ -582,7 +589,7 @@ export interface IDebugAdapterImpl { readonly implementation: IDebugAdapterInlineImpl; } -export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterImpl; +export type IAdapterDescriptor = IDebugAdapterExecutable | IDebugAdapterServer | IDebugAdapterNamedPipeServer | IDebugAdapterImpl; export interface IPlatformSpecificAdapterContribution { program?: string; @@ -657,6 +664,8 @@ export interface IConfigurationManager { getLaunches(): ReadonlyArray; + hasDebuggers(): boolean; + getLaunch(workspaceUri: uri | undefined): ILaunch | undefined; getAllConfigurations(): { launch: ILaunch, name: string, presentation?: IConfigPresentation }[]; diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index 0a5620c8d4b..1d302de30e3 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -597,6 +597,26 @@ export abstract class BaseBreakpoint extends Enablement implements IBaseBreakpoi return data ? data.id : undefined; } + getDebugProtocolBreakpoint(sessionId: string): DebugProtocol.Breakpoint | undefined { + const data = this.sessionData.get(sessionId); + if (data) { + const bp: DebugProtocol.Breakpoint = { + id: data.id, + verified: data.verified, + message: data.message, + source: data.source, + line: data.line, + column: data.column, + endLine: data.endLine, + endColumn: data.endColumn, + instructionReference: data.instructionReference, + offset: data.offset + }; + return bp; + } + return undefined; + } + toJSON(): any { const result = Object.create(null); result.enabled = this.enabled; @@ -1094,6 +1114,14 @@ export class DebugModel implements IDebugModel { }); } + getDebugProtocolBreakpoint(breakpointId: string, sessionId: string): DebugProtocol.Breakpoint | undefined { + const bp = this.breakpoints.find(bp => bp.getId() === breakpointId); + if (bp) { + return bp.getDebugProtocolBreakpoint(sessionId); + } + return undefined; + } + private sortAndDeDup(): void { this.breakpoints = this.breakpoints.sort((first, second) => { if (first.uri.toString() !== second.uri.toString()) { diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index 87d3348b97c..b0aa0128449 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -174,13 +174,18 @@ export class ReplGroup implements IReplElement { } } +type FilterFunc = ((element: IReplElement) => void); + export class ReplModel { private replElements: IReplElement[] = []; private readonly _onDidChangeElements = new Emitter(); readonly onDidChangeElements = this._onDidChangeElements.event; + private filterFunc: FilterFunc | undefined; getReplElements(): IReplElement[] { - return this.replElements; + return this.replElements.filter(element => + this.filterFunc ? this.filterFunc(element) : true + ); } async addReplExpression(session: IDebugSession, stackFrame: IStackFrame | undefined, name: string): Promise { @@ -315,6 +320,10 @@ export class ReplModel { } } + setFilter(filterFunc: FilterFunc): void { + this.filterFunc = filterFunc; + } + removeReplExpressions(): void { if (this.replElements.length > 0) { this.replElements = []; diff --git a/src/vs/workbench/contrib/debug/node/debugAdapter.ts b/src/vs/workbench/contrib/debug/node/debugAdapter.ts index 70ddc23e1b5..dc6d941a068 100644 --- a/src/vs/workbench/contrib/debug/node/debugAdapter.ts +++ b/src/vs/workbench/contrib/debug/node/debugAdapter.ts @@ -14,7 +14,7 @@ import * as objects from 'vs/base/common/objects'; import * as platform from 'vs/base/common/platform'; import { ExtensionsChannelId } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; -import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugAdapterExecutable, IDebuggerContribution, IPlatformSpecificAdapterContribution, IDebugAdapterServer, IDebugAdapterNamedPipeServer } from 'vs/workbench/contrib/debug/common/debug'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { AbstractDebugAdapter } from '../common/abstractDebugAdapter'; @@ -91,25 +91,22 @@ export abstract class StreamDebugAdapter extends AbstractDebugAdapter { } } -/** - * An implementation that connects to a debug adapter via a socket. -*/ -export class SocketDebugAdapter extends StreamDebugAdapter { +export abstract class NetworkDebugAdapter extends StreamDebugAdapter { - private socket?: net.Socket; + protected socket?: net.Socket; - constructor(private adapterServer: IDebugAdapterServer) { - super(); - } + protected abstract createConnection(connectionListener: () => void): net.Socket; startSession(): Promise { return new Promise((resolve, reject) => { let connected = false; - this.socket = net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', () => { + + this.socket = this.createConnection(() => { this.connect(this.socket!, this.socket!); resolve(); connected = true; }); + this.socket.on('close', () => { if (connected) { this._onError.fire(new Error('connection closed')); @@ -117,6 +114,7 @@ export class SocketDebugAdapter extends StreamDebugAdapter { reject(new Error('connection closed')); } }); + this.socket.on('error', error => { if (connected) { this._onError.fire(error); @@ -136,6 +134,34 @@ export class SocketDebugAdapter extends StreamDebugAdapter { } } +/** + * An implementation that connects to a debug adapter via a socket. +*/ +export class SocketDebugAdapter extends NetworkDebugAdapter { + + constructor(private adapterServer: IDebugAdapterServer) { + super(); + } + + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.port, this.adapterServer.host || '127.0.0.1', connectionListener); + } +} + +/** + * An implementation that connects to a debug adapter via a NamedPipe (on Windows)/UNIX Domain Socket (on non-Windows). + */ +export class NamedPipeDebugAdapter extends NetworkDebugAdapter { + + constructor(private adapterServer: IDebugAdapterNamedPipeServer) { + super(); + } + + protected createConnection(connectionListener: () => void): net.Socket { + return net.createConnection(this.adapterServer.path, connectionListener); + } +} + /** * An implementation that launches the debug adapter as a separate process and communicates via stdin/stdout. */ 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 9449af5368e..596687ed5a0 100644 --- a/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/callStack.test.ts @@ -297,7 +297,7 @@ suite('Debug - CallStack', () => { const session = createMockSession(model); model.addSession(session); const { firstStackFrame, secondStackFrame } = createTwoStackFrames(session); - let decorations = createDecorationsForStackFrame(firstStackFrame, firstStackFrame.range); + let decorations = createDecorationsForStackFrame(firstStackFrame, firstStackFrame.range, true); assert.equal(decorations.length, 2); assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe'); @@ -305,7 +305,7 @@ suite('Debug - CallStack', () => { assert.equal(decorations[1].options.className, 'debug-top-stack-frame-line'); assert.equal(decorations[1].options.isWholeLine, true); - decorations = createDecorationsForStackFrame(secondStackFrame, firstStackFrame.range); + decorations = createDecorationsForStackFrame(secondStackFrame, firstStackFrame.range, true); assert.equal(decorations.length, 2); assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe-focused'); @@ -313,7 +313,7 @@ suite('Debug - CallStack', () => { assert.equal(decorations[1].options.className, 'debug-focused-stack-frame-line'); assert.equal(decorations[1].options.isWholeLine, true); - decorations = createDecorationsForStackFrame(firstStackFrame, new Range(1, 5, 1, 6)); + decorations = createDecorationsForStackFrame(firstStackFrame, new Range(1, 5, 1, 6), true); assert.equal(decorations.length, 3); assert.deepEqual(decorations[0].range, new Range(1, 2, 1, 1)); assert.equal(decorations[0].options.glyphMarginClassName, 'codicon-debug-stackframe'); 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 a8c0c42173e..7440c4df8b8 100644 --- a/src/vs/workbench/contrib/debug/test/browser/repl.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/repl.test.ts @@ -12,6 +12,8 @@ import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplModel import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; import { timeout } from 'vs/base/common/async'; import { createMockSession } from 'vs/workbench/contrib/debug/test/browser/callStack.test'; +import { ReplFilter } from 'vs/workbench/contrib/debug/browser/replFilter'; +import { TreeVisibility } from 'vs/base/browser/ui/tree/tree'; suite('Debug - REPL', () => { let model: DebugModel; @@ -189,4 +191,59 @@ suite('Debug - REPL', () => { assert.equal(repl.getReplElements().length, 3); assert.equal((repl.getReplElements()[2]).value, 'second global line'); }); + + test('repl filter', async () => { + const session = createMockSession(model); + const repl = new ReplModel(); + const replFilter = new ReplFilter(); + + repl.setFilter((element) => { + const filterResult = replFilter.filter(element, TreeVisibility.Visible); + return filterResult === true || filterResult === TreeVisibility.Visible; + }); + + repl.appendToRepl(session, 'first line\n', severity.Info); + repl.appendToRepl(session, 'second line\n', severity.Info); + repl.appendToRepl(session, 'third line\n', severity.Info); + repl.appendToRepl(session, 'fourth line\n', severity.Info); + + replFilter.filterQuery = 'first'; + let r1 = repl.getReplElements(); + assert.equal(r1.length, 1); + assert.equal(r1[0].value, 'first line\n'); + + replFilter.filterQuery = '!first'; + let r2 = repl.getReplElements(); + assert.equal(r1.length, 1); + assert.equal(r2[0].value, 'second line\n'); + assert.equal(r2[1].value, 'third line\n'); + assert.equal(r2[2].value, 'fourth line\n'); + + replFilter.filterQuery = 'first, line'; + let r3 = repl.getReplElements(); + assert.equal(r3.length, 4); + assert.equal(r3[0].value, 'first line\n'); + assert.equal(r3[1].value, 'second line\n'); + assert.equal(r3[2].value, 'third line\n'); + assert.equal(r3[3].value, 'fourth line\n'); + + replFilter.filterQuery = 'line, !second'; + let r4 = repl.getReplElements(); + assert.equal(r4.length, 3); + assert.equal(r4[0].value, 'first line\n'); + assert.equal(r4[1].value, 'third line\n'); + assert.equal(r4[2].value, 'fourth line\n'); + + replFilter.filterQuery = '!second, line'; + let r4_same = repl.getReplElements(); + assert.equal(r4.length, r4_same.length); + + replFilter.filterQuery = '!line'; + let r5 = repl.getReplElements(); + assert.equal(r5.length, 0); + + replFilter.filterQuery = 'smth'; + let r6 = repl.getReplElements(); + assert.equal(r6.length, 0); + }); }); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index f3057da0e95..9f4002da8b9 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -293,6 +293,9 @@ export class MockSession implements IDebugSession { sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise { throw new Error('Method not implemented.'); } + getDebugProtocolBreakpoint(breakpointId: string): DebugProtocol.Breakpoint | undefined { + throw new Error('Method not implemented.'); + } customRequest(request: string, args: any): Promise { throw new Error('Method not implemented.'); } diff --git a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts index 9b504a180a9..abe90db10c1 100644 --- a/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/electron-browser/debugANSIHandling.test.ts @@ -88,7 +88,6 @@ suite('Debug - ANSI Handling', () => { return child; } else { assert.fail('Unexpected assertion error'); - return null!; } } diff --git a/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts new file mode 100644 index 00000000000..470565e2d7a --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/node/streamDebugAdapter.test.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as crypto from 'crypto'; +import * as net from 'net'; +import * as platform from 'vs/base/common/platform'; +import { tmpdir } from 'os'; +import { join } from 'vs/base/common/path'; +import { SocketDebugAdapter, NamedPipeDebugAdapter, StreamDebugAdapter } from 'vs/workbench/contrib/debug/node/debugAdapter'; + +function rndPort(): number { + const min = 8000; + const max = 9000; + return Math.floor(Math.random() * (max - min) + min); +} + +function sendInitializeRequest(debugAdapter: StreamDebugAdapter): Promise { + return new Promise((resolve, reject) => { + debugAdapter.sendRequest('initialize', { adapterID: 'test' }, (result) => { + resolve(result); + }); + }); +} + +function serverConnection(socket: net.Socket) { + socket.on('data', (data: Buffer) => { + const str = data.toString().split('\r\n')[2]; + const request = JSON.parse(str); + const response: any = { + seq: request.seq, + request_seq: request.seq, + type: 'response', + command: request.command + }; + if (request.arguments.adapterID === 'test') { + response.success = true; + } else { + response.success = false; + response.message = 'failed'; + } + + const responsePayload = JSON.stringify(response); + socket.write(`Content-Length: ${responsePayload.length}\r\n\r\n${responsePayload}`); + }); +} + +suite('Debug - StreamDebugAdapter', () => { + const port = rndPort(); + const pipeName = crypto.randomBytes(10).toString('hex'); + const pipePath = platform.isWindows ? join('\\\\.\\pipe\\', pipeName) : join(tmpdir(), pipeName); + + const testCases: { testName: string, debugAdapter: StreamDebugAdapter, connectionDetail: string | number }[] = [ + { + testName: 'NamedPipeDebugAdapter', + debugAdapter: new NamedPipeDebugAdapter({ + type: 'pipeServer', + path: pipePath + }), + connectionDetail: pipePath + }, + { + testName: 'SocketDebugAdapter', + debugAdapter: new SocketDebugAdapter({ + type: 'server', + port + }), + connectionDetail: port + } + ]; + + for (const testCase of testCases) { + test(`StreamDebugAdapter (${testCase.testName}) can initialize a connection`, async () => { + const server = net.createServer(serverConnection).listen(testCase.connectionDetail); + const debugAdapter = testCase.debugAdapter; + try { + await debugAdapter.startSession(); + const response: DebugProtocol.Response = await sendInitializeRequest(debugAdapter); + assert.strictEqual(response.command, 'initialize'); + assert.strictEqual(response.request_seq, 1); + assert.strictEqual(response.success, true, response.message); + } finally { + await debugAdapter.stopSession(); + server.close(); + debugAdapter.dispose(); + } + }); + } +}); diff --git a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts index 048c43ef01a..c56afa2d157 100644 --- a/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts +++ b/src/vs/workbench/contrib/emmet/test/browser/emmetAction.test.ts @@ -56,14 +56,12 @@ suite('Emmet', () => { const model = editor.getModel(); if (!model) { assert.fail('Editor model not found'); - return; } model.setMode(languageIdentifier); let langOutput = EmmetEditorAction.getLanguage(languageIdentifierResolver, editor, new MockGrammarContributions(scopeName)); if (!langOutput) { assert.fail('langOutput not found'); - return; } assert.equal(langOutput.language, expectedLanguage); diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index 8215282b35b..ca5d30091db 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IConfigBasedExtensionTip } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionTipsService, IConfigBasedExtensionTip, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { localize } from 'vs/nls'; @@ -14,13 +14,17 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IWorkspaceContextService, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; -import { distinct } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; export class ConfigBasedRecommendations extends ExtensionRecommendations { private importantTips: IConfigBasedExtensionTip[] = []; private otherTips: IConfigBasedExtensionTip[] = []; + private _onDidChangeRecommendations = this._register(new Emitter()); + readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; + private _otherRecommendations: ExtensionRecommendation[] = []; get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } @@ -32,8 +36,9 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @INotificationService notificationService: INotificationService, @@ -41,13 +46,12 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { await this.fetch(); this._register(this.workspaceContextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); - this.promptWorkspaceRecommendations(); } private async fetch(): Promise { @@ -70,54 +74,13 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { this._importantRecommendations = this.importantTips.map(tip => this.toExtensionRecommendation(tip)); } - private async promptWorkspaceRecommendations(): Promise { - if (this.hasToIgnoreRecommendationNotifications()) { - return; - } - - if (this.importantTips.length === 0) { - return; - } - - const local = await this.extensionManagementService.getInstalled(); - const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local); - if (uninstalled.length === 0) { - return; - } - - const importantExtensions = this.filterIgnoredOrNotAllowed(uninstalled); - if (importantExtensions.length === 0) { - return; - } - - for (const extension of importantExtensions) { - const tip = this.importantTips.filter(tip => tip.extensionId === extension)[0]; - const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended for this workspace.", tip.extensionName) - : localize('extensionRecommended', "The '{0}' extension is recommended for this workspace.", tip.extensionName); - this.promptImportantExtensionsInstallNotification([extension], message); - } - } - - private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } { - const installed: string[] = [], uninstalled: string[] = []; - const installedExtensionsIds = local.reduce((result, i) => { result.add(i.identifier.id.toLowerCase()); return result; }, new Set()); - recommendationsToSuggest.forEach(id => { - if (installedExtensionsIds.has(id.toLowerCase())) { - installed.push(id); - } else { - uninstalled.push(id); - } - }); - return { installed, uninstalled }; - } - private async onWorkspaceFoldersChanged(event: IWorkspaceFoldersChangeEvent): Promise { if (event.added.length) { const oldImportantRecommended = this.importantTips; await this.fetch(); // Suggest only if at least one of the newly added recommendations was not suggested before if (this.importantTips.some(current => oldImportantRecommended.every(old => current.extensionId !== old.extensionId))) { - return this.promptWorkspaceRecommendations(); + this._onDidChangeRecommendations.fire(); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts index 39694d6c0a7..17c8c513833 100644 --- a/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/dynamicWorkspaceRecommendations.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionTipsService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IFileService } from 'vs/platform/files/common/files'; @@ -18,6 +18,7 @@ import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionMa import { localize } from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; type DynamicWorkspaceRecommendationsClassification = { count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -44,9 +45,11 @@ export class DynamicWorkspaceRecommendations extends ExtensionRecommendations { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 60a16d6fc88..52b4b91a0c6 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -8,7 +8,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { timeout } from 'vs/base/common/async'; import { localize } from 'vs/nls'; -import { IStringDictionary } from 'vs/base/common/collections'; import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { basename } from 'vs/base/common/path'; @@ -17,6 +16,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ITASExperimentService } from 'vs/workbench/services/experiment/common/experimentService'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; type ExeExtensionRecommendationsClassification = { extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; @@ -25,12 +25,11 @@ type ExeExtensionRecommendationsClassification = { export class ExeBasedRecommendations extends ExtensionRecommendations { + private _otherTips: IExecutableBasedExtensionTip[] = []; + private _importantTips: IExecutableBasedExtensionTip[] = []; - private readonly _otherRecommendations: ExtensionRecommendation[] = []; - get otherRecommendations(): ReadonlyArray { return this._otherRecommendations; } - - private readonly _importantRecommendations: ExtensionRecommendation[] = []; - get importantRecommendations(): ReadonlyArray { return this._importantRecommendations; } + get otherRecommendations(): ReadonlyArray { return this._otherTips.map(tip => this.toExtensionRecommendation(tip)); } + get importantRecommendations(): ReadonlyArray { return this._importantTips.map(tip => this.toExtensionRecommendation(tip)); } get recommendations(): ReadonlyArray { return [...this.importantRecommendations, ...this.otherRecommendations]; } @@ -39,7 +38,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, @IExtensionTipsService private readonly extensionTipsService: IExtensionTipsService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @optional(ITASExperimentService) tasExperimentService: ITASExperimentService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @@ -48,7 +48,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); this.tasExperimentService = tasExperimentService; /* @@ -58,27 +58,35 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { timeout(3000).then(() => this.fetchAndPromptImportantExeBasedRecommendations()); } + getRecommendations(exe: string): { important: ExtensionRecommendation[], others: ExtensionRecommendation[] } { + const important = this._importantTips + .filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase()) + .map(tip => this.toExtensionRecommendation(tip)); + + const others = this._otherTips + .filter(tip => tip.exeName.toLowerCase() === exe.toLowerCase()) + .map(tip => this.toExtensionRecommendation(tip)); + + return { important, others }; + } + protected async doActivate(): Promise { - const otherExectuableBasedTips = await this.extensionTipsService.getOtherExecutableBasedTips(); - otherExectuableBasedTips.forEach(tip => this._otherRecommendations.push(this.toExtensionRecommendation(tip))); + this._otherTips = await this.extensionTipsService.getOtherExecutableBasedTips(); await this.fetchImportantExeBasedRecommendations(); } - private _importantExeBasedRecommendations: Promise> | undefined; - private async fetchImportantExeBasedRecommendations(): Promise> { + private _importantExeBasedRecommendations: Promise> | undefined; + private async fetchImportantExeBasedRecommendations(): Promise> { if (!this._importantExeBasedRecommendations) { this._importantExeBasedRecommendations = this.doFetchImportantExeBasedRecommendations(); } return this._importantExeBasedRecommendations; } - private async doFetchImportantExeBasedRecommendations(): Promise> { - const importantExeBasedRecommendations: IStringDictionary = {}; - const importantExectuableBasedTips = await this.extensionTipsService.getImportantExecutableBasedTips(); - importantExectuableBasedTips.forEach(tip => { - this._importantRecommendations.push(this.toExtensionRecommendation(tip)); - importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip; - }); + private async doFetchImportantExeBasedRecommendations(): Promise> { + const importantExeBasedRecommendations = new Map(); + this._importantTips = await this.extensionTipsService.getImportantExecutableBasedTips(); + this._importantTips.forEach(tip => importantExeBasedRecommendations.set(tip.extensionId.toLowerCase(), tip)); return importantExeBasedRecommendations; } @@ -86,22 +94,26 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { const importantExeBasedRecommendations = await this.fetchImportantExeBasedRecommendations(); const local = await this.extensionManagementService.getInstalled(); - const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local); + const { installed, uninstalled } = this.groupByInstalled([...importantExeBasedRecommendations.keys()], local); /* Log installed and uninstalled exe based recommendations */ for (const extensionId of installed) { - const tip = importantExeBasedRecommendations[extensionId]; - this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:alreadyInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + } } for (const extensionId of uninstalled) { - const tip = importantExeBasedRecommendations[extensionId]; - this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + this.telemetryService.publicLog2<{ exeName: string, extensionId: string }, ExeExtensionRecommendationsClassification>('exeExtensionRecommendations:notInstalled', { extensionId, exeName: basename(tip.windowsPath!) }); + } } this.promptImportantExeBasedRecommendations(uninstalled, importantExeBasedRecommendations); } - private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: IStringDictionary): Promise { + private async promptImportantExeBasedRecommendations(recommendations: string[], importantExeBasedRecommendations: Map): Promise { if (this.hasToIgnoreRecommendationNotifications()) { return; } @@ -112,13 +124,15 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { const recommendationsByExe = new Map(); for (const extensionId of recommendations) { - const tip = importantExeBasedRecommendations[extensionId]; - let tips = recommendationsByExe.get(tip.exeFriendlyName); - if (!tips) { - tips = []; - recommendationsByExe.set(tip.exeFriendlyName, tips); + const tip = importantExeBasedRecommendations.get(extensionId); + if (tip) { + let tips = recommendationsByExe.get(tip.exeFriendlyName); + if (!tips) { + tips = []; + recommendationsByExe.set(tip.exeFriendlyName, tips); + } + tips.push(tip); } - tips.push(tip); } for (const [, tips] of recommendationsByExe) { @@ -127,22 +141,8 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { await this.tasExperimentService.getTreatment('wslpopupaa'); } - if (tips.length === 1) { - const tip = tips[0]; - const message = tip.isExtensionPack ? localize('extensionPackRecommended', "The '{0}' extension pack is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!)) - : localize('exeRecommended', "The '{0}' extension is recommended as you have {1} installed on your system.", tip.extensionName, tip.exeFriendlyName || basename(tip.windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } - - else if (tips.length === 2) { - const message = localize('two extensions recommended', "The '{0}' and '{1}' extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } - - else if (tips.length > 2) { - const message = localize('more than two extensions recommended', "The '{0}', '{1}' and other extensions are recommended as you have {2} installed on your system.", tips[0].extensionName, tips[1].extensionName, tips[0].exeFriendlyName || basename(tips[0].windowsPath!)); - this.promptImportantExtensionsInstallNotification(extensionIds, message); - } + const message = localize('exeRecommended', "You have {0} installed on your system. Do you want to install recommendations for it?", tips[0].exeFriendlyName); + this.promptImportantExtensionsInstallNotification(extensionIds, message, `@exe:"${tips[0].exeName}"`); } } diff --git a/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts index 1e30aee3b93..c24644779fd 100644 --- a/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/experimentalRecommendations.ts @@ -13,6 +13,8 @@ import { IExperimentService, ExperimentActionType, ExperimentState } from 'vs/wo import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; export class ExperimentalRecommendations extends ExtensionRecommendations { @@ -27,9 +29,11 @@ export class ExperimentalRecommendations extends ExtensionRecommendations { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } /** diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 347f08e869c..502a12a626a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -15,7 +15,7 @@ import { isPromiseCanceledError } from 'vs/base/common/errors'; import { dispose, toDisposable, Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { domEvent } from 'vs/base/browser/event'; import { append, $, addClass, removeClass, finalHandler, join, toggleClass, hide, show, addDisposableListener, EventType } from 'vs/base/browser/dom'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; @@ -25,7 +25,7 @@ import { ResolvedKeybinding, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, IExtension, ExtensionContainers } from 'vs/workbench/contrib/extensions/common/extensions'; import { RatingsWidget, InstallCountWidget, RemoteBadgeWidget } from 'vs/workbench/contrib/extensions/browser/extensionsWidgets'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { CombinedInstallAction, UpdateAction, ExtensionEditorDropDownAction, ReloadAction, MaliciousStatusLabelAction, IgnoreExtensionRecommendationAction, UndoIgnoreExtensionRecommendationAction, EnableDropDownAction, DisableDropDownAction, StatusLabelAction, SetFileIconThemeAction, SetColorThemeAction, RemoteInstallAction, ExtensionToolTipAction, SystemDisabledWarningAction, LocalInstallAction, SyncIgnoredIconAction, SetProductIconThemeAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -164,7 +164,7 @@ interface IExtensionEditorTemplate { header: HTMLElement; } -export class ExtensionEditor extends BaseEditor { +export class ExtensionEditor extends EditorPane { static readonly ID: string = 'workbench.editor.extension'; @@ -313,8 +313,8 @@ export class ExtensionEditor extends BaseEditor { return disposables; } - async setInput(input: ExtensionsInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - await super.setInput(input, options, token); + async setInput(input: ExtensionsInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); if (this.template) { await this.updateTemplate(input, this.template, !!options?.preserveFocus); } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts index 4347c796632..caf2db0f27a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendations.ts @@ -8,13 +8,15 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { localize } from 'vs/nls'; -import { InstallRecommendedExtensionAction, ShowRecommendedExtensionAction, ShowRecommendedExtensionsAction, InstallRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionRecommendationSource, IExtensionRecommendationReson } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { IExtensionsConfiguration, ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsConfiguration, ConfigurationKey, IExtension, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IAction } from 'vs/base/common/actions'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { CancellationToken } from 'vs/base/common/cancellation'; type ExtensionRecommendationsNotificationClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -42,6 +44,8 @@ export abstract class ExtensionRecommendations extends Disposable { @INotificationService protected readonly notificationService: INotificationService, @ITelemetryService protected readonly telemetryService: ITelemetryService, @IStorageService protected readonly storageService: IStorageService, + @IExtensionsWorkbenchService protected readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { super(); @@ -57,47 +61,60 @@ export abstract class ExtensionRecommendations extends Disposable { return this._activationPromise; } - private runAction(action: IAction) { + private async runAction(action: IAction): Promise { try { - action.run(); + await action.run(); } finally { action.dispose(); } } - protected promptImportantExtensionsInstallNotification(extensionIds: string[], message: string): void { + private async getInstallableExtensions(extensionIds: string[]): Promise { + const extensions: IExtension[] = []; + if (extensionIds.length) { + const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + } + return extensions; + } + + protected async promptImportantExtensionsInstallNotification(extensionIds: string[], message: string, searchValue: string): Promise { + const extensions = await this.getInstallableExtensions(extensionIds); + if (!extensions.length) { + return; + } + this.notificationService.prompt(Severity.Info, message, [{ - label: extensionIds.length === 1 ? localize('install', 'Install') : localize('installAll', "Install All"), + label: localize('install', 'Install'), run: async () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId }); - } - if (extensionIds.length === 1) { - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionAction, extensionIds[0])); - } else { - this.runAction(this.instantiationService.createInstance(InstallRecommendedExtensionsAction, InstallRecommendedExtensionsAction.ID, InstallRecommendedExtensionsAction.LABEL, extensionIds, 'install-recommendations')); - } + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); + await Promise.all(extensions.map(async extension => { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'install', extensionId: extension.identifier.id }); + this.extensionsWorkbenchService.open(extension, { pinned: true }); + await this.extensionManagementService.installFromGallery(extension.gallery!); + })); } }, { - label: extensionIds.length === 1 ? localize('moreInformation', "More Information") : localize('showRecommendations', "Show Recommendations"), - run: () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId }); - } - if (extensionIds.length === 1) { - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionAction, extensionIds[0])); - } else { - this.runAction(this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, ShowRecommendedExtensionsAction.LABEL)); + label: localize('show recommendations', "Show Recommendations"), + run: async () => { + for (const extension of extensions) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'show', extensionId: extension.identifier.id }); + this.extensionsWorkbenchService.open(extension, { pinned: true }); } + this.runAction(this.instantiationService.createInstance(SearchExtensionsAction, searchValue)); } }, { label: choiceNever, isSecondary: true, run: () => { - for (const extensionId of extensionIds) { - this.addToImportantRecommendationsIgnore(extensionId); - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId }); + for (const extension of extensions) { + this.addToImportantRecommendationsIgnore(extension.identifier.id); + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'neverShowAgain', extensionId: extension.identifier.id }); } this.notificationService.prompt( Severity.Info, @@ -115,8 +132,8 @@ export abstract class ExtensionRecommendations extends Disposable { { sticky: true, onCancel: () => { - for (const extensionId of extensionIds) { - this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId }); + for (const extension of extensions) { + this.telemetryService.publicLog2<{ userReaction: string, extensionId: string }, ExtensionRecommendationsNotificationClassification>('extensionRecommendations:popup', { userReaction: 'cancelled', extensionId: extension.identifier.id }); } } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts index 559f5346458..7a04a1f71c2 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionRecommendationsService.ts @@ -5,10 +5,10 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IExtensionManagementService, IExtensionGalleryService, InstallOperation, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { IExtensionRecommendationsService, ExtensionRecommendationReason, RecommendationChangeNotification, IExtensionRecommendation, ExtensionRecommendationSource } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { IExtensionRecommendationsService, ExtensionRecommendationReason, RecommendationChangeNotification, IExtensionRecommendation, ExtensionRecommendationSource, EnablementState, IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ShowRecommendationsOnlyOnDemandKey } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ConfigurationKey, IExtension, IExtensionsConfiguration, IExtensionsWorkbenchService, ShowRecommendationsOnlyOnDemandKey } from 'vs/workbench/contrib/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { distinct, shuffle } from 'vs/base/common/arrays'; @@ -25,13 +25,24 @@ import { KeymapRecommendations } from 'vs/workbench/contrib/extensions/browser/k import { ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ConfigBasedRecommendations } from 'vs/workbench/contrib/extensions/browser/configBasedRecommendations'; +import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import Severity from 'vs/base/common/severity'; +import { localize } from 'vs/nls'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { SearchExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { CancellationToken } from 'vs/base/common/cancellation'; type IgnoreRecommendationClassification = { recommendationReason: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' }; }; +type ExtensionWorkspaceRecommendationsNotificationClassification = { + userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; +}; + const ignoredRecommendationsStorageKey = 'extensionsAssistant/ignored_recommendations'; +const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; export class ExtensionRecommendationsService extends Disposable implements IExtensionRecommendationsService { @@ -49,15 +60,15 @@ export class ExtensionRecommendationsService extends Disposable implements IExte // Ignored Recommendations private globallyIgnoredRecommendations: string[] = []; - public loadWorkspaceConfigPromise: Promise; + public readonly activationPromise: Promise; private sessionSeed: number; private readonly _onRecommendationChange = this._register(new Emitter()); onRecommendationChange: Event = this._onRecommendationChange.event; constructor( - @IInstantiationService instantiationService: IInstantiationService, - @ILifecycleService lifecycleService: ILifecycleService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @ILifecycleService private readonly lifecycleService: ILifecycleService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @IStorageService private readonly storageService: IStorageService, @@ -65,6 +76,9 @@ export class ExtensionRecommendationsService extends Disposable implements IExte @ITelemetryService private readonly telemetryService: ITelemetryService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, + @INotificationService private readonly notificationService: INotificationService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super(); @@ -81,7 +95,7 @@ export class ExtensionRecommendationsService extends Disposable implements IExte if (!this.isEnabled()) { this.sessionSeed = 0; - this.loadWorkspaceConfigPromise = Promise.resolve(); + this.activationPromise = Promise.resolve(); return; } @@ -89,17 +103,33 @@ export class ExtensionRecommendationsService extends Disposable implements IExte this.globallyIgnoredRecommendations = this.getCachedIgnoredRecommendations(); // Activation - this.loadWorkspaceConfigPromise = this.workspaceRecommendations.activate().then(() => this.fileBasedRecommendations.activate()); - this.experimentalRecommendations.activate(); - this.keymapRecommendations.activate(); - if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { - lifecycleService.when(LifecyclePhase.Eventually).then(() => this.activateProactiveRecommendations()); - } + this.activationPromise = this.activate(); this._register(this.extensionManagementService.onDidInstallExtension(e => this.onDidInstallExtension(e))); this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); } + private async activate(): Promise { + await this.lifecycleService.when(LifecyclePhase.Restored); + + // activate all recommendations + await Promise.all([ + this.workspaceRecommendations.activate(), + this.fileBasedRecommendations.activate(), + this.experimentalRecommendations.activate(), + this.keymapRecommendations.activate(), + this.lifecycleService.when(LifecyclePhase.Eventually) + .then(() => { + if (!this.configurationService.getValue(ShowRecommendationsOnlyOnDemandKey)) { + this.activateProactiveRecommendations(); + } + }) + ]); + + await this.promptWorkspaceRecommendations(); + this._register(Event.any(this.workspaceRecommendations.onDidChangeRecommendations, this.configBasedRecommendations.onDidChangeRecommendations)(() => this.promptWorkspaceRecommendations())); + } + private isEnabled(): boolean { return this.galleryService.isEnabled() && !this.environmentService.extensionDevelopmentLocationURI; } @@ -191,6 +221,13 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return this.toExtensionRecommendations(this.workspaceRecommendations.recommendations); } + async getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }> { + await this.exeBasedRecommendations.activate(); + const { important, others } = exe ? this.exeBasedRecommendations.getRecommendations(exe) + : { important: this.exeBasedRecommendations.importantRecommendations, others: this.exeBasedRecommendations.otherRecommendations }; + return { important: this.toExtensionRecommendations(important), others: this.toExtensionRecommendations(others) }; + } + getFileBasedRecommendations(): IExtensionRecommendation[] { return this.toExtensionRecommendations(this.fileBasedRecommendations.recommendations); } @@ -254,6 +291,83 @@ export class ExtensionRecommendationsService extends Disposable implements IExte return allIgnoredRecommendations.indexOf(id.toLowerCase()) === -1; } + private async getInstallableExtensions(extensionIds: string[]): Promise { + const extensions: IExtension[] = []; + const pager = await this.extensionsWorkbenchService.queryGallery({ names: extensionIds, pageSize: extensionIds.length, source: 'install-recommendations' }, CancellationToken.None); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + return extensions; + } + + private async promptWorkspaceRecommendations(): Promise { + const allowedRecommendations = [...this.workspaceRecommendations.recommendations, ...this.configBasedRecommendations.importantRecommendations] + .map(({ extensionId }) => extensionId) + .filter(extensionId => this.isExtensionAllowedToBeRecommended(extensionId)); + + const config = this.configurationService.getValue(ConfigurationKey); + if (allowedRecommendations.length === 0 + || config.ignoreRecommendations || config.showRecommendationsOnlyOnDemand + || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false)) { + return; + } + + let installed = await this.extensionManagementService.getInstalled(); + installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind + const recommendations = allowedRecommendations.filter(extensionId => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); + + if (!recommendations.length) { + return; + } + + const extensions = await this.getInstallableExtensions(recommendations); + if (!extensions.length) { + return; + } + + const searchValue = '@recommended '; + this.notificationService.prompt( + Severity.Info, + localize('workspaceRecommended', "Do you want to install recommendations for this repository?"), + [{ + label: localize('install', "Install"), + run: async () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); + await Promise.all(extensions.map(async extension => { + this.extensionsWorkbenchService.open(extension, { pinned: true }); + await this.extensionManagementService.installFromGallery(extension.gallery!); + })); + } + }, { + label: localize('showRecommendations', "Show Recommendations"), + run: async () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); + const action = this.instantiationService.createInstance(SearchExtensionsAction, searchValue); + try { + await action.run(); + } finally { + action.dispose(); + } + } + }, { + label: localize('neverShowAgain', "Don't Show Again"), + isSecondary: true, + run: () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); + this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); + } + }], + { + sticky: true, + onCancel: () => { + this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); + } + } + ); + } + private onDidStorageChange(e: IWorkspaceStorageChangeEvent): void { if (e.key === ignoredRecommendationsStorageKey && e.scope === StorageScope.GLOBAL && this.ignoredRecommendationsValue !== this.getStoredIgnoredRecommendationsValue() /* This checks if current window changed the value or not */) { diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index b6a43859392..5287eea642b 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -15,11 +15,10 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWo import { IOutputChannelRegistry, Extensions as OutputExtensions } from 'vs/workbench/services/output/common/output'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { OpenExtensionsViewletAction, InstallExtensionsAction, ShowOutdatedExtensionsAction, ShowRecommendedExtensionsAction, ShowRecommendedKeymapExtensionsAction, ShowPopularExtensionsAction, ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowDisabledExtensionsAction, ShowBuiltInExtensionsAction, UpdateAllAction, - EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction + EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction, ClearExtensionsSearchResultsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; @@ -55,7 +54,7 @@ import { MultiCommand } from 'vs/editor/browser/editorExtensions'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; // Singletons -registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); +// registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben uncomment when 'semver-umd' can be loaded registerSingleton(IExtensionRecommendationsService, ExtensionRecommendationsService); Registry.as(OutputExtensions.OutputChannels) @@ -113,9 +112,6 @@ actionRegistry.registerWorkbenchAction(keymapRecommendationsActionDescriptor, 'P const languageExtensionsActionDescriptor = SyncActionDescriptor.from(ShowLanguageExtensionsAction); actionRegistry.registerWorkbenchAction(languageExtensionsActionDescriptor, 'Preferences: Language Extensions', PreferencesLabel); -const azureExtensionsActionDescriptor = SyncActionDescriptor.from(ShowAzureExtensionsAction); -actionRegistry.registerWorkbenchAction(azureExtensionsActionDescriptor, 'Preferences: Azure Extensions', PreferencesLabel); - const popularActionDescriptor = SyncActionDescriptor.from(ShowPopularExtensionsAction); actionRegistry.registerWorkbenchAction(popularActionDescriptor, 'Extensions: Show Popular Extensions', ExtensionsLabel); @@ -472,7 +468,7 @@ function overrideActionForActiveExtensionEditorWebview(command: MultiCommand | u const editorService = accessor.get(IEditorService); const editor = editorService.activeEditorPane; if (editor instanceof ExtensionEditor) { - if (editor.activeWebview) { + if (editor.activeWebview?.isFocused) { f(editor.activeWebview); return true; } diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts new file mode 100644 index 00000000000..619906cc521 --- /dev/null +++ b/src/vs/workbench/contrib/extensions/browser/extensions.web.contribution.ts @@ -0,0 +1,11 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; + +// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index 16ca60f34a5..70f25aa91b9 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -1842,6 +1842,7 @@ export class InstallRecommendedExtensionsAction extends Action { id: string, label: string, recommendations: string[], + private readonly searchValue: string, private readonly source: string, @IViewletService private readonly viewletService: IViewletService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -1854,22 +1855,17 @@ export class InstallRecommendedExtensionsAction extends Action { this.recommendations = recommendations; } - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@recommended '); - viewlet.focus(); - const names = this.recommendations; - return this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None).then(pager => { - let installPromises: Promise[] = []; - let model = new PagedModel(pager); - for (let i = 0; i < pager.total; i++) { - installPromises.push(model.resolve(i, CancellationToken.None).then(e => this.installExtension(e))); - } - return Promise.all(installPromises); - }); - }); + async run(): Promise { + await new SearchExtensionsAction(this.searchValue, this.viewletService).run(); + const names = this.recommendations; + const pager = await this.extensionWorkbenchService.queryGallery({ names, source: this.source }, CancellationToken.None); + const installPromises: Promise[] = []; + const model = new PagedModel(pager); + for (let i = 0; i < pager.total; i++) { + installPromises.push(model.resolve(i, CancellationToken.None) + .then(e => this.installExtension(e))); + } + return Promise.all(installPromises); } private async installExtension(extension: IExtension): Promise { @@ -1885,6 +1881,7 @@ export class InstallRecommendedExtensionsAction extends Action { return; } } + this.extensionWorkbenchService.open(extension, { pinned: true }); await this.extensionWorkbenchService.install(extension); } catch (err) { console.error(err); @@ -1904,7 +1901,7 @@ export class InstallWorkspaceRecommendedExtensionsAction extends InstallRecommen @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, ) { - super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, 'install-all-workspace-recommendations', + super('workbench.extensions.action.installWorkspaceRecommendedExtensions', localize('installWorkspaceRecommendedExtensions', "Install Workspace Recommended Extensions"), recommendations, '@recommended ', 'install-all-workspace-recommendations', viewletService, instantiationService, extensionWorkbenchService, configurationService, extensionManagementServerService, productService); } } @@ -2075,29 +2072,6 @@ export class ShowLanguageExtensionsAction extends Action { } } -export class ShowAzureExtensionsAction extends Action { - - static readonly ID = 'workbench.extensions.action.showAzureExtensions'; - static readonly LABEL = localize('showAzureExtensionsShort', "Azure Extensions"); - - constructor( - id: string, - label: string, - @IViewletService private readonly viewletService: IViewletService - ) { - super(id, label, undefined, true); - } - - run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search('@sort:installs azure '); - viewlet.focus(); - }); - } -} - export class SearchCategoryAction extends Action { constructor( @@ -2110,12 +2084,23 @@ export class SearchCategoryAction extends Action { } run(): Promise { - return this.viewletService.openViewlet(VIEWLET_ID, true) - .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) - .then(viewlet => { - viewlet.search(`@category:"${this.category.toLowerCase()}"`); - viewlet.focus(); - }); + return new SearchExtensionsAction(`@category:"${this.category.toLowerCase()}"`, this.viewletService).run(); + } +} + +export class SearchExtensionsAction extends Action { + + constructor( + private readonly searchValue: string, + @IViewletService private readonly viewletService: IViewletService + ) { + super('extensions.searchExtensions', localize('search recommendations', "Search Extensions"), undefined, true); + } + + async run(): Promise { + const viewPaneContainer = (await this.viewletService.openViewlet(VIEWLET_ID, true))?.getViewPaneContainer() as IExtensionsViewPaneContainer; + viewPaneContainer.search(this.searchValue); + viewPaneContainer.focus(); } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index 8e34bdaf743..fb17f22cb23 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -9,7 +9,7 @@ import { assign } from 'vs/base/common/objects'; import { Event, Emitter } from 'vs/base/common/event'; import { isPromiseCanceledError, getErrorMessage } from 'vs/base/common/errors'; import { PagedModel, IPagedModel, IPager, DelayedPagedModel } from 'vs/base/common/paging'; -import { SortBy, SortOrder, IQueryOptions } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { SortBy, SortOrder, IQueryOptions, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendationsService, IExtensionRecommendation, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -31,7 +31,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { distinct, coalesce } from 'vs/base/common/arrays'; +import { coalesce, distinct, flatten } from 'vs/base/common/arrays'; import { IExperimentService, IExperiment, ExperimentActionType } from 'vs/workbench/contrib/experiments/common/experimentService'; import { alert } from 'vs/base/browser/ui/aria/aria'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; @@ -53,6 +53,10 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; +type WorkspaceRecommendationsClassification = { + count: { classification: 'SystemMetaData', purpose: 'FeatureInsight', 'isMeasurement': true }; +}; + class ExtensionsViewState extends Disposable implements IExtensionsViewState { private readonly _onFocus: Emitter = this._register(new Emitter()); @@ -98,12 +102,13 @@ export class ExtensionsListView extends ViewPane { @IThemeService themeService: IThemeService, @IExtensionService private readonly extensionService: IExtensionService, @IExtensionsWorkbenchService protected extensionsWorkbenchService: IExtensionsWorkbenchService, - @IExtensionRecommendationsService protected tipsService: IExtensionRecommendationsService, + @IExtensionRecommendationsService protected extensionRecommendationsService: IExtensionRecommendationsService, @ITelemetryService telemetryService: ITelemetryService, @IConfigurationService configurationService: IConfigurationService, @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IExperimentService private readonly experimentService: IExperimentService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManagementService protected readonly extensionManagementService: IExtensionManagementService, @IProductService protected readonly productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @@ -456,14 +461,8 @@ export class ExtensionsListView extends ViewPane { options.sortBy = SortBy.InstallCount; } - if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { - return this.getWorkspaceRecommendationsModel(query, options, token); - } else if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { - return this.getKeymapRecommendationsModel(query, options, token); - } else if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { - return this.getAllRecommendationsModel(query, options, token); - } else if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { - return this.getRecommendationsModel(query, options, token); + if (this.isRecommendationsQuery(query)) { + return this.queryRecommendations(query, options, token); } if (/\bcurated:([^\s]+)\b/.test(query.value)) { @@ -540,51 +539,6 @@ export class ExtensionsListView extends ViewPane { return extensions; } - // Get All types of recommendations, trimmed to show a max of 8 at any given time - private getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:all/g, '').replace(/@recommended/g, '').trim().toLowerCase(); - - return this.extensionsWorkbenchService.queryLocal(this.server) - .then(result => result.filter(e => e.type === ExtensionType.User)) - .then(local => { - const fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations(); - const othersPromise = this.tipsService.getOtherRecommendations(); - const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - const importantRecommendationsPromise = this.tipsService.getImportantRecommendations(); - - return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise]) - .then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => { - const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, workspaceRecommendations); - const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); - /* __GDPR__ - "extensionAllRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionAllRecommendations:open', { - count: names.length, - recommendations: names.map(id => { - return { - id, - recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId - }; - }) - }); - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-all'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => { - this.sortFirstPage(pager, names); - return this.getPagedModel(pager || []); - }); - }); - }); - } - private async getCuratedModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/curated:/g, '').trim(); const names = await this.experimentService.getCuratedExtensionsList(value); @@ -597,139 +551,128 @@ export class ExtensionsListView extends ViewPane { return new PagedModel([]); } - // Get All types of recommendations other than Workspace recommendations, trimmed to show a max of 8 at any given time - private getRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + private isRecommendationsQuery(query: Query): boolean { + return ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isExeRecommendedExtensionsQuery(query.value) + || /@recommended:all/i.test(query.value) + || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value) + || ExtensionsListView.isRecommendedExtensionsQuery(query.value); + } + + private async queryRecommendations(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + // Workspace recommendations + if (ExtensionsListView.isWorkspaceRecommendedExtensionsQuery(query.value)) { + return this.getWorkspaceRecommendationsModel(query, options, token); + } + + // Keymap recommendations + if (ExtensionsListView.isKeymapsRecommendedExtensionsQuery(query.value)) { + return this.getKeymapRecommendationsModel(query, options, token); + } + + // Exe recommendations + if (ExtensionsListView.isExeRecommendedExtensionsQuery(query.value)) { + return this.getExeRecommendationsModel(query, options, token); + } + + // All recommendations + if (/@recommended:all/i.test(query.value) || ExtensionsListView.isSearchRecommendedExtensionsQuery(query.value)) { + return this.getAllRecommendationsModel(query, options, token); + } + + // Other recommendations + if (ExtensionsListView.isRecommendedExtensionsQuery(query.value)) { + return this.getOtherRecommendationsModel(query, options, token); + } + + return new PagedModel([]); + } + + private async getInstallableRecommendations(recommendations: IExtensionRecommendation[], options: IQueryOptions, token: CancellationToken): Promise { + const extensions: IExtension[] = []; + if (recommendations.length) { + const names = recommendations.map(({ extensionId }) => extensionId); + const pager = await this.extensionsWorkbenchService.queryGallery({ ...options, names, pageSize: names.length }, token); + for (const extension of pager.firstPage) { + if (extension.gallery && (await this.extensionManagementService.canInstall(extension.gallery))) { + extensions.push(extension); + } + } + } + return extensions; + } + + private async getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); + const recommendations = await this.extensionRecommendationsService.getWorkspaceRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-workspace' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + this.telemetryService.publicLog2<{ count: number }, WorkspaceRecommendationsClassification>('extensionWorkspaceRecommendations:open', { count: installableRecommendations.length }); + return new PagedModel(installableRecommendations); + } + + private async getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); + const recommendations = this.extensionRecommendationsService.getKeymapRecommendations(); + const installableRecommendations = (await this.getInstallableRecommendations(recommendations, { ...options, source: 'recommendations-keymaps' }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); + return new PagedModel(installableRecommendations); + } + + private async getExeRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const exe = query.value.replace(/@exe:/g, '').trim().toLowerCase(); + const { important, others } = await this.extensionRecommendationsService.getExeBasedRecommendations(exe.startsWith('"') ? exe.substring(1, exe.length - 1) : exe); + const installableRecommendations = await this.getInstallableRecommendations([...important, ...others], { ...options, source: 'recommendations-exe' }, token); + return new PagedModel(installableRecommendations); + } + + private async getOtherRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { const value = query.value.replace(/@recommended/g, '').trim().toLowerCase(); - return this.extensionsWorkbenchService.queryLocal(this.server) - .then(result => result.filter(e => e.type === ExtensionType.User)) - .then(local => { - let fileBasedRecommendations = this.tipsService.getFileBasedRecommendations(); - const configBasedRecommendationsPromise = this.tipsService.getConfigBasedRecommendations(); - const othersPromise = this.tipsService.getOtherRecommendations(); - const workspacePromise = this.tipsService.getWorkspaceRecommendations(); - const importantRecommendationsPromise = this.tipsService.getImportantRecommendations(); + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)) + .filter(e => e.type === ExtensionType.User) + .map(e => e.identifier.id.toLowerCase()); + const workspaceRecommendations = (await this.extensionRecommendationsService.getWorkspaceRecommendations()) + .map(r => r.extensionId.toLowerCase()); - return Promise.all([othersPromise, workspacePromise, configBasedRecommendationsPromise, importantRecommendationsPromise]) - .then(([others, workspaceRecommendations, configBasedRecommendations, importantRecommendations]) => { - configBasedRecommendations = configBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - fileBasedRecommendations = fileBasedRecommendations.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); - others = others.filter(x => workspaceRecommendations.every(({ extensionId }) => x.extensionId !== extensionId)); + const otherRecommendations = distinct( + flatten(await Promise.all([ + // Order is important + this.extensionRecommendationsService.getImportantRecommendations(), + this.extensionRecommendationsService.getConfigBasedRecommendations(), + this.extensionRecommendationsService.getFileBasedRecommendations(), + this.extensionRecommendationsService.getOtherRecommendations() + ])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) && !workspaceRecommendations.includes(extensionId.toLowerCase()) + ), r => r.extensionId.toLowerCase()); - const names = this.getTrimmedRecommendations(local, value, importantRecommendations, fileBasedRecommendations, configBasedRecommendations, others, []); - const recommendationsWithReason = this.tipsService.getAllRecommendationsWithReason(); + const installableRecommendations = (await this.getInstallableRecommendations(otherRecommendations, { ...options, source: 'recommendations-other', sortBy: undefined }, token)) + .filter(extension => extension.identifier.id.toLowerCase().indexOf(value) > -1); - /* __GDPR__ - "extensionRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, - "recommendations": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - this.telemetryService.publicLog('extensionRecommendations:open', { - count: names.length, - recommendations: names.map(id => { - return { - id, - recommendationReason: recommendationsWithReason[id.toLowerCase()].reasonId - }; - }) - }); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => { - this.sortFirstPage(pager, names); - return this.getPagedModel(pager || []); - }); - }); - }); + const result: IExtension[] = coalesce(otherRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result); } - // Given all recommendations, trims and returns recommendations in the relevant order after filtering out installed extensions - private getTrimmedRecommendations(installedExtensions: IExtension[], value: string, importantRecommendations: IExtensionRecommendation[], fileBasedRecommendations: IExtensionRecommendation[], configBasedRecommendations: IExtensionRecommendation[], otherRecommendations: IExtensionRecommendation[], workspaceRecommendations: IExtensionRecommendation[]): string[] { - const totalCount = 10; - workspaceRecommendations = workspaceRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - importantRecommendations = importantRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - configBasedRecommendations = configBasedRecommendations - .filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - fileBasedRecommendations = fileBasedRecommendations.filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); - otherRecommendations = otherRecommendations.filter(recommendation => { - return !this.isRecommendationInstalled(recommendation, installedExtensions) - && fileBasedRecommendations.every(fileBasedRecommendation => fileBasedRecommendation.extensionId !== recommendation.extensionId) - && workspaceRecommendations.every(workspaceRecommendation => workspaceRecommendation.extensionId !== recommendation.extensionId) - && importantRecommendations.every(importantRecommendation => importantRecommendation.extensionId !== recommendation.extensionId) - && configBasedRecommendations.every(configBasedRecommendation => configBasedRecommendation.extensionId !== recommendation.extensionId) - && recommendation.extensionId.toLowerCase().indexOf(value) > -1; - }); + // Get All types of recommendations, trimmed to show a max of 8 at any given time + private async getAllRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { + const local = (await this.extensionsWorkbenchService.queryLocal(this.server)) + .filter(e => e.type === ExtensionType.User) + .map(e => e.identifier.id.toLowerCase()); - const otherCount = Math.min(2, otherRecommendations.length); - const fileBasedCount = Math.min(fileBasedRecommendations.length, totalCount - workspaceRecommendations.length - importantRecommendations.length - configBasedRecommendations.length - otherCount); - const recommendations = [...workspaceRecommendations, ...importantRecommendations, ...configBasedRecommendations]; - recommendations.push(...fileBasedRecommendations.splice(0, fileBasedCount)); - recommendations.push(...otherRecommendations.splice(0, otherCount)); + const allRecommendations = distinct( + flatten(await Promise.all([ + // Order is important + this.extensionRecommendationsService.getWorkspaceRecommendations(), + this.extensionRecommendationsService.getImportantRecommendations(), + this.extensionRecommendationsService.getConfigBasedRecommendations(), + this.extensionRecommendationsService.getFileBasedRecommendations(), + this.extensionRecommendationsService.getOtherRecommendations() + ])).filter(({ extensionId }) => !local.includes(extensionId.toLowerCase()) + ), r => r.extensionId.toLowerCase()); - return distinct(recommendations.map(({ extensionId }) => extensionId)); - } - - private isRecommendationInstalled(recommendation: IExtensionRecommendation, installed: IExtension[]): boolean { - return installed.some(i => areSameExtensions(i.identifier, { id: recommendation.extensionId })); - } - - private getWorkspaceRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:workspace/g, '').trim().toLowerCase(); - return this.tipsService.getWorkspaceRecommendations() - .then(recommendations => { - const names = recommendations.map(({ extensionId }) => extensionId).filter(name => name.toLowerCase().indexOf(value) > -1); - /* __GDPR__ - "extensionWorkspaceRecommendations:open" : { - "count" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true } - } - */ - this.telemetryService.publicLog('extensionWorkspaceRecommendations:open', { count: names.length }); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-workspace'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(pager => this.getPagedModel(pager || [])); - }); - } - - private getKeymapRecommendationsModel(query: Query, options: IQueryOptions, token: CancellationToken): Promise> { - const value = query.value.replace(/@recommended:keymaps/g, '').trim().toLowerCase(); - const names: string[] = this.tipsService.getKeymapRecommendations().map(({ extensionId }) => extensionId) - .filter(extensionId => extensionId.toLowerCase().indexOf(value) > -1); - - if (!names.length) { - return Promise.resolve(new PagedModel([])); - } - options.source = 'recommendations-keymaps'; - return this.extensionsWorkbenchService.queryGallery(assign(options, { names, pageSize: names.length }), token) - .then(result => this.getPagedModel(result)); + const installableRecommendations = await this.getInstallableRecommendations(allRecommendations, { ...options, source: 'recommendations-all', sortBy: undefined }, token); + const result: IExtension[] = coalesce(allRecommendations.map(({ extensionId: id }) => installableRecommendations.find(i => areSameExtensions(i.identifier, { id })))); + return new PagedModel(result.slice(0, 8)); } // Sorts the firstPage of the pager in the same order as given array of extension ids @@ -864,6 +807,10 @@ export class ExtensionsListView extends ViewPane { return /@recommended:workspace/i.test(query); } + static isExeRecommendedExtensionsQuery(query: string): boolean { + return /@exe:.+/i.test(query); + } + static isKeymapsRecommendedExtensionsQuery(query: string): boolean { return /@recommended:keymaps/i.test(query); } @@ -900,6 +847,7 @@ export class ServerExtensionsView extends ExtensionsListView { @IExperimentService experimentService: IExperimentService, @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IProductService productService: IProductService, @IContextKeyService contextKeyService: IContextKeyService, @IMenuService menuService: IMenuService, @@ -908,7 +856,9 @@ export class ServerExtensionsView extends ExtensionsListView { @IPreferencesService preferencesService: IPreferencesService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, productService, contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, tipsService, + telemetryService, configurationService, contextService, experimentService, extensionManagementServerService, extensionManagementService, productService, + contextKeyService, viewDescriptorService, menuService, openerService, preferencesService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } @@ -992,7 +942,7 @@ export class DefaultRecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => { + this._register(this.extensionRecommendationsService.onRecommendationChange(() => { this.show(''); })); } @@ -1017,7 +967,7 @@ export class RecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => { + this._register(this.extensionRecommendationsService.onRecommendationChange(() => { this.show(''); })); } @@ -1034,7 +984,7 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { renderBody(container: HTMLElement): void { super.renderBody(container); - this._register(this.tipsService.onRecommendationChange(() => this.update())); + this._register(this.extensionRecommendationsService.onRecommendationChange(() => this.update())); this._register(this.extensionsWorkbenchService.onChange(() => this.setRecommendationsToInstall())); this._register(this.contextService.onDidChangeWorkbenchState(() => this.update())); } @@ -1070,7 +1020,7 @@ export class WorkspaceRecommendedExtensionsView extends ExtensionsListView { } private getRecommendationsToInstall(): Promise { - return this.tipsService.getWorkspaceRecommendations() + return this.extensionRecommendationsService.getWorkspaceRecommendations() .then(recommendations => recommendations.filter(({ extensionId }) => { const extension = this.extensionsWorkbenchService.local.filter(i => areSameExtensions({ id: extensionId }, i.identifier))[0]; if (!extension diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index fcc7c701ba1..6c3ed3be9d3 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -26,6 +26,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IModelService } from 'vs/editor/common/services/modelService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { setImmediate } from 'vs/base/common/platform'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; type FileExtensionSuggestionClassification = { userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; @@ -39,28 +40,28 @@ const processedFileExtensions: string[] = []; export class FileBasedRecommendations extends ExtensionRecommendations { - private readonly extensionTips: IStringDictionary = Object.create(null); - private readonly importantExtensionTips: IStringDictionary<{ name: string; pattern: string; isExtensionPack?: boolean }> = Object.create(null); + private readonly extensionTips = new Map(); + private readonly importantExtensionTips = new Map(); - private fileBasedRecommendationsByPattern: IStringDictionary = Object.create(null); - private fileBasedRecommendations: IStringDictionary<{ recommendedTime: number, sources: ExtensionRecommendationSource[] }> = Object.create(null); + private readonly fileBasedRecommendationsByPattern = new Map(); + private readonly fileBasedRecommendations = new Map(); get recommendations(): ReadonlyArray { const recommendations: ExtensionRecommendation[] = []; - Object.keys(this.fileBasedRecommendations) + [...this.fileBasedRecommendations.keys()] .sort((a, b) => { - if (this.fileBasedRecommendations[a].recommendedTime === this.fileBasedRecommendations[b].recommendedTime) { - if (this.importantExtensionTips[a]) { + if (this.fileBasedRecommendations.get(a)!.recommendedTime === this.fileBasedRecommendations.get(b)!.recommendedTime) { + if (this.importantExtensionTips.has(a)) { return -1; } - if (this.importantExtensionTips[b]) { + if (this.importantExtensionTips.has(b)) { return 1; } } - return this.fileBasedRecommendations[a].recommendedTime > this.fileBasedRecommendations[b].recommendedTime ? -1 : 1; + return this.fileBasedRecommendations.get(a)!.recommendedTime > this.fileBasedRecommendations.get(b)!.recommendedTime ? -1 : 1; }) .forEach(extensionId => { - for (const source of this.fileBasedRecommendations[extensionId].sources) { + for (const source of this.fileBasedRecommendations.get(extensionId)!.sources) { recommendations.push({ extensionId, source, @@ -75,16 +76,17 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } get importantRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => this.importantExtensionTips[e.extensionId]); + return this.recommendations.filter(e => this.importantExtensionTips.has(e.extensionId)); } get otherRecommendations(): ReadonlyArray { - return this.recommendations.filter(e => !this.importantExtensionTips[e.extensionId]); + return this.recommendations.filter(e => !this.importantExtensionTips.has(e.extensionId)); } constructor( isExtensionAllowedToBeRecommended: (extensionId: string) => boolean, - @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, @IExtensionService private readonly extensionService: IExtensionService, @IViewletService private readonly viewletService: IViewletService, @IModelService private readonly modelService: IModelService, @@ -96,13 +98,13 @@ export class FileBasedRecommendations extends ExtensionRecommendations { @IStorageService storageService: IStorageService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); if (productService.extensionTips) { - forEach(productService.extensionTips, ({ key, value }) => this.extensionTips[key.toLowerCase()] = value); + forEach(productService.extensionTips, ({ key, value }) => this.extensionTips.set(key.toLowerCase(), value)); } if (productService.extensionImportantTips) { - forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips[key.toLowerCase()] = value); + forEach(productService.extensionImportantTips, ({ key, value }) => this.importantExtensionTips.set(key.toLowerCase(), value)); } } @@ -110,18 +112,18 @@ export class FileBasedRecommendations extends ExtensionRecommendations { const allRecommendations: string[] = []; // group extension recommendations by pattern, like {**/*.md} -> [ext.foo1, ext.bar2] - forEach(this.extensionTips, ({ key: extensionId, value: pattern }) => { - const ids = this.fileBasedRecommendationsByPattern[pattern] || []; + for (const [extensionId, pattern] of this.extensionTips) { + const ids = this.fileBasedRecommendationsByPattern.get(pattern) || []; ids.push(extensionId); - this.fileBasedRecommendationsByPattern[pattern] = ids; + this.fileBasedRecommendationsByPattern.set(pattern, ids); allRecommendations.push(extensionId); - }); - forEach(this.importantExtensionTips, ({ key: extensionId, value }) => { - const ids = this.fileBasedRecommendationsByPattern[value.pattern] || []; + } + for (const [extensionId, value] of this.importantExtensionTips) { + const ids = this.fileBasedRecommendationsByPattern.get(value.pattern) || []; ids.push(extensionId); - this.fileBasedRecommendationsByPattern[value.pattern] = ids; + this.fileBasedRecommendationsByPattern.set(value.pattern, ids); allRecommendations.push(extensionId); - }); + } const cachedRecommendations = this.getCachedRecommendations(); const now = Date.now(); @@ -129,7 +131,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { forEach(cachedRecommendations, ({ key, value }) => { const diff = (now - value) / milliSecondsInADay; if (diff <= 7 && allRecommendations.indexOf(key) > -1) { - this.fileBasedRecommendations[key] = { recommendedTime: value, sources: ['cached'] }; + this.fileBasedRecommendations.set(key.toLowerCase(), { recommendedTime: value, sources: ['cached'] }); } }); @@ -161,26 +163,28 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } private async promptRecommendations(uri: URI, fileExtension: string): Promise { - const recommendationsToPrompt: string[] = []; - forEach(this.fileBasedRecommendationsByPattern, ({ key: pattern, value: extensionIds }) => { - if (match(pattern, uri.toString())) { - for (const extensionId of extensionIds) { - // Add to recommendation to prompt if it is an important tip - // Only prompt if the pattern matches the extensionImportantTips pattern - // Otherwise, assume pattern is from extensionTips, which means it should be a file based "passive" recommendation - if (this.importantExtensionTips[extensionId]?.pattern === pattern) { - recommendationsToPrompt.push(extensionId); - } - // Update file based recommendations - const filedBasedRecommendation = this.fileBasedRecommendations[extensionId] || { recommendedTime: Date.now(), sources: [] }; - filedBasedRecommendation.recommendedTime = Date.now(); - if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { - filedBasedRecommendation.sources.push(uri); - } - this.fileBasedRecommendations[extensionId.toLowerCase()] = filedBasedRecommendation; - } + const recommendationsToPrompt: { extensionId: string, languageName: string }[] = []; + for (const { 0: pattern, 1: extensionIds } of this.fileBasedRecommendationsByPattern) { + if (!match(pattern, uri.toString())) { + continue; } - }); + for (const extensionId of extensionIds) { + // Add to recommendation to prompt if it is an important tip + // Only prompt if the pattern matches the extensionImportantTips pattern + // Otherwise, assume pattern is from extensionTips, which means it should be a file based "passive" recommendation + if (this.importantExtensionTips.get(extensionId)?.pattern === pattern) { + recommendationsToPrompt.push({ extensionId, languageName: this.importantExtensionTips.get(extensionId)!.name }); + } + + // Update file based recommendations + const filedBasedRecommendation = this.fileBasedRecommendations.get(extensionId) || { recommendedTime: Date.now(), sources: [] }; + filedBasedRecommendation.recommendedTime = Date.now(); + if (!filedBasedRecommendation.sources.some(s => s instanceof URI && s.toString() === uri.toString())) { + filedBasedRecommendation.sources.push(uri); + } + this.fileBasedRecommendations.set(extensionId, filedBasedRecommendation); + } + } this.storeCachedRecommendations(); @@ -189,7 +193,8 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } const installed = await this.extensionsWorkbenchService.queryLocal(); - if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) { + if (recommendationsToPrompt.length && + await this.promptRecommendedExtensionForFileType(fileExtension.substring(1), recommendationsToPrompt[0].languageName, recommendationsToPrompt.map(r => r.extensionId), installed)) { return; } @@ -209,7 +214,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { this.promptRecommendedExtensionForFileExtension(fileExtension, installed); } - private async promptRecommendedExtensionForFileType(recommendations: string[], installed: IExtension[]): Promise { + private async promptRecommendedExtensionForFileType(ext: string, languageName: string, recommendations: string[], installed: IExtension[]): Promise { recommendations = this.filterIgnoredOrNotAllowed(recommendations); if (recommendations.length === 0) { @@ -222,17 +227,12 @@ export class FileBasedRecommendations extends ExtensionRecommendations { } const extensionId = recommendations[0]; - const entry = this.importantExtensionTips[extensionId]; + const entry = this.importantExtensionTips.get(extensionId); if (!entry) { return false; } - const extensionName = entry.name; - let message = localize('reallyRecommended2', "The '{0}' extension is recommended for this file type.", extensionName); - if (entry.isExtensionPack) { - message = localize('reallyRecommendedExtensionPack', "The '{0}' extension pack is recommended for this file type.", extensionName); - } - this.promptImportantExtensionsInstallNotification([extensionId], message); + this.promptImportantExtensionsInstallNotification([extensionId], localize('reallyRecommended', "Do you want to install recommendations for {0}?", languageName), `@id:${extensionId}`); return true; } @@ -300,7 +300,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private getCachedRecommendations(): IStringDictionary { let storedRecommendations = JSON.parse(this.storageService.get(recommendationsStorageKey, StorageScope.GLOBAL, '[]')); - if (Array.isArray(storedRecommendations)) { + if (Array.isArray(storedRecommendations)) { storedRecommendations = storedRecommendations.reduce((result, id) => { result[id] = Date.now(); return result; }, >{}); } const result: IStringDictionary = {}; @@ -314,7 +314,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { private storeCachedRecommendations(): void { const storedRecommendations: IStringDictionary = {}; - forEach(this.fileBasedRecommendations, ({ key, value }) => storedRecommendations[key] = value.recommendedTime); + this.fileBasedRecommendations.forEach((value, key) => storedRecommendations[key] = value.recommendedTime); this.storageService.store(recommendationsStorageKey, JSON.stringify(storedRecommendations), StorageScope.GLOBAL); } } diff --git a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts index 4bfefe4941c..517086adfa5 100644 --- a/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/keymapRecommendations.ts @@ -12,6 +12,8 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; export class KeymapRecommendations extends ExtensionRecommendations { @@ -26,9 +28,11 @@ export class KeymapRecommendations extends ExtensionRecommendations { @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 1175c59a129..ac8603ff2f8 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -10,31 +10,26 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { distinct, flatten, coalesce } from 'vs/base/common/arrays'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason, IWorkbenchExtensionEnablementService, EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IExtensionsConfigContent, ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { parse } from 'vs/base/common/json'; -import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extensions'; +import { EXTENSIONS_CONFIG, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; -import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ShowRecommendedExtensionsAction, InstallWorkspaceRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; -import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; - -type ExtensionWorkspaceRecommendationsNotificationClassification = { - userReaction: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; -}; - -const choiceNever = localize('neverShowAgain', "Don't Show Again"); -const ignoreWorkspaceRecommendationsStorageKey = 'extensionsAssistant/workspaceRecommendationsIgnore'; +import { Emitter } from 'vs/base/common/event'; export class WorkspaceRecommendations extends ExtensionRecommendations { private _recommendations: ExtensionRecommendation[] = []; get recommendations(): ReadonlyArray { return this._recommendations; } + private _onDidChangeRecommendations = this._register(new Emitter()); + readonly onDidChangeRecommendations = this._onDidChangeRecommendations.event; + private _ignoredRecommendations: string[] = []; get ignoredRecommendations(): ReadonlyArray { return this._ignoredRecommendations; } @@ -44,22 +39,21 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService, @IFileService private readonly fileService: IFileService, - @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, - @IWorkbenchExtensionEnablementService private readonly extensionEnablementService: IWorkbenchExtensionEnablementService, @IInstantiationService instantiationService: IInstantiationService, @IConfigurationService configurationService: IConfigurationService, @INotificationService notificationService: INotificationService, @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, + @IExtensionManagementService extensionManagementService: IExtensionManagementService, + @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, storageKeysSyncRegistryService); + super(isExtensionAllowedToBeRecommended, instantiationService, configurationService, notificationService, telemetryService, storageService, extensionsWorkbenchService, extensionManagementService, storageKeysSyncRegistryService); } protected async doActivate(): Promise { await this.fetch(); this._register(this.contextService.onDidChangeWorkspaceFolders(e => this.onWorkspaceFoldersChanged(e))); - this.promptWorkspaceRecommendations(); } /** @@ -71,7 +65,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { const { invalidRecommendations, message } = await this.validateExtensions(extensionsConfigBySource.map(({ contents }) => contents)); if (invalidRecommendations.length) { - this.notificationService.warn(`The below ${invalidRecommendations.length} extension(s) in workspace recommendations have issues:\n${message}`); + this.notificationService.warn(`The ${invalidRecommendations.length} extension(s) below, in workspace recommendations have issues:\n${message}`); } this._ignoredRecommendations = []; @@ -97,63 +91,6 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { } } - private async promptWorkspaceRecommendations(): Promise { - const allowedRecommendations = this.recommendations.filter(rec => this.isExtensionAllowedToBeRecommended(rec.extensionId)); - - if (allowedRecommendations.length === 0 || this.hasToIgnoreWorkspaceRecommendationNotifications()) { - return; - } - - let installed = await this.extensionManagementService.getInstalled(); - installed = installed.filter(l => this.extensionEnablementService.getEnablementState(l) !== EnablementState.DisabledByExtensionKind); // Filter extensions disabled by kind - const recommendations = allowedRecommendations.filter(({ extensionId }) => installed.every(local => !areSameExtensions({ id: extensionId }, local.identifier))); - - if (!recommendations.length) { - return; - } - - return new Promise(c => { - this.notificationService.prompt( - Severity.Info, - localize('workspaceRecommended', "This workspace has extension recommendations."), - [{ - label: localize('installAll', "Install All"), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'install' }); - const installAllAction = this.instantiationService.createInstance(InstallWorkspaceRecommendedExtensionsAction, recommendations.map(({ extensionId }) => extensionId)); - installAllAction.run(); - installAllAction.dispose(); - c(undefined); - } - }, { - label: localize('showRecommendations', "Show Recommendations"), - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'show' }); - const showAction = this.instantiationService.createInstance(ShowRecommendedExtensionsAction, ShowRecommendedExtensionsAction.ID, localize('showRecommendations', "Show Recommendations")); - showAction.run(); - showAction.dispose(); - c(undefined); - } - }, { - label: choiceNever, - isSecondary: true, - run: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'neverShowAgain' }); - this.storageService.store(ignoreWorkspaceRecommendationsStorageKey, true, StorageScope.WORKSPACE); - c(undefined); - } - }], - { - sticky: true, - onCancel: () => { - this.telemetryService.publicLog2<{ userReaction: string }, ExtensionWorkspaceRecommendationsNotificationClassification>('extensionWorkspaceRecommendations:popup', { userReaction: 'cancelled' }); - c(undefined); - } - } - ); - }); - } - private async fetchExtensionsConfigBySource(): Promise<{ contents: IExtensionsConfigContent, source: ExtensionRecommendationSource }[]> { const workspace = this.contextService.getWorkspace(); const result = await Promise.all([ @@ -235,7 +172,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { await this.fetch(); // Suggest only if at least one of the newly added recommendations was not suggested before if (this._recommendations.some(current => oldWorkspaceRecommended.every(old => current.extensionId !== old.extensionId))) { - this.promptWorkspaceRecommendations(); + this._onDidChangeRecommendations.fire(); } } } @@ -250,8 +187,5 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { return null; } - private hasToIgnoreWorkspaceRecommendationNotifications(): boolean { - return this.hasToIgnoreRecommendationNotifications() || this.storageService.getBoolean(ignoreWorkspaceRecommendationsStorageKey, StorageScope.WORKSPACE, false); - } } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts index 6d0222c798e..4a098ffd88f 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensions.contribution.ts @@ -24,8 +24,11 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { OpenExtensionsFolderAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { ExtensionsLabel } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; // Singletons +registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); // TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded registerSingleton(IExtensionHostProfileService, ExtensionHostProfileService, true); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 1c780f7fb4f..05f5b3e2dd0 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import * as os from 'os'; import { IProductService } from 'vs/platform/product/common/productService'; import { Action, IAction, Separator } from 'vs/base/common/actions'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -100,7 +100,7 @@ interface IRuntimeExtension { unresponsiveProfile?: IExtensionHostProfile; } -export class RuntimeExtensionsEditor extends BaseEditor { +export class RuntimeExtensionsEditor extends EditorPane { public static readonly ID: string = 'workbench.editor.runtimeExtensions'; diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts index b20db7d9a94..be5ec682bc4 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionRecommendationsService.test.ts @@ -33,8 +33,7 @@ import { IPager } from 'vs/base/common/paging'; import { assign } from 'vs/base/common/objects'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; +import { ConfigurationKey, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { IURLService } from 'vs/platform/url/common/url'; import { ITextModel } from 'vs/editor/common/model'; @@ -58,6 +57,7 @@ import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions import { NoOpWorkspaceTagsService } from 'vs/workbench/contrib/tags/browser/workspaceTagsService'; import { IWorkspaceTagsService } from 'vs/workbench/contrib/tags/common/workspaceTags'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; const mockExtensionGallery: IGalleryExtension[] = [ aGalleryExtension('MockExtension1', { @@ -199,11 +199,15 @@ suite('ExtensionRecommendationsService Test', () => { testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); instantiationService.stub(INotificationService, new TestNotificationService()); - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementService, >{ + onInstallExtension: installEvent.event, + onDidInstallExtension: didInstallEvent.event, + onUninstallExtension: uninstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + async getInstalled() { return []; }, + async canInstall() { return true; }, + async getExtensionsReport() { return []; }, + }); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.stub(ITelemetryService, NullTelemetryService); instantiationService.stub(IURLService, NativeURLService); @@ -231,6 +235,7 @@ suite('ExtensionRecommendationsService Test', () => { experimentService = instantiationService.createInstance(TestExperimentService); instantiationService.stub(IExperimentService, experimentService); + instantiationService.set(IExtensionsWorkbenchService, instantiationService.createInstance(ExtensionsWorkbenchService)); instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); onModelAddedEvent = new Emitter(); @@ -302,7 +307,7 @@ suite('ExtensionRecommendationsService Test', () => { function testNoPromptForValidRecommendations(recommendations: string[]) { return setUpFolderWorkspace('myFolder', recommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { assert.equal(Object.keys(testObject.getAllRecommendationsWithReason()).length, recommendations.length); assert.ok(!prompted); }); @@ -338,20 +343,18 @@ suite('ExtensionRecommendationsService Test', () => { return testNoPromptForValidRecommendations([]); }); - test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', () => { - return setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions).then(() => { - testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { - const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); + test('ExtensionRecommendationsService: Prompt for valid workspace recommendations', async () => { + await setUpFolderWorkspace('myFolder', mockTestData.recommendedExtensions); + testObject = instantiationService.createInstance(ExtensionRecommendationsService); + await testObject.activationPromise; - assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); - mockTestData.validRecommendedExtensions.forEach(x => { - assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true); - }); - - assert.ok(prompted); - }); + const recommendations = Object.keys(testObject.getAllRecommendationsWithReason()); + assert.equal(recommendations.length, mockTestData.validRecommendedExtensions.length); + mockTestData.validRecommendedExtensions.forEach(x => { + assert.equal(recommendations.indexOf(x.toLowerCase()) > -1, true); }); + + assert.ok(prompted); }); test('ExtensionRecommendationsService: No Prompt for valid workspace recommendations if they are already installed', () => { @@ -373,7 +376,7 @@ suite('ExtensionRecommendationsService Test', () => { testConfigurationService.setUserConfiguration(ConfigurationKey, { showRecommendationsOnlyOnDemand: true }); return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { assert.ok(!prompted); }); }); @@ -391,7 +394,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been globally ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -409,7 +412,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, ignoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(!recommendations['ms-dotnettools.csharp']); // stored recommendation that has been workspace ignored assert.ok(recommendations['ms-python.python']); // stored recommendation @@ -430,7 +433,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions, workspaceIgnoredRecommendations).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); @@ -449,7 +452,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', mockTestData.validRecommendedExtensions).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getAllRecommendationsWithReason(); assert.ok(recommendations['ms-python.python']); assert.ok(recommendations['mockpublisher1.mockextension1']); @@ -486,7 +489,7 @@ suite('ExtensionRecommendationsService Test', () => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); testObject.onRecommendationChange(changeHandlerTarget); testObject.toggleIgnoredRecommendation(ignoredExtensionId, true); - await testObject.loadWorkspaceConfigPromise; + await testObject.activationPromise; assert.ok(changeHandlerTarget.calledOnce); assert.ok(changeHandlerTarget.getCall(0).calledWithMatch({ extensionId: ignoredExtensionId.toLowerCase(), isRecommended: false })); @@ -498,7 +501,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips @@ -517,7 +520,7 @@ suite('ExtensionRecommendationsService Test', () => { return setUpFolderWorkspace('myFolder', []).then(() => { testObject = instantiationService.createInstance(ExtensionRecommendationsService); - return testObject.loadWorkspaceConfigPromise.then(() => { + return testObject.activationPromise.then(() => { const recommendations = testObject.getFileBasedRecommendations(); assert.equal(recommendations.length, 2); assert.ok(recommendations.some(({ extensionId }) => extensionId === 'ms-dotnettools.csharp')); // stored recommendation that exists in product.extensionTips diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts index 7c83b8ea7b1..8f260d90da0 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsActions.test.ts @@ -101,7 +101,7 @@ async function setupTest() { instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { #localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; constructor() { - super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService)); + super(instantiationService.get(ISharedProcessService), instantiationService, instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService)); } get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; } set localExtensionManagementServer(server: IExtensionManagementServer) { } diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts index cbbaddea537..6c077780df1 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsViews.test.ts @@ -16,7 +16,6 @@ import { } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService, ExtensionRecommendationReason, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; import { ExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionGalleryService'; import { IURLService } from 'vs/platform/url/common/url'; @@ -40,7 +39,6 @@ import { RemoteAgentService } from 'vs/workbench/services/remote/electron-browse import { ExtensionIdentifier, ExtensionType, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { ExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService'; -import { IProductService } from 'vs/platform/product/common/productService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; @@ -89,11 +87,15 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(ISharedProcessService, TestSharedProcessService); instantiationService.stub(IExperimentService, ExperimentService); - instantiationService.stub(IExtensionManagementService, ExtensionManagementService); - instantiationService.stub(IExtensionManagementService, 'onInstallExtension', installEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidInstallExtension', didInstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onUninstallExtension', uninstallEvent.event); - instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); + instantiationService.stub(IExtensionManagementService, >{ + onInstallExtension: installEvent.event, + onDidInstallExtension: didInstallEvent.event, + onUninstallExtension: uninstallEvent.event, + onDidUninstallExtension: didUninstallEvent.event, + async getInstalled() { return []; }, + async canInstall() { return true; }, + async getExtensionsReport() { return []; }, + }); instantiationService.stub(IRemoteAgentService, RemoteAgentService); instantiationService.stub(IContextKeyService, new MockContextKeyService()); instantiationService.stub(IMenuService, new TestMenuService()); @@ -101,7 +103,7 @@ suite('ExtensionsListView Tests', () => { instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { #localExtensionManagementServer: IExtensionManagementServer = { extensionManagementService: instantiationService.get(IExtensionManagementService), label: 'local', id: 'vscode-local' }; constructor() { - super(instantiationService.get(ISharedProcessService), instantiationService.get(IRemoteAgentService), instantiationService.get(IExtensionGalleryService), instantiationService.get(IConfigurationService), instantiationService.get(IProductService), instantiationService.get(ILogService), instantiationService.get(ILabelService)); + super(instantiationService.get(ISharedProcessService), instantiationService, instantiationService.get(IRemoteAgentService), instantiationService.get(ILabelService)); } get localExtensionManagementServer(): IExtensionManagementServer { return this.#localExtensionManagementServer; } set localExtensionManagementServer(server: IExtensionManagementServer) { } diff --git a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts index 067cc377d6c..42dba3506e5 100644 --- a/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts +++ b/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts @@ -306,7 +306,6 @@ export class LinuxExternalTerminalService implements IExternalTerminalService { LinuxExternalTerminalService._DEFAULT_TERMINAL_LINUX_READY = new Promise(async r => { if (env.isLinux) { const isDebian = await pfs.exists('/etc/debian_version'); - await process.lazyEnv; if (isDebian) { r('x-terminal-emulator'); } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 4beec737735..d40c20d8c66 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -12,7 +12,7 @@ import { Action } from 'vs/base/common/actions'; import { VIEWLET_ID, TEXT_FILE_EDITOR_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { ITextFileService, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions, TextEditorOptions, IEditorInput } from 'vs/workbench/common/editor'; +import { EditorOptions, TextEditorOptions, IEditorInput, IEditorOpenContext } from 'vs/workbench/common/editor'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -91,13 +91,13 @@ export class TextFileEditor extends BaseTextEditor { return this._input as FileEditorInput; } - async setInput(input: FileEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: FileEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); // Set input and resolve - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); try { const resolvedModel = await input.resolve(); @@ -119,10 +119,12 @@ export class TextFileEditor extends BaseTextEditor { const textEditor = assertIsDefined(this.getControl()); textEditor.setModel(textFileModel.textEditorModel); - // Always restore View State if any associated - const editorViewState = this.loadTextEditorViewState(input.resource); - if (editorViewState) { - textEditor.restoreViewState(editorViewState); + // Always restore View State if any associated and not disabled via settings + if (this.shouldRestoreTextEditorViewState(input, context)) { + const editorViewState = this.loadTextEditorViewState(input.resource); + if (editorViewState) { + textEditor.restoreViewState(editorViewState); + } } // TextOptions (avoiding instanceof here for a reason, do not change!) @@ -242,7 +244,7 @@ export class TextFileEditor extends BaseTextEditor { // If the user configured to not restore view state, we clear the view // state unless the editor is still opened in the group. - if (!this.shouldRestoreViewState && (!this.group || !this.group.isOpened(input))) { + if (!this.shouldRestoreTextEditorViewState(input) && (!this.group || !this.group.isOpened(input))) { this.clearTextEditorViewState([input.resource], this.group); } diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index 441537060af..fa238a5bf0f 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -11,7 +11,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; -import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition } from 'vs/workbench/contrib/files/common/files'; +import { ExplorerFocusCondition, TextFileContentProvider, VIEWLET_ID, IExplorerService, ExplorerCompressedFocusContext, ExplorerCompressedFirstFocusContext, ExplorerCompressedLastFocusContext, FilesExplorerFocusCondition, ExplorerFolderContext } from 'vs/workbench/contrib/files/common/files'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { toErrorMessage } from 'vs/base/common/errorMessage'; @@ -144,6 +144,24 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } }); +KeybindingsRegistry.registerCommandAndKeybindingRule({ + weight: KeybindingWeight.WorkbenchContrib + 10, + when: ContextKeyExpr.and(ExplorerFocusCondition, ExplorerFolderContext.toNegated()), + primary: KeyCode.Enter, + mac: { + primary: KeyMod.CtrlCmd | KeyCode.DownArrow + }, + id: 'explorer.openAndPassFocus', handler: async (accessor, _resource: URI | object) => { + const editorService = accessor.get(IEditorService); + const explorerService = accessor.get(IExplorerService); + const resources = explorerService.getContext(true); + + if (resources.length) { + await editorService.openEditors(resources.map(r => ({ resource: r.resource, options: { preserveFocus: false } }))); + } + } +}); + const COMPARE_WITH_SAVED_SCHEMA = 'showModifications'; let providerDisposables: IDisposable[] = []; KeybindingsRegistry.registerCommandAndKeybindingRule({ @@ -637,3 +655,5 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ } } }); + + diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 71f86a19285..7be3b5c9b81 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -361,10 +361,23 @@ configurationRegistry.registerConfiguration({ properties: { 'editor.formatOnSave': { 'type': 'boolean', - 'default': false, 'description': nls.localize('formatOnSave', "Format a file on save. A formatter must be available, the file must not be saved after delay, and the editor must not be shutting down."), - scope: ConfigurationScope.LANGUAGE_OVERRIDABLE, - } + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, + }, + 'editor.formatOnSaveMode': { + 'type': 'string', + 'default': 'file', + 'enum': [ + 'file', + 'modifications' + ], + 'enumDescriptions': [ + nls.localize('everything', "Format the whole file."), + nls.localize('modification', "Format modifications (requires source control)."), + ], + 'markdownDescription': nls.localize('formatOnSaveMode', "Controls if format on save formats the whole file or only modifications. Only applies when `#editor.formatOnSave#` is `true`."), + 'scope': ConfigurationScope.LANGUAGE_OVERRIDABLE, + }, } }); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 66d94162794..c35e3fa820b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -66,7 +66,7 @@ function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree { + async provideTextContent(resource: URI): Promise { + if (!resource.query) { + // We require the URI to use the `query` to transport the original scheme and query + // as done by `resourceToTextFile` + return null; + } + const savedFileResource = TextFileContentProvider.textFileToResource(resource); // Make sure our text file is resolved up to date diff --git a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts index 6b29c6e66d9..70c2fa9e2d4 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileOnDiskProvider.test.ts @@ -26,7 +26,9 @@ suite('Files - FileOnDiskContentProvider', () => { const content = await provider.provideTextContent(uri.with({ scheme: 'conflictResolution', query: JSON.stringify({ scheme: uri.scheme }) })); - assert.equal(snapshotToString(content.createSnapshot()), 'Hello Html'); - assert.equal(accessor.fileService.getLastReadFileUri().toString(), uri.toString()); + assert.ok(content); + assert.equal(snapshotToString(content!.createSnapshot()), 'Hello Html'); + assert.equal(accessor.fileService.getLastReadFileUri().scheme, uri.scheme); + assert.equal(accessor.fileService.getLastReadFileUri().path, uri.path); }); }); diff --git a/src/vs/workbench/contrib/format/browser/format.contribution.ts b/src/vs/workbench/contrib/format/browser/format.contribution.ts index 56a34671c9e..a91827cdc47 100644 --- a/src/vs/workbench/contrib/format/browser/format.contribution.ts +++ b/src/vs/workbench/contrib/format/browser/format.contribution.ts @@ -5,3 +5,4 @@ import './formatActionsMultiple'; import './formatActionsNone'; +import './formatModified'; diff --git a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts index 26599aef2be..eb9e20df494 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsMultiple.ts @@ -12,7 +12,7 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { formatDocumentRangeWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format'; +import { formatDocumentRangesWithProvider, formatDocumentWithProvider, getRealAndSyntheticDocumentFormattersOrdered, FormattingConflicts, FormattingMode } from 'vs/editor/contrib/format/format'; import { Range } from 'vs/editor/common/core/range'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; @@ -308,7 +308,7 @@ registerEditorAction(class FormatSelectionMultipleAction extends EditorAction { const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model); const pick = await instaService.invokeFunction(showFormatterPick, model, provider); if (typeof pick === 'number') { - await instaService.invokeFunction(formatDocumentRangeWithProvider, provider[pick], editor, range, CancellationToken.None); + await instaService.invokeFunction(formatDocumentRangesWithProvider, provider[pick], editor, range, CancellationToken.None); } logFormatterTelemetry(telemetryService, 'range', provider, typeof pick === 'number' && provider[pick] || undefined); diff --git a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts index 18b3e2b6f8d..9355b4e6dc1 100644 --- a/src/vs/workbench/contrib/format/browser/formatActionsNone.ts +++ b/src/vs/workbench/contrib/format/browser/formatActionsNone.ts @@ -14,7 +14,14 @@ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegis import { ICommandService } from 'vs/platform/commands/common/commands'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { showExtensionQuery } from 'vs/workbench/contrib/format/browser/showExtensionQuery'; +import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; + +async function showExtensionQuery(viewletService: IViewletService, query: string) { + const viewlet = await viewletService.openViewlet(VIEWLET_ID, true); + if (viewlet) { + (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); + } +} registerEditorAction(class FormatDocumentMultipleAction extends EditorAction { diff --git a/src/vs/workbench/contrib/format/browser/formatModified.ts b/src/vs/workbench/contrib/format/browser/formatModified.ts new file mode 100644 index 00000000000..e1ae53814d7 --- /dev/null +++ b/src/vs/workbench/contrib/format/browser/formatModified.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { Range } from 'vs/editor/common/core/range'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { ITextModel } from 'vs/editor/common/model'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { formatDocumentRangesWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; +import * as nls from 'vs/nls'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { Progress } from 'vs/platform/progress/common/progress'; +import { getOriginalResource } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; + +registerEditorAction(class FormatModifiedAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.formatChanges', + label: nls.localize('formatChanges', "Format Modified Lines"), + alias: 'Format Modified Lines', + precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider), + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const instaService = accessor.get(IInstantiationService); + + if (!editor.hasModel()) { + return; + } + + const ranges = await instaService.invokeFunction(getModifiedRanges, editor.getModel()); + if (isNonEmptyArray(ranges)) { + return instaService.invokeFunction( + formatDocumentRangesWithSelectedProvider, editor, ranges, + FormattingMode.Explicit, Progress.None, CancellationToken.None + ); + } + } +}); + + +export async function getModifiedRanges(accessor: ServicesAccessor, modified: ITextModel): Promise { + const scmService = accessor.get(ISCMService); + const workerService = accessor.get(IEditorWorkerService); + const modelService = accessor.get(ITextModelService); + + const original = await getOriginalResource(scmService, modified.uri); + if (!original) { + return undefined; + } + + const ranges: Range[] = []; + const ref = await modelService.createModelReference(original); + try { + if (!workerService.canComputeDirtyDiff(original, modified.uri)) { + return undefined; + } + const changes = await workerService.computeDirtyDiff(original, modified.uri, true); + if (!isNonEmptyArray(changes)) { + return undefined; + } + for (let change of changes) { + ranges.push(modified.validateRange(new Range( + change.modifiedStartLineNumber, 1, + change.modifiedEndLineNumber || change.modifiedStartLineNumber /*endLineNumber is 0 when things got deleted*/, Number.MAX_SAFE_INTEGER) + )); + } + } finally { + ref.dispose(); + } + + return ranges; +} diff --git a/src/vs/workbench/contrib/format/browser/showExtensionQuery.ts b/src/vs/workbench/contrib/format/browser/showExtensionQuery.ts deleted file mode 100644 index cfb1a4da14d..00000000000 --- a/src/vs/workbench/contrib/format/browser/showExtensionQuery.ts +++ /dev/null @@ -1,15 +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 { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { VIEWLET_ID, IExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/common/extensions'; - -export function showExtensionQuery(viewletService: IViewletService, query: string) { - return viewletService.openViewlet(VIEWLET_ID, true).then(viewlet => { - if (viewlet) { - (viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer).search(query); - } - }); -} diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index c7cff71ecf5..528a7beeffc 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -333,7 +333,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { private setTreeSelection(): void { if (this.tree && this.tree.getSelection().length === 0) { - const firstMarker = this.markersWorkbenchService.markersModel.resourceMarkers[0].markers[0]; + const firstMarker = this.markersWorkbenchService.markersModel.resourceMarkers[0]?.markers[0]; if (firstMarker) { this.tree.setFocus([firstMarker]); this.tree.setSelection([firstMarker]); @@ -692,18 +692,20 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { if (typeof autoReveal === 'boolean' && autoReveal) { let currentActiveResource = this.getResourceForCurrentActiveResource(); if (currentActiveResource) { - if (this.tree.hasElement(currentActiveResource) && !this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { - this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); - if (focus) { - this.tree.setFocus(this.tree.getSelection()); - } - } else { - this.tree.expand(currentActiveResource); - this.tree.reveal(currentActiveResource, 0); + if (this.tree.hasElement(currentActiveResource)) { + if (!this.tree.isCollapsed(currentActiveResource) && this.hasSelectedMarkerFor(currentActiveResource)) { + this.tree.reveal(this.tree.getSelection()[0], this.lastSelectedRelativeTop); + if (focus) { + this.tree.setFocus(this.tree.getSelection()); + } + } else { + this.tree.expand(currentActiveResource); + this.tree.reveal(currentActiveResource, 0); - if (focus) { - this.tree.setFocus([currentActiveResource]); - this.tree.setSelection([currentActiveResource]); + if (focus) { + this.tree.setFocus([currentActiveResource]); + this.tree.setSelection([currentActiveResource]); + } } } } else if (focus) { diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index b898358d421..ec967357971 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -65,7 +65,7 @@ } .panel > .title .monaco-action-bar .action-item.markers-panel-action-filter-container { - max-width: 600px; + max-width: 400px; min-width: 300px; margin-right: 10px; } diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index b6b383674be..245f72a8786 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -13,8 +13,8 @@ export const CELL_RUN_GUTTER = 28; export const CODE_CELL_LEFT_MARGIN = 32; export const EDITOR_TOOLBAR_HEIGHT = 0; -export const BOTTOM_CELL_TOOLBAR_HEIGHT = 18; -export const BOTTOM_CELL_TOOLBAR_OFFSET = 12; +export const BOTTOM_CELL_TOOLBAR_GAP = 18; +export const BOTTOM_CELL_TOOLBAR_HEIGHT = 50; export const CELL_STATUSBAR_HEIGHT = 22; // Margin above editor @@ -24,6 +24,7 @@ export const CELL_BOTTOM_MARGIN = 6; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 12; export const EDITOR_BOTTOM_PADDING = 4; +export const EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR = 12; export const CELL_OUTPUT_PADDING = 14; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 63f2adeb309..55419ec854e 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -87,8 +87,7 @@ const enum CellToolbarOrder { EditCell, SplitCell, SaveCell, - ClearCellOutput, - DeleteCell + ClearCellOutput } const enum CellOverflowToolbarGroups { @@ -261,6 +260,23 @@ export class CancelCellAction extends MenuItemAction { } } +export class DeleteCellAction extends MenuItemAction { + constructor( + @IContextKeyService contextKeyService: IContextKeyService, + @ICommandService commandService: ICommandService + ) { + super( + { + id: DELETE_CELL_COMMAND_ID, + title: localize('notebookActions.deleteCell', "Delete Cell"), + icon: { id: 'codicon/trash' } + }, + undefined, + { shouldForwardArgs: true }, + contextKeyService, + commandService); + } +} registerAction2(class extends NotebookCellAction { constructor() { @@ -775,9 +791,7 @@ registerAction2(class extends NotebookCellAction { title: localize('notebookActions.deleteCell', "Delete Cell"), menu: { id: MenuId.NotebookCellTitle, - order: CellToolbarOrder.DeleteCell, - when: NOTEBOOK_EDITOR_EDITABLE, - group: CELL_TITLE_CELL_GROUP_ID + when: NOTEBOOK_EDITOR_EDITABLE }, keybinding: { primary: KeyCode.Delete, @@ -1254,7 +1268,7 @@ registerAction2(class extends NotebookCellAction { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, - title: localize('clearActiveCellOutputs', 'Clear Active Cell Outputs'), + title: localize('clearCellOutputs', 'Clear Cell Outputs'), menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_CELL_TYPE.isEqualTo('code'), NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_CELL_HAS_OUTPUTS), @@ -1277,6 +1291,14 @@ registerAction2(class extends NotebookCellAction { } editor.viewModel.notebookDocument.clearCellOutput(context.cell.handle); + if (context.cell.metadata && context.cell.metadata?.runState !== NotebookCellRunState.Running) { + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { + runState: NotebookCellRunState.Idle, + runStartTime: undefined, + lastRunDuration: undefined, + statusMessage: undefined + }); + } } }); @@ -1464,7 +1486,8 @@ registerAction2(class extends NotebookCellAction { menu: { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), - group: '2_edit', + group: CellOverflowToolbarGroups.Edit, + order: 10 } }); } @@ -1489,6 +1512,7 @@ registerAction2(class extends NotebookCellAction { id: MenuId.NotebookCellTitle, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_EDITABLE), group: CellOverflowToolbarGroups.Edit, + order: 11 } }); } @@ -1538,7 +1562,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: true }); } }); @@ -1561,7 +1585,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { inputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { inputCollapsed: false }); } }); @@ -1584,7 +1608,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: true }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: true }); } }); @@ -1607,7 +1631,7 @@ registerAction2(class extends NotebookCellAction { } async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { - context.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(context.cell.handle, { outputCollapsed: false }); + context.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(context.cell.handle, { outputCollapsed: false }); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts index 75400be17fb..51d77a18fbd 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/test/notebookFolding.test.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; +import { setupInstantiationService, withTestNotebook } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; @@ -17,7 +16,7 @@ function updateFoldingStateAtIndex(foldingModel: FoldingModel, index: number, co } suite('Notebook Folding', () => { - const instantiationService = new TestInstantiationService(); + const instantiationService = setupInstantiationService(); const blukEditService = instantiationService.get(IBulkEditService); const undoRedoService = instantiationService.stub(IUndoRedoService, () => { }); instantiationService.spy(IUndoRedoService, 'pushElement'); @@ -28,13 +27,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingController = new FoldingModel(); @@ -57,13 +56,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1\n# header3'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1\n# header3', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingController = new FoldingModel(); @@ -91,13 +90,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -115,13 +114,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -140,13 +139,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -167,13 +166,13 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -224,18 +223,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -255,18 +254,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -290,18 +289,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -327,18 +326,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); @@ -366,18 +365,18 @@ suite('Notebook Folding', () => { blukEditService, undoRedoService, [ - [['# header 1'], 'markdown', CellKind.Markdown, [], {}], - [['body'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], - [['# header 2.1\n'], 'markdown', CellKind.Markdown, [], {}], - [['body 2'], 'markdown', CellKind.Markdown, [], {}], - [['body 3'], 'markdown', CellKind.Markdown, [], {}], - [['## header 2.2'], 'markdown', CellKind.Markdown, [], {}], - [['var e = 7;'], 'markdown', CellKind.Markdown, [], {}], + ['# header 1', 'markdown', CellKind.Markdown, [], {}], + ['body', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], + ['# header 2.1\n', 'markdown', CellKind.Markdown, [], {}], + ['body 2', 'markdown', CellKind.Markdown, [], {}], + ['body 3', 'markdown', CellKind.Markdown, [], {}], + ['## header 2.2', 'markdown', CellKind.Markdown, [], {}], + ['var e = 7;', 'markdown', CellKind.Markdown, [], {}], ], (editor, viewModel) => { const foldingModel = new FoldingModel(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts index 6cb8b4a2bb0..446f4940e2a 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/format/formatting.ts @@ -17,8 +17,7 @@ import { DisposableStore } from 'vs/base/common/lifecycle'; import { getDocumentFormattingEditsUntilResult, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format'; import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -63,7 +62,7 @@ registerAction2(class extends Action2 { const dispoables = new DisposableStore(); try { - const edits: WorkspaceTextEdit[] = []; + const edits: ResourceTextEdit[] = []; for (const cell of notebook.cells) { @@ -78,18 +77,13 @@ registerAction2(class extends Action2 { ); if (formatEdits) { - formatEdits.forEach(edit => edits.push({ - edit, - resource: model.uri, - modelVersionId: model.getVersionId() - })); + for (let edit of formatEdits) { + edits.push(new ResourceTextEdit(model.uri, edit, model.getVersionId())); + } } } - await bulkEditService.apply( - { edits }, - { label: localize('label', "Format Notebook") } - ); + await bulkEditService.apply(edits, { label: localize('label', "Format Notebook") }); } finally { dispoables.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts new file mode 100644 index 00000000000..3b3601c8d7f --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/scm/scm.ts @@ -0,0 +1,167 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { INotebookEditorContribution, INotebookEditor } from '../../notebookBrowser'; +import { registerNotebookContribution } from '../../notebookEditorExtensions'; +import { ISCMService } from 'vs/workbench/contrib/scm/common/scm'; +import { createProviderComparer } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; +import { first, ThrottledDelayer } from 'vs/base/common/async'; +import { INotebookService } from '../../../common/notebookService'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { URI } from 'vs/base/common/uri'; + +export class SCMController extends Disposable implements INotebookEditorContribution { + static id: string = 'workbench.notebook.findController'; + private _lastDecorationId: string[] = []; + private _localDisposable = new DisposableStore(); + private _originalDocument: NotebookTextModel | undefined = undefined; + private _originalResourceDisposableStore = new DisposableStore(); + private _diffDelayer = new ThrottledDelayer(200); + + private _lastVersion = -1; + + + constructor( + private readonly _notebookEditor: INotebookEditor, + @IFileService private readonly _fileService: FileService, + @ISCMService private readonly _scmService: ISCMService, + @INotebookService private readonly _notebookService: INotebookService + + ) { + super(); + + if (!this._notebookEditor.isEmbedded) { + this._register(this._notebookEditor.onDidChangeModel(() => { + this._localDisposable.clear(); + this._originalResourceDisposableStore.clear(); + this._diffDelayer.cancel(); + this.update(); + + if (this._notebookEditor.textModel) { + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeContent(() => { + this.update(); + })); + + this._localDisposable.add(this._notebookEditor.textModel.onDidChangeCells(() => { + this.update(); + })); + } + })); + + this._register(this._notebookEditor.onWillDispose(() => { + this._localDisposable.clear(); + this._originalResourceDisposableStore.clear(); + })); + + this.update(); + } + } + + private async _resolveNotebookDocument(uri: URI, viewType: string) { + const providers = this._scmService.repositories.map(r => r.provider); + const rootedProviders = providers.filter(p => !!p.rootUri); + + rootedProviders.sort(createProviderComparer(uri)); + + const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); + + if (!result) { + this._originalDocument = undefined; + this._originalResourceDisposableStore.clear(); + return; + } + + if (result.toString() === this._originalDocument?.uri.toString()) { + // original document not changed + return; + } + + this._originalResourceDisposableStore.add(this._fileService.watch(result)); + this._originalResourceDisposableStore.add(this._fileService.onDidFilesChange(e => { + if (e.changes.find(change => change.resource.toString() === result.toString())) { + this._originalDocument = undefined; + this._originalResourceDisposableStore.clear(); + this.update(); + } + })); + + const originalDocument = await this._notebookService.resolveNotebook(viewType, result, false); + this._originalResourceDisposableStore.add({ + dispose: () => { + this._originalDocument?.dispose(); + this._originalDocument = undefined; + } + }); + + this._originalDocument = originalDocument; + } + + async update() { + if (!this._diffDelayer) { + return; + } + + await this._diffDelayer + .trigger(async () => { + const modifiedDocument = this._notebookEditor.textModel; + if (!modifiedDocument) { + return; + } + + if (this._lastVersion >= modifiedDocument.versionId) { + return; + } + + this._lastVersion = modifiedDocument.versionId; + await this._resolveNotebookDocument(modifiedDocument.uri, modifiedDocument.viewType); + + if (!this._originalDocument) { + this._clear(); + return; + } + + // const diff = new LcsDiff(new CellSequence(this._originalDocument), new CellSequence(modifiedDocument)); + // const diffResult = diff.ComputeDiff(false); + + // const decorations: INotebookDeltaDecoration[] = []; + // diffResult.changes.forEach(change => { + // if (change.originalLength === 0) { + // // doesn't exist in original + // for (let i = 0; i < change.modifiedLength; i++) { + // decorations.push({ + // handle: modifiedDocument.cells[change.modifiedStart + i].handle, + // options: { gutterClassName: 'nb-gutter-cell-inserted' } + // }); + // } + // } else { + // if (change.modifiedLength === 0) { + // // diff.deleteCount + // // removed from original + // } else { + // // modification + // for (let i = 0; i < change.modifiedLength; i++) { + // decorations.push({ + // handle: modifiedDocument.cells[change.modifiedStart + i].handle, + // options: { gutterClassName: 'nb-gutter-cell-changed' } + // }); + // } + // } + // } + // }); + + + // this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, decorations); + }); + } + + private _clear() { + this._lastDecorationId = this._notebookEditor.deltaCellDecorations(this._lastDecorationId, []); + } +} + +registerNotebookContribution(SCMController.id, SCMController); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts new file mode 100644 index 00000000000..868c3ab9a8e --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/cellComponents.ts @@ -0,0 +1,785 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDiffEditorOptions, IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellDiffViewModel, PropertyFoldingState } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { CellDiffRenderTemplate, CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; +import { renderCodicons } from 'vs/base/common/codicons'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; +import { format } from 'vs/base/common/jsonFormatter'; +import { applyEdits } from 'vs/base/common/jsonEdit'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { hash } from 'vs/base/common/hash'; + +const fixedEditorOptions: IEditorOptions = { + padding: { + top: 12, + bottom: 12 + }, + scrollBeyondLastLine: false, + scrollbar: { + verticalScrollbarSize: 14, + horizontal: 'auto', + useShadows: true, + verticalHasArrows: false, + horizontalHasArrows: false, + alwaysConsumeMouseWheel: false + }, + renderLineHighlightOnlyWhenFocus: true, + overviewRulerLanes: 0, + selectOnLineNumbers: false, + wordWrap: 'off', + lineNumbers: 'off', + lineDecorationsWidth: 0, + glyphMargin: false, + fixedOverflowWidgets: true, + minimap: { enabled: false }, + renderValidationDecorations: 'on', + renderLineHighlight: 'none', + readOnly: true +}; + +const fixedDiffEditorOptions: IDiffEditorOptions = { + ...fixedEditorOptions, + glyphMargin: true, + enableSplitViewResizing: false, + renderIndicators: false, + readOnly: false +}; + + + +class PropertyHeader extends Disposable { + protected _foldingIndicator!: HTMLElement; + protected _statusSpan!: HTMLElement; + + constructor( + readonly cell: CellDiffViewModel, + readonly metadataHeaderContainer: HTMLElement, + readonly notebookEditor: INotebookTextDiffEditor, + readonly accessor: { + updateInfoRendering: () => void; + checkIfModified: (cell: CellDiffViewModel) => boolean; + getFoldingState: (cell: CellDiffViewModel) => PropertyFoldingState; + updateFoldingState: (cell: CellDiffViewModel, newState: PropertyFoldingState) => void; + unChangedLabel: string; + changedLabel: string; + prefix: string; + } + ) { + super(); + } + + buildHeader(): void { + let metadataChanged = this.accessor.checkIfModified(this.cell); + this._foldingIndicator = DOM.append(this.metadataHeaderContainer, DOM.$('.property-folding-indicator')); + DOM.addClass(this._foldingIndicator, this.accessor.prefix); + + + this._updateFoldingIcon(); + const metadataStatus = DOM.append(this.metadataHeaderContainer, DOM.$('div.property-status')); + this._statusSpan = DOM.append(metadataStatus, DOM.$('span')); + + if (metadataChanged) { + this._statusSpan.textContent = this.accessor.changedLabel; + this._statusSpan.style.fontWeight = 'bold'; + DOM.addClass(this.metadataHeaderContainer, 'modified'); + } else { + this._statusSpan.textContent = this.accessor.unChangedLabel; + } + + this._register(this.notebookEditor.onMouseUp(e => { + if (!e.event.target) { + return; + } + + const target = e.event.target as HTMLElement; + + if (DOM.hasClass(target, 'codicon-chevron-down') || DOM.hasClass(target, 'codicon-chevron-right')) { + const parent = target.parentElement as HTMLElement; + + if (!parent) { + return; + } + + if (!DOM.hasClass(parent, this.accessor.prefix)) { + return; + } + + if (!DOM.hasClass(parent, 'property-folding-indicator')) { + return; + } + + // folding icon + + const cellViewModel = e.target; + + if (cellViewModel === this.cell) { + const oldFoldingState = this.accessor.getFoldingState(this.cell); + this.accessor.updateFoldingState(this.cell, oldFoldingState === PropertyFoldingState.Expanded ? PropertyFoldingState.Collapsed : PropertyFoldingState.Expanded); + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(); + } + } + + return; + })); + + this._updateFoldingIcon(); + this.accessor.updateInfoRendering(); + } + + private _updateFoldingIcon() { + if (this.accessor.getFoldingState(this.cell) === PropertyFoldingState.Collapsed) { + this._foldingIndicator.innerHTML = renderCodicons('$(chevron-right)'); + } else { + this._foldingIndicator.innerHTML = renderCodicons('$(chevron-down)'); + } + } +} + +abstract class AbstractCellRenderer extends Disposable { + protected _metadataHeaderContainer!: HTMLElement; + protected _metadataHeader!: PropertyHeader; + protected _metadataInfoContainer!: HTMLElement; + protected _metadataEditorContainer?: HTMLElement; + protected _metadataEditorDisposeStore!: DisposableStore; + protected _metadataEditor?: CodeEditorWidget | DiffEditorWidget; + + protected _outputHeaderContainer!: HTMLElement; + protected _outputHeader!: PropertyHeader; + protected _outputInfoContainer!: HTMLElement; + protected _outputEditorContainer?: HTMLElement; + protected _outputEditorDisposeStore!: DisposableStore; + protected _outputEditor?: CodeEditorWidget | DiffEditorWidget; + + + protected _diffEditorContainer!: HTMLElement; + protected _diagonalFill?: HTMLElement; + protected _layoutInfo!: { + editorHeight: number; + editorMargin: number; + metadataStatusHeight: number; + metadataHeight: number; + outputStatusHeight: number; + outputHeight: number; + bodyMargin: number; + }; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + readonly style: 'left' | 'right' | 'full', + protected readonly instantiationService: IInstantiationService, + protected readonly modeService: IModeService, + protected readonly modelService: IModelService, + + ) { + super(); + // init + this._layoutInfo = { + editorHeight: 0, + editorMargin: 0, + metadataHeight: 0, + metadataStatusHeight: 25, + outputHeight: 0, + outputStatusHeight: 25, + bodyMargin: 32 + }; + this._metadataEditorDisposeStore = new DisposableStore(); + this._outputEditorDisposeStore = new DisposableStore(); + this._register(this._metadataEditorDisposeStore); + this.initData(); + this.buildBody(templateData.container); + this._register(cell.onDidLayoutChange(e => this.onDidLayoutChange(e))); + } + + buildBody(container: HTMLElement) { + const body = DOM.$('.cell-body'); + DOM.append(container, body); + this._diffEditorContainer = DOM.$('.cell-diff-editor-container'); + switch (this.style) { + case 'left': + DOM.addClass(body, 'left'); + break; + case 'right': + DOM.addClass(body, 'right'); + break; + default: + DOM.addClass(body, 'full'); + break; + } + + DOM.append(body, this._diffEditorContainer); + this._diagonalFill = DOM.append(body, DOM.$('.diagonal-fill')); + this.styleContainer(this._diffEditorContainer); + const sourceContainer = DOM.append(this._diffEditorContainer, DOM.$('.source-container')); + this.buildSourceEditor(sourceContainer); + + this._metadataHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-header-container')); + this._metadataInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.metadata-info-container')); + + this._metadataHeader = new PropertyHeader( + this.cell, + this._metadataHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateMetadataRendering.bind(this), + checkIfModified: (cell) => { + return cell.type === 'modified' && hash(this._getFormatedMetadataJSON(cell.original?.metadata || {})) !== hash(this._getFormatedMetadataJSON(cell.modified?.metadata ?? {})); + }, + getFoldingState: (cell) => { + return cell.metadataFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.metadataFoldingState = state; + }, + unChangedLabel: 'Metadata', + changedLabel: 'Metadata changed', + prefix: 'metadata' + } + ); + this._register(this._metadataHeader); + this._metadataHeader.buildHeader(); + + this._outputHeaderContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-header-container')); + this._outputInfoContainer = DOM.append(this._diffEditorContainer, DOM.$('.output-info-container')); + + this._outputHeader = new PropertyHeader( + this.cell, + this._outputHeaderContainer, + this.notebookEditor, + { + updateInfoRendering: this.updateOutputRendering.bind(this), + checkIfModified: (cell) => { + return !this.notebookEditor.textModel!.transientOptions.transientOutputs && cell.type === 'modified' && hash(cell.original?.outputs ?? []) !== hash(cell.modified?.outputs ?? []); + }, + getFoldingState: (cell) => { + return this.cell.outputFoldingState; + }, + updateFoldingState: (cell, state) => { + cell.outputFoldingState = state; + }, + unChangedLabel: 'Outputs', + changedLabel: 'Outputs changed', + prefix: 'output' + } + ); + this._register(this._outputHeader); + this._outputHeader.buildHeader(); + } + + updateMetadataRendering() { + if (this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + // we should expand the metadata editor + this._metadataInfoContainer.style.display = 'block'; + + if (!this._metadataEditorContainer || !this._metadataEditor) { + // create editor + this._metadataEditorContainer = DOM.append(this._metadataInfoContainer, DOM.$('.metadata-editor-container')); + this._buildMetadataEditor(); + } else { + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + } + } else { + // we should collapse the metadata editor + this._metadataInfoContainer.style.display = 'none'; + this._metadataEditorDisposeStore.clear(); + this._layoutInfo.metadataHeight = 0; + this.layout({}); + } + } + + updateOutputRendering() { + if (this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._outputInfoContainer.style.display = 'block'; + + if (!this._outputEditorContainer || !this._outputEditor) { + // create editor + this._outputEditorContainer = DOM.append(this._outputInfoContainer, DOM.$('.output-editor-container')); + this._buildOutputEditor(); + } else { + console.log(this.cell); + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + } + } else { + this._outputInfoContainer.style.display = 'none'; + this._outputEditorDisposeStore.clear(); + this._layoutInfo.outputHeight = 0; + this.layout({}); + } + } + + protected _getFormatedMetadataJSON(metadata: NotebookCellMetadata, language?: string) { + let filteredMetadata: { [key: string]: any } = {}; + if (this.notebookEditor.textModel) { + const transientMetadata = this.notebookEditor.textModel!.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = metadata[key as keyof NotebookCellMetadata]; + } + } + } else { + filteredMetadata = metadata; + } + + const content = JSON.stringify({ + language, + ...filteredMetadata + }); + + const edits = format(content, undefined, {}); + const metadataSource = applyEdits(content, edits); + + return metadataSource; + } + + private _buildMetadataEditor() { + if (this.cell.type === 'modified') { + const originalMetadataSource = this._getFormatedMetadataJSON(this.cell.original?.metadata || {}, this.cell.original?.language); + const modifiedMetadataSource = this._getFormatedMetadataJSON(this.cell.modified?.metadata || {}, this.cell.modified?.language); + if (originalMetadataSource !== modifiedMetadataSource) { + this._metadataEditor = this.instantiationService.createInstance(DiffEditorWidget, this._metadataEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true + }); + + DOM.addClass(this._metadataEditorContainer!, 'diff'); + + const mode = this.modeService.create('json'); + const originalMetadataModel = this.modelService.createModel(originalMetadataSource, mode, undefined, true); + const modifiedMetadataModel = this.modelService.createModel(modifiedMetadataSource, mode, undefined, true); + this._metadataEditor.setModel({ + original: originalMetadataModel, + modified: modifiedMetadataModel + }); + + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + + this._register(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.metadataHeight = e.contentHeight; + this.layout({ metadataEditor: true }); + } + })); + + return; + } + } + + this._metadataEditor = this.instantiationService.createInstance(CodeEditorWidget, this._metadataEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: 0 + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + + const mode = this.modeService.create('json'); + const originalMetadataSource = this._getFormatedMetadataJSON( + this.cell.type === 'insert' + ? this.cell.modified!.metadata || {} + : this.cell.original!.metadata || {}); + const metadataModel = this.modelService.createModel(originalMetadataSource, mode, undefined, true); + this._metadataEditor.setModel(metadataModel); + + this._layoutInfo.metadataHeight = this._metadataEditor.getContentHeight(); + this.layout({ metadataEditor: true }); + + this._register(this._metadataEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.metadataFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.metadataHeight = e.contentHeight; + this.layout({ metadataEditor: true }); + } + })); + } + + private _getFormatedOutputJSON(outputs: any[]) { + const content = JSON.stringify(outputs); + + const edits = format(content, undefined, {}); + const source = applyEdits(content, edits); + + return source; + } + + private _buildOutputEditor() { + if (this.cell.type === 'modified' && !this.notebookEditor.textModel!.transientOptions.transientOutputs) { + const originalOutputsSource = this._getFormatedOutputJSON(this.cell.original?.outputs || []); + const modifiedOutputsSource = this._getFormatedOutputJSON(this.cell.modified?.outputs || []); + if (originalOutputsSource !== modifiedOutputsSource) { + this._outputEditor = this.instantiationService.createInstance(DiffEditorWidget, this._outputEditorContainer!, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: true + }); + + DOM.addClass(this._outputEditorContainer!, 'diff'); + + const mode = this.modeService.create('json'); + const originalModel = this.modelService.createModel(originalOutputsSource, mode, undefined, true); + const modifiedModel = this.modelService.createModel(modifiedOutputsSource, mode, undefined, true); + this._outputEditor.setModel({ + original: originalModel, + modified: modifiedModel + }); + + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + + this._register(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.outputHeight = e.contentHeight; + this.layout({ outputEditor: true }); + } + })); + + return; + } + } + + this._outputEditor = this.instantiationService.createInstance(CodeEditorWidget, this._outputEditorContainer!, { + ...fixedEditorOptions, + dimension: { + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: 0 + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + + const mode = this.modeService.create('json'); + const originaloutputSource = this._getFormatedOutputJSON( + this.notebookEditor.textModel!.transientOptions + ? [] + : this.cell.type === 'insert' + ? this.cell.modified!.outputs || [] + : this.cell.original!.outputs || []); + const outputModel = this.modelService.createModel(originaloutputSource, mode, undefined, true); + this._outputEditor.setModel(outputModel); + + this._layoutInfo.outputHeight = this._outputEditor.getContentHeight(); + this.layout({ outputEditor: true }); + + this._register(this._outputEditor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged && this.cell.outputFoldingState === PropertyFoldingState.Expanded) { + this._layoutInfo.outputHeight = e.contentHeight; + this.layout({ outputEditor: true }); + } + })); + } + + protected layoutNotebookCell() { + this.notebookEditor.layoutNotebookCell( + this.cell, + this._layoutInfo.editorHeight + + this._layoutInfo.editorMargin + + this._layoutInfo.metadataHeight + + this._layoutInfo.metadataStatusHeight + + this._layoutInfo.outputHeight + + this._layoutInfo.outputStatusHeight + + this._layoutInfo.bodyMargin + ); + } + + abstract initData(): void; + abstract styleContainer(container: HTMLElement): void; + abstract buildSourceEditor(sourceContainer: HTMLElement): void; + abstract onDidLayoutChange(event: CellDiffViewModelLayoutChangeEvent): void; + abstract layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }): void; +} + +export class DeletedCell extends AbstractCellRenderer { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + ) { + super(notebookEditor, cell, templateData, 'left', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement) { + DOM.addClass(container, 'removed'); + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const originalCell = this.cell.original!; + const lineCount = originalCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { + ...fixedEditorOptions, + dimension: { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode() + }, {}); + this._layoutInfo.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + originalCell.resolveTextModelRef().then(ref => { + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._layoutInfo.editorHeight = this._editor.getContentHeight(); + this.layout({ editorHeight: true }); + }); + + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.editorHeight + }); + } + + if (state.metadataEditor || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.metadataHeight + }); + } + + if (state.outputEditor || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.outputHeight + }); + } + + this.layoutNotebookCell(); + } +} + +export class InsertCell extends AbstractCellRenderer { + private _editor!: CodeEditorWidget; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + ) { + super(notebookEditor, cell, templateData, 'right', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement): void { + DOM.addClass(container, 'inserted'); + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + const editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(CodeEditorWidget, editorContainer, { + ...fixedEditorOptions, + dimension: { + width: (this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN) / 2 - 18, + height: editorHeight + }, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + readOnly: false + }, {}); + + this._layoutInfo.editorHeight = editorHeight; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + modifiedCell.resolveTextModelRef().then(ref => { + this._register(ref); + + const textModel = ref.object.textEditorModel; + this._editor.setModel(textModel); + this._layoutInfo.editorHeight = this._editor.getContentHeight(); + this.layout({ editorHeight: true }); + }); + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editor.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, false), + height: this._layoutInfo.editorHeight + }); + } + + if (state.metadataEditor || state.outerWidth) { + this._metadataEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this._layoutInfo.metadataHeight + }); + } + + if (state.outputEditor || state.outerWidth) { + this._outputEditor?.layout({ + width: this.cell.getComputedCellContainerWidth(this.notebookEditor.getLayoutInfo(), false, true), + height: this._layoutInfo.outputHeight + }); + } + + this.layoutNotebookCell(); + } +} + +export class ModifiedCell extends AbstractCellRenderer { + private _editor?: DiffEditorWidget; + private _editorContainer!: HTMLElement; + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + readonly cell: CellDiffViewModel, + readonly templateData: CellDiffRenderTemplate, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IModeService readonly modeService: IModeService, + @IModelService readonly modelService: IModelService, + ) { + super(notebookEditor, cell, templateData, 'full', instantiationService, modeService, modelService); + } + + initData(): void { + } + + styleContainer(container: HTMLElement): void { + } + + buildSourceEditor(sourceContainer: HTMLElement): void { + const modifiedCell = this.cell.modified!; + const lineCount = modifiedCell.textBuffer.getLineCount(); + const lineHeight = this.notebookEditor.getLayoutInfo().fontInfo.lineHeight || 17; + const editorHeight = lineCount * lineHeight + EDITOR_TOP_PADDING + EDITOR_BOTTOM_PADDING; + this._editorContainer = DOM.append(sourceContainer, DOM.$('.editor-container')); + + this._editor = this.instantiationService.createInstance(DiffEditorWidget, this._editorContainer, { + ...fixedDiffEditorOptions, + overflowWidgetsDomNode: this.notebookEditor.getOverflowContainerDomNode(), + originalEditable: false + }); + DOM.addClass(this._editorContainer, 'diff'); + + this._editor.layout({ + width: this.notebookEditor.getLayoutInfo().width - 2 * DIFF_CELL_MARGIN, + height: editorHeight + }); + + this._editorContainer.style.height = `${editorHeight}px`; + + this._register(this._editor.onDidContentSizeChange((e) => { + if (e.contentHeightChanged) { + this._layoutInfo.editorHeight = e.contentHeight; + this.layout({ editorHeight: true }); + } + })); + + this._initializeSourceDiffEditor(); + } + + private async _initializeSourceDiffEditor() { + const originalCell = this.cell.original!; + const modifiedCell = this.cell.modified!; + + const originalRef = await originalCell.resolveTextModelRef(); + const modifiedRef = await modifiedCell.resolveTextModelRef(); + const textModel = originalRef.object.textEditorModel; + const modifiedTextModel = modifiedRef.object.textEditorModel; + this._register(originalRef); + this._register(modifiedRef); + + this._editor!.setModel({ + original: textModel, + modified: modifiedTextModel + }); + + const contentHeight = this._editor!.getContentHeight(); + this._layoutInfo.editorHeight = contentHeight; + this.layout({ editorHeight: true }); + + } + + onDidLayoutChange(e: CellDiffViewModelLayoutChangeEvent) { + if (e.outerWidth !== undefined) { + this.layout({ outerWidth: true }); + } + } + + layout(state: { outerWidth?: boolean, editorHeight?: boolean, metadataEditor?: boolean, outputEditor?: boolean }) { + if (state.editorHeight || state.outerWidth) { + this._editorContainer.style.height = `${this._layoutInfo.editorHeight}px`; + this._editor!.layout(); + } + + if (state.metadataEditor || state.outerWidth) { + if (this._metadataEditorContainer) { + this._metadataEditorContainer.style.height = `${this._layoutInfo.metadataHeight}px`; + this._metadataEditor?.layout(); + } + } + + if (state.outputEditor || state.outerWidth) { + if (this._outputEditorContainer) { + this._outputEditorContainer.style.height = `${this._layoutInfo.outputHeight}px`; + this._outputEditor?.layout(); + } + } + + this.layoutNotebookCell(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts new file mode 100644 index 00000000000..55e25cee313 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookDiffEditorEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CellDiffViewModelLayoutChangeEvent, DIFF_CELL_MARGIN } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DiffEditorWidget } from 'vs/editor/browser/widget/diffEditorWidget'; + +export enum PropertyFoldingState { + Expanded, + Collapsed +} + +export class CellDiffViewModel extends Disposable { + public metadataFoldingState: PropertyFoldingState; + public outputFoldingState: PropertyFoldingState; + private _layoutInfoEmitter = new Emitter(); + + onDidLayoutChange = this._layoutInfoEmitter.event; + + constructor( + readonly original: NotebookCellTextModel | undefined, + readonly modified: NotebookCellTextModel | undefined, + readonly type: 'unchanged' | 'insert' | 'delete' | 'modified', + readonly editorEventDispatcher: NotebookDiffEditorEventDispatcher + ) { + super(); + this.metadataFoldingState = PropertyFoldingState.Collapsed; + this.outputFoldingState = PropertyFoldingState.Collapsed; + + this._register(this.editorEventDispatcher.onDidChangeLayout(e => { + this._layoutInfoEmitter.fire({ outerWidth: e.value.width }); + })); + } + + getComputedCellContainerWidth(layoutInfo: NotebookLayoutInfo, diffEditor: boolean, fullWidth: boolean) { + if (fullWidth) { + return layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0) - 2; + } + + return (layoutInfo.width - 2 * DIFF_CELL_MARGIN + (diffEditor ? DiffEditorWidget.ENTIRE_DIFF_OVERVIEW_WIDTH : 0)) / 2 - 18 - 2; + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/common.ts b/src/vs/workbench/contrib/notebook/browser/diff/common.ts new file mode 100644 index 00000000000..505718cea7c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/common.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { Event } from 'vs/base/common/event'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; + +export interface INotebookTextDiffEditor { + readonly textModel?: NotebookTextModel; + onMouseUp: Event<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>; + getOverflowContainerDomNode(): HTMLElement; + getLayoutInfo(): NotebookLayoutInfo; + layoutNotebookCell(cell: CellDiffViewModel, height: number): void; +} + +export interface CellDiffRenderTemplate { + readonly container: HTMLElement; + readonly elementDisposables: DisposableStore; +} + +export interface CellDiffViewModelLayoutChangeEvent { + font?: BareFontInfo; + outerWidth?: number; +} + +export const DIFF_CELL_MARGIN = 16; diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css new file mode 100644 index 00000000000..dda6833c788 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiff.css @@ -0,0 +1,101 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* .notebook-diff-editor { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; +} +.notebook-diff-editor-modified, +.notebook-diff-editor-original { + display: flex; + height: 100%; + width: 50%; +} */ + +.notebook-text-diff-editor .cell-body { + display: flex; + flex-direction: row; +} + +.notebook-text-diff-editor .cell-body.right { + flex-direction: row-reverse; +} + +.notebook-text-diff-editor .cell-body .diagonal-fill { + display: none; + width: 50%; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container { + width: 100%; + overflow: hidden; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container.diff, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .output-editor-container.diff, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff { + /** 100% + diffOverviewWidth */ + width: calc(100% + 30px); +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container .monaco-diff-editor .diffOverview, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container.diff .monaco-diff-editor .diffOverview { + display: none; +} + +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .metadata-editor-container, +.notebook-text-diff-editor .cell-body .cell-diff-editor-container .editor-container { + box-sizing: border-box; +} + +.notebook-text-diff-editor .cell-body.left .cell-diff-editor-container, +.notebook-text-diff-editor .cell-body.right .cell-diff-editor-container { + display: inline-block; + width: 50%; +} + +.notebook-text-diff-editor .cell-body.left .diagonal-fill, +.notebook-text-diff-editor .cell-body.right .diagonal-fill { + display: inline-block; + width: 50%; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + display: flex; + height: 24px; + align-items: center; + cursor: default; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-folding-indicator .codicon, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-folding-indicator .codicon { + visibility: visible; + padding: 4px 0 0 10px; + cursor: pointer; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status { + font-size: 12px; +} + +.notebook-text-diff-editor .cell-diff-editor-container .output-header-container .property-status span, +.notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container .property-status span { + margin: 0 8px; + line-height: 21px; +} + +.notebook-text-diff-editor { + overflow: hidden; +} + +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, +.monaco-workbench .notebook-text-diff-editor > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { + outline: none !important; +} diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.ts new file mode 100644 index 00000000000..2ffd15bb0c2 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookDiffActions.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 { localize } from 'vs/nls'; +import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { viewColumnToEditorGroup } from 'vs/workbench/api/common/shared/editor'; +import { ActiveEditorContext } from 'vs/workbench/common/editor'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +// ActiveEditorContext.isEqualTo(SearchEditorConstants.SearchEditorID) + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.diff.switchToText', + icon: { id: 'codicon/file-code' }, + title: { value: localize('notebook.diff.switchToText', "Open Text Diff Editor"), original: 'Open Text Diff Editor' }, + precondition: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID), + menu: [{ + id: MenuId.EditorTitle, + group: 'navigation', + when: ActiveEditorContext.isEqualTo(NotebookTextDiffEditor.ID) + }] + }); + } + + async run(accessor: ServicesAccessor): Promise { + const editorService = accessor.get(IEditorService); + const editorGroupService = accessor.get(IEditorGroupsService); + + const activeEditor = editorService.activeEditorPane; + if (activeEditor && activeEditor instanceof NotebookTextDiffEditor) { + const leftResource = (activeEditor.input as NotebookDiffEditorInput).originalResource; + const rightResource = (activeEditor.input as NotebookDiffEditorInput).resource; + const options = { + preserveFocus: false + }; + + const label = localize('diffLeftRightLabel', "{0} ⟷ {1}", leftResource.toString(true), rightResource.toString(true)); + + await editorService.openEditor({ leftResource, rightResource, label, options }, viewColumnToEditorGroup(editorGroupService, undefined)); + } + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts new file mode 100644 index 00000000000..72e3d478988 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor.ts @@ -0,0 +1,414 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import * as DOM from 'vs/base/browser/dom'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { notebookCellBorder, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookDiffEditorInput } from '../notebookDiffEditorInput'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { CellDiffRenderer, NotebookCellTextDiffListDelegate, NotebookTextDiffList } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { diffDiagonalFill, diffInserted, diffRemoved, editorBackground, focusBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { getZoomLevel } from 'vs/base/browser/browser'; +import { NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { DIFF_CELL_MARGIN, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { Emitter } from 'vs/base/common/event'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { NotebookDiffEditorEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const IN_NOTEBOOK_TEXT_DIFF_EDITOR = new RawContextKey('isInNotebookTextDiffEditor', false); + +export class NotebookTextDiffEditor extends EditorPane implements INotebookTextDiffEditor { + static readonly ID: string = 'workbench.editor.notebookTextDiffEditor'; + + private _rootElement!: HTMLElement; + private _overflowContainer!: HTMLElement; + private _dimension: DOM.Dimension | null = null; + private _list!: WorkbenchList; + private _fontInfo: BareFontInfo | undefined; + + private readonly _onMouseUp = this._register(new Emitter<{ readonly event: MouseEvent; readonly target: CellDiffViewModel; }>()); + public readonly onMouseUp = this._onMouseUp.event; + private _eventDispatcher: NotebookDiffEditorEventDispatcher | undefined; + protected _scopeContextKeyService!: IContextKeyService; + private _model: INotebookDiffEditorModel | null = null; + get textModel() { + return this._model?.modified.notebook; + } + + constructor( + @IInstantiationService readonly instantiationService: IInstantiationService, + @IThemeService readonly themeService: IThemeService, + @IContextKeyService readonly contextKeyService: IContextKeyService, + @INotebookEditorWorkerService readonly notebookEditorWorkerService: INotebookEditorWorkerService, + @IConfigurationService private readonly configurationService: IConfigurationService, + + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + ) { + super(NotebookTextDiffEditor.ID, telemetryService, themeService, storageService); + const editorOptions = this.configurationService.getValue('editor'); + this._fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + } + + protected createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, DOM.$('.notebook-text-diff-editor')); + this._overflowContainer = document.createElement('div'); + DOM.addClass(this._overflowContainer, 'notebook-overflow-widget-container'); + DOM.addClass(this._overflowContainer, 'monaco-editor'); + DOM.append(parent, this._overflowContainer); + + const renderer = this.instantiationService.createInstance(CellDiffRenderer, this); + + this._list = this.instantiationService.createInstance( + NotebookTextDiffList, + 'NotebookTextDiff', + this._rootElement, + this.instantiationService.createInstance(NotebookCellTextDiffListDelegate), + [ + renderer + ], + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + additionalScrollHeight: 0, + // transformOptimization: (isMacintosh && isNative) || getTitleBarStyle(this.configurationService, this.environmentService) === 'native', + styleController: (_suffix: string) => { return this._list!; }, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + }, + accessibilityProvider: { + getAriaLabel() { return null; }, + getWidgetAriaLabel() { + return nls.localize('notebookTreeAriaLabel', "Notebook Text Diff"); + } + }, + // focusNextPreviousDelegate: { + // onFocusNext: (applyFocusNext: () => void) => this._updateForCursorNavigationMode(applyFocusNext), + // onFocusPrevious: (applyFocusPrevious: () => void) => this._updateForCursorNavigationMode(applyFocusPrevious), + // } + } + ); + + this._register(this._list.onMouseUp(e => { + if (e.element) { + this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); + } + })); + } + + async setInput(input: NotebookDiffEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + await super.setInput(input, options, context, token); + + this._model = await input.resolve(); + if (this._model === null) { + return; + } + + this._eventDispatcher = new NotebookDiffEditorEventDispatcher(); + + const diffResult = await this.notebookEditorWorkerService.computeDiff(this._model.original.resource, this._model.modified.resource); + const cellChanges = diffResult.cellsDiff.changes; + + const cellDiffViewModels: CellDiffViewModel[] = []; + const originalModel = this._model.original.notebook; + const modifiedModel = this._model.modified.notebook; + let originalCellIndex = 0; + let modifiedCellIndex = 0; + + for (let i = 0; i < cellChanges.length; i++) { + const change = cellChanges[i]; + // common cells + + for (let j = 0; j < change.originalStart - originalCellIndex; j++) { + const originalCell = originalModel.cells[originalCellIndex + j]; + const modifiedCell = modifiedModel.cells[modifiedCellIndex + j]; + if (originalCell.getHashValue() === modifiedCell.getHashValue()) { + cellDiffViewModels.push(new CellDiffViewModel( + originalCell, + modifiedCell, + 'unchanged', + this._eventDispatcher! + )); + } else { + cellDiffViewModels.push(new CellDiffViewModel( + originalCell, + modifiedCell, + 'modified', + this._eventDispatcher! + )); + } + } + + // modified cells + const modifiedLen = Math.min(change.originalLength, change.modifiedLength); + + for (let j = 0; j < modifiedLen; j++) { + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[change.originalStart + j], + modifiedModel.cells[change.modifiedStart + j], + 'modified', + this._eventDispatcher! + )); + } + + for (let j = modifiedLen; j < change.originalLength; j++) { + // deletion + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[change.originalStart + j], + undefined, + 'delete', + this._eventDispatcher! + )); + } + + for (let j = modifiedLen; j < change.modifiedLength; j++) { + // insertion + cellDiffViewModels.push(new CellDiffViewModel( + undefined, + modifiedModel.cells[change.modifiedStart + j], + 'insert', + this._eventDispatcher! + )); + } + + originalCellIndex = change.originalStart + change.originalLength; + modifiedCellIndex = change.modifiedStart + change.modifiedLength; + } + + for (let i = originalCellIndex; i < originalModel.cells.length; i++) { + cellDiffViewModels.push(new CellDiffViewModel( + originalModel.cells[i], + undefined, + 'delete', + this._eventDispatcher! + )); + } + + for (let i = modifiedCellIndex; i < modifiedModel.cells.length; i++) { + cellDiffViewModels.push(new CellDiffViewModel( + undefined, + modifiedModel.cells[i], + 'insert', + this._eventDispatcher! + )); + } + + this._list.splice(0, this._list.length, cellDiffViewModels); + } + + private pendingLayouts = new WeakMap(); + + + layoutNotebookCell(cell: CellDiffViewModel, height: number) { + const relayout = (cell: CellDiffViewModel, height: number) => { + const viewIndex = this._list!.indexOf(cell); + + this._list?.updateElementHeight(viewIndex, height); + }; + + if (this.pendingLayouts.has(cell)) { + this.pendingLayouts.get(cell)!.dispose(); + } + + let r: () => void; + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(() => { + this.pendingLayouts.delete(cell); + + relayout(cell, height); + r(); + }); + + this.pendingLayouts.set(cell, toDisposable(() => { + layoutDisposable.dispose(); + r(); + })); + + return new Promise(resolve => { r = resolve; }); + } + + getDomNode() { + return this._rootElement; + } + + getOverflowContainerDomNode(): HTMLElement { + return this._overflowContainer; + } + + getControl(): NotebookEditorWidget | undefined { + return undefined; + } + + setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { + super.setEditorVisible(visible, group); + } + + focus() { + super.focus(); + } + + clearInput(): void { + super.clearInput(); + } + + getLayoutInfo(): NotebookLayoutInfo { + if (!this._list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this._dimension!.width, + height: this._dimension!.height, + fontInfo: this._fontInfo! + }; + } + + layout(dimension: DOM.Dimension): void { + this._rootElement.classList.toggle('mid-width', dimension.width < 1000 && dimension.width >= 600); + this._rootElement.classList.toggle('narrow-width', dimension.width < 600); + this._dimension = dimension; + this._rootElement.style.height = `${dimension.height}px`; + + this._list?.layout(this._dimension.height, this._dimension.width); + this._eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } +} + +registerThemingParticipant((theme, collector) => { + const cellBorderColor = theme.getColor(notebookCellBorder); + if (cellBorderColor) { + collector.addRule(`.notebook-text-diff-editor .cell-body { border: 1px solid ${cellBorderColor};}`); + collector.addRule(`.notebook-text-diff-editor .cell-diff-editor-container .output-header-container, + .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container { + border-top: 1px solid ${cellBorderColor}; + }`); + } + + const diffDiagonalFillColor = theme.getColor(diffDiagonalFill); + collector.addRule(` + .notebook-text-diff-editor .diagonal-fill { + background-image: linear-gradient( + -45deg, + ${diffDiagonalFillColor} 12.5%, + #0000 12.5%, #0000 50%, + ${diffDiagonalFillColor} 50%, ${diffDiagonalFillColor} 62.5%, + #0000 62.5%, #0000 100% + ); + background-size: 8px 8px; + } + `); + + const added = theme.getColor(diffInserted); + if (added) { + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .source-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-editor-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-editor-container .monaco-editor .monaco-editor-background { + background-color: ${added}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .metadata-header-container { background-color: ${added}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.inserted .output-header-container { background-color: ${added}; } + ` + ); + } + const removed = theme.getColor(diffRemoved); + if (added) { + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .source-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-editor-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .margin, + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-editor-container .monaco-editor .monaco-editor-background { + background-color: ${removed}; + } + ` + ); + collector.addRule(` + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .metadata-header-container { background-color: ${removed}; } + .notebook-text-diff-editor .cell-body .cell-diff-editor-container.removed .output-header-container { background-color: ${removed}; } + ` + ); + } + + // const changed = theme.getColor(editorGutterModifiedBackground); + + // if (changed) { + // collector.addRule(` + // .notebook-text-diff-editor .cell-diff-editor-container .metadata-header-container.modified { + // background-color: ${changed}; + // } + // `); + // } + + collector.addRule(`.notebook-text-diff-editor .cell-body { margin: ${DIFF_CELL_MARGIN}px; }`); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts new file mode 100644 index 00000000000..2931710c259 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/diff/notebookTextDiffList.ts @@ -0,0 +1,228 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./notebookDiff'; +import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import * as DOM from 'vs/base/browser/dom'; +import { IListStyles, IStyleController } from 'vs/base/browser/ui/list/listWidget'; +import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { CellDiffViewModel } from 'vs/workbench/contrib/notebook/browser/diff/celllDiffViewModel'; +import { CellDiffRenderTemplate, INotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/common'; +import { isMacintosh } from 'vs/base/common/platform'; +import { DeletedCell, InsertCell, ModifiedCell } from 'vs/workbench/contrib/notebook/browser/diff/cellComponents'; + +export class NotebookCellTextDiffListDelegate implements IListVirtualDelegate { + // private readonly lineHeight: number; + + constructor( + @IConfigurationService readonly configurationService: IConfigurationService + ) { + // const editorOptions = this.configurationService.getValue('editor'); + // this.lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight; + } + + getHeight(element: CellDiffViewModel): number { + return 100; + } + + hasDynamicHeight(element: CellDiffViewModel): boolean { + return false; + } + + getTemplateId(element: CellDiffViewModel): string { + return CellDiffRenderer.TEMPLATE_ID; + } +} +export class CellDiffRenderer implements IListRenderer { + static readonly TEMPLATE_ID = 'cell_diff'; + + constructor( + readonly notebookEditor: INotebookTextDiffEditor, + @IInstantiationService protected readonly instantiationService: IInstantiationService + ) { } + + get templateId() { + return CellDiffRenderer.TEMPLATE_ID; + } + + renderTemplate(container: HTMLElement): CellDiffRenderTemplate { + return { + container, + elementDisposables: new DisposableStore() + }; + } + + renderElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate, height: number | undefined): void { + templateData.container.innerText = ''; + switch (element.type) { + case 'unchanged': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData)); + return; + case 'delete': + templateData.elementDisposables.add(this.instantiationService.createInstance(DeletedCell, this.notebookEditor, element, templateData)); + return; + case 'insert': + templateData.elementDisposables.add(this.instantiationService.createInstance(InsertCell, this.notebookEditor, element, templateData)); + return; + case 'modified': + templateData.elementDisposables.add(this.instantiationService.createInstance(ModifiedCell, this.notebookEditor, element, templateData)); + return; + default: + break; + } + } + + disposeTemplate(templateData: CellDiffRenderTemplate): void { + templateData.container.innerText = ''; + } + + disposeElement(element: CellDiffViewModel, index: number, templateData: CellDiffRenderTemplate): void { + templateData.elementDisposables.clear(); + } +} + + +export class NotebookTextDiffList extends WorkbenchList implements IDisposable, IStyleController { + private styleElement?: HTMLStyleElement; + + constructor( + listUser: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + contextKeyService: IContextKeyService, + options: IWorkbenchListOptions, + @IListService listService: IListService, + @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService) { + super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); + } + + style(styles: IListStyles) { + const selectorSuffix = this.view.domId; + if (!this.styleElement) { + this.styleElement = DOM.createStyleSheet(this.view.domNode); + } + const suffix = selectorSuffix && `.${selectorSuffix}`; + const content: string[] = []; + + if (styles.listBackground) { + if (styles.listBackground.isOpaque()) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows { background: ${styles.listBackground}; }`); + } else if (!isMacintosh) { // subpixel AA doesn't exist in macOS + console.warn(`List with id '${selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`); + } + } + + if (styles.listFocusBackground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listFocusBackground}; }`); + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listFocusForeground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { color: ${styles.listFocusForeground}; }`); + } + + if (styles.listActiveSelectionBackground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listActiveSelectionBackground}; }`); + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listActiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listActiveSelectionForeground) { + content.push(`.monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listActiveSelectionForeground}; }`); + } + + if (styles.listFocusAndSelectionBackground) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; } + `); + } + + if (styles.listFocusAndSelectionForeground) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; } + `); + } + + if (styles.listInactiveFocusBackground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { background-color: ${styles.listInactiveFocusBackground}; }`); + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused:hover { background-color: ${styles.listInactiveFocusBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionBackground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { background-color: ${styles.listInactiveSelectionBackground}; }`); + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected:hover { background-color: ${styles.listInactiveSelectionBackground}; }`); // overwrite :hover style in this case! + } + + if (styles.listInactiveSelectionForeground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { color: ${styles.listInactiveSelectionForeground}; }`); + } + + if (styles.listHoverBackground) { + content.push(`.monaco-list${suffix}:not(.drop-target) > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`); + } + + if (styles.listHoverForeground) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover:not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`); + } + + if (styles.listSelectionOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected { outline: 1px dotted ${styles.listSelectionOutline}; outline-offset: -1px; }`); + } + + if (styles.listFocusOutline) { + content.push(` + .monaco-drag-image, + .monaco-list${suffix}:focus > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; } + `); + } + + if (styles.listInactiveFocusOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: 1px dotted ${styles.listInactiveFocusOutline}; outline-offset: -1px; }`); + } + + if (styles.listHoverOutline) { + content.push(`.monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover { outline: 1px dashed ${styles.listHoverOutline}; outline-offset: -1px; }`); + } + + if (styles.listDropBackground) { + content.push(` + .monaco-list${suffix}.drop-target, + .monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-rows.drop-target, + .monaco-list${suffix} > div.monaco-scrollable-element > .monaco-list-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; } + `); + } + + if (styles.listFilterWidgetBackground) { + content.push(`.monaco-list-type-filter { background-color: ${styles.listFilterWidgetBackground} }`); + } + + if (styles.listFilterWidgetOutline) { + content.push(`.monaco-list-type-filter { border: 1px solid ${styles.listFilterWidgetOutline}; }`); + } + + if (styles.listFilterWidgetNoMatchesOutline) { + content.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${styles.listFilterWidgetNoMatchesOutline}; }`); + } + + if (styles.listMatchesShadow) { + content.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${styles.listMatchesShadow}; }`); + } + + const newStyles = content.join('\n'); + if (newStyles !== this.styleElement.innerHTML) { + this.styleElement.innerHTML = newStyles; + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts index c7b5c94f98d..f9b0f8bd14d 100644 --- a/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts +++ b/src/vs/workbench/contrib/notebook/browser/extensionPoint.ts @@ -25,16 +25,18 @@ export interface INotebookEditorContribution { namespace NotebookRendererContribution { export const viewType = 'viewType'; + export const id = 'id'; export const displayName = 'displayName'; export const mimeTypes = 'mimeTypes'; export const entrypoint = 'entrypoint'; } export interface INotebookRendererContribution { - readonly [NotebookRendererContribution.viewType]: string; + readonly [NotebookRendererContribution.id]?: string; + readonly [NotebookRendererContribution.viewType]?: string; readonly [NotebookRendererContribution.displayName]: string; readonly [NotebookRendererContribution.mimeTypes]?: readonly string[]; - readonly [NotebookRendererContribution.entrypoint]?: string; + readonly [NotebookRendererContribution.entrypoint]: string; } const notebookProviderContribution: IJSONSchema = { @@ -94,17 +96,23 @@ const notebookProviderContribution: IJSONSchema = { const notebookRendererContribution: IJSONSchema = { description: nls.localize('contributes.notebook.renderer', 'Contributes notebook output renderer provider.'), type: 'array', - defaultSnippets: [{ body: [{ viewType: '', displayName: '', mimeTypes: [''] }] }], + defaultSnippets: [{ body: [{ id: '', displayName: '', mimeTypes: [''] }] }], items: { type: 'object', required: [ - NotebookRendererContribution.viewType, + NotebookRendererContribution.id, NotebookRendererContribution.displayName, NotebookRendererContribution.mimeTypes, + NotebookRendererContribution.entrypoint, ], properties: { + [NotebookRendererContribution.id]: { + type: 'string', + description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), + }, [NotebookRendererContribution.viewType]: { type: 'string', + deprecationMessage: nls.localize('contributes.notebook.provider.viewType.deprecated', 'Rename `viewType` to `id`.'), description: nls.localize('contributes.notebook.renderer.viewType', 'Unique identifier of the notebook output renderer.'), }, [NotebookRendererContribution.displayName]: { diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 6d3c9fbc887..f0e489a8675 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -55,12 +55,22 @@ width: 100%; } +.monaco-workbench .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { + cursor: default; + overflow: visible !important; + width: 100%; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { position: absolute; top: -500px; z-index: 1000; } +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .execution-count-label { + display: none; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div { padding: 12px 16px; } @@ -291,15 +301,26 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { visibility: hidden; - display: inline-block; + display: inline-flex; position: absolute; height: 26px; - right: 44px; top: -12px; /* this lines up the bottom toolbar border with the current line when on line 01 */ z-index: 30; } +.monaco-workbench .notebookOverlay.cell-title-toolbar-right > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + right: 44px; +} + +.monaco-workbench .notebookOverlay.cell-title-toolbar-left > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + left: 76px; +} + +.monaco-workbench .notebookOverlay.cell-title-toolbar-hidden > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar { + display: none; +} + .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-title-toolbar .action-item { width: 24px; height: 24px; @@ -323,12 +344,16 @@ } .monaco-workbench .notebookOverlay .cell-statusbar-container { - height: 21px; + height: 22px; font-size: 12px; display: flex; position: relative; } +.monaco-workbench .notebookOverlay.cell-statusbar-hidden .cell-statusbar-container { + display: none; +} + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; @@ -336,10 +361,30 @@ .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; + z-index: 26; + display: flex; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right .cell-contributed-items-right { + display: flex; + flex-wrap: wrap; + overflow: hidden; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item { + display: flex; + align-items: center; + white-space: pre; + + height: 21px; /* Editor outline is -1px in, don't overlap */ + padding: 0px 6px; +} + +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command { + cursor: pointer; } .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker { - padding: 0px 6px; cursor: pointer; } @@ -353,6 +398,10 @@ 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; @@ -374,51 +423,42 @@ bottom: 0px; top: 0px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { - position: relative; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { + display: flex; + align-items: center; + justify-content: flex-end; + position: absolute; + top: 17px; height: 16px; - flex-shrink: 0; - top: 9px; +} + +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar { + visibility: hidden; z-index: 27; /* Above the drag handle */ } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { - visibility: hidden; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .codicon { margin: 0; padding-right: 4px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .actions-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container .monaco-toolbar .actions-container { justify-content: center; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .runnable .run-button-container .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .runnable .run-button-container .monaco-toolbar { visibility: visible; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { - position: absolute; - top: -2px; +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .execution-count-label { font-size: 10px; font-family: var(--monaco-monospace-font); - visibility: visible; white-space: pre; - width: 100%; - text-align: center; - padding-right: 8px; - box-sizing: border-box; opacity: .6; -} - -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell .run-button-container .execution-count-label, -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { - visibility: hidden; + padding-top: 1px; + margin-right: 1px; } .monaco-workbench .notebookOverlay .cell .cell-editor-part { @@ -438,7 +478,7 @@ height: 2px; } -.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.focused .cell-title-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell-has-toolbar-actions .cell-title-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions.cell-output-hover .cell-title-toolbar, .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-has-toolbar-actions:hover .cell-title-toolbar { visibility: visible; @@ -523,6 +563,7 @@ .monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { position: absolute; display: flex; + align-items: center; justify-content: center; z-index: 25; /* over the focus outline on the editor, below the title toolbar */ width: 100%; @@ -726,16 +767,21 @@ } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { + height: 20px; + width: 20px; + position: absolute; - top: 0; - left: 0; - right: 0; - height: 100%; + top: 6px; + left: 8px; + display: flex; + justify-content: center; + align-items: center; + z-index: 26; } .monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { visibility: visible; - padding: 8px 0 0 10px; + height: 16px; } /** Theming */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 7a9953b2be8..74df84fbeb1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -30,17 +30,24 @@ import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEd import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; -import { CellKind, CellUri, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellToolbarLocKey, CellUri, DisplayOrderKey, getCellUndoRedoComparisonKey, NotebookDocumentBackupData, NotebookEditorPriority, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorOpenWith'; import { CustomEditorInfo } from 'vs/workbench/contrib/customEditor/common/customEditor'; -import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { INotebookEditorModelResolverService, NotebookModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; +import { NotebookDiffEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookDiffEditorInput'; +import { NotebookTextDiffEditor } from 'vs/workbench/contrib/notebook/browser/diff/notebookTextDiffEditor'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; +import { NotebookEditorWorkerServiceImpl } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { NotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl'; // Editor Contribution @@ -51,13 +58,16 @@ import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; +// import 'vs/workbench/contrib/notebook/browser/contrib/scm/scm'; + +// Diff Editor Contribution +import 'vs/workbench/contrib/notebook/browser/diff/notebookDiffActions'; // Output renderers registration import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; -import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; /*--------------------------------------------------------------------------------------------- */ @@ -72,6 +82,17 @@ Registry.as(EditorExtensions.Editors).registerEditor( ] ); +Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + NotebookTextDiffEditor, + NotebookTextDiffEditor.ID, + 'Notebook Diff Editor' + ), + [ + new SyncDescriptor(NotebookDiffEditorInput) + ] +); + class NotebookEditorFactory implements IEditorInputFactory { canSerialize(): boolean { return true; @@ -226,6 +247,10 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } + if (originalInput instanceof DiffEditorInput && this.configurationService.getValue('notebook.diff.enablePreview')) { + return this._handleDiffEditorInput(originalInput, options, group); + } + if (!originalInput.resource) { return undefined; } @@ -301,6 +326,49 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri const notebookOptions = new NotebookEditorOptions({ ...options, cellOptions, override: false, index }); return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) }; } + + private _handleDiffEditorInput(diffEditorInput: DiffEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup): IOpenEditorOverride | undefined { + const modifiedInput = diffEditorInput.modifiedInput; + const originalInput = diffEditorInput.originalInput; + const notebookUri = modifiedInput.resource; + const originalNotebookUri = originalInput.resource; + + if (!notebookUri || !originalNotebookUri) { + return undefined; + } + + const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, notebookUri) && !(editor instanceof NotebookEditorInput)); + + if (existingEditors.length) { + return undefined; + } + + const userAssociatedEditors = this.getUserAssociatedEditors(notebookUri); + const notebookEditor = userAssociatedEditors.filter(association => this.notebookService.getContributedNotebookProvider(association.viewType)); + + if (userAssociatedEditors.length && !notebookEditor.length) { + // user pick a non-notebook editor for this resource + return undefined; + } + + // user might pick a notebook editor + + const associatedEditors = distinct([ + ...this.getUserAssociatedNotebookEditors(notebookUri), + ...(this.getContributedEditors(notebookUri).filter(editor => editor.priority === NotebookEditorPriority.default)) + ], editor => editor.id); + + if (!associatedEditors.length) { + // there is no notebook editor contribution which is enabled by default + return undefined; + } + + const info = associatedEditors[0]; + + const notebookInput = NotebookDiffEditorInput.create(this.instantiationService, notebookUri, modifiedInput.getName(), originalNotebookUri, originalInput.getName(), info.id); + const notebookOptions = new NotebookEditorOptions({ ...options, override: false }); + return { override: this.editorService.openEditor(notebookInput, notebookOptions, group) }; + } } class CellContentProvider implements ITextModelContentProvider { @@ -377,7 +445,9 @@ workbenchContributionsRegistry.registerWorkbenchContribution(NotebookContributio workbenchContributionsRegistry.registerWorkbenchContribution(CellContentProvider, LifecyclePhase.Starting); registerSingleton(INotebookService, NotebookService); +registerSingleton(INotebookEditorWorkerService, NotebookEditorWorkerServiceImpl); registerSingleton(INotebookEditorModelResolverService, NotebookModelResolverService, true); +registerSingleton(INotebookCellStatusBarService, NotebookCellStatusBarService, true); const configurationRegistry = Registry.as(Extensions.Configuration); configurationRegistry.registerConfiguration({ @@ -386,13 +456,24 @@ configurationRegistry.registerConfiguration({ title: nls.localize('notebookConfigurationTitle', "Notebook"), type: 'object', properties: { - 'notebook.displayOrder': { - markdownDescription: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), + [DisplayOrderKey]: { + description: nls.localize('notebook.displayOrder.description', "Priority list for output mime types"), type: ['array'], items: { type: 'string' }, default: [] + }, + [CellToolbarLocKey]: { + description: nls.localize('notebook.cellToolbarLocation.description', "Where the cell toolbar should be shown, or whether it should be hidden."), + type: 'string', + enum: ['left', 'right', 'hidden'], + default: 'right' + }, + [ShowCellStatusbarKey]: { + description: nls.localize('notebook.showCellStatusbar.description', "Whether the cell statusbar should be shown."), + type: 'boolean', + default: true } } }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index c51b949525b..a3b61443510 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -19,14 +19,18 @@ import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { CellLanguageStatusBarItem, RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { RunStateRenderer, TimerRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2 } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor, INotebookKernelInfo2, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { IMenu } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorOptions } from 'vs/workbench/common/editor'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; +import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -161,6 +165,7 @@ export interface INotebookEditorContribution { export interface INotebookCellDecorationOptions { className?: string; + gutterClassName?: string; outputClassName?: string; } @@ -169,7 +174,35 @@ export interface INotebookDeltaDecoration { options: INotebookCellDecorationOptions; } +export class NotebookEditorOptions extends EditorOptions { + + readonly cellOptions?: IResourceEditorInput; + + constructor(options: Partial) { + super(); + this.overwrite(options); + this.cellOptions = options.cellOptions; + } + + with(options: Partial): NotebookEditorOptions { + return new NotebookEditorOptions({ ...this, ...options }); + } +} + +export type INotebookEditorContributionCtor = IConstructorSignature1; + +export interface INotebookEditorContributionDescription { + id: string; + ctor: INotebookEditorContributionCtor; +} + +export interface INotebookEditorCreationOptions { + readonly isEmbedded?: boolean; + readonly contributions?: INotebookEditorContributionDescription[]; +} + export interface INotebookEditor extends IEditor { + isEmbedded: boolean; cursorNavigationMode: boolean; @@ -184,13 +217,14 @@ export interface INotebookEditor extends IEditor { */ readonly onDidChangeModel: Event; readonly onDidFocusEditorWidget: Event; - isNotebookEditor: boolean; + readonly isNotebookEditor: boolean; activeKernel: INotebookKernelInfo | INotebookKernelInfo2 | undefined; multipleKernelsAvailable: boolean; readonly onDidChangeAvailableKernels: Event; readonly onDidChangeKernel: Event; readonly onDidChangeActiveCell: Event; readonly onDidScroll: Event; + readonly onWillDispose: Event; isDisposed: boolean; @@ -208,6 +242,7 @@ export interface INotebookEditor extends IEditor { hasWebviewFocus(): boolean; hasOutputTextSelection(): boolean; + setOptions(options: NotebookEditorOptions | undefined): Promise; /** * Select & focus cell @@ -302,7 +337,7 @@ export interface INotebookEditor extends IEditor { /** * Render the output in webview layer */ - createInset(cell: ICellViewModel, output: IProcessedOutput, shadowContent: string, offset: number): Promise; + createInset(cell: ICellViewModel, output: IInsetRenderOutput, offset: number): Promise; /** * Remove the output from the webview layer @@ -393,6 +428,8 @@ export interface INotebookEditor extends IEditor { setCellSelection(cell: ICellViewModel, selection: Range): void; + deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[]; + /** * Change the decorations on cells. * The notebook is virtualized and this method should be called to create/delete editor decorations safely. @@ -420,7 +457,7 @@ export interface INotebookEditor extends IEditor { } export interface INotebookCellList { - isDisposed: boolean + isDisposed: boolean; readonly contextKeyService: IContextKeyService; elementAt(position: number): ICellViewModel | undefined; elementHeight(element: ICellViewModel): number; @@ -481,14 +518,14 @@ export interface BaseCellRenderTemplate { container: HTMLElement; cellContainer: HTMLElement; toolbar: ToolBar; + deleteToolbar: ToolBar; betweenCellToolbar: ToolBar; focusIndicatorLeft: HTMLElement; disposables: DisposableStore; elementDisposables: DisposableStore; bottomCellContainer: HTMLElement; currentRenderedCell?: ICellViewModel; - statusBarContainer: HTMLElement; - languageStatusBarItem: CellLanguageStatusBarItem; + statusBar: CellEditorStatusBar; titleMenu: IMenu; toJSON: () => object; } @@ -501,7 +538,6 @@ export interface MarkdownCellRenderTemplate extends BaseCellRenderTemplate { export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { cellRunState: RunStateRenderer; - cellStatusMessageContainer: HTMLElement; runToolbar: ToolBar; runButtonContainer: HTMLElement; executionOrderLabel: HTMLElement; @@ -525,6 +561,11 @@ export interface IOutputTransformContribution { */ dispose(): void; + /** + * Returns contents to place in the webview inset, or the {@link IRenderNoOutput}. + * This call is allowed to have side effects, such as placing output + * directly into the container element. + */ render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts new file mode 100644 index 00000000000..27abf1c095a --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookCellStatusBarServiceImpl.ts @@ -0,0 +1,54 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from 'vs/base/common/event'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { URI } from 'vs/base/common/uri'; +import { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export class NotebookCellStatusBarService extends Disposable implements INotebookCellStatusBarService { + + private _onDidChangeEntriesForCell = new Emitter(); + readonly onDidChangeEntriesForCell: Event = this._onDidChangeEntriesForCell.event; + + private _entries = new ResourceMap>(); + + private removeEntry(entry: INotebookCellStatusBarEntry) { + const existingEntries = this._entries.get(entry.cellResource); + if (existingEntries) { + existingEntries.delete(entry); + if (!existingEntries.size) { + this._entries.delete(entry.cellResource); + } + } + + this._onDidChangeEntriesForCell.fire(entry.cellResource); + } + + addEntry(entry: INotebookCellStatusBarEntry): IDisposable { + const existingEntries = this._entries.get(entry.cellResource) ?? new Set(); + existingEntries.add(entry); + this._entries.set(entry.cellResource, existingEntries); + + this._onDidChangeEntriesForCell.fire(entry.cellResource); + + return { + dispose: () => { + this.removeEntry(entry); + } + }; + } + + getEntries(cell: URI): INotebookCellStatusBarEntry[] { + const existingEntries = this._entries.get(cell); + return existingEntries ? + Array.from(existingEntries.values()) : + []; + } + + readonly _serviceBrand: undefined; +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts new file mode 100644 index 00000000000..be382dcd70d --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookDiffEditorInput.ts @@ -0,0 +1,224 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { EditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IMoveResult, IRevertOptions, EditorModel } from 'vs/workbench/common/editor'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { URI } from 'vs/base/common/uri'; +import { isEqual } from 'vs/base/common/resources'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { INotebookEditorModelResolverService } from 'vs/workbench/contrib/notebook/common/notebookEditorModelResolverService'; +import { IReference } from 'vs/base/common/lifecycle'; +import { INotebookEditorModel, INotebookDiffEditorModel } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; + +interface NotebookEditorInputOptions { + startDirty?: boolean; +} + +class NotebookDiffEditorModel extends EditorModel implements INotebookDiffEditorModel { + constructor( + readonly original: NotebookEditorModel, + readonly modified: NotebookEditorModel, + ) { + super(); + } + + async load(): Promise { + await this.original.load(); + await this.modified.load(); + + return this; + } + + async resolveOriginalFromDisk() { + await this.original.load({ forceReadFromDisk: true }); + } + + dispose(): void { + + } + +} + +export class NotebookDiffEditorInput extends EditorInput { + static create(instantiationService: IInstantiationService, resource: URI, name: string, originalResource: URI, originalName: string, viewType: string | undefined, options: NotebookEditorInputOptions = {}) { + return instantiationService.createInstance(NotebookDiffEditorInput, resource, name, originalResource, originalName, viewType, options); + } + + static readonly ID: string = 'workbench.input.diffNotebookInput'; + + private _textModel: IReference | null = null; + private _originalTextModel: IReference | null = null; + private _defaultDirtyState: boolean = false; + + constructor( + public readonly resource: URI, + public readonly name: string, + public readonly originalResource: URI, + public readonly originalName: string, + public readonly viewType: string | undefined, + public readonly options: NotebookEditorInputOptions, + @INotebookService private readonly _notebookService: INotebookService, + @INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService, + @IFilesConfigurationService private readonly _filesConfigurationService: IFilesConfigurationService, + @IFileDialogService private readonly _fileDialogService: IFileDialogService, + // @IInstantiationService private readonly _instantiationService: IInstantiationService + ) { + super(); + this._defaultDirtyState = !!options.startDirty; + } + + getTypeId(): string { + return NotebookDiffEditorInput.ID; + } + + getName(): string { + return nls.localize('sideBySideLabels', "{0} ↔ {1}", this.originalName, this.name); + } + + isDirty() { + if (!this._textModel) { + return !!this._defaultDirtyState; + } + return this._textModel.object.isDirty(); + } + + isUntitled(): boolean { + return this._textModel?.object.isUntitled() || false; + } + + isReadonly() { + return false; + } + + isSaving(): boolean { + if (this.isUntitled()) { + return false; // untitled is never saving automatically + } + + if (!this.isDirty()) { + return false; // the editor needs to be dirty for being saved + } + + if (this._filesConfigurationService.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY) { + return true; // a short auto save is configured, treat this as being saved + } + + return false; + } + + async save(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (this._textModel) { + + if (this.isUntitled()) { + return this.saveAs(group, options); + } else { + await this._textModel.object.save(); + } + + return this; + } + + return undefined; + } + + async saveAs(group: GroupIdentifier, options?: ISaveOptions): Promise { + if (!this._textModel || !this.viewType) { + return undefined; + } + + const provider = this._notebookService.getContributedNotebookProvider(this.viewType!); + + if (!provider) { + return undefined; + } + + const dialogPath = this._textModel.object.resource; + const target = await this._fileDialogService.pickFileToSave(dialogPath, options?.availableFileSystems); + if (!target) { + return undefined; // save cancelled + } + + if (!provider.matches(target)) { + const patterns = provider.selector.map(pattern => { + if (pattern.excludeFileNamePattern) { + return `${pattern.filenamePattern} (exclude: ${pattern.excludeFileNamePattern})`; + } + + return pattern.filenamePattern; + }).join(', '); + throw new Error(`File name ${target} is not supported by ${provider.providerDisplayName}. + +Please make sure the file name matches following patterns: +${patterns} +`); + } + + if (!await this._textModel.object.saveAs(target)) { + return undefined; + } + + return this._move(group, target)?.editor; + } + + // called when users rename a notebook document + rename(group: GroupIdentifier, target: URI): IMoveResult | undefined { + if (this._textModel) { + const contributedNotebookProviders = this._notebookService.getContributedNotebookProviders(target); + + if (contributedNotebookProviders.find(provider => provider.id === this._textModel!.object.viewType)) { + return this._move(group, target); + } + } + return undefined; + } + + private _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { + return undefined; + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + if (this._textModel && this._textModel.object.isDirty()) { + await this._textModel.object.revert(options); + } + + return; + } + + async resolve(editorId?: string): Promise { + if (!await this._notebookService.canResolve(this.viewType!)) { + return null; + } + + if (!this._textModel) { + this._textModel = await this._notebookModelResolverService.resolve(this.resource, this.viewType!, editorId); + this._originalTextModel = await this._notebookModelResolverService.resolve(this.originalResource, this.viewType!, editorId); + } + + return new NotebookDiffEditorModel(this._originalTextModel!.object as NotebookEditorModel, this._textModel.object as NotebookEditorModel); + } + + matches(otherInput: unknown): boolean { + if (this === otherInput) { + return true; + } + if (otherInput instanceof NotebookDiffEditorInput) { + return this.viewType === otherInput.viewType + && isEqual(this.resource, otherInput.resource); + } + return false; + } + + dispose() { + if (this._textModel) { + this._textModel.dispose(); + this._textModel = null; + } + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index a954e866d9a..a60746ceea9 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -15,19 +15,20 @@ import { INotificationService, Severity } from 'vs/platform/notification/common/ import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditorInput, IEditorMemento } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorInput, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { NotebookEditorOptions, NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; import { IBorrowValue, INotebookEditorWidgetService } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidgetService'; import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; import { IEditorGroup, IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditor extends BaseEditor { +export class NotebookEditor extends EditorPane { static readonly ID: string = 'workbench.editor.notebook'; private readonly _editorMemento: IEditorMemento; @@ -73,7 +74,7 @@ export class NotebookEditor extends BaseEditor { get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -125,12 +126,12 @@ export class NotebookEditor extends BaseEditor { this._widget.value?.focus(); } - async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const group = this.group!; this._saveEditorViewState(this.input); - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); // Check for cancellation if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts index 752a81728e1..01ea12b11a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorExtensions.ts @@ -3,16 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookEditor, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BrandedService } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookEditor, INotebookEditorContribution, INotebookEditorContributionCtor, INotebookEditorContributionDescription } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -export type INotebookEditorContributionCtor = IConstructorSignature1; - - -export interface INotebookEditorContributionDescription { - id: string; - ctor: INotebookEditorContributionCtor; -} class EditorContributionRegistry { public static readonly INSTANCE = new EditorContributionRegistry(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 669fc06564a..7bdc292f3bf 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -20,24 +20,23 @@ import { IEditor } from 'vs/editor/common/editorCommon'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, errorForeground, transparent, listFocusBackground, listInactiveSelectionBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, errorForeground, transparent, listFocusBackground, listInactiveSelectionBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground, diffRemoved, diffInserted } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; -import { CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED, INotebookDeltaDecoration } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorMemento } from 'vs/workbench/common/editor'; +import { CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, SCROLLABLE_ELEMENT_PADDING_TOP, BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, BOTTOM_CELL_TOOLBAR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS, NOTEBOOK_OUTPUT_FOCUSED, INotebookDeltaDecoration, NotebookEditorOptions, INotebookEditorCreationOptions, INotebookEditorContributionDescription } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, ListTopCellToolbar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto, INotebookKernelInfo2, NotebookRunState, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto, INotebookKernelInfo2, NotebookRunState, NotebookCellRunState, IInsetRenderOutput, CellToolbarLocKey, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -51,6 +50,8 @@ import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/deb import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { notebookKernelProviderAssociationsSettingId, NotebookKernelProviderAssociations } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; +import { ScrollEvent } from 'vs/base/common/scrollable'; +import { editorGutterModifiedBackground } from 'vs/workbench/contrib/scm/browser/dirtydiffDecorator'; import { IListContextMenuEvent } from 'vs/base/browser/ui/list/list'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IMenuService, MenuId } from 'vs/platform/actions/common/actions'; @@ -58,25 +59,10 @@ import { IAction, Separator } from 'vs/base/common/actions'; import { isMacintosh, isNative } from 'vs/base/common/platform'; import { getTitleBarStyle } from 'vs/platform/windows/common/windows'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { ScrollEvent } from 'vs/base/common/scrollable'; +import { CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; const $ = DOM.$; -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } -} - const NotebookEditorActiveKernelCache = 'workbench.editor.notebook.activeKernel'; export class NotebookEditorWidget extends Disposable implements INotebookEditor { @@ -113,6 +99,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private readonly _activeKernelMemento: Memento; private readonly _onDidFocusEmitter = this._register(new Emitter()); public readonly onDidFocus = this._onDidFocusEmitter.event; + private readonly _onWillScroll = this._register(new Emitter()); + public readonly onWillScroll: Event = this._onWillScroll.event; + private readonly _onWillDispose = this._register(new Emitter()); + public readonly onWillDispose: Event = this._onWillDispose.event; + + set scrollTop(top: number) { + if (this._list) { + this._list.scrollTop = top; + } + } + private _cellContextKeyManager: CellContextKeyManager | null = null; private _isVisible = false; private readonly _uuid = generateUuid(); @@ -217,7 +214,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._cursorNavigationMode = v; } + readonly isEmbedded: boolean; + constructor( + readonly creationOptions: INotebookEditorCreationOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @INotebookService private notebookService: INotebookService, @@ -229,6 +229,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor @IMenuService private readonly menuService: IMenuService, ) { super(); + this.isEmbedded = creationOptions.isEmbedded || false; this._memento = new Memento(NotebookEditorWidget.ID, storageService); this._activeKernelMemento = new Memento(NotebookEditorActiveKernelCache, storageService); @@ -243,6 +244,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.layout(this._dimension); } } + + if (e.affectsConfiguration(CellToolbarLocKey) || e.affectsConfiguration(ShowCellStatusbarKey)) { + this._updateForNotebookConfiguration(); + } }); this.notebookService.addNotebookEditor(this); @@ -281,6 +286,24 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return true; } + private _updateForNotebookConfiguration() { + if (!this._overlayContainer) { + return; + } + + const cellToolbarLocation = this.configurationService.getValue(CellToolbarLocKey); + this._overlayContainer.classList.remove('cell-title-toolbar-left'); + this._overlayContainer.classList.remove('cell-title-toolbar-right'); + this._overlayContainer.classList.remove('cell-title-toolbar-hidden'); + + if (cellToolbarLocation === 'left' || cellToolbarLocation === 'right' || cellToolbarLocation === 'hidden') { + this._overlayContainer.classList.add(`cell-title-toolbar-${cellToolbarLocation}`); + } + + const showCellStatusBar = this.configurationService.getValue(ShowCellStatusbarKey); + this._overlayContainer.classList.toggle('cell-statusbar-hidden', !showCellStatusBar); + } + updateEditorFocus() { // Note - focus going to the webview will fire 'blur', but the webview element will be // a descendent of the notebook editor root. @@ -354,7 +377,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.contextKeyService); this._notebookHasMultipleKernels.set(false); - const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + let contributions: INotebookEditorContributionDescription[]; + if (Array.isArray(this.creationOptions.contributions)) { + contributions = this.creationOptions.contributions; + } else { + contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + } for (const desc of contributions) { try { @@ -364,6 +392,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor onUnexpectedError(err); } } + + this._updateForNotebookConfiguration(); } private _generateFontInfo(): void { @@ -396,6 +426,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list = this.instantiationService.createInstance( NotebookCellList, 'NotebookCellList', + this._overlayContainer, this._body, this.instantiationService.createInstance(NotebookCellListDelegate), renderers, @@ -854,10 +885,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } })); - if (this.viewModel && this.viewModel!.renderers.size) { - this._webview?.updateRendererPreloads(this.viewModel!.renderers); - } - this._webviewResolved = true; resolve(this._webview!); @@ -903,12 +930,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - if (this.viewModel.renderers.size) { - await this._resolveWebview(); - this._webview?.updateRendererPreloads(this.viewModel.renderers); - } - this._localStore.add(this._list!.onWillScroll(e => { + this._onWillScroll.fire(e); if (!this._webviewResolved) { return; } @@ -1242,7 +1265,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor const insertIndex = cell ? (direction === 'above' ? index : nextIndex) : index; - const newCell = this._notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, undefined, true); + const newCell = this._notebookViewModel!.createCell(insertIndex, initialText, language, type, undefined, true); return newCell as CellViewModel; } @@ -1580,23 +1603,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this._list?.triggerScrollFromMouseWheelEvent(event); } - async createInset(cell: CodeCellViewModel, output: IProcessedOutput, shadowContent: string, offset: number) { + async createInset(cell: CodeCellViewModel, output: IInsetRenderOutput, offset: number) { if (!this._webview) { return; } await this._resolveWebview(); - const preloads = this._notebookViewModel!.renderers; - - if (!this._webview!.insetMapping.has(output)) { + if (!this._webview!.insetMapping.has(output.source)) { const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; - await this._webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); + await this._webview!.createInset(cell, output, cellTop, offset); } else { const cellTop = this._list?.getAbsoluteTopOfElement(cell) || 0; const scrollTop = this._list?.scrollTop || 0; - this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell: cell, output: output, cellTop: cellTop }]); + this._webview!.updateViewScrollTop(-scrollTop, true, [{ cell, output: output.source, cellTop }]); } } @@ -1656,6 +1677,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor dispose() { this._isDisposed = true; + this._onWillDispose.fire(); // dispose webview first this._webview?.dispose(); @@ -1784,19 +1806,12 @@ export const cellSymbolHighlight = registerColor('notebook.symbolHighlightBackgr }, nls.localize('notebook.symbolHighlightBackground', "Background color of highlighted cell")); registerThemingParticipant((theme, collector) => { - collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element, + .notebookOverlay > .cell-list-container > .notebook-gutter > .monaco-list > .monaco-scrollable-element { padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; box-sizing: border-box; }`); - // const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); - const color = theme.getColor(editorBackground); - if (color) { - collector.addRule(`.notebookOverlay .cell .monaco-editor-background, - .notebookOverlay .cell .margin-view-overlays, - .notebookOverlay .cell .cell-statusbar-container { background: ${color}; }`); - collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); - } const link = theme.getColor(textLinkForeground); if (link) { collector.addRule(`.notebookOverlay .output a, @@ -1833,7 +1848,11 @@ registerThemingParticipant((theme, collector) => { const editorBackgroundColor = theme.getColor(editorBackground); if (editorBackgroundColor) { - collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .cell .monaco-editor-background, + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${editorBackgroundColor} !important; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-title-toolbar { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .action-item { background-color: ${editorBackgroundColor} }`); @@ -1909,7 +1928,8 @@ registerThemingParticipant((theme, collector) => { const cellStatusBarHoverBg = theme.getColor(cellStatusBarItemHover); if (cellStatusBarHoverBg) { - collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: ${cellStatusBarHoverBg}; }`); + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover, + .monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-item.cell-status-item-has-command:hover { background-color: ${cellStatusBarHoverBg}; }`); } const cellInsertionIndicatorColor = theme.getColor(cellInsertionIndicator); @@ -1935,18 +1955,58 @@ registerThemingParticipant((theme, collector) => { collector.addRule(` .notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .scrollbar > .slider.active:before { content: ""; width: 100%; height: 100%; position: absolute; background: ${scrollbarSliderActiveBackgroundColor}; } `); /* hack to not have cells see through scroller */ } + // case ChangeType.Modify: return theme.getColor(editorGutterModifiedBackground); + // case ChangeType.Add: return theme.getColor(editorGutterAddedBackground); + // case ChangeType.Delete: return theme.getColor(editorGutterDeletedBackground); + // diff + + const modifiedBackground = theme.getColor(editorGutterModifiedBackground); + if (modifiedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-modified .cell-focus-indicator { + background-color: ${modifiedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-modified { + background-color: ${modifiedBackground} !important; + }`); + } + + const addedBackground = theme.getColor(diffInserted); + if (addedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-added .cell-focus-indicator { + background-color: ${addedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-added { + background-color: ${addedBackground} !important; + }`); + } + const deletedBackground = theme.getColor(diffRemoved); + if (deletedBackground) { + collector.addRule(` + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .nb-cell-deleted .cell-focus-indicator { + background-color: ${deletedBackground} !important; + } + + .monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .nb-cell-deleted { + background-color: ${deletedBackground} !important; + }`); + } + // Cell Margin collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell { margin: 0px ${CELL_MARGIN * 2}px 0px ${CELL_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.code { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .run-button-container { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .cell-inner-container { padding-top: ${CELL_TOP_MARGIN}px; }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .markdown-cell-row > .cell-inner-container { padding-bottom: ${CELL_BOTTOM_MARGIN}px; }`); collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .output { width: calc(100% - ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER + (CELL_MARGIN * 2)}px); }`); collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.notebookOverlay .cell .run-button-container { width: 20px; margin: 0px ${Math.floor(CELL_RUN_GUTTER - 20) / 2}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-top { height: ${CELL_TOP_MARGIN}px; }`); - collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-focus-indicator-side { bottom: ${BOTTOM_CELL_TOOLBAR_GAP}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-focus-indicator-left, .notebookOverlay .monaco-list .monaco-list-row.code-cell-row .cell-drag-handle { width: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; }`); collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row.markdown-cell-row .cell-focus-indicator-left { width: ${CODE_CELL_LEFT_MARGIN}px; }`); @@ -1956,4 +2016,6 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-collapsed-part { margin-left: ${CODE_CELL_LEFT_MARGIN + CELL_RUN_GUTTER}px; height: ${COLLAPSED_INDICATOR_HEIGHT}px; }`); collector.addRule(`.notebookOverlay .cell-list-top-cell-toolbar-container { top: -${SCROLLABLE_ELEMENT_PADDING_TOP}px }`); + + collector.addRule(`.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { height: ${BOTTOM_CELL_TOOLBAR_HEIGHT}px }`); }); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts index def8c4e957a..783b82070c7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidgetService.ts @@ -126,7 +126,7 @@ class NotebookEditorWidgetService implements INotebookEditorWidgetService { if (!value) { // NEW widget const instantiationService = accessor.get(IInstantiationService); - const widget = instantiationService.createInstance(NotebookEditorWidget); + const widget = instantiationService.createInstance(NotebookEditorWidget, { isEmbedded: false }); widget.createEditor(); const token = this._tokenPool++; value = { widget, token }; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts deleted file mode 100644 index af6c59fc4ed..00000000000 --- a/src/vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer.ts +++ /dev/null @@ -1,49 +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, UriComponents } from 'vs/base/common/uri'; -import { INotebookRendererInfo, IOutputRenderResponse, IOutputRenderRequest } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { joinPath } from 'vs/base/common/resources'; - -/** - * A 'stub' output renderer used when the contribution has an `entrypoint` - * property. Include the entrypoint as its reload and renders an empty string. - */ -export class PureNotebookOutputRenderer implements INotebookRendererInfo { - - public readonly extensionId: ExtensionIdentifier; - public readonly extensionLocation: URI; - public readonly preloads: URI[]; - - - constructor(public readonly id: string, public readonly displayName: string, extension: IExtensionDescription, entrypoint: string) { - this.extensionId = extension.identifier; - this.extensionLocation = extension.extensionLocation; - this.preloads = [joinPath(extension.extensionLocation, entrypoint)]; - } - - public render(uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return this.render2(uri, request); - } - - public render2(_uri: URI, request: IOutputRenderRequest): Promise | undefined> { - return Promise.resolve({ - items: request.items.map(cellInfo => ({ - key: cellInfo.key, - outputs: cellInfo.outputs.map(output => ({ - index: output.index, - outputId: output.outputId, - mimeType: output.mimeType, - handlerId: this.id, - // todo@connor4312: temp approach exploring this API: - transformedOutput: `` - })) - })) - }); - } -} diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index a45e3a397c0..bff66c05e13 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -3,43 +3,38 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; -import { Disposable, IDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { URI, UriComponents } from 'vs/base/common/uri'; -import { notebookProviderExtensionPoint, notebookRendererExtensionPoint, INotebookEditorContribution } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; -import { NotebookProviderInfo, NotebookEditorDescriptor } from 'vs/workbench/contrib/notebook/common/notebookProvider'; -import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; -import { Emitter, Event } from 'vs/base/common/event'; -import { INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, CellOutputKind, ITransformedDisplayOutputDto, IDisplayOutput, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes, IOrderedMimeType, mimeTypeSupportedByCore, IOutputRenderRequestOutputInfo, IOutputRenderRequestCellInfo, NotebookCellOutputsSplice, ICellEditOperation, CellEditType, ICellInsertEdit, IOutputRenderResponse, IProcessedOutput, BUILTIN_RENDERER_ID, NotebookEditorPriority, INotebookKernelProvider, notebookDocumentFilterMatch, INotebookKernelInfo2, CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; -import { Iterable } from 'vs/base/common/iterator'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { IEditorService, ICustomEditorViewTypesHandler, ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import * as glob from 'vs/base/common/glob'; -import { basename } from 'vs/base/common/path'; -import { getActiveNotebookEditor, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; -import { Memento } from 'vs/workbench/common/memento'; -import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; -import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { generateUuid } from 'vs/base/common/uuid'; import { flatten } from 'vs/base/common/arrays'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { NotebookKernelProviderAssociationRegistry, updateNotebookKernelProvideAssociationSchema, NotebookViewTypesExtensionRegistry } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; -import { PureNotebookOutputRenderer } from 'vs/workbench/contrib/notebook/browser/notebookPureOutputRenderer'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Emitter, Event } from 'vs/base/common/event'; +import * as glob from 'vs/base/common/glob'; +import { Iterable } from 'vs/base/common/iterator'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ResourceMap } from 'vs/base/common/map'; +import { basename } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; import { RedoCommand, UndoCommand } from 'vs/editor/browser/editorExtensions'; import { CopyAction, CutAction, PasteAction } from 'vs/editor/contrib/clipboard/clipboard'; +import * as nls from 'vs/nls'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; +import { Memento } from 'vs/workbench/common/memento'; +import { INotebookEditorContribution, notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; +import { getActiveNotebookEditor, INotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookKernelProviderAssociationRegistry, NotebookViewTypesExtensionRegistry, updateNotebookKernelProvideAssociationSchema } from 'vs/workbench/contrib/notebook/browser/notebookKernelAssociation'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; - -function MODEL_ID(resource: URI): string { - return resource.toString(); -} +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, BUILTIN_RENDERER_ID, CellEditType, CellOutputKind, CellUri, DisplayOrderKey, ICellEditOperation, IDisplayOutput, INotebookKernelInfo, INotebookKernelInfo2, INotebookKernelProvider, INotebookRendererInfo, INotebookTextModel, IOrderedMimeType, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellOutputsSplice, notebookDocumentFilterMatch, NotebookEditorPriority, NOTEBOOK_DISPLAY_ORDER, sortMimeTypes } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookOutputRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookOutputRenderer'; +import { NotebookEditorDescriptor, NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; +import { IMainNotebookController, INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { ICustomEditorInfo, ICustomEditorViewTypesHandler, IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; export class NotebookKernelProviderInfoStore extends Disposable { private readonly _notebookKernelProviders: INotebookKernelProvider[] = []; @@ -234,12 +229,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu declare readonly _serviceBrand: undefined; static mainthreadNotebookDocumentHandle: number = 0; private readonly _notebookProviders = new Map(); - private readonly _notebookRenderers = new Map(); private readonly _notebookKernels = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore; notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); notebookKernelProviderInfoStore: NotebookKernelProviderInfoStore = new NotebookKernelProviderInfoStore(); - private readonly _models = new Map(); + private readonly _models = new ResourceMap(); private _onDidChangeActiveEditor = new Emitter(); onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; private _activeEditorDisposables = new DisposableStore(); @@ -291,25 +285,32 @@ export class NotebookService extends Disposable implements INotebookService, ICu for (const extension of renderers) { for (const notebookContribution of extension.value) { + if (!notebookContribution.entrypoint) { // avoid crashing + console.error(`Cannot register renderer for ${extension.description.identifier.value} since it did not have an entrypoint. This is now required: https://github.com/microsoft/vscode/issues/102644`); + continue; + } + + const id = notebookContribution.id ?? notebookContribution.viewType; + if (!id) { + console.error(`Notebook renderer from ${extension.description.identifier.value} is missing an 'id'`); + continue; + } + this.notebookRenderersInfoStore.add(new NotebookOutputRendererInfo({ - id: notebookContribution.viewType, + id, + extension: extension.description, + entrypoint: notebookContribution.entrypoint, displayName: notebookContribution.displayName, mimeTypes: notebookContribution.mimeTypes || [], })); - - if (notebookContribution.entrypoint) { - this._notebookRenderers.set(notebookContribution.viewType, new PureNotebookOutputRenderer(notebookContribution.viewType, notebookContribution.displayName, extension.description, notebookContribution.entrypoint)); - } } } - - // console.log(this.notebookRenderersInfoStore); }); this._editorService.registerCustomEditorViewTypesHandler('Notebook', this); const updateOrder = () => { - const userOrder = this._configurationService.getValue('notebook.displayOrder'); + const userOrder = this._configurationService.getValue(DisplayOrderKey); this._displayOrder = { defaultOrder: this._accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, userOrder: userOrder @@ -319,7 +320,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu updateOrder(); this._register(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) { + if (e.affectedKeys.indexOf(DisplayOrderKey) >= 0) { updateOrder(); } })); @@ -342,7 +343,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._register(UndoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { - editor?.viewModel.undo(); + editor?.viewModel.undo().then(cellResources => { + if (cellResources?.length) { + editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] } })); + } + }); return true; } @@ -352,7 +357,11 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._register(RedoCommand.addImplementation(PRIORITY, () => { const { editor } = getContext(); if (editor?.viewModel) { - editor?.viewModel.redo(); + editor?.viewModel.redo().then(cellResources => { + if (cellResources?.length) { + editor?.setOptions(new NotebookEditorOptions({ cellOptions: { resource: cellResources[0] } })); + } + }); return true; } @@ -529,6 +538,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu // notebook providers/kernels/renderers might use `*` as activation event. await this._extensionService.activateByEvent(`*`); // this awaits full activation of all matching extensions + await this._extensionService.activateByEvent(`onNotebook:${viewType}`); + + // TODO@jrieken deprecated, remove this await this._extensionService.activateByEvent(`onNotebookEditor:${viewType}`); } return this._notebookProviders.has(viewType); @@ -545,19 +557,6 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeViewTypes.fire(); } - registerNotebookRenderer(id: string, renderer: INotebookRendererInfo) { - this._notebookRenderers.set(id, renderer); - const staticInfo = this.notebookRenderersInfoStore.get(id); - - if (staticInfo) { - - } - } - - unregisterNotebookRenderer(id: string) { - this._notebookRenderers.delete(id); - } - registerNotebookKernel(notebook: INotebookKernelInfo): void { this._notebookKernels.set(notebook.id, notebook); this._onDidChangeKernels.fire(); @@ -656,23 +655,22 @@ export class NotebookService extends Disposable implements INotebookService, ICu } getRendererInfo(id: string): INotebookRendererInfo | undefined { - const renderer = this._notebookRenderers.get(id); - - return renderer; + return this.notebookRenderersInfoStore.get(id); } async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string, backupId?: string): Promise { + + await this.canResolve(viewType); + const provider = this._notebookProviders.get(viewType); if (!provider) { - return undefined; + throw new Error(`CANNOT load notebook, no provider for '${viewType}'`); } - const modelId = MODEL_ID(uri); - let notebookModel: NotebookTextModel | undefined = undefined; - if (this._models.has(modelId)) { + if (this._models.has(uri)) { // the model already exists - notebookModel = this._models.get(modelId)!.model; + notebookModel = this._models.get(uri)!.model; if (forceReload) { await provider.controller.reloadNotebook(notebookModel); } @@ -693,7 +691,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu (model) => this._onWillDisposeDocument(model), ); - this._models.set(modelId, modelData); + this._models.set(uri, modelData); this._onNotebookDocumentAdd.fire([notebookModel!.uri]); // after the document is added to the store and sent to ext host, we transform the ouputs await this.transformTextModelOutputs(notebookModel!); @@ -706,60 +704,14 @@ export class NotebookService extends Disposable implements INotebookService, ICu } getNotebookTextModel(uri: URI): NotebookTextModel | undefined { - const modelId = MODEL_ID(uri); - - return this._models.get(modelId)?.model; + return this._models.get(uri)?.model; } - private async _fillInTransformedOutputs( - renderers: Set, - requestItems: IOutputRenderRequestCellInfo[], - renderFunc: (rendererId: string, items: IOutputRenderRequestCellInfo[]) => Promise | undefined>, - lookUp: (key: T) => { outputs: IProcessedOutput[] } - ) { - for (const id of renderers) { - const requestsPerRenderer: IOutputRenderRequestCellInfo[] = requestItems.map(req => { - return { - key: req.key, - outputs: req.outputs.filter(output => output.handlerId === id) - }; - }); - - const response = await renderFunc(id, requestsPerRenderer); - - // mix the response with existing outputs, which will replace the picked transformed mimetype with resolved result - if (response) { - response.items.forEach(cellInfo => { - const cell = lookUp(cellInfo.key)!; - cellInfo.outputs.forEach(outputInfo => { - const output = cell.outputs[outputInfo.index]; - if (output.outputKind === CellOutputKind.Rich && output.orderedMimeTypes && output.orderedMimeTypes.length) { - output.orderedMimeTypes[0] = { - mimeType: outputInfo.mimeType, - isResolved: true, - rendererId: outputInfo.handlerId, - output: outputInfo.transformedOutput - }; - } - }); - }); - } - } - } - - async transformTextModelOutputs(textModel: NotebookTextModel) { - const renderers = new Set(); - - const cellMapping: Map = new Map(); - - const requestItems: IOutputRenderRequestCellInfo[] = []; + private async transformTextModelOutputs(textModel: NotebookTextModel) { for (let i = 0; i < textModel.cells.length; i++) { const cell = textModel.cells[i]; - cellMapping.set(cell.uri.fragment, cell); - const outputs = cell.outputs; - const outputRequest: IOutputRenderRequestOutputInfo[] = []; - outputs.forEach((output, index) => { + cell.outputs.forEach((output) => { if (output.outputKind === CellOutputKind.Rich) { // TODO no string[] casting const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); @@ -767,125 +719,53 @@ export class NotebookService extends Disposable implements INotebookService, ICu const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; output.pickedMimeTypeIndex = pickedMimeTypeIndex; output.orderedMimeTypes = orderedMimeTypes; - - if (orderedMimeTypes[pickedMimeTypeIndex!].rendererId && orderedMimeTypes[pickedMimeTypeIndex].rendererId !== BUILTIN_RENDERER_ID) { - outputRequest.push({ index, handlerId: orderedMimeTypes[pickedMimeTypeIndex].rendererId!, mimeType: orderedMimeTypes[pickedMimeTypeIndex].mimeType, outputId: output.outputId }); - renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); - } } }); - - requestItems.push({ key: cell.uri, outputs: outputRequest }); } - - await this._fillInTransformedOutputs(renderers, requestItems, async (rendererId, items) => { - return await this._notebookRenderers.get(rendererId)?.render(textModel.uri, { items: items }); - }, (key: UriComponents) => { return cellMapping.get(URI.revive(key).fragment)!; }); - - textModel.updateRenderers([...renderers]); } - async transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]) { - const renderers = new Set(); - const requestItems: IOutputRenderRequestCellInfo<[number, number]>[] = []; - - edits.forEach((edit, editIndex) => { - if (edit.editType === CellEditType.Insert) { - edit.cells.forEach((cell, cellIndex) => { + transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]) { + edits.forEach((edit) => { + if (edit.editType === CellEditType.Replace) { + edit.cells.forEach((cell) => { const outputs = cell.outputs; - const outputRequest: IOutputRenderRequestOutputInfo[] = []; - outputs.map((output, index) => { + outputs.map((output) => { if (output.outputKind === CellOutputKind.Rich) { const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); const orderedMimeTypes = ret.orderedMimeTypes!; const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; output.pickedMimeTypeIndex = pickedMimeTypeIndex; output.orderedMimeTypes = orderedMimeTypes; - - if (orderedMimeTypes[pickedMimeTypeIndex!].rendererId && orderedMimeTypes[pickedMimeTypeIndex].rendererId !== BUILTIN_RENDERER_ID) { - outputRequest.push({ index, handlerId: orderedMimeTypes[pickedMimeTypeIndex].rendererId!, mimeType: orderedMimeTypes[pickedMimeTypeIndex].mimeType, output: output, outputId: output.outputId }); - renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); - } } }); - - requestItems.push({ key: [editIndex, cellIndex], outputs: outputRequest }); + }); + } else if (edit.editType === CellEditType.Output) { + edit.outputs.map((output) => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); + const orderedMimeTypes = ret.orderedMimeTypes!; + const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; + output.pickedMimeTypeIndex = pickedMimeTypeIndex; + output.orderedMimeTypes = orderedMimeTypes; + } }); } }); - - await this._fillInTransformedOutputs<[number, number]>(renderers, requestItems, async (rendererId, items) => { - return await this._notebookRenderers.get(rendererId)?.render2<[number, number]>(textModel.uri, { items: items }); - }, (key: [number, number]) => { - return (edits[key[0]] as ICellInsertEdit).cells[key[1]]; - }); - - textModel.updateRenderers([...renderers]); } - async transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]) { - const renderers = new Set(); - const requestItems: IOutputRenderRequestCellInfo[] = []; - - splices.forEach((splice, spliceIndex) => { + transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]) { + splices.forEach((splice) => { const outputs = splice[2]; - const outputRequest: IOutputRenderRequestOutputInfo[] = []; - outputs.map((output, index) => { + outputs.map((output) => { if (output.outputKind === CellOutputKind.Rich) { const ret = this._transformMimeTypes(output, output.outputId, textModel.metadata.displayOrder as string[] || []); const orderedMimeTypes = ret.orderedMimeTypes!; const pickedMimeTypeIndex = ret.pickedMimeTypeIndex!; output.pickedMimeTypeIndex = pickedMimeTypeIndex; output.orderedMimeTypes = orderedMimeTypes; - - if (orderedMimeTypes[pickedMimeTypeIndex!].rendererId && orderedMimeTypes[pickedMimeTypeIndex].rendererId !== BUILTIN_RENDERER_ID) { - outputRequest.push({ index, handlerId: orderedMimeTypes[pickedMimeTypeIndex].rendererId!, mimeType: orderedMimeTypes[pickedMimeTypeIndex].mimeType, output: output, outputId: output.outputId }); - renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); - } } }); - requestItems.push({ key: spliceIndex, outputs: outputRequest }); }); - - await this._fillInTransformedOutputs(renderers, requestItems, async (rendererId, items) => { - return await this._notebookRenderers.get(rendererId)?.render2(textModel.uri, { items: items }); - }, (key: number) => { - return { outputs: splices[key][2] }; - }); - - textModel.updateRenderers([...renderers]); - } - - async transformSingleOutput(textModel: NotebookTextModel, output: IProcessedOutput, rendererId: string, mimeType: string): Promise { - const items = [ - { - key: 0, - outputs: [ - { - index: 0, - outputId: generateUuid(), - handlerId: rendererId, - mimeType: mimeType, - output: output - } - ] - } - ]; - const response = await this._notebookRenderers.get(rendererId)?.render2(textModel.uri, { items: items }); - - if (response) { - textModel.updateRenderers([rendererId]); - const outputInfo = response.items[0].outputs[0]; - - return { - mimeType: outputInfo.mimeType, - isResolved: true, - rendererId: outputInfo.handlerId, - output: outputInfo.transformedOutput - }; - } - - return; } private _transformMimeTypes(output: IDisplayOutput, outputId: string, documentDisplayOrder: string[]): ITransformedDisplayOutputDto { @@ -903,14 +783,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: handler.id, }); for (let i = 1; i < handlers.length; i++) { orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: handlers[i].id }); } @@ -918,14 +796,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu if (mimeTypeSupportedByCore(mimeType)) { orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: BUILTIN_RENDERER_ID }); } } else { orderMimeTypes.push({ mimeType: mimeType, - isResolved: false, rendererId: BUILTIN_RENDERER_ID }); } @@ -1133,10 +1009,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu } private _onWillDisposeDocument(model: INotebookTextModel): void { - const modelId = MODEL_ID(model.uri); - const modelData = this._models.get(modelId); - this._models.delete(modelId); + const modelData = this._models.get(model.uri); + this._models.delete(model.uri); if (modelData) { // delete editors and documents diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 6a257986a44..76c404fe169 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -64,6 +64,7 @@ export class NotebookCellList extends WorkbenchList implements ID constructor( private listUser: string, + parentContainer: HTMLElement, container: HTMLElement, delegate: IListVirtualDelegate, renderers: IListRenderer[], diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts index 7e988c86142..56114e323a5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/outputRenderer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IProcessedOutput, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, IRenderOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { onUnexpectedError } from 'vs/base/common/errors'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -38,9 +38,7 @@ export class OutputRenderer { contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; container.appendChild(contentNode); - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts index 7998129daa4..9e0c4ba4230 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, IErrorOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IRenderOutput, CellOutputKind, IErrorOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; import { RGBA, Color } from 'vs/base/common/color'; @@ -36,9 +36,7 @@ class ErrorTransform implements IOutputTransformContribution { } container.appendChild(traceback); DOM.addClasses(container, 'error'); - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts index 73a1fd4af7e..76ffcc3f352 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IRenderOutput, CellOutputKind, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import * as DOM from 'vs/base/browser/dom'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -46,10 +46,7 @@ class RichRenderer implements IOutputTransformContribution { const contentNode = document.createElement('p'); contentNode.innerText = `No data could be found for output.`; container.appendChild(contentNode); - - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } if (!preferredMimeType || !this._richMimeTypeRenderers.has(preferredMimeType)) { @@ -68,17 +65,14 @@ class RichRenderer implements IOutputTransformContribution { } container.appendChild(contentNode); - - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } const renderer = this._richMimeTypeRenderers.get(preferredMimeType); return renderer!(output, container); } - renderJSON(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderJSON(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['application/json']; const str = JSON.stringify(data, null, '\t'); @@ -108,12 +102,10 @@ class RichRenderer implements IOutputTransformContribution { container.style.height = `${height + 16}px`; - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderCode(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderCode(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['text/x-javascript']; const str = (isArray(data) ? data.join('') : data) as string; @@ -143,87 +135,81 @@ class RichRenderer implements IOutputTransformContribution { container.style.height = `${height + 16}px`; - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJavaScript(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderJavaScript(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['application/javascript']; const str = isArray(data) ? data.join('') : data; const scriptVal = ``; return { - shadowContent: scriptVal, + type: RenderOutputType.Html, + source: output, + htmlContent: scriptVal, hasDynamicHeight: false }; } - renderHTML(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderHTML(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['text/html']; const str = (isArray(data) ? data.join('') : data) as string; return { - shadowContent: str, + type: RenderOutputType.Html, + source: output, + htmlContent: str, hasDynamicHeight: false }; - } - renderSVG(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderSVG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['image/svg+xml']; const str = (isArray(data) ? data.join('') : data) as string; return { - shadowContent: str, + type: RenderOutputType.Html, + source: output, + htmlContent: str, hasDynamicHeight: false }; } - renderMarkdown(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderMarkdown(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['text/markdown']; const str = (isArray(data) ? data.join('') : data) as string; const mdOutput = document.createElement('div'); mdOutput.appendChild(this._mdRenderer.render({ value: str, isTrusted: true, supportThemeIcons: true }).element); container.appendChild(mdOutput); - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPNG(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderPNG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); image.src = `data:image/png;base64,${output.data['image/png']}`; const display = document.createElement('div'); DOM.addClasses(display, 'display'); display.appendChild(image); container.appendChild(display); - return { - hasDynamicHeight: true - }; - + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderJPEG(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderJPEG(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const image = document.createElement('img'); image.src = `data:image/jpeg;base64,${output.data['image/jpeg']}`; const display = document.createElement('div'); DOM.addClasses(display, 'display'); display.appendChild(image); container.appendChild(display); - return { - hasDynamicHeight: true - }; + return { type: RenderOutputType.None, hasDynamicHeight: true }; } - renderPlainText(output: ITransformedDisplayOutputDto, container: HTMLElement) { + renderPlainText(output: ITransformedDisplayOutputDto, container: HTMLElement): IRenderOutput { const data = output.data['text/plain']; const str = (isArray(data) ? data.join('') : data) as string; const contentNode = DOM.$('.output-plaintext'); contentNode.appendChild(handleANSIOutput(str, this.themeService)); container.appendChild(contentNode); - return { - hasDynamicHeight: false - }; + return { type: RenderOutputType.None, hasDynamicHeight: false }; } dispose(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts index b3d7698c233..65f1826f750 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; -import { IRenderOutput, CellOutputKind, IStreamOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IRenderOutput, CellOutputKind, IStreamOutput, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -18,10 +18,7 @@ class StreamRenderer implements IOutputTransformContribution { const contentNode = DOM.$('.output-stream'); contentNode.innerText = output.text; container.appendChild(contentNode); - return { - hasDynamicHeight: false - }; - + return { type: RenderOutputType.None, hasDynamicHeight: false }; } dispose(): void { 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 7f37bd88b20..3a0eab9dda2 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -16,7 +16,7 @@ import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener' import { CELL_MARGIN, CELL_RUN_GUTTER, CODE_CELL_LEFT_MARGIN, CELL_OUTPUT_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IProcessedOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellOutputKind, IDisplayOutput, IInsetRenderOutput, INotebookRendererInfo, IProcessedOutput, ITransformedDisplayOutputDto, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement, WebviewContentPurpose } from 'vs/workbench/contrib/webview/browser/webview'; import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; @@ -88,12 +88,14 @@ export interface IClearMessage { export interface ICreationRequestMessage { type: 'html'; - content: string; + content: + | { type: RenderOutputType.Html; htmlContent: string } + | { type: RenderOutputType.Extension; output: IDisplayOutput; mimeType: string }; cellId: string; outputId: string; top: number; left: number; - requiredPreloads: IPreloadResource[]; + requiredPreloads: ReadonlyArray; initiallyHidden?: boolean; apiNamespace?: string | undefined; } @@ -200,7 +202,7 @@ export type AnyMessage = FromWebviewMessage | ToWebviewMessage; interface ICachedInset { outputId: string; cell: CodeCellViewModel; - preloads: ReadonlySet; + renderer?: INotebookRendererInfo; cachedCreation: ICreationRequestMessage; } @@ -224,11 +226,11 @@ export class BackLayerWebView extends Disposable { insetMapping: Map = new Map(); hiddenInsetMapping: Set = new Set(); reversedInsetMapping: Map = new Map(); - preloadsCache: Map = new Map(); localResourceRootsCache: URI[] | undefined = undefined; rendererRootsCache: URI[] = []; kernelRootsCache: URI[] = []; private readonly _onMessage = this._register(new Emitter()); + private readonly _preloadsCache = new Set(); public readonly onMessage: Event = this._onMessage.event; private _loaded!: Promise; private _initalized?: Promise; @@ -425,9 +427,17 @@ ${loaderJs} return; } - this.preloadsCache.clear(); + let renderers = new Set(); + for (const inset of this.insetMapping.values()) { + if (inset.renderer) { + renderers.add(inset.renderer); + } + } + + this._preloadsCache.clear(); + this.updateRendererPreloads(renderers); + for (const [output, inset] of this.insetMapping.entries()) { - this.updateRendererPreloads(inset.preloads); this._sendMessageToWebview({ ...inset.cachedCreation, initiallyHidden: this.hiddenInsetMapping.has(output) }); } })); @@ -622,19 +632,18 @@ ${loaderJs} }); } - async createInset(cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number, offset: number, shadowContent: string, preloads: Set) { + async createInset(cell: CodeCellViewModel, content: IInsetRenderOutput, cellTop: number, offset: number) { if (this._disposed) { return; } - const requiredPreloads = await this.updateRendererPreloads(preloads); const initialTop = cellTop + offset; - if (this.insetMapping.has(output)) { - const outputCache = this.insetMapping.get(output); + if (this.insetMapping.has(content.source)) { + const outputCache = this.insetMapping.get(content.source); if (outputCache) { - this.hiddenInsetMapping.delete(output); + this.hiddenInsetMapping.delete(content.source); this._sendMessageToWebview({ type: 'showOutput', cellId: outputCache.cell.id, @@ -645,30 +654,49 @@ ${loaderJs} } } - const outputId = output.outputKind === CellOutputKind.Rich ? output.outputId : UUID.generateUuid(); - let apiNamespace: string | undefined; - if (output.outputKind === CellOutputKind.Rich && output.pickedMimeTypeIndex !== undefined) { - const pickedMimeTypeRenderer = output.orderedMimeTypes?.[output.pickedMimeTypeIndex]; - if (pickedMimeTypeRenderer?.rendererId) { - apiNamespace = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId)?.id; - } + const messageBase = { + type: 'html', + cellId: cell.id, + top: initialTop, + left: 0, + requiredPreloads: [], + } as const; + + let message: ICreationRequestMessage; + let renderer: INotebookRendererInfo | undefined; + if (content.type === RenderOutputType.Extension) { + const output = content.source as ITransformedDisplayOutputDto; + renderer = content.renderer; + message = { + ...messageBase, + outputId: output.outputId, + apiNamespace: content.renderer.id, + requiredPreloads: await this.updateRendererPreloads([content.renderer]), + content: { + type: RenderOutputType.Extension, + mimeType: content.mimeType, + output: { + outputKind: CellOutputKind.Rich, + metadata: output.metadata, + data: output.data, + }, + }, + }; + } else { + message = { + ...messageBase, + outputId: UUID.generateUuid(), + content: { + type: content.type, + htmlContent: content.htmlContent, + } + }; } - const message: ICreationRequestMessage = { - type: 'html', - content: shadowContent, - cellId: cell.id, - apiNamespace, - outputId: outputId, - top: initialTop, - requiredPreloads, - left: 0 - }; - this._sendMessageToWebview(message); - this.insetMapping.set(output, { outputId: outputId, cell: cell, preloads, cachedCreation: message }); - this.hiddenInsetMapping.delete(output); - this.reversedInsetMapping.set(outputId, output); + this.insetMapping.set(content.source, { outputId: message.outputId, cell, renderer, cachedCreation: message }); + this.hiddenInsetMapping.delete(content.source); + this.reversedInsetMapping.set(message.outputId, content.source); } removeInset(output: IProcessedOutput) { @@ -774,9 +802,9 @@ ${loaderJs} }); preloads.forEach(e => { - if (!this.preloadsCache.has(e.toString())) { + if (!this._preloadsCache.has(e.toString())) { resources.push({ uri: e.toString() }); - this.preloadsCache.set(e.toString(), true); + this._preloadsCache.add(e.toString()); } }); @@ -788,7 +816,7 @@ ${loaderJs} this._updatePreloads(resources, 'kernel'); } - async updateRendererPreloads(preloads: ReadonlySet) { + async updateRendererPreloads(renderers: Iterable) { if (this._disposed) { return []; } @@ -798,28 +826,21 @@ ${loaderJs} const requiredPreloads: IPreloadResource[] = []; const resources: IPreloadResource[] = []; const extensionLocations: URI[] = []; - preloads.forEach(preload => { - const rendererInfo = this.notebookService.getRendererInfo(preload); + for (const rendererInfo of renderers) { + const preloads = [rendererInfo.entrypoint, ...rendererInfo.preloads] + .map(preload => asWebviewUri(this.workbenchEnvironmentService, this.id, preload)); + extensionLocations.push(rendererInfo.extensionLocation); - if (rendererInfo) { - const preloadResources = rendererInfo.preloads.map(preloadResource => { - if (this.environmentService.isExtensionDevelopment && (preloadResource.scheme === 'http' || preloadResource.scheme === 'https')) { - return preloadResource; - } - return asWebviewUri(this.workbenchEnvironmentService, this.id, preloadResource); - }); - extensionLocations.push(rendererInfo.extensionLocation); - preloadResources.forEach(e => { - const resource: IPreloadResource = { uri: e.toString() }; - requiredPreloads.push(resource); + preloads.forEach(e => { + const resource: IPreloadResource = { uri: e.toString() }; + requiredPreloads.push(resource); - if (!this.preloadsCache.has(e.toString())) { - resources.push(resource); - this.preloadsCache.set(e.toString(), true); - } - }); - } - }); + if (!this._preloadsCache.has(e.toString())) { + resources.push(resource); + this._preloadsCache.add(e.toString()); + } + }); + } if (!resources.length) { return requiredPreloads; @@ -855,7 +876,7 @@ ${loaderJs} } clearPreloadsCache() { - this.preloadsCache.clear(); + this._preloadsCache.clear(); } dispose() { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts index 69e4ddad204..66ffa090a93 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellActionView.ts @@ -45,7 +45,7 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.primary; + const to = Array.isArray(target) ? target : target.primary; if (to.length > 0) { to.push(new VerticalSeparator()); @@ -55,7 +55,7 @@ function fillInActions(groups: ReadonlyArray<[string, ReadonlyArray(target) ? target : target.secondary; + const to = Array.isArray(target) ? target : target.secondary; if (to.length > 0) { to.push(new Separator()); diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts index f75607d8563..eb5c7a65cb1 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer.ts @@ -10,7 +10,6 @@ import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/lis import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; import { IAction } from 'vs/base/common/actions'; -import { Delayer } from 'vs/base/common/async'; import { renderCodicons } from 'vs/base/common/codicons'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -26,7 +25,6 @@ import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ITextModel } from 'vs/editor/common/model'; import * as modes from 'vs/editor/common/modes'; import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer'; -import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions'; @@ -37,17 +35,20 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; -import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EXPAND_CELL_CONTENT_COMMAND_ID, ICellViewModel, INotebookCellList, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { BOTTOM_CELL_TOOLBAR_GAP, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CancelCellAction, DeleteCellAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; +import { BaseCellRenderTemplate, CellEditState, CodeCellRenderTemplate, EXPAND_CELL_CONTENT_COMMAND_ID, ICellViewModel, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys'; import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus'; +import { CellEditorStatusBar } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets'; import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell'; +import { CodiconActionViewItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents'; +import { CellDragAndDropController, DRAGGING_CLASS } from 'vs/workbench/contrib/notebook/browser/view/renderers/dnd'; import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel'; import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookCellRunState, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView'; const $ = DOM.$; @@ -79,29 +80,9 @@ export class NotebookCellListDelegate implements IListVirtualDelegate { - if (e.affectsConfiguration('editor')) { + if (e.affectsConfiguration('editor') || e.affectsConfiguration(ShowCellStatusbarKey)) { this._value = computeEditorOptions(); this._onDidChange.fire(this.value); } }); const computeEditorOptions = () => { + const showCellStatusBar = configurationService.getValue(ShowCellStatusbarKey); + const editorPadding = { + top: EDITOR_TOP_PADDING, + bottom: showCellStatusBar ? EDITOR_BOTTOM_PADDING : EDITOR_BOTTOM_PADDING_WITHOUT_STATUSBAR + }; + const editorOptions = deepClone(configurationService.getValue('editor', { overrideIdentifier: language })); const computed = { ...editorOptions, - ...CellEditorOptions.fixedEditorOptions + ...CellEditorOptions.fixedEditorOptions, + ...{ padding: editorPadding } }; if (!computed.folding) { @@ -330,7 +318,7 @@ abstract class AbstractCellRenderer { this.addExpandListener(templateData); } - protected commonRenderElement(element: ICellViewModel, index: number, templateData: BaseCellRenderTemplate): void { + protected commonRenderElement(element: ICellViewModel, templateData: BaseCellRenderTemplate): void { if (element.dragging) { templateData.container.classList.add(DRAGGING_CLASS); } else { @@ -345,9 +333,9 @@ abstract class AbstractCellRenderer { } if (templateData.currentRenderedCell.metadata?.inputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { inputCollapsed: false }); } else if (templateData.currentRenderedCell.metadata?.outputCollapsed) { - this.notebookEditor.viewModel!.notebookDocument.changeCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); + this.notebookEditor.viewModel!.notebookDocument.deltaCellMetadata(templateData.currentRenderedCell.handle, { outputCollapsed: false }); } })); } @@ -395,7 +383,12 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const container = DOM.append(rootContainer, DOM.$('.cell-inner-container')); const disposables = new DisposableStore(); const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); - const toolbar = disposables.add(this.createToolbar(container, 'cell-title-toolbar')); + + const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); + const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); + const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar')); + deleteToolbar.setActions([this.instantiationService.createInstance(DeleteCellAction)]); + const focusIndicatorLeft = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); const codeInnerContent = DOM.append(container, $('.cell.code')); @@ -411,7 +404,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container')); const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService)); - const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); + const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService)); const templateData: MarkdownCellRenderTemplate = { @@ -427,11 +420,11 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR disposables, elementDisposables: new DisposableStore(), toolbar, + deleteToolbar, betweenCellToolbar, bottomCellContainer, - statusBarContainer: statusBar.statusBarContainer, - languageStatusBarItem: statusBar.languageStatusBarItem, titleMenu, + statusBar, toJSON: () => { return {}; } }; this.dndController.registerDragHandle(templateData, rootContainer, container, () => this.getDragImage(templateData)); @@ -467,7 +460,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } renderElement(element: MarkdownCellViewModel, index: number, templateData: MarkdownCellRenderTemplate, height: number | undefined): void { - this.commonRenderElement(element, index, templateData); + this.commonRenderElement(element, templateData); templateData.currentRenderedCell = element; templateData.currentEditor = undefined; @@ -491,6 +484,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR $mid: 12 }; templateData.toolbar.context = toolbarContext; + templateData.deleteToolbar.context = toolbarContext; this.setBetweenCellToolbarContext(templateData, element, toolbarContext); @@ -499,7 +493,7 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue))); elementDisposables.add(markdownCell); - templateData.languageStatusBarItem.update(element, this.notebookEditor); + templateData.statusBar.update(toolbarContext); } disposeTemplate(templateData: MarkdownCellRenderTemplate): void { @@ -516,308 +510,6 @@ export class MarkdownCellRenderer extends AbstractCellRenderer implements IListR } } -const DRAGGING_CLASS = 'cell-dragging'; -const GLOBAL_DRAG_CLASS = 'global-drag-active'; - -type DragImageProvider = () => HTMLElement; - -interface CellDragEvent { - browserEvent: DragEvent; - draggedOverCell: ICellViewModel; - cellTop: number; - cellHeight: number; - dragPosRatio: number; -} - -export class CellDragAndDropController extends Disposable { - // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need - // to figure out how to prevent that - private currentDraggedCell: ICellViewModel | undefined; - - private listInsertionIndicator: HTMLElement; - - private list!: INotebookCellList; - - private isScrolling = false; - private scrollingDelayer: Delayer; - - constructor( - private readonly notebookEditor: INotebookEditor, - insertionIndicatorContainer: HTMLElement - ) { - super(); - - this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator')); - - this._register(domEvent(document.body, DOM.EventType.DRAG_START, true)(this.onGlobalDragStart.bind(this))); - this._register(domEvent(document.body, DOM.EventType.DRAG_END, true)(this.onGlobalDragEnd.bind(this))); - - const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { - this._register(DOM.addDisposableListener( - notebookEditor.getDomNode(), - eventType, - e => { - const cellDragEvent = this.toCellDragEvent(e); - if (cellDragEvent) { - handler(cellDragEvent); - } - })); - }; - - addCellDragListener(DOM.EventType.DRAG_OVER, event => { - event.browserEvent.preventDefault(); - this.onCellDragover(event); - }); - addCellDragListener(DOM.EventType.DROP, event => { - event.browserEvent.preventDefault(); - this.onCellDrop(event); - }); - addCellDragListener(DOM.EventType.DRAG_LEAVE, event => { - event.browserEvent.preventDefault(); - this.onCellDragLeave(event); - }); - - this.scrollingDelayer = new Delayer(200); - } - - setList(value: INotebookCellList) { - this.list = value; - - this.list.onWillScroll(e => { - if (!e.scrollTopChanged) { - return; - } - - this.setInsertIndicatorVisibility(false); - this.isScrolling = true; - this.scrollingDelayer.trigger(() => { - this.isScrolling = false; - }); - }); - } - - private setInsertIndicatorVisibility(visible: boolean) { - this.listInsertionIndicator.style.opacity = visible ? '1' : '0'; - } - - private toCellDragEvent(event: DragEvent): CellDragEvent | undefined { - const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top; - const dragOffset = this.list.scrollTop + event.clientY - targetTop; - const draggedOverCell = this.list.elementAt(dragOffset); - if (!draggedOverCell) { - return undefined; - } - - const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); - const cellHeight = this.list.elementHeight(draggedOverCell); - - const dragPosInElement = dragOffset - cellTop; - const dragPosRatio = dragPosInElement / cellHeight; - - return { - browserEvent: event, - draggedOverCell, - cellTop, - cellHeight, - dragPosRatio - }; - } - - clearGlobalDragState() { - this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); - } - - private onGlobalDragStart() { - this.notebookEditor.getDomNode().classList.add(GLOBAL_DRAG_CLASS); - } - - private onGlobalDragEnd() { - this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); - } - - private onCellDragover(event: CellDragEvent): void { - if (!event.browserEvent.dataTransfer) { - return; - } - - if (!this.currentDraggedCell) { - event.browserEvent.dataTransfer.dropEffect = 'none'; - return; - } - - if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { - this.setInsertIndicatorVisibility(false); - return; - } - - const dropDirection = this.getDropInsertDirection(event); - const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_HEIGHT / 2; - if (insertionIndicatorTop >= 0) { - this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; - this.setInsertIndicatorVisibility(true); - } else { - this.setInsertIndicatorVisibility(false); - } - } - - private getDropInsertDirection(event: CellDragEvent): 'above' | 'below' { - return event.dragPosRatio < 0.5 ? 'above' : 'below'; - } - - private onCellDrop(event: CellDragEvent): void { - const draggedCell = this.currentDraggedCell!; - - if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { - return; - } - - let draggedCells: ICellViewModel[] = [draggedCell]; - let draggedCellRange: [number, number] = [this.notebookEditor.viewModel!.getCellIndex(draggedCell), 1]; - - if (draggedCell.cellKind === CellKind.Markdown) { - const currCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); - const nextVisibleCellIndex = this.notebookEditor.viewModel!.getNextVisibleCellIndex(currCellIndex); - - if (nextVisibleCellIndex > currCellIndex + 1) { - // folding ;) - draggedCells = this.notebookEditor.viewModel!.viewCells.slice(currCellIndex, nextVisibleCellIndex); - draggedCellRange = [currCellIndex, nextVisibleCellIndex - currCellIndex]; - } - } - - this.dragCleanup(); - - const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh); - - const dropDirection = this.getDropInsertDirection(event); - const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; - const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_HEIGHT / 2; - const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; - if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { - // Ignore drop, insertion point is off-screen - return; - } - - if (isCopy) { - this.copyCells(draggedCells, event.draggedOverCell, dropDirection); - } else { - const viewModel = this.notebookEditor.viewModel!; - let originalToIdx = viewModel.getCellIndex(event.draggedOverCell); - if (dropDirection === 'below') { - const relativeToIndex = viewModel.getCellIndex(event.draggedOverCell); - const newIdx = viewModel.getNextVisibleCellIndex(relativeToIndex); - originalToIdx = newIdx; - } - - this.notebookEditor.moveCellsToIdx(draggedCellRange[0], draggedCellRange[1], originalToIdx); - } - } - - private onCellDragLeave(event: CellDragEvent): void { - if (!event.browserEvent.relatedTarget || !DOM.isAncestor(event.browserEvent.relatedTarget as HTMLElement, this.notebookEditor.getDomNode())) { - this.setInsertIndicatorVisibility(false); - } - } - - private dragCleanup(): void { - if (this.currentDraggedCell) { - this.currentDraggedCell.dragging = false; - this.currentDraggedCell = undefined; - } - - this.setInsertIndicatorVisibility(false); - } - - registerDragHandle(templateData: BaseCellRenderTemplate, cellRoot: HTMLElement, dragHandle: HTMLElement, dragImageProvider: DragImageProvider): void { - const container = templateData.container; - dragHandle.setAttribute('draggable', 'true'); - - templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { - // Note, templateData may have a different element rendered into it by now - container.classList.remove(DRAGGING_CLASS); - this.dragCleanup(); - })); - - templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_START)(event => { - if (!event.dataTransfer) { - return; - } - - this.currentDraggedCell = templateData.currentRenderedCell!; - this.currentDraggedCell.dragging = true; - - const dragImage = dragImageProvider(); - cellRoot.parentElement!.appendChild(dragImage); - event.dataTransfer.setDragImage(dragImage, 0, 0); - setTimeout(() => cellRoot.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout - - container.classList.add(DRAGGING_CLASS); - })); - } - - private copyCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { - this.notebookEditor.textModel!.pushStackElement('Copy Cells'); - let firstNewCell: ICellViewModel | undefined = undefined; - let firstNewCellState: CellEditState = CellEditState.Preview; - for (let i = 0; i < draggedCells.length; i++) { - const draggedCell = draggedCells[i]; - const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); - - if (newCell && !firstNewCell) { - firstNewCell = newCell; - firstNewCellState = draggedCell.editState; - } - } - - if (firstNewCell) { - this.notebookEditor.focusNotebookCell(firstNewCell, firstNewCellState === CellEditState.Editing ? 'editor' : 'container'); - } - - this.notebookEditor.textModel!.pushStackElement('Copy Cells'); - } -} - -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')); - this.labelElement.tabIndex = 0; - - this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { - this.instantiationService.invokeFunction(accessor => { - new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! }); - }); - })); - this._register(this.cellDisposables = new DisposableStore()); - } - - 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'); - } -} - class EditorTextRenderer { getRichText(editor: ICodeEditor, modelRange: Range): string | null { @@ -913,27 +605,6 @@ class CodeCellDragImageRenderer { } } -class CellEditorStatusBar { - readonly cellStatusMessageContainer: HTMLElement; - readonly cellRunStatusContainer: HTMLElement; - readonly statusBarContainer: HTMLElement; - readonly languageStatusBarItem: CellLanguageStatusBarItem; - readonly durationContainer: HTMLElement; - - constructor( - container: HTMLElement, - @IInstantiationService instantiationService: IInstantiationService - ) { - this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); - const leftStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-left')); - const rightStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-right')); - this.cellRunStatusContainer = DOM.append(leftStatusBarItems, $('.cell-run-status')); - this.durationContainer = DOM.append(leftStatusBarItems, $('.cell-run-duration')); - this.cellStatusMessageContainer = DOM.append(leftStatusBarItems, $('.cell-status-message')); - this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightStatusBarItems); - } -} - export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer { static readonly TEMPLATE_ID = 'code_cell'; @@ -962,15 +633,18 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende const contextKeyService = disposables.add(this.contextKeyServiceProvider(container)); DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top')); - const toolbar = disposables.add(this.createToolbar(container, 'cell-title-toolbar')); + const titleToolbarContainer = DOM.append(container, $('.cell-title-toolbar')); + const toolbar = disposables.add(this.createToolbar(titleToolbarContainer)); + const deleteToolbar = disposables.add(this.createToolbar(titleToolbarContainer, 'cell-delete-toolbar')); + deleteToolbar.setActions([this.instantiationService.createInstance(DeleteCellAction)]); + const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left')); const dragHandle = DOM.append(container, DOM.$('.cell-drag-handle')); const cellContainer = DOM.append(container, $('.cell.code')); - const runButtonContainer = DOM.append(cellContainer, $('.run-button-container')); - const runToolbar = this.createToolbar(runButtonContainer); - disposables.add(runToolbar); + const runButtonContainer = DOM.append(container, $('.run-button-container')); + const runToolbar = disposables.add(this.createToolbar(runButtonContainer)); const executionOrderLabel = DOM.append(runButtonContainer, $('div.execution-count-label')); // create a special context key service that set the inCompositeEditor-contextkey @@ -997,7 +671,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende progressBar.hide(); disposables.add(progressBar); - const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart); + const statusBar = disposables.add(this.instantiationService.createInstance(CellEditorStatusBar, editorPart)); const timer = new TimerRenderer(statusBar.durationContainer); const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService); @@ -1020,15 +694,14 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende contextKeyService, container, cellContainer, - statusBarContainer: statusBar.statusBarContainer, cellRunState, - cellStatusMessageContainer: statusBar.cellStatusMessageContainer, - languageStatusBarItem: statusBar.languageStatusBarItem, progressBar, + statusBar, focusIndicatorLeft: focusIndicator, focusIndicatorRight, focusIndicatorBottom, toolbar, + deleteToolbar, betweenCellToolbar, focusSinkElement, runToolbar, @@ -1068,9 +741,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende private updateForMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata); - DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable); + DOM.toggleClass(templateData.container, 'runnable', !!metadata.runnable); this.updateExecutionOrder(metadata, templateData); - templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; + templateData.statusBar.cellStatusMessageContainer.textContent = metadata?.statusMessage || ''; templateData.cellRunState.renderState(element.metadata?.runState); @@ -1115,9 +788,9 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void { templateData.focusIndicatorLeft.style.height = `${element.layoutInfo.indicatorHeight}px`; templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`; - templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - CELL_BOTTOM_MARGIN}px`; + templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_GAP - CELL_BOTTOM_MARGIN}px`; templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`; - templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT}px`; + templateData.dragHandle.style.height = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_GAP}px`; } renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void { @@ -1132,7 +805,7 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende templateData.container.classList.remove(className); }); - this.commonRenderElement(element, index, templateData); + this.commonRenderElement(element, templateData); templateData.currentRenderedCell = element; @@ -1180,10 +853,11 @@ export class CodeCellRenderer extends AbstractCellRenderer implements IListRende }; templateData.toolbar.context = toolbarContext; templateData.runToolbar.context = toolbarContext; + templateData.deleteToolbar.context = toolbarContext; this.setBetweenCellToolbarContext(templateData, element, toolbarContext); - templateData.languageStatusBarItem.update(element, this.notebookEditor); + templateData.statusBar.update(toolbarContext); } disposeTemplate(templateData: CodeCellRenderTemplate): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts new file mode 100644 index 00000000000..9f86d687c07 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/cellWidgets.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; +import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions'; +import { stripCodicons } from 'vs/base/common/codicons'; +import { toErrorMessage } from 'vs/base/common/errorMessage'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { extUri } from 'vs/base/common/resources'; +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 { INotebookCellStatusBarService } from 'vs/workbench/contrib/notebook/common/notebookCellStatusBarService'; +import { CellKind, CellStatusbarAlignment, INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const $ = DOM.$; + +export class CellEditorStatusBar extends Disposable { + readonly cellStatusMessageContainer: HTMLElement; + readonly cellRunStatusContainer: HTMLElement; + readonly statusBarContainer: HTMLElement; + readonly languageStatusBarItem: CellLanguageStatusBarItem; + readonly durationContainer: HTMLElement; + + private readonly leftContributedItemsContainer: HTMLElement; + private readonly rightContributedItemsContainer: HTMLElement; + private readonly itemsDisposable: DisposableStore; + + private currentContext: INotebookCellActionContext | undefined; + + constructor( + container: HTMLElement, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @INotebookCellStatusBarService private readonly notebookCellStatusBarService: INotebookCellStatusBarService + ) { + super(); + this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container')); + const leftItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-left')); + const rightItemsContainer = DOM.append(this.statusBarContainer, $('.cell-status-right')); + this.cellRunStatusContainer = DOM.append(leftItemsContainer, $('.cell-run-status')); + this.durationContainer = DOM.append(leftItemsContainer, $('.cell-run-duration')); + this.cellStatusMessageContainer = DOM.append(leftItemsContainer, $('.cell-status-message')); + this.leftContributedItemsContainer = DOM.append(leftItemsContainer, $('.cell-contributed-items-left')); + this.rightContributedItemsContainer = DOM.append(rightItemsContainer, $('.cell-contributed-items-right')); + this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightItemsContainer); + + this.itemsDisposable = this._register(new DisposableStore()); + this._register(this.notebookCellStatusBarService.onDidChangeEntriesForCell(e => { + if (this.currentContext && extUri.isEqual(e, this.currentContext.cell.uri)) { + this.updateStatusBarItems(); + } + })); + } + + update(context: INotebookCellActionContext) { + this.currentContext = context; + this.languageStatusBarItem.update(context.cell, context.notebookEditor); + this.updateStatusBarItems(); + } + + layout(width: number): void { + this.statusBarContainer.style.width = `${width}px`; + } + + private updateStatusBarItems() { + if (!this.currentContext) { + return; + } + + this.leftContributedItemsContainer.innerHTML = ''; + this.rightContributedItemsContainer.innerHTML = ''; + this.itemsDisposable.clear(); + + const items = this.notebookCellStatusBarService.getEntries(this.currentContext.cell.uri); + items.sort((itemA, itemB) => { + return (itemB.priority ?? 0) - (itemA.priority ?? 0); + }); + items.forEach(item => { + const itemView = this.itemsDisposable.add(this.instantiationService.createInstance(CellStatusBarItem, this.currentContext!, item)); + if (item.alignment === CellStatusbarAlignment.LEFT) { + this.leftContributedItemsContainer.appendChild(itemView.container); + } else { + this.rightContributedItemsContainer.appendChild(itemView.container); + } + }); + } +} + +class CellStatusBarItem extends Disposable { + + readonly container = $('.cell-status-item'); + + constructor( + private readonly _context: INotebookCellActionContext, + private readonly _itemModel: INotebookCellStatusBarEntry, + @ITelemetryService private readonly telemetryService: ITelemetryService, + @ICommandService private readonly commandService: ICommandService, + @INotificationService private readonly notificationService: INotificationService + ) { + super(); + new CodiconLabel(this.container).text = this._itemModel.text; + + let ariaLabel: string; + let role: string | undefined; + if (this._itemModel.accessibilityInformation) { + ariaLabel = this._itemModel.accessibilityInformation.label; + role = this._itemModel.accessibilityInformation.role; + } else { + ariaLabel = this._itemModel.text ? stripCodicons(this._itemModel.text).trim() : ''; + } + + if (ariaLabel) { + this.container.setAttribute('aria-label', ariaLabel); + } + + if (role) { + this.container.setAttribute('role', role); + } + + this.container.title = this._itemModel.tooltip ?? ''; + + if (this._itemModel.command) { + this.container.classList.add('cell-status-item-has-command'); + this.container.tabIndex = 0; + + this._register(DOM.addDisposableListener(this.container, DOM.EventType.CLICK, _e => { + this.executeCommand(); + })); + this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, e => { + const event = new StandardKeyboardEvent(e); + if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { + this.executeCommand(); + } + })); + } + } + + private async executeCommand(): Promise { + const command = this._itemModel.command; + if (!command) { + return; + } + + const id = typeof command === 'string' ? command : command.id; + const args = typeof command === 'string' ? [] : command.arguments ?? []; + + args.unshift(this._context); + + this.telemetryService.publicLog2('workbenchActionExecuted', { id, from: 'cell status bar' }); + try { + await this.commandService.executeCommand(id, ...args); + } catch (error) { + this.notificationService.error(toErrorMessage(error)); + } + } +} + +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._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => { + this.run(); + })); + this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.KEY_UP, 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 => { + 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"); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts index ef7e8c202a1..524f3326aaf 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/codeCell.ts @@ -4,21 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { raceCancellation } from 'vs/base/common/async'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IDimension } from 'vs/editor/common/editorCommon'; import { IModeService } from 'vs/editor/common/services/modeService'; import * as nls from 'vs/nls'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { EDITOR_BOTTOM_PADDING, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellFocusMode, CodeCellRenderTemplate, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { getResizesObserver } from 'vs/workbench/contrib/notebook/browser/view/renderers/sizeObserver'; import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CellOutputKind, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, BUILTIN_RENDERER_ID } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode } from 'vs/base/common/keyCodes'; -import { IDimension } from 'vs/editor/common/editorCommon'; +import { BUILTIN_RENDERER_ID, CellOutputKind, IProcessedOutput, IRenderOutput, ITransformedDisplayOutputDto, outputHasDynamicHeight, RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; interface IMimeTypeRenderer extends IQuickPickItem { index: number; @@ -32,6 +32,9 @@ interface IRenderedOutput { export class CodeCell extends Disposable { private outputResizeListeners = new Map(); private outputElements = new Map(); + + private modifyInsetQueue = Promise.resolve(); + constructor( private notebookEditor: INotebookEditor, private viewCell: CodeCellViewModel, @@ -170,7 +173,7 @@ export class CodeCell extends Disposable { removedKeys.push(key); // remove element from DOM this.templateData?.outputContainer?.removeChild(value.element); - this.notebookEditor.removeInset(key); + this.modifyInsetQueue = this.modifyInsetQueue.finally(() => this.notebookEditor.removeInset(key)); } }); @@ -322,8 +325,9 @@ export class CodeCell extends Disposable { const renderedOutput = this.outputElements.get(currOutput); if (renderedOutput) { - if (renderedOutput.renderResult.shadowContent) { + if (renderedOutput.renderResult.type !== RenderOutputType.None) { // Show inset in webview, or render output that isn't rendered + // TODO@roblou skipHeightInit flag is a hack - the webview only sends the real height once. Don't wipe it out here. this.renderOutput(currOutput, index, undefined); } else { // Anything else, just update the height @@ -337,6 +341,7 @@ export class CodeCell extends Disposable { private viewUpdateInputCollapsed(): void { DOM.hide(this.templateData.cellContainer); + DOM.hide(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.show(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', true); @@ -354,6 +359,7 @@ export class CodeCell extends Disposable { private viewUpdateOutputCollapsed(): void { DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.hide(this.templateData.outputContainer); @@ -367,6 +373,7 @@ export class CodeCell extends Disposable { private viewUpdateAllCollapsed(): void { DOM.hide(this.templateData.cellContainer); + DOM.hide(this.templateData.runButtonContainer); DOM.show(this.templateData.collapsedPart); DOM.hide(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', true); @@ -381,6 +388,7 @@ export class CodeCell extends Disposable { private viewUpdateExpanded(): void { DOM.show(this.templateData.cellContainer); + DOM.show(this.templateData.runButtonContainer); DOM.hide(this.templateData.collapsedPart); DOM.show(this.templateData.outputContainer); this.templateData.container.classList.toggle('collapsed', false); @@ -393,7 +401,7 @@ export class CodeCell extends Disposable { private layoutEditor(dimension: IDimension): void { this.templateData.editor?.layout(dimension); - this.templateData.statusBarContainer.style.width = `${dimension.width}px`; + this.templateData.statusBar.layout(dimension.width); } private onCellWidthChange(): void { @@ -408,9 +416,10 @@ export class CodeCell extends Disposable { } ); + // for contents for which we don't observe for dynamic height, update them manually this.viewCell.outputs.forEach((o, i) => { const renderedOutput = this.outputElements.get(o); - if (renderedOutput && !renderedOutput.renderResult.hasDynamicHeight && !renderedOutput.renderResult.shadowContent) { + if (renderedOutput && renderedOutput.renderResult.type === RenderOutputType.None && !renderedOutput.renderResult.hasDynamicHeight) { this.viewCell.updateOutputHeight(i, renderedOutput.element.clientHeight); } }); @@ -469,9 +478,12 @@ export class CodeCell extends Disposable { const innerContainer = DOM.$('.output-inner-container'); DOM.append(outputItemDiv, innerContainer); - if (pickedMimeTypeRenderer.isResolved) { - // html - result = this.notebookEditor.getOutputRenderer().render({ outputId: currOutput.outputId, outputKind: CellOutputKind.Rich, data: { 'text/html': pickedMimeTypeRenderer.output! } }, innerContainer, 'text/html'); + + if (pickedMimeTypeRenderer.rendererId !== BUILTIN_RENDERER_ID) { + const renderer = this.notebookService.getRendererInfo(pickedMimeTypeRenderer.rendererId); + result = renderer + ? { type: RenderOutputType.Extension, renderer, source: currOutput, mimeType: pickedMimeTypeRenderer.mimeType } + : this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType); } else { result = this.notebookEditor.getOutputRenderer().render(currOutput, innerContainer, pickedMimeTypeRenderer.mimeType); } @@ -496,18 +508,16 @@ export class CodeCell extends Disposable { this.templateData.outputContainer?.appendChild(outputItemDiv); } - if (result.shadowContent) { + if (result.type !== RenderOutputType.None) { this.viewCell.selfSizeMonitoring = true; - this.notebookEditor.createInset(this.viewCell, currOutput, result.shadowContent, this.viewCell.getOutputOffset(index)); + this.modifyInsetQueue = this.modifyInsetQueue.finally(() => this.notebookEditor.createInset(this.viewCell, result as any, this.viewCell.getOutputOffset(index))); } else { DOM.addClass(outputItemDiv, 'foreground'); DOM.addClass(outputItemDiv, 'output-element'); outputItemDiv.style.position = 'absolute'; } - const hasDynamicHeight = result.hasDynamicHeight; - - if (hasDynamicHeight) { + if (outputHasDynamicHeight(result)) { this.viewCell.selfSizeMonitoring = true; const clientHeight = outputItemDiv.clientHeight; @@ -535,18 +545,12 @@ export class CodeCell extends Disposable { elementSizeObserver.startObserving(); this.outputResizeListeners.get(currOutput)!.add(elementSizeObserver); this.viewCell.updateOutputHeight(index, clientHeight); - } else { - if (result.shadowContent) { - // webview - // noop - } else { - // static output - const clientHeight = Math.ceil(outputItemDiv.clientHeight); - this.viewCell.updateOutputHeight(index, clientHeight); + } else if (result.type === RenderOutputType.None) { // no-op if it's a webview + const clientHeight = Math.ceil(outputItemDiv.clientHeight); + this.viewCell.updateOutputHeight(index, clientHeight); - const top = this.viewCell.getOutputOffsetInContainer(index); - outputItemDiv.style.top = `${top}px`; - } + const top = this.viewCell.getOutputOffsetInContainer(index); + outputItemDiv.style.top = `${top}px`; } } @@ -601,21 +605,10 @@ export class CodeCell extends Disposable { const element = this.outputElements.get(output)?.element; if (element) { this.templateData?.outputContainer?.removeChild(element); - this.notebookEditor.removeInset(output); + await (this.modifyInsetQueue = this.modifyInsetQueue.finally(() => this.notebookEditor.removeInset(output))); } output.pickedMimeTypeIndex = pick; - - if (!output.orderedMimeTypes![pick].isResolved && output.orderedMimeTypes![pick].rendererId !== BUILTIN_RENDERER_ID) { - // since it's not build in renderer and not resolved yet - // let's see if we can activate the extension and then render - // await this.notebookService.transformSpliceOutputs(this.notebookEditor.textModel!, [[0, 0, output]]) - const outputRet = await this.notebookService.transformSingleOutput(this.notebookEditor.textModel!, output, output.orderedMimeTypes![pick].rendererId!, output.orderedMimeTypes![pick].mimeType); - if (outputRet) { - output.orderedMimeTypes![pick] = outputRet; - } - } - this.renderOutput(output, index, nextElement); this.relayoutCell(); } diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts new file mode 100644 index 00000000000..989f392ea4c --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/commonViewComponents.ts @@ -0,0 +1,27 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { renderCodicons } from 'vs/base/common/codicons'; +import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; +import { MenuItemAction } from 'vs/platform/actions/common/actions'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { INotificationService } from 'vs/platform/notification/common/notification'; + +export class CodiconActionViewItem extends MenuEntryActionViewItem { + constructor( + readonly _action: MenuItemAction, + keybindingService: IKeybindingService, + notificationService: INotificationService, + contextMenuService: IContextMenuService + ) { + super(_action, keybindingService, notificationService, contextMenuService); + } + updateLabel(): void { + if (this.options.label && this.label) { + this.label.innerHTML = renderCodicons(this._commandAction.label ?? ''); + } + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/dnd.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/dnd.ts new file mode 100644 index 00000000000..7f5e4ad30a0 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/dnd.ts @@ -0,0 +1,278 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +import * as DOM from 'vs/base/browser/dom'; +import { domEvent } from 'vs/base/browser/event'; +import { Delayer } from 'vs/base/common/async'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as platform from 'vs/base/common/platform'; +import { BOTTOM_CELL_TOOLBAR_GAP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { BaseCellRenderTemplate, CellEditState, ICellViewModel, INotebookCellList, INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +const $ = DOM.$; + +export const DRAGGING_CLASS = 'cell-dragging'; +export const GLOBAL_DRAG_CLASS = 'global-drag-active'; + +type DragImageProvider = () => HTMLElement; + +interface CellDragEvent { + browserEvent: DragEvent; + draggedOverCell: ICellViewModel; + cellTop: number; + cellHeight: number; + dragPosRatio: number; +} + +export class CellDragAndDropController extends Disposable { + // TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need + // to figure out how to prevent that + private currentDraggedCell: ICellViewModel | undefined; + + private listInsertionIndicator: HTMLElement; + + private list!: INotebookCellList; + + private isScrolling = false; + private scrollingDelayer: Delayer; + + constructor( + private readonly notebookEditor: INotebookEditor, + insertionIndicatorContainer: HTMLElement + ) { + super(); + + this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator')); + + this._register(domEvent(document.body, DOM.EventType.DRAG_START, true)(this.onGlobalDragStart.bind(this))); + this._register(domEvent(document.body, DOM.EventType.DRAG_END, true)(this.onGlobalDragEnd.bind(this))); + + const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => { + this._register(DOM.addDisposableListener( + notebookEditor.getDomNode(), + eventType, + e => { + const cellDragEvent = this.toCellDragEvent(e); + if (cellDragEvent) { + handler(cellDragEvent); + } + })); + }; + + addCellDragListener(DOM.EventType.DRAG_OVER, event => { + event.browserEvent.preventDefault(); + this.onCellDragover(event); + }); + addCellDragListener(DOM.EventType.DROP, event => { + event.browserEvent.preventDefault(); + this.onCellDrop(event); + }); + addCellDragListener(DOM.EventType.DRAG_LEAVE, event => { + event.browserEvent.preventDefault(); + this.onCellDragLeave(event); + }); + + this.scrollingDelayer = new Delayer(200); + } + + setList(value: INotebookCellList) { + this.list = value; + + this.list.onWillScroll(e => { + if (!e.scrollTopChanged) { + return; + } + + this.setInsertIndicatorVisibility(false); + this.isScrolling = true; + this.scrollingDelayer.trigger(() => { + this.isScrolling = false; + }); + }); + } + + private setInsertIndicatorVisibility(visible: boolean) { + this.listInsertionIndicator.style.opacity = visible ? '1' : '0'; + } + + private toCellDragEvent(event: DragEvent): CellDragEvent | undefined { + const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top; + const dragOffset = this.list.scrollTop + event.clientY - targetTop; + const draggedOverCell = this.list.elementAt(dragOffset); + if (!draggedOverCell) { + return undefined; + } + + const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell); + const cellHeight = this.list.elementHeight(draggedOverCell); + + const dragPosInElement = dragOffset - cellTop; + const dragPosRatio = dragPosInElement / cellHeight; + + return { + browserEvent: event, + draggedOverCell, + cellTop, + cellHeight, + dragPosRatio + }; + } + + clearGlobalDragState() { + this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); + } + + private onGlobalDragStart() { + this.notebookEditor.getDomNode().classList.add(GLOBAL_DRAG_CLASS); + } + + private onGlobalDragEnd() { + this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS); + } + + private onCellDragover(event: CellDragEvent): void { + if (!event.browserEvent.dataTransfer) { + return; + } + + if (!this.currentDraggedCell) { + event.browserEvent.dataTransfer.dropEffect = 'none'; + return; + } + + if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { + this.setInsertIndicatorVisibility(false); + return; + } + + const dropDirection = this.getDropInsertDirection(event); + const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_GAP / 2; + if (insertionIndicatorTop >= 0) { + this.listInsertionIndicator.style.top = `${insertionIndicatorTop}px`; + this.setInsertIndicatorVisibility(true); + } else { + this.setInsertIndicatorVisibility(false); + } + } + + private getDropInsertDirection(event: CellDragEvent): 'above' | 'below' { + return event.dragPosRatio < 0.5 ? 'above' : 'below'; + } + + private onCellDrop(event: CellDragEvent): void { + const draggedCell = this.currentDraggedCell!; + + if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) { + return; + } + + let draggedCells: ICellViewModel[] = [draggedCell]; + let draggedCellRange: [number, number] = [this.notebookEditor.viewModel!.getCellIndex(draggedCell), 1]; + + if (draggedCell.cellKind === CellKind.Markdown) { + const currCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell); + const nextVisibleCellIndex = this.notebookEditor.viewModel!.getNextVisibleCellIndex(currCellIndex); + + if (nextVisibleCellIndex > currCellIndex + 1) { + // folding ;) + draggedCells = this.notebookEditor.viewModel!.viewCells.slice(currCellIndex, nextVisibleCellIndex); + draggedCellRange = [currCellIndex, nextVisibleCellIndex - currCellIndex]; + } + } + + this.dragCleanup(); + + const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh); + + const dropDirection = this.getDropInsertDirection(event); + const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight; + const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop + BOTTOM_CELL_TOOLBAR_GAP / 2; + const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height; + if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) { + // Ignore drop, insertion point is off-screen + return; + } + + if (isCopy) { + this.copyCells(draggedCells, event.draggedOverCell, dropDirection); + } else { + const viewModel = this.notebookEditor.viewModel!; + let originalToIdx = viewModel.getCellIndex(event.draggedOverCell); + if (dropDirection === 'below') { + const relativeToIndex = viewModel.getCellIndex(event.draggedOverCell); + const newIdx = viewModel.getNextVisibleCellIndex(relativeToIndex); + originalToIdx = newIdx; + } + + this.notebookEditor.moveCellsToIdx(draggedCellRange[0], draggedCellRange[1], originalToIdx); + } + } + + private onCellDragLeave(event: CellDragEvent): void { + if (!event.browserEvent.relatedTarget || !DOM.isAncestor(event.browserEvent.relatedTarget as HTMLElement, this.notebookEditor.getDomNode())) { + this.setInsertIndicatorVisibility(false); + } + } + + private dragCleanup(): void { + if (this.currentDraggedCell) { + this.currentDraggedCell.dragging = false; + this.currentDraggedCell = undefined; + } + + this.setInsertIndicatorVisibility(false); + } + + registerDragHandle(templateData: BaseCellRenderTemplate, cellRoot: HTMLElement, dragHandle: HTMLElement, dragImageProvider: DragImageProvider): void { + const container = templateData.container; + dragHandle.setAttribute('draggable', 'true'); + + templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => { + // Note, templateData may have a different element rendered into it by now + container.classList.remove(DRAGGING_CLASS); + this.dragCleanup(); + })); + + templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_START)(event => { + if (!event.dataTransfer) { + return; + } + + this.currentDraggedCell = templateData.currentRenderedCell!; + this.currentDraggedCell.dragging = true; + + const dragImage = dragImageProvider(); + cellRoot.parentElement!.appendChild(dragImage); + event.dataTransfer.setDragImage(dragImage, 0, 0); + setTimeout(() => cellRoot.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout + + container.classList.add(DRAGGING_CLASS); + })); + } + + private copyCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') { + this.notebookEditor.textModel!.pushStackElement('Copy Cells'); + let firstNewCell: ICellViewModel | undefined = undefined; + let firstNewCellState: CellEditState = CellEditState.Preview; + for (let i = 0; i < draggedCells.length; i++) { + const draggedCell = draggedCells[i]; + const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText()); + + if (newCell && !firstNewCell) { + firstNewCell = newCell; + firstNewCellState = draggedCell.editState; + } + } + + if (firstNewCell) { + this.notebookEditor.focusNotebookCell(firstNewCell, firstNewCellState === CellEditState.Editing ? 'editor' : 'container'); + } + + this.notebookEditor.textModel!.pushStackElement('Copy Cells'); + } +} diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts index 260889e19b9..a1e81ae72e5 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/markdownCell.ts @@ -282,7 +282,7 @@ export class StatefulMarkdownCell extends Disposable { private layoutEditor(dimension: DOM.IDimension): void { this.editor?.layout(dimension); - this.templateData.statusBarContainer.style.width = `${dimension.width}px`; + this.templateData.statusBar.layout(dimension.width); } private onCellEditorWidthChange(): void { diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts index 622c1104e97..9caa12e52b7 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts @@ -6,6 +6,7 @@ import type { Event } from 'vs/base/common/event'; import type { IDisposable } from 'vs/base/common/lifecycle'; import { ToWebviewMessage } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { RenderOutputType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; // !! IMPORTANT !! everything must be in-line within the webviewPreloads // function. Imports are not allowed. This is stringifies and injected into @@ -373,24 +374,21 @@ function webviewPreloads() { addMouseoverListeners(outputNode, outputId); const content = data.content; - outputNode.innerHTML = content; - cellOutputContainer.appendChild(outputNode); - - let pureData: { mimeType: string, output: unknown } | undefined; - const outputScript = cellOutputContainer.querySelector('script.vscode-pure-data'); - if (outputScript) { - try { pureData = JSON.parse(outputScript.innerHTML); } catch { } + if (content.type === RenderOutputType.Html) { + outputNode.innerHTML = content.htmlContent; + cellOutputContainer.appendChild(outputNode); + domEval(outputNode); + } else { + onDidCreateOutput.fire([data.apiNamespace, { + element: outputNode, + output: content.output, + mimeType: content.mimeType, + outputId + }]); + cellOutputContainer.appendChild(outputNode); } - // eval - domEval(outputNode); resizeObserve(outputNode, outputId); - onDidCreateOutput.fire([data.apiNamespace, { - element: outputNode, - output: pureData?.output, - mimeType: pureData?.mimeType, - outputId - }]); vscode.postMessage({ __vscode_notebook_message: true, diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts index d70f6e4c842..d145744d489 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel.ts @@ -12,12 +12,14 @@ import { IPosition } from 'vs/editor/common/core/position'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { SearchParams } from 'vs/editor/common/model/textModelSearch'; -import { EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CELL_STATUSBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; import { CellEditState, CellFocusMode, CursorAtBoundary, CellViewModelStateChangeEvent, IEditableCellViewModel, INotebookCellDecorationOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, NotebookCellMetadata, NotebookDocumentMetadata, INotebookSearchOptions, ShowCellStatusbarKey } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export abstract class BaseCellViewModel extends Disposable { + protected readonly _onDidChangeEditorAttachState = new Emitter(); // Do not merge this event with `onDidChangeState` as we are using `Event.once(onDidChangeEditorAttachState)` elsewhere. readonly onDidChangeEditorAttachState = this._onDidChangeEditorAttachState.event; @@ -106,7 +108,12 @@ export abstract class BaseCellViewModel extends Disposable { this._dragging = v; } - constructor(readonly viewType: string, readonly model: NotebookCellTextModel, public id: string) { + constructor( + readonly viewType: string, + readonly model: NotebookCellTextModel, + public id: string, + private readonly _configurationService: IConfigurationService + ) { super(); this._register(model.onDidChangeLanguage(() => { @@ -116,12 +123,24 @@ export abstract class BaseCellViewModel extends Disposable { this._register(model.onDidChangeMetadata(() => { this._onDidChangeState.fire({ metadataChanged: true }); })); + + this._register(this._configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(ShowCellStatusbarKey)) { + this.layoutChange({}); + } + })); + } + + protected getEditorStatusbarHeight() { + const showCellStatusBar = this._configurationService.getValue(ShowCellStatusbarKey); + return showCellStatusBar ? CELL_STATUSBAR_HEIGHT : 0; } // abstract resolveTextModel(): Promise; abstract hasDynamicHeight(): boolean; abstract getHeight(lineHeight: number): number; abstract onDeselect(): void; + abstract layoutChange(change: any): void; assertTextModelAttached(): boolean { if (this.textModel && this._textEditor && this._textEditor.getModel() === this.textModel) { diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts index 306c9722c8d..0a7c21cf9be 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel.ts @@ -8,12 +8,13 @@ import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_RUN_GUTTER, CELL_STATUSBAR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, CELL_TOP_MARGIN, EDITOR_TOP_PADDING, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_OFFSET, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, ICellViewModel, NotebookLayoutInfo, CodeCellLayoutState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { CellKind, NotebookCellOutputsSplice, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { BaseCellViewModel } from './baseCellViewModel'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_RUN_GUTTER, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFindMatch, CodeCellLayoutChangeEvent, CodeCellLayoutInfo, CodeCellLayoutState, ICellViewModel, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { CellKind, INotebookSearchOptions, NotebookCellOutputsSplice } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BaseCellViewModel } from './baseCellViewModel'; export class CodeCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Code; @@ -68,9 +69,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod readonly viewType: string, readonly model: NotebookCellTextModel, initialNotebookLayoutInfo: NotebookLayoutInfo | null, - readonly eventDispatcher: NotebookEventDispatcher + readonly eventDispatcher: NotebookEventDispatcher, + @IConfigurationService configurationService: IConfigurationService ) { - super(viewType, model, UUID.generateUuid()); + super(viewType, model, UUID.generateUuid(), configurationService); this._register(this.model.onDidChangeOutputs((splices) => { this._outputCollection = new Array(this.model.outputs.length); this._outputsTop = null; @@ -121,9 +123,10 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod newState = CodeCellLayoutState.Estimated; } - const indicatorHeight = editorHeight + CELL_STATUSBAR_HEIGHT + outputTotalHeight; - const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT; - const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET; + const statusbarHeight = this.getEditorStatusbarHeight(); + const indicatorHeight = editorHeight + statusbarHeight + outputTotalHeight; + const outputContainerOffset = EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + statusbarHeight; + const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; this._layoutInfo = { @@ -141,8 +144,8 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod outputTotalHeight = this.metadata?.inputCollapsed && this.metadata.outputCollapsed ? 0 : outputTotalHeight; const indicatorHeight = COLLAPSED_INDICATOR_HEIGHT + outputTotalHeight; const outputContainerOffset = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT; - const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_HEIGHT + outputTotalHeight; - const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET; + const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + outputTotalHeight; + const bottomToolbarOffset = totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2; const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo?.editorWidth; this._layoutInfo = { @@ -209,7 +212,7 @@ export class CodeCellViewModel extends BaseCellViewModel implements ICellViewMod } private computeTotalHeight(editorHeight: number, outputsTotalHeight: number): number { - return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + CELL_STATUSBAR_HEIGHT + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_BOTTOM_MARGIN; + return EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN + editorHeight + this.getEditorStatusbarHeight() + outputsTotalHeight + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; } /** diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts index 8700ba68862..bca810545ec 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher.ts @@ -70,3 +70,24 @@ export class NotebookEventDispatcher { } } } + +export class NotebookDiffEditorEventDispatcher { + protected readonly _onDidChangeLayout = new Emitter(); + readonly onDidChangeLayout = this._onDidChangeLayout.event; + + constructor() { + } + + emit(events: NotebookViewEvent[]) { + for (let i = 0, len = events.length; i < len; i++) { + const e = events[i]; + + switch (e.type) { + case NotebookViewEventType.LayoutChanged: + this._onDidChangeLayout.fire(e); + break; + } + } + } + +} diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts index 9c1dc125163..2916dcd868e 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel.ts @@ -3,19 +3,20 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Emitter, Event } from 'vs/base/common/event'; import * as UUID from 'vs/base/common/uuid'; import * as editorCommon from 'vs/editor/common/editorCommon'; import * as model from 'vs/editor/common/model'; -import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_MARGIN, CELL_STATUSBAR_HEIGHT, CELL_TOP_MARGIN, CELL_BOTTOM_MARGIN, CODE_CELL_LEFT_MARGIN, BOTTOM_CELL_TOOLBAR_OFFSET, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { BOTTOM_CELL_TOOLBAR_GAP, BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_MARGIN, CELL_TOP_MARGIN, CODE_CELL_LEFT_MARGIN, COLLAPSED_INDICATOR_HEIGHT } from 'vs/workbench/contrib/notebook/browser/constants'; +import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellFindMatch, ICellViewModel, MarkdownCellLayoutChangeEvent, MarkdownCellLayoutInfo, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; import { BaseCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/baseCellViewModel'; -import { EditorFoldingStateDelegate } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; +import { NotebookCellStateChangedEvent, NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { CellKind, INotebookSearchOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookEventDispatcher, NotebookCellStateChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; export class MarkdownCellViewModel extends BaseCellViewModel implements ICellViewModel { readonly cellKind = CellKind.Markdown; @@ -27,7 +28,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie } set renderedMarkdownHeight(newHeight: number) { - const newTotalHeight = newHeight + BOTTOM_CELL_TOOLBAR_HEIGHT; + const newTotalHeight = newHeight + BOTTOM_CELL_TOOLBAR_GAP; this.totalHeight = newTotalHeight; } @@ -45,7 +46,7 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie set editorHeight(newHeight: number) { this._editorHeight = newHeight; - this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_STATUSBAR_HEIGHT; + this.totalHeight = this._editorHeight + CELL_TOP_MARGIN + CELL_BOTTOM_MARGIN + BOTTOM_CELL_TOOLBAR_GAP + this.getEditorStatusbarHeight(); } get editorHeight() { @@ -65,15 +66,16 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie initialNotebookLayoutInfo: NotebookLayoutInfo | null, readonly foldingDelegate: EditorFoldingStateDelegate, readonly eventDispatcher: NotebookEventDispatcher, - private readonly _mdRenderer: MarkdownRenderer + private readonly _mdRenderer: MarkdownRenderer, + @IConfigurationService configurationService: IConfigurationService ) { - super(viewType, model, UUID.generateUuid()); + super(viewType, model, UUID.generateUuid(), configurationService); this._layoutInfo = { editorHeight: 0, fontInfo: initialNotebookLayoutInfo?.fontInfo || null, editorWidth: initialNotebookLayoutInfo?.width ? this.computeEditorWidth(initialNotebookLayoutInfo.width) : 0, - bottomToolbarOffset: BOTTOM_CELL_TOOLBAR_HEIGHT, + bottomToolbarOffset: BOTTOM_CELL_TOOLBAR_GAP, totalHeight: 0 }; @@ -101,19 +103,19 @@ export class MarkdownCellViewModel extends BaseCellViewModel implements ICellVie fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, editorHeight: this._editorHeight, - bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET, + bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2, totalHeight }; } else { const editorWidth = state.outerWidth !== undefined ? this.computeEditorWidth(state.outerWidth) : this._layoutInfo.editorWidth; - const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + BOTTOM_CELL_TOOLBAR_HEIGHT + CELL_BOTTOM_MARGIN; + const totalHeight = CELL_TOP_MARGIN + COLLAPSED_INDICATOR_HEIGHT + BOTTOM_CELL_TOOLBAR_GAP + CELL_BOTTOM_MARGIN; state.totalHeight = totalHeight; this._layoutInfo = { fontInfo: state.font || this._layoutInfo.fontInfo, editorWidth, editorHeight: this._editorHeight, - bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - BOTTOM_CELL_TOOLBAR_OFFSET, + bottomToolbarOffset: totalHeight - BOTTOM_CELL_TOOLBAR_GAP - BOTTOM_CELL_TOOLBAR_HEIGHT / 2, totalHeight }; } diff --git a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts index 2f2f79f1746..0c02d11cf09 100644 --- a/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts +++ b/src/vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel.ts @@ -8,7 +8,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { IModelDecorationOptions, IModelDeltaDecoration, TrackedRangeStickiness, IReadonlyTextBuffer } from 'vs/editor/common/model'; @@ -170,10 +170,6 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return this._notebook; } - get renderers() { - return this._notebook!.renderers; - } - get handle() { return this._notebook.handle; } @@ -613,7 +609,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return result; } - createCell(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) { + createCell(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean = true) { this._notebook.createCell2(index, source, language, type, metadata, synchronous, pushUndoStop, undefined, undefined); // TODO, rely on createCell to be sync return this.viewCells[index]; @@ -759,7 +755,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD language, kind, { - createCell: (index: number, source: string | string[], language: string, type: CellKind) => { + createCell: (index: number, source: string, language: string, type: CellKind) => { return this.createCell(index, source, language, type, undefined, true, false) as BaseCellViewModel; }, deleteCell: (index: number) => { @@ -1031,7 +1027,10 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const viewCell = cell as CellViewModel; this._lastNotebookEditResource.push(viewCell.uri); return viewCell.resolveTextModel().then(() => { - this._bulkEditService.apply({ edits: [{ edit: { range: range, text: text }, resource: cell.uri }] }, { quotableLabel: 'Notebook Replace' }); + this._bulkEditService.apply( + [new ResourceTextEdit(cell.uri, { range, text })], + { quotableLabel: 'Notebook Replace' } + ); }); } @@ -1055,7 +1054,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD return Promise.all(matches.map(match => { return match.cell.resolveTextModel(); })).then(async () => { - this._bulkEditService.apply({ edits: textEdits }, { quotableLabel: 'Notebook Replace All' }); + this._bulkEditService.apply(ResourceEdit.convert({ edits: textEdits }), { quotableLabel: 'Notebook Replace All' }); return; }); } @@ -1076,12 +1075,15 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const element = editStack.past.length ? editStack.past[editStack.past.length - 1] : undefined; if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { - return await this.withElement(element, async () => { + await this.withElement(element, async () => { await this._undoService.undo(this.uri); }); + + return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources; } await this._undoService.undo(this.uri); + return []; } async redo() { @@ -1093,13 +1095,16 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD const element = editStack.future[0]; if (element && element instanceof SingleModelEditStackElement || element instanceof MultiModelEditStackElement) { - return await this.withElement(element, async () => { + await this.withElement(element, async () => { await this._undoService.redo(this.uri); }); + + return (element instanceof SingleModelEditStackElement) ? [element.resource] : element.resources; } await this._undoService.redo(this.uri); + return []; } equal(notebook: NotebookTextModel) { diff --git a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts index aaf080d20d1..5cde6b3bfaa 100644 --- a/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts +++ b/src/vs/workbench/contrib/notebook/common/model/cellEdit.ts @@ -6,6 +6,7 @@ import { IResourceUndoRedoElement, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; +import { NotebookCellMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; /** * It should not modify Undo/Redo stack @@ -14,6 +15,7 @@ export interface ITextCellEditingDelegate { insertCell?(index: number, cell: NotebookCellTextModel): void; deleteCell?(index: number): void; moveCell?(fromIndex: number, length: number, toIndex: number, beforeSelections: number[] | undefined, endSelections: number[] | undefined): void; + updateCellMetadata?(index: number, newMetadata: NotebookCellMetadata): void; emitSelections(selections: number[]): void; } @@ -183,3 +185,33 @@ export class SpliceCellsEdit implements IResourceUndoRedoElement { } } } + +export class CellMetadataEdit implements IResourceUndoRedoElement { + type: UndoRedoElementType.Resource = UndoRedoElementType.Resource; + label: string = 'Update Cell Metadata'; + constructor( + public resource: URI, + readonly index: number, + readonly oldMetadata: NotebookCellMetadata, + readonly newMetadata: NotebookCellMetadata, + private editingDelegate: ITextCellEditingDelegate, + ) { + + } + + undo(): void { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.oldMetadata); + } + + redo(): void | Promise { + if (!this.editingDelegate.updateCellMetadata) { + return; + } + + this.editingDelegate.updateCellMetadata(this.index, this.newMetadata); + } +} diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts index 015f62a96e3..c2b34adefcf 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookCellTextModel.ts @@ -4,13 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; -import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { ICell, IProcessedOutput, NotebookCellOutputsSplice, CellKind, NotebookCellMetadata, NotebookDocumentMetadata, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; import { URI } from 'vs/base/common/uri'; import * as model from 'vs/editor/common/model'; import { Range } from 'vs/editor/common/core/range'; import { Disposable } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { hash } from 'vs/base/common/hash'; export class NotebookCellTextModel extends Disposable implements ICell { private _onDidChangeOutputs = new Emitter(); @@ -31,14 +32,15 @@ export class NotebookCellTextModel extends Disposable implements ICell { return this._outputs; } - private _metadata: NotebookCellMetadata | undefined; + private _metadata: NotebookCellMetadata; get metadata() { return this._metadata; } - set metadata(newMetadata: NotebookCellMetadata | undefined) { + set metadata(newMetadata: NotebookCellMetadata) { this._metadata = newMetadata; + this._hash = null; this._onDidChangeMetadata.fire(); } @@ -48,6 +50,7 @@ export class NotebookCellTextModel extends Disposable implements ICell { set language(newLanguage: string) { this._language = newLanguage; + this._hash = null; this._onDidChangeLanguage.fire(newLanguage); } @@ -59,31 +62,35 @@ export class NotebookCellTextModel extends Disposable implements ICell { } const builder = new PieceTreeTextBufferBuilder(); - builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + builder.acceptChunk(this._source); const bufferFactory = builder.finish(true); this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); this._register(this._textBuffer.onDidChangeContent(() => { + this._hash = null; this._onDidChangeContent.fire(); })); return this._textBuffer; } + private _hash: number | null = null; + constructor( readonly uri: URI, public handle: number, - private _source: string | string[], + private _source: string, private _language: string, public cellKind: CellKind, outputs: IProcessedOutput[], metadata: NotebookCellMetadata | undefined, + public readonly transientOptions: TransientOptions, private readonly _modelService: ITextModelService ) { super(); this._outputs = outputs; - this._metadata = metadata; + this._metadata = metadata || {}; } getValue(): string { @@ -96,6 +103,31 @@ export class NotebookCellTextModel extends Disposable implements ICell { } } + getHashValue(): number { + if (this._hash !== null) { + return this._hash; + } + + // TODO, raw outputs + this._hash = hash([hash(this.getValue()), this._getPersisentMetadata, this.transientOptions.transientOutputs ? [] : this._outputs]); + return this._hash; + } + + private _getPersisentMetadata() { + let filteredMetadata: { [key: string]: any } = {}; + const transientMetadata = this.transientOptions.transientMetadata; + + const keys = new Set([...Object.keys(this.metadata)]); + for (let key of keys) { + if (!(transientMetadata[key as keyof NotebookCellMetadata]) + ) { + filteredMetadata[key] = this.metadata[key as keyof NotebookCellMetadata]; + } + } + + return filteredMetadata; + } + getTextLength(): number { return this.textBuffer.getLength(); } diff --git a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts index d48a7968a42..1f68435cbdc 100644 --- a/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts +++ b/src/vs/workbench/contrib/notebook/common/model/notebookTextModel.ts @@ -8,20 +8,12 @@ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; -import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, ICellInsertEdit, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, ICellDeleteEdit, NotebookCellsChangeType, ICellDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, NotebookCellOutputsSplice, NotebookCellTextModelSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, CellEditType, CellUri, NotebookCellsChangedEvent, CellKind, IProcessedOutput, notebookDocumentMetadataDefaults, diff, NotebookCellsChangeType, ICellDto2, IMainCellDto, TransientOptions } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { ITextSnapshot } from 'vs/editor/common/model'; import { IUndoRedoService, UndoRedoElementType, IUndoRedoElement, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; -import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; +import { InsertCellEdit, DeleteCellEdit, MoveCellEdit, SpliceCellsEdit, CellMetadataEdit } from 'vs/workbench/contrib/notebook/common/model/cellEdit'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -function compareRangesUsingEnds(a: [number, number], b: [number, number]): number { - if (a[1] === b[1]) { - return a[1] - b[1]; - - } - return a[1] - b[1]; -} - export class NotebookTextModelSnapshot implements ITextSnapshot { // private readonly _pieces: Ce[] = []; private _index: number = -1; @@ -136,7 +128,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel cells: NotebookCellTextModel[]; languages: string[] = []; metadata: NotebookDocumentMetadata = notebookDocumentMetadataDefaults; - renderers = new Set(); + transientOptions: TransientOptions = { transientMetadata: {}, transientOutputs: false }; private _isUntitled: boolean | undefined = undefined; private _versionId = 0; @@ -187,7 +179,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } createCellTextModel( - source: string | string[], + source: string, language: string, cellKind: CellKind, outputs: IProcessedOutput[], @@ -195,7 +187,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel ) { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, source, language, cellKind, outputs || [], metadata || {}, this.transientOptions, this._modelService); } initialize(cells: ICellDto2[]) { @@ -205,7 +197,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const mainCells = cells.map(cell => { const cellHandle = this._cellhandlePool++; const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); + return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this.transientOptions, this._modelService); }); this._isUntitled = false; @@ -214,6 +206,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._mapping.set(mainCells[i].handle, mainCells[i]); const dirtyStateListener = mainCells[i].onDidChangeContent(() => { this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -228,7 +221,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._operationManager.pushStackElement(label); } - $applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { + applyEdit(modelVersionId: number, rawEdits: ICellEditOperation[], synchronous: boolean): boolean { if (modelVersionId !== this._versionId) { return false; } @@ -236,49 +229,30 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const oldViewCells = this.cells.slice(0); const oldMap = new Map(this._mapping); - let operations: ({ sortIndex: number; start: number; end: number; } & ICellEditOperation)[] = []; - for (let i = 0; i < rawEdits.length; i++) { - if (rawEdits[i].editType === CellEditType.Insert) { - const edit = rawEdits[i] as ICellInsertEdit; - operations.push({ - sortIndex: i, - start: edit.index, - end: edit.index, - ...edit - }); - } else { - const edit = rawEdits[i] as ICellDeleteEdit; - operations.push({ - sortIndex: i, - start: edit.index, - end: edit.index + edit.count, - ...edit - }); - } - } - - // const edits - operations = operations.sort((a, b) => { - const r = compareRangesUsingEnds([a.start, a.end], [b.start, b.end]); - if (r === 0) { - return b.sortIndex - a.sortIndex; - } - return -r; + const edits = rawEdits.map((edit, index) => { + return { + edit, + end: edit.editType === CellEditType.Replace ? edit.index + edit.count : edit.index, + originalIndex: index, + }; + }).sort((a, b) => { + return b.end - a.end || b.originalIndex - a.originalIndex; }); - for (let i = 0; i < operations.length; i++) { - switch (operations[i].editType) { - case CellEditType.Insert: - const insertEdit = operations[i] as ICellInsertEdit; - const mainCells = insertEdit.cells.map(cell => { - const cellHandle = this._cellhandlePool++; - const cellUri = CellUri.generate(this.uri, cellHandle); - return new NotebookCellTextModel(cellUri, cellHandle, cell.source, cell.language, cell.cellKind, cell.outputs || [], cell.metadata, this._modelService); - }); - this.insertNewCell(insertEdit.index, mainCells, false); + for (const { edit } of edits) { + switch (edit.editType) { + case CellEditType.Replace: + this._replaceCells(edit.index, edit.count, edit.cells); break; - case CellEditType.Delete: - this.removeCell(operations[i].index, operations[i].end - operations[i].start, false); + case CellEditType.Output: + //TODO@joh,@rebornix no event, no undo stop (?) + this.assertIndex(edit.index); + const cell = this.cells[edit.index]; + this.spliceNotebookCellOutputs(cell.handle, [[0, cell.outputs.length, edit.outputs]]); + break; + case CellEditType.Metadata: + this.assertIndex(edit.index); + this.deltaCellMetadata(this.cells[edit.index].handle, edit.metadata); break; } } @@ -320,7 +294,48 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel return true; } - $handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { + private _replaceCells(index: number, count: number, cellDtos: ICellDto2[]): void { + + if (count === 0 && cellDtos.length === 0) { + return; + } + + this._isUntitled = false; //TODO@rebornix fishy? + + // prepare remove + for (let i = index; i < index + count; i++) { + const cell = this.cells[i]; + this._cellListeners.get(cell.handle)?.dispose(); + this._cellListeners.delete(cell.handle); + } + + // prepare add + const cells = cellDtos.map(cellDto => { + const cellHandle = this._cellhandlePool++; + const cellUri = CellUri.generate(this.uri, cellHandle); + const cell = new NotebookCellTextModel( + cellUri, cellHandle, + cellDto.source, cellDto.language, cellDto.cellKind, cellDto.outputs || [], cellDto.metadata, this.transientOptions, + this._modelService + ); + const dirtyStateListener = cell.onDidChangeContent(() => { + this.setDirty(true); + this._increaseVersionId(); + this._onDidChangeContent.fire(); + }); + this._cellListeners.set(cell.handle, dirtyStateListener); + this._mapping.set(cell.handle, cell); + return cell; + }); + + // make change + this.cells.splice(index, count, ...cells); + this.setDirty(true); + this._increaseVersionId(); + this._onDidChangeContent.fire(); + } + + handleEdit(label: string | undefined, undo: () => void, redo: () => void): void { this._operationManager.pushEditOperation({ type: UndoRedoElementType.Resource, resource: this.uri, @@ -361,20 +376,6 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._onDidChangeMetadata.fire(this.metadata); } - updateNotebookCellMetadata(handle: number, metadata: NotebookCellMetadata) { - const cell = this.cells.find(cell => cell.handle === handle); - - if (cell) { - cell.metadata = metadata; - } - } - - updateRenderers(renderers: string[]) { - renderers.forEach(render => { - this.renderers.add(render); - }); - } - insertTemplateCell(cell: NotebookCellTextModel) { if (this.cells.length > 0 || this._isUntitled !== undefined) { return; @@ -387,6 +388,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel const dirtyStateListener = cell.onDidChangeContent(() => { this._isUntitled = false; this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -423,6 +425,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._mapping.set(cells[i].handle, cells[i]); const dirtyStateListener = cells[i].onDidChangeContent(() => { this.setDirty(true); + this._increaseVersionId(); this._onDidChangeContent.fire(); }); @@ -500,9 +503,15 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } // TODO@rebornix should this trigger content change event? - $spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { + spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]): void { const cell = this._mapping.get(cellHandle); cell?.spliceNotebookCellOutputs(splices); + + if (!this.transientOptions.transientOutputs) { + this._increaseVersionId(); + this.setDirty(true); + this._onDidChangeContent.fire(); + } } clearCellOutput(handle: number) { @@ -527,16 +536,62 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel } } - changeCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { + private _isCellMetadataChanged(a: NotebookCellMetadata, b: NotebookCellMetadata) { + const keys = new Set([...Object.keys(a || {}), ...Object.keys(b || {})]); + for (let key of keys) { + if ( + (a[key as keyof NotebookCellMetadata] !== b[key as keyof NotebookCellMetadata]) + && + !(this.transientOptions.transientMetadata[key as keyof NotebookCellMetadata]) + ) { + return true; + } + } + + return false; + } + + changeCellMetadata(handle: number, metadata: NotebookCellMetadata, pushUndoStop: boolean) { + const cell = this.cells.find(cell => cell.handle === handle); + + if (!cell) { + return; + } + + const triggerDirtyChange = this._isCellMetadataChanged(cell.metadata, metadata); + + if (triggerDirtyChange) { + if (pushUndoStop) { + const index = this.cells.indexOf(cell); + this._operationManager.pushEditOperation(new CellMetadataEdit(this.uri, index, Object.freeze(cell.metadata), Object.freeze(metadata), { + updateCellMetadata: (index, newMetadata) => { + const cell = this.cells[index]; + if (!cell) { + return; + } + this.changeCellMetadata(cell.handle, newMetadata, false); + }, + emitSelections: this._emitSelectionsDelegate.bind(this) + })); + } + cell.metadata = metadata; + this.setDirty(true); + this._onDidChangeContent.fire(); + } else { + cell.metadata = metadata; + } + + this._increaseVersionId(); + this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + } + + deltaCellMetadata(handle: number, newMetadata: NotebookCellMetadata) { const cell = this._mapping.get(handle); if (cell) { - cell.metadata = { + this.changeCellMetadata(handle, { ...cell.metadata, ...newMetadata - }; - - this._increaseVersionId(); - this._onDidModelChangeProxy.fire({ kind: NotebookCellsChangeType.ChangeMetadata, versionId: this._versionId, index: this.cells.indexOf(cell), metadata: cell.metadata }); + }, true); } } @@ -566,7 +621,7 @@ export class NotebookTextModel extends Disposable implements INotebookTextModel this._emitSelections.fire(selections); } - createCell2(index: number, source: string | string[], language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { + createCell2(index: number, source: string, language: string, type: CellKind, metadata: NotebookCellMetadata | undefined, synchronous: boolean, pushUndoStop: boolean, beforeSelections: number[] | undefined, endSelections: number[] | undefined) { const cell = this.createCellTextModel(source, language, type, [], metadata); if (pushUndoStop) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts b/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts new file mode 100644 index 00000000000..e99fa061a11 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/notebookCellStatusBarService.ts @@ -0,0 +1,21 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookCellStatusBarEntry } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const INotebookCellStatusBarService = createDecorator('notebookCellStatusBarService'); + +export interface INotebookCellStatusBarService { + readonly _serviceBrand: undefined; + + onDidChangeEntriesForCell: Event; + + addEntry(entry: INotebookCellStatusBarEntry): IDisposable; + getEntries(cell: URI): INotebookCellStatusBarEntry[]; +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts index 319c8fd9130..c4e05bd2be4 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookCommon.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookCommon.ts @@ -3,21 +3,25 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDiffResult, ISequence } from 'vs/base/common/diff/diff'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import * as UUID from 'vs/base/common/uuid'; +import { Schemas } from 'vs/base/common/network'; +import { basename } from 'vs/base/common/path'; import { isWindows } from 'vs/base/common/platform'; import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as editorCommon from 'vs/editor/common/editorCommon'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { Command } from 'vs/editor/common/modes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IEditorModel } from 'vs/platform/editor/common/editor'; -import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken } from 'vs/base/common/cancellation'; -import { Schemas } from 'vs/base/common/network'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { IRevertOptions } from 'vs/workbench/common/editor'; -import { basename } from 'vs/base/common/path'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { IDisposable } from 'vs/base/common/lifecycle'; export enum CellKind { Markdown = 1, @@ -102,6 +106,13 @@ export interface NotebookCellMetadata { custom?: { [key: string]: unknown }; } +export type TransientMetadata = { [K in keyof NotebookCellMetadata]?: boolean }; + +export interface TransientOptions { + transientOutputs: boolean; + transientMetadata: TransientMetadata; +} + export interface INotebookDisplayOrder { defaultOrder: string[]; userOrder?: string[]; @@ -114,11 +125,12 @@ export interface INotebookMimeTypeSelector { export interface INotebookRendererInfo { id: string; displayName: string; + entrypoint: URI; + preloads: ReadonlyArray; + extensionLocation: URI; extensionId: ExtensionIdentifier; - extensionLocation: URI, - preloads: URI[], - render(uri: URI, request: IOutputRenderRequest): Promise | undefined>; - render2(uri: URI, request: IOutputRenderRequest): Promise | undefined>; + + matches(mimeType: string): boolean; } export interface INotebookKernelInfo { @@ -190,9 +202,7 @@ export enum MimeTypeRendererResolver { export interface IOrderedMimeType { mimeType: string; - isResolved: boolean; - rendererId?: string; - output?: string; + rendererId: string; } export interface ITransformedDisplayOutputDto { @@ -212,6 +222,11 @@ export interface IGenericOutput { transformedOutput?: { [key: string]: IDisplayOutput }; } + +export const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich + ? ({ ...output, outputId: id }) : output; + + export type IProcessedOutput = ITransformedDisplayOutputDto | IStreamOutput | IErrorOutput; export type IRawOutput = IDisplayOutput | IStreamOutput | IErrorOutput; @@ -280,17 +295,41 @@ export interface INotebookTextModel { readonly versionId: number; languages: string[]; cells: ICell[]; - renderers: Set; onDidChangeCells?: Event<{ synchronous: boolean, splices: NotebookCellTextModelSplice[] }>; onDidChangeContent: Event; onWillDispose(listener: () => void): IDisposable; } -export interface IRenderOutput { - shadowContent?: string; +export const enum RenderOutputType { + None, + Html, + Extension +} + +export interface IRenderNoOutput { + type: RenderOutputType.None; hasDynamicHeight: boolean; } +export interface IRenderPlainHtmlOutput { + type: RenderOutputType.Html; + source: IProcessedOutput; + htmlContent: string; + hasDynamicHeight: boolean; +} + +export interface IRenderOutputViaExtension { + type: RenderOutputType.Extension; + source: IProcessedOutput; + mimeType: string; + renderer: INotebookRendererInfo; +} + +export type IInsetRenderOutput = IRenderPlainHtmlOutput | IRenderOutputViaExtension; +export type IRenderOutput = IRenderNoOutput | IInsetRenderOutput; + +export const outputHasDynamicHeight = (o: IRenderOutput) => o.type !== RenderOutputType.Extension && o.hasDynamicHeight; + export type NotebookCellTextModelSplice = [ number /* start */, number, @@ -371,41 +410,49 @@ export interface NotebookCellsChangeMetadataEvent { readonly kind: NotebookCellsChangeType.ChangeMetadata; readonly versionId: number; readonly index: number; - readonly metadata: NotebookCellMetadata; + readonly metadata: NotebookCellMetadata | undefined; } export type NotebookCellsChangedEvent = NotebookCellsInitializeEvent | NotebookCellsModelChangedEvent | NotebookCellsModelMoveEvent | NotebookCellClearOutputEvent | NotebookCellsClearOutputEvent | NotebookCellsChangeLanguageEvent | NotebookCellsChangeMetadataEvent; -export enum CellEditType { - Insert = 1, - Delete = 2 + +export const enum CellEditType { + Replace = 1, + Output = 2, + Metadata = 3, } export interface ICellDto2 { - source: string | string[]; + source: string; language: string; cellKind: CellKind; outputs: IProcessedOutput[]; metadata?: NotebookCellMetadata; } -export interface ICellInsertEdit { - editType: CellEditType.Insert; +export interface ICellReplaceEdit { + editType: CellEditType.Replace; index: number; + count: number; cells: ICellDto2[]; } -export interface ICellDeleteEdit { - editType: CellEditType.Delete; +export interface ICellOutputEdit { + editType: CellEditType.Output; index: number; - count: number; + outputs: IProcessedOutput[]; } -export type ICellEditOperation = ICellInsertEdit | ICellDeleteEdit; +export interface ICellMetadataEdit { + editType: CellEditType.Metadata; + index: number; + metadata: NotebookCellMetadata; +} + +export type ICellEditOperation = ICellReplaceEdit | ICellOutputEdit | ICellMetadataEdit; export interface INotebookEditData { documentVersionId: number; edits: ICellEditOperation[]; - renderers: number[]; } export interface NotebookDataDto { @@ -527,7 +574,7 @@ interface IMutableSplice extends ISplice { deleteCount: number; } -export function diff(before: T[], after: T[], contains: (a: T) => boolean): ISplice[] { +export function diff(before: T[], after: T[], contains: (a: T) => boolean, equal: (a: T, b: T) => boolean = (a: T, b: T) => a === b): ISplice[] { const result: IMutableSplice[] = []; function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { @@ -562,7 +609,7 @@ export function diff(before: T[], after: T[], contains: (a: T) => boolean): I const beforeElement = before[beforeIdx]; const afterElement = after[afterIdx]; - if (beforeElement === afterElement) { + if (equal(beforeElement, afterElement)) { // equal beforeIdx += 1; afterIdx += 1; @@ -602,6 +649,12 @@ export interface INotebookEditorModel extends IEditorModel { revert(options?: IRevertOptions | undefined): Promise; } +export interface INotebookDiffEditorModel extends IEditorModel { + original: INotebookEditorModel; + modified: INotebookEditorModel; + resolveOriginalFromDisk(): Promise; +} + export interface INotebookTextModelBackup { metadata: NotebookDocumentMetadata; languages: string[]; @@ -692,3 +745,43 @@ export interface INotebookKernelProvider { executeNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; cancelNotebook(uri: URI, kernelId: string, handle: number | undefined): Promise; } + +export class CellSequence implements ISequence { + + constructor(readonly textModel: NotebookTextModel) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getHashValue(); + } + + return hashValue; + } +} + +export interface INotebookDiffResult { + cellsDiff: IDiffResult, + linesDiff?: { originalCellhandle: number, modifiedCellhandle: number, lineChanges: editorCommon.ILineChange[] }[]; +} + +export interface INotebookCellStatusBarEntry { + readonly cellResource: URI; + readonly alignment: CellStatusbarAlignment; + readonly priority?: number; + readonly text: string; + readonly tooltip: string | undefined; + readonly command: string | Command | undefined; + readonly accessibilityInformation?: IAccessibilityInformation; + readonly visible: boolean; +} + +export const DisplayOrderKey = 'notebook.displayOrder'; +export const CellToolbarLocKey = 'notebook.cellToolbarLocation'; +export const ShowCellStatusbarKey = 'notebook.showCellStatusbar'; + +export const enum CellStatusbarAlignment { + LEFT, + RIGHT +} diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts index a0e064a2aec..9ef0ab0a5ff 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModel.ts @@ -84,7 +84,7 @@ export class NotebookEditorModel extends EditorModel implements IWorkingCopy, IN this._register(this._workingCopyService.registerWorkingCopy(workingCopyAdapter)); } - capabilities = 0; + capabilities = WorkingCopyCapabilities.None; async backup(): Promise> { if (this._notebook.supportBackup) { diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts index cedfaeef088..11587e61242 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorModelResolverService.ts @@ -15,7 +15,7 @@ export const INotebookEditorModelResolverService = createDecorator>; + resolve(resource: URI, viewType?: string, editorId?: string): Promise>; } @@ -30,9 +30,16 @@ export class NotebookModelReferenceCollection extends ReferenceCollection { - const [viewType, editorId] = args as [string, string | undefined]; - const resource = URI.parse(key); + + let [viewType, editorId] = args as [string | undefined, string | undefined]; + if (!viewType) { + viewType = this._notebookService.getContributedNotebookProviders(resource)[0]?.id; + } + if (!viewType) { + throw new Error('Missing viewType'); + } + const model = this._instantiationService.createInstance(NotebookEditorModel, resource, viewType); const promise = model.load({ editorId }); return promise; @@ -60,7 +67,7 @@ export class NotebookModelResolverService implements INotebookEditorModelResolve this._data = instantiationService.createInstance(NotebookModelReferenceCollection); } - async resolve(resource: URI, viewType: string, editorId?: string | undefined): Promise> { + async resolve(resource: URI, viewType?: string, editorId?: string | undefined): Promise> { const reference = this._data.acquire(resource.toString(), viewType, editorId); const model = await reference.object; return { @@ -69,3 +76,9 @@ export class NotebookModelResolverService implements INotebookEditorModelResolve }; } } + +// notebookService.onDidAddDocument + +// resolve() + +// notebookService.onDidRemoveDocument ... diff --git a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts index b22c64145eb..4ac836f1f9d 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookOutputRenderer.ts @@ -4,31 +4,42 @@ *--------------------------------------------------------------------------------------------*/ import * as glob from 'vs/base/common/glob'; +import { joinPath } from 'vs/base/common/resources'; +import { URI } from 'vs/base/common/uri'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { INotebookRendererInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -export class NotebookOutputRendererInfo { +export class NotebookOutputRendererInfo implements INotebookRendererInfo { readonly id: string; + readonly entrypoint: URI; readonly displayName: string; - readonly mimeTypes: readonly string[]; - readonly mimeTypeGlobs: glob.ParsedPattern[]; + readonly extensionLocation: URI; + readonly extensionId: ExtensionIdentifier; + // todo: re-add preloads in pure renderer API + readonly preloads: ReadonlyArray = []; + + private readonly mimeTypes: readonly string[]; + private readonly mimeTypeGlobs: glob.ParsedPattern[]; constructor(descriptor: { readonly id: string; readonly displayName: string; + readonly entrypoint: string; readonly mimeTypes: readonly string[]; + readonly extension: IExtensionDescription; }) { this.id = descriptor.id; + this.extensionId = descriptor.extension.identifier; + this.extensionLocation = descriptor.extension.extensionLocation; + this.entrypoint = joinPath(this.extensionLocation, descriptor.entrypoint); this.displayName = descriptor.displayName; this.mimeTypes = descriptor.mimeTypes; this.mimeTypeGlobs = this.mimeTypes.map(pattern => glob.parse(pattern)); } matches(mimeType: string) { - const matched = this.mimeTypeGlobs.find(pattern => pattern(mimeType)); - if (matched) { - return true; - } - - return this.mimeTypes.find(pattern => pattern === mimeType); + return this.mimeTypeGlobs.some(pattern => pattern(mimeType)) + || this.mimeTypes.some(pattern => pattern === mimeType); } } diff --git a/src/vs/workbench/contrib/notebook/common/notebookService.ts b/src/vs/workbench/contrib/notebook/common/notebookService.ts index 87fad7fb364..9e7a32ec7a1 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookService.ts @@ -10,7 +10,7 @@ import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.pr import { Event } from 'vs/base/common/event'; import { INotebookTextModel, INotebookRendererInfo, INotebookKernelInfo, INotebookKernelInfoDto, - IEditor, ICellEditOperation, NotebookCellOutputsSplice, IOrderedMimeType, IProcessedOutput, INotebookKernelProvider, INotebookKernelInfo2 + IEditor, ICellEditOperation, NotebookCellOutputsSplice, INotebookKernelProvider, INotebookKernelInfo2, TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -24,6 +24,7 @@ export const INotebookService = createDecorator('notebookServi export interface IMainNotebookController { kernel: INotebookKernelInfoDto | undefined; supportBackup: boolean; + options: { transientOutputs: boolean; transientMetadata: TransientMetadata; }; createNotebook(textModel: NotebookTextModel, editorId?: string, backupId?: string): Promise; reloadNotebook(mainthreadTextModel: NotebookTextModel): Promise; resolveNotebookEditor(viewType: string, uri: URI, editorId: string): Promise; @@ -52,11 +53,8 @@ export interface INotebookService { onDidChangeNotebookActiveKernel: Event<{ uri: URI, providerHandle: number | undefined, kernelId: string | undefined }>; registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController): void; unregisterNotebookProvider(viewType: string): void; - registerNotebookRenderer(id: string, renderer: INotebookRendererInfo): void; - unregisterNotebookRenderer(id: string): void; - transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]): Promise; - transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]): Promise; - transformSingleOutput(textModel: NotebookTextModel, output: IProcessedOutput, rendererId: string, mimeType: string): Promise; + transformEditsOutputs(textModel: NotebookTextModel, edits: ICellEditOperation[]): void; + transformSpliceOutputs(textModel: NotebookTextModel, splices: NotebookCellOutputsSplice[]): void; registerNotebookKernel(kernel: INotebookKernelInfo): void; unregisterNotebookKernel(id: string): void; registerNotebookKernelProvider(provider: INotebookKernelProvider): IDisposable; diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts new file mode 100644 index 00000000000..547f3678545 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookSimpleWorker.ts @@ -0,0 +1,221 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { ISequence, LcsDiff } from 'vs/base/common/diff/diff'; +import { hash } from 'vs/base/common/hash'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IRequestHandler } from 'vs/base/common/worker/simpleWorker'; +import * as model from 'vs/editor/common/model'; +import { PieceTreeTextBufferBuilder } from 'vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder'; +import { CellKind, ICellDto2, IMainCellDto, INotebookDiffResult, IProcessedOutput, NotebookCellMetadata, NotebookDataDto, NotebookDocumentMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { Range } from 'vs/editor/common/core/range'; +import { EditorWorkerHost } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl'; + +class MirrorCell { + private _textBuffer!: model.IReadonlyTextBuffer; + + get textBuffer() { + if (this._textBuffer) { + return this._textBuffer; + } + + const builder = new PieceTreeTextBufferBuilder(); + builder.acceptChunk(Array.isArray(this._source) ? this._source.join('\n') : this._source); + const bufferFactory = builder.finish(true); + this._textBuffer = bufferFactory.create(model.DefaultEndOfLine.LF); + + return this._textBuffer; + } + + private _primaryKey?: number | null = null; + primaryKey(): number | null { + if (this._primaryKey === undefined) { + this._primaryKey = hash(this.getValue()); + } + + return this._primaryKey; + } + + private _hash: number | null = null; + + constructor( + readonly handle: number, + private _source: string | string[], + readonly language: string, + readonly cellKind: CellKind, + readonly outputs: IProcessedOutput[], + readonly metadata?: NotebookCellMetadata + + ) { } + + getFullModelRange() { + const lineCount = this.textBuffer.getLineCount(); + return new Range(1, 1, lineCount, this.textBuffer.getLineLength(lineCount) + 1); + } + + getValue(): string { + const fullRange = this.getFullModelRange(); + const eol = this.textBuffer.getEOL(); + if (eol === '\n') { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.LF); + } else { + return this.textBuffer.getValueInRange(fullRange, model.EndOfLinePreference.CRLF); + } + } + + getComparisonValue(): number { + if (this._primaryKey !== null) { + return this._primaryKey!; + } + + this._hash = hash([hash(this.getValue()), this.metadata]); + return this._hash; + } + + getHashValue() { + if (this._hash !== null) { + return this._hash; + } + + this._hash = hash([hash(this.getValue()), this.language, this.metadata]); + return this._hash; + } +} + +class MirrorNotebookDocument { + constructor( + readonly uri: URI, + readonly cells: MirrorCell[], + readonly languages: string[], + readonly metadata: NotebookDocumentMetadata, + ) { + } +} + +export class CellSequence implements ISequence { + + constructor(readonly textModel: MirrorNotebookDocument) { + } + + getElements(): string[] | number[] | Int32Array { + const hashValue = new Int32Array(this.textModel.cells.length); + for (let i = 0; i < this.textModel.cells.length; i++) { + hashValue[i] = this.textModel.cells[i].getComparisonValue(); + } + + return hashValue; + } + + getCellHash(cell: ICellDto2) { + const source = Array.isArray(cell.source) ? cell.source.join('\n') : cell.source; + const hashVal = hash([hash(source), cell.metadata]); + return hashVal; + } +} + +export class NotebookEditorSimpleWorker implements IRequestHandler, IDisposable { + _requestHandlerBrand: any; + + private _models: { [uri: string]: MirrorNotebookDocument; }; + + constructor() { + this._models = Object.create(null); + } + dispose(): void { + } + + public acceptNewModel(uri: string, data: NotebookDataDto): void { + this._models[uri] = new MirrorNotebookDocument(URI.parse(uri), data.cells.map(dto => new MirrorCell( + (dto as unknown as IMainCellDto).handle, + dto.source, + dto.language, + dto.cellKind, + dto.outputs, + dto.metadata + )), data.languages, data.metadata); + } + + public acceptRemovedModel(strURL: string): void { + if (!this._models[strURL]) { + return; + } + delete this._models[strURL]; + } + + computeDiff(originalUrl: string, modifiedUrl: string): INotebookDiffResult { + const original = this._getModel(originalUrl); + const modified = this._getModel(modifiedUrl); + + const diff = new LcsDiff(new CellSequence(original), new CellSequence(modified)); + const diffResult = diff.ComputeDiff(false); + + /* let cellLineChanges: { originalCellhandle: number, modifiedCellhandle: number, lineChanges: editorCommon.ILineChange[] }[] = []; + + diffResult.changes.forEach(change => { + if (change.modifiedLength === 0) { + // deletion ... + return; + } + + if (change.originalLength === 0) { + // insertion + return; + } + + for (let i = 0, len = Math.min(change.modifiedLength, change.originalLength); i < len; i++) { + let originalIndex = change.originalStart + i; + let modifiedIndex = change.modifiedStart + i; + + const originalCell = original.cells[originalIndex]; + const modifiedCell = modified.cells[modifiedIndex]; + + if (originalCell.getValue() !== modifiedCell.getValue()) { + // console.log(`original cell ${originalIndex} content change`); + const originalLines = originalCell.textBuffer.getLinesContent(); + const modifiedLines = modifiedCell.textBuffer.getLinesContent(); + const diffComputer = new DiffComputer(originalLines, modifiedLines, { + shouldComputeCharChanges: true, + shouldPostProcessCharChanges: true, + shouldIgnoreTrimWhitespace: false, + shouldMakePrettyDiff: true, + maxComputationTime: 5000 + }); + + const lineChanges = diffComputer.computeDiff().changes; + + cellLineChanges.push({ + originalCellhandle: originalCell.handle, + modifiedCellhandle: modifiedCell.handle, + lineChanges + }); + + // console.log(lineDecorations); + + } else { + // console.log(`original cell ${originalIndex} metadata change`); + } + + } + }); + */ + return { + cellsDiff: diffResult, + // linesDiff: cellLineChanges + }; + } + + protected _getModel(uri: string): MirrorNotebookDocument { + return this._models[uri]; + } +} + +/** + * Called on the worker side + * @internal + */ +export function create(host: EditorWorkerHost): IRequestHandler { + return new NotebookEditorSimpleWorker(); +} + diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts new file mode 100644 index 00000000000..f85fd6f5cc9 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ILineChange } from 'vs/editor/common/editorCommon'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; + +export const ID_NOTEBOOK_EDITOR_WORKER_SERVICE = 'notebookEditorWorkerService'; +export const INotebookEditorWorkerService = createDecorator(ID_NOTEBOOK_EDITOR_WORKER_SERVICE); + +export interface IDiffComputationResult { + quitEarly: boolean; + identical: boolean; + changes: ILineChange[]; +} + +export interface INotebookEditorWorkerService { + readonly _serviceBrand: undefined; + + canComputeDiff(original: URI, modified: URI): boolean; + computeDiff(original: URI, modified: URI): Promise; +} diff --git a/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts new file mode 100644 index 00000000000..b209d8c7fd4 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/common/services/notebookWorkerServiceImpl.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableStore, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { SimpleWorkerClient } from 'vs/base/common/worker/simpleWorker'; +import { DefaultWorkerFactory } from 'vs/base/worker/defaultWorkerFactory'; +import { INotebookDiffResult } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { NotebookEditorSimpleWorker } from 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker'; +import { INotebookEditorWorkerService } from 'vs/workbench/contrib/notebook/common/services/notebookWorkerService'; + +export class NotebookEditorWorkerServiceImpl extends Disposable implements INotebookEditorWorkerService { + declare readonly _serviceBrand: undefined; + + private readonly _workerManager: WorkerManager; + + constructor( + @INotebookService notebookService: INotebookService + ) { + super(); + + this._workerManager = this._register(new WorkerManager(notebookService)); + } + canComputeDiff(original: URI, modified: URI): boolean { + throw new Error('Method not implemented.'); + } + + computeDiff(original: URI, modified: URI): Promise { + return this._workerManager.withWorker().then(client => { + return client.computeDiff(original, modified); + }); + } +} + +export class WorkerManager extends Disposable { + private _editorWorkerClient: NotebookWorkerClient | null; + // private _lastWorkerUsedTime: number; + + constructor( + private readonly _notebookService: INotebookService + ) { + super(); + this._editorWorkerClient = null; + // this._lastWorkerUsedTime = (new Date()).getTime(); + } + + withWorker(): Promise { + // this._lastWorkerUsedTime = (new Date()).getTime(); + if (!this._editorWorkerClient) { + this._editorWorkerClient = new NotebookWorkerClient(this._notebookService, 'notebookEditorWorkerService'); + } + return Promise.resolve(this._editorWorkerClient); + } +} + +export interface IWorkerClient { + getProxyObject(): Promise; + dispose(): void; +} + +export class NotebookEditorModelManager extends Disposable { + private _syncedModels: { [modelUrl: string]: IDisposable; } = Object.create(null); + private _syncedModelsLastUsedTime: { [modelUrl: string]: number; } = Object.create(null); + + constructor( + private readonly _proxy: NotebookEditorSimpleWorker, + private readonly _notebookService: INotebookService + ) { + super(); + } + + public ensureSyncedResources(resources: URI[]): void { + for (const resource of resources) { + let resourceStr = resource.toString(); + + if (!this._syncedModels[resourceStr]) { + this._beginModelSync(resource); + } + if (this._syncedModels[resourceStr]) { + this._syncedModelsLastUsedTime[resourceStr] = (new Date()).getTime(); + } + } + } + + private _beginModelSync(resource: URI): void { + let model = this._notebookService.listNotebookDocuments().find(document => document.uri.toString() === resource.toString()); + if (!model) { + return; + } + + let modelUrl = resource.toString(); + + this._proxy.acceptNewModel( + model.uri.toString(), + { + cells: model.cells.map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.getValue(), + eol: cell.textBuffer.getEOL(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + })), + languages: model.languages, + metadata: model.metadata + } + ); + + const toDispose = new DisposableStore(); + + // TODO, accept Model change + + // toDispose.add(model.onDidChangeContent((e) => { + // this._proxy.acceptModelChanged(modelUrl.toString(), e); + // })); + toDispose.add(model.onWillDispose(() => { + this._stopModelSync(modelUrl); + })); + toDispose.add(toDisposable(() => { + this._proxy.acceptRemovedModel(modelUrl); + })); + + this._syncedModels[modelUrl] = toDispose; + } + + private _stopModelSync(modelUrl: string): void { + let toDispose = this._syncedModels[modelUrl]; + delete this._syncedModels[modelUrl]; + delete this._syncedModelsLastUsedTime[modelUrl]; + dispose(toDispose); + } +} + +export class EditorWorkerHost { + + private readonly _workerClient: NotebookWorkerClient; + + constructor(workerClient: NotebookWorkerClient) { + this._workerClient = workerClient; + } + + // foreign host request + public fhr(method: string, args: any[]): Promise { + return this._workerClient.fhr(method, args); + } +} + +export class NotebookWorkerClient extends Disposable { + private _worker: IWorkerClient | null; + private readonly _workerFactory: DefaultWorkerFactory; + private _modelManager: NotebookEditorModelManager | null; + + + constructor(private readonly _notebookService: INotebookService, label: string) { + super(); + this._workerFactory = new DefaultWorkerFactory(label); + this._worker = null; + this._modelManager = null; + + } + + // foreign host request + public fhr(method: string, args: any[]): Promise { + throw new Error(`Not implemented!`); + } + + computeDiff(original: URI, modified: URI) { + return this._withSyncedResources([original, modified]).then(proxy => { + return proxy.computeDiff(original.toString(), modified.toString()); + }); + } + + private _getOrCreateModelManager(proxy: NotebookEditorSimpleWorker): NotebookEditorModelManager { + if (!this._modelManager) { + this._modelManager = this._register(new NotebookEditorModelManager(proxy, this._notebookService)); + } + return this._modelManager; + } + + protected _withSyncedResources(resources: URI[]): Promise { + return this._getProxy().then((proxy) => { + this._getOrCreateModelManager(proxy).ensureSyncedResources(resources); + return proxy; + }); + } + + private _getOrCreateWorker(): IWorkerClient { + if (!this._worker) { + try { + this._worker = this._register(new SimpleWorkerClient( + this._workerFactory, + 'vs/workbench/contrib/notebook/common/services/notebookSimpleWorker', + new EditorWorkerHost(this) + )); + } catch (err) { + // logOnceWebWorkerWarning(err); + // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); + throw (err); + } + } + return this._worker; + } + + protected _getProxy(): Promise { + return this._getOrCreateWorker().getProxyObject().then(undefined, (err) => { + // logOnceWebWorkerWarning(err); + // this._worker = new SynchronousWorkerClient(new EditorSimpleWorker(new EditorWorkerHost(this), null)); + // return this._getOrCreateWorker().getProxyObject(); + throw (err); + }); + } + + +} diff --git a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts index d092389a66e..38a91a236ce 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookCommon.test.ts @@ -269,7 +269,7 @@ suite('NotebookCommon', () => { for (let i = 0; i < 5; i++) { cells.push( - new TestCell('notebook', i, [`var a = ${i};`], 'javascript', CellKind.Code, [], textModelService) + new TestCell('notebook', i, `var a = ${i};`, 'javascript', CellKind.Code, [], textModelService) ); } @@ -295,8 +295,8 @@ suite('NotebookCommon', () => { ] ); - const cellA = new TestCell('notebook', 6, ['var a = 6;'], 'javascript', CellKind.Code, [], textModelService); - const cellB = new TestCell('notebook', 7, ['var a = 7;'], 'javascript', CellKind.Code, [], textModelService); + const cellA = new TestCell('notebook', 6, 'var a = 6;', 'javascript', CellKind.Code, [], textModelService); + const cellB = new TestCell('notebook', 7, 'var a = 7;', 'javascript', CellKind.Code, [], textModelService); const modifiedCells = [ cells[0], diff --git a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts index 23ffd1fb439..010ddba6af2 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookTextModel.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { CellKind, CellEditType } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellEditType, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { withTestNotebook, TestCell, setupInstantiationService } from 'vs/workbench/contrib/notebook/test/testNotebookEditor'; import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; @@ -23,15 +23,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(viewModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 6); @@ -48,15 +48,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 6, ['var f = 6;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 6, 'var f = 6;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 6); @@ -73,15 +73,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Delete, index: 3, count: 1 }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 3, count: 1, cells: [] }, ], true); assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); @@ -96,15 +96,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Insert, index: 3, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 3, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 4); @@ -121,15 +121,15 @@ suite('NotebookTextModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel, textModel) => { - textModel.$applyEdit(textModel.versionId, [ - { editType: CellEditType.Delete, index: 1, count: 1 }, - { editType: CellEditType.Insert, index: 1, cells: [new TestCell(viewModel.viewType, 5, ['var e = 5;'], 'javascript', CellKind.Code, [], textModelService)] }, + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [] }, + { editType: CellEditType.Replace, index: 1, count: 0, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, ], true); assert.equal(textModel.cells.length, 4); @@ -139,5 +139,113 @@ suite('NotebookTextModel', () => { } ); }); -}); + test('(replace) delete + insert at same position', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false }] + ], + (editor, viewModel, textModel) => { + textModel.applyEdit(textModel.versionId, [ + { editType: CellEditType.Replace, index: 1, count: 1, cells: [new TestCell(viewModel.viewType, 5, 'var e = 5;', 'javascript', CellKind.Code, [], textModelService)] }, + ], true); + + assert.equal(textModel.cells.length, 4); + assert.equal(textModel.cells[0].getValue(), 'var a = 1;'); + assert.equal(textModel.cells[1].getValue(), 'var e = 5;'); + assert.equal(textModel.cells[2].getValue(), 'var c = 3;'); + } + ); + }); + + test('output', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor, viewModel, textModel) => { + + // invalid index 1 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: Number.MAX_VALUE, + editType: CellEditType.Output, + outputs: [] + }], true); + }); + + // invalid index 2 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: -1, + editType: CellEditType.Output, + outputs: [] + }], true); + }); + + textModel.applyEdit(textModel.versionId, [{ + index: 0, + editType: CellEditType.Output, + outputs: [{ + outputKind: CellOutputKind.Rich, + outputId: 'someId', + data: { 'text/markdown': '_Hello_' } + }] + }], true); + + assert.equal(textModel.cells.length, 1); + assert.equal(textModel.cells[0].outputs.length, 1); + assert.equal(textModel.cells[0].outputs[0].outputKind, CellOutputKind.Rich); + } + ); + }); + + test('metadata', function () { + withTestNotebook( + instantiationService, + blukEditService, + undoRedoService, + [ + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ], + (editor, viewModel, textModel) => { + + // invalid index 1 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: Number.MAX_VALUE, + editType: CellEditType.Metadata, + metadata: { editable: false } + }], true); + }); + + // invalid index 2 + assert.throws(() => { + textModel.applyEdit(textModel.versionId, [{ + index: -1, + editType: CellEditType.Metadata, + metadata: { editable: false } + }], true); + }); + + textModel.applyEdit(textModel.versionId, [{ + index: 0, + editType: CellEditType.Metadata, + metadata: { editable: false }, + }], true); + + assert.equal(textModel.cells.length, 1); + assert.equal(textModel.cells[0].metadata?.editable, false); + } + ); + }); +}); diff --git a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts index 052842994fa..70d9b950c3f 100644 --- a/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts +++ b/src/vs/workbench/contrib/notebook/test/notebookViewModel.test.ts @@ -36,14 +36,14 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: false }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: false }] ], (editor, viewModel) => { assert.equal(viewModel.viewCells[0].metadata?.editable, true); assert.equal(viewModel.viewCells[1].metadata?.editable, false); - const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, ['var c = 3;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell = viewModel.insertCell(1, new TestCell(viewModel.viewType, 0, 'var c = 3;', 'javascript', CellKind.Code, [], textModelService), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); assert.equal(viewModel.getCellIndex(cell), 1); @@ -62,9 +62,9 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['//a'], 'javascript', CellKind.Code, [], { editable: true }], - [['//b'], 'javascript', CellKind.Code, [], { editable: true }], - [['//c'], 'javascript', CellKind.Code, [], { editable: true }], + ['//a', 'javascript', CellKind.Code, [], { editable: true }], + ['//b', 'javascript', CellKind.Code, [], { editable: true }], + ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { viewModel.moveCellToIdx(0, 1, 0, false); @@ -93,9 +93,9 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['//a'], 'javascript', CellKind.Code, [], { editable: true }], - [['//b'], 'javascript', CellKind.Code, [], { editable: true }], - [['//c'], 'javascript', CellKind.Code, [], { editable: true }], + ['//a', 'javascript', CellKind.Code, [], { editable: true }], + ['//b', 'javascript', CellKind.Code, [], { editable: true }], + ['//c', 'javascript', CellKind.Code, [], { editable: true }], ], (editor, viewModel) => { viewModel.moveCellToIdx(1, 1, 0, false); @@ -118,21 +118,21 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], { editable: true }], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true }] + ['var a = 1;', 'javascript', CellKind.Code, [], { editable: true }], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true }] ], (editor, viewModel) => { const firstViewCell = viewModel.viewCells[0]; const lastViewCell = viewModel.viewCells[viewModel.viewCells.length - 1]; const insertIndex = viewModel.getCellIndex(firstViewCell) + 1; - const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, ['var c = 3;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell = viewModel.insertCell(insertIndex, new TestCell(viewModel.viewType, 3, 'var c = 3;', 'javascript', CellKind.Code, [], textModelService), true); const addedCellIndex = viewModel.getCellIndex(cell); viewModel.deleteCell(addedCellIndex, true); const secondInsertIndex = viewModel.getCellIndex(lastViewCell) + 1; - const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, ['var d = 4;'], 'javascript', CellKind.Code, [], textModelService), true); + const cell2 = viewModel.insertCell(secondInsertIndex, new TestCell(viewModel.viewType, 4, 'var d = 4;', 'javascript', CellKind.Code, [], textModelService), true); assert.equal(viewModel.viewCells.length, 3); assert.equal(viewModel.notebookDocument.cells.length, 3); @@ -147,11 +147,11 @@ suite('NotebookViewModel', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { viewModel.notebookDocument.metadata = { editable: true, runnable: true, cellRunnable: true, cellEditable: true, cellHasExecutionOrder: true }; @@ -269,11 +269,11 @@ suite('NotebookViewModel Decorations', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 2 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -283,7 +283,7 @@ suite('NotebookViewModel Decorations', () => { end: 2, }); - viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, ['var d = 6;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(0, new TestCell(viewModel.viewType, 5, 'var d = 6;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 2, @@ -297,7 +297,7 @@ suite('NotebookViewModel Decorations', () => { end: 2 }); - viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, ['var d = 7;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(3, new TestCell(viewModel.viewType, 6, 'var d = 7;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, @@ -327,13 +327,13 @@ suite('NotebookViewModel Decorations', () => { blukEditService, undoRedoService, [ - [['var a = 1;'], 'javascript', CellKind.Code, [], {}], - [['var b = 2;'], 'javascript', CellKind.Code, [], { editable: true, runnable: true }], - [['var c = 3;'], 'javascript', CellKind.Code, [], { editable: true, runnable: false }], - [['var d = 4;'], 'javascript', CellKind.Code, [], { editable: false, runnable: true }], - [['var e = 5;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], - [['var e = 6;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], - [['var e = 7;'], 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var a = 1;', 'javascript', CellKind.Code, [], {}], + ['var b = 2;', 'javascript', CellKind.Code, [], { editable: true, runnable: true }], + ['var c = 3;', 'javascript', CellKind.Code, [], { editable: true, runnable: false }], + ['var d = 4;', 'javascript', CellKind.Code, [], { editable: false, runnable: true }], + ['var e = 5;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 6;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], + ['var e = 7;', 'javascript', CellKind.Code, [], { editable: false, runnable: false }], ], (editor, viewModel) => { const trackedId = viewModel.setTrackedRange('test', { start: 1, end: 3 }, TrackedRangeStickiness.GrowsOnlyWhenTypingAfter); @@ -343,14 +343,14 @@ suite('NotebookViewModel Decorations', () => { end: 3 }); - viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, ['var d = 9;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(5, new TestCell(viewModel.viewType, 8, 'var d = 9;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, end: 3 }); - viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, ['var d = 10;'], 'javascript', CellKind.Code, [], textModelService), true); + viewModel.insertCell(4, new TestCell(viewModel.viewType, 9, 'var d = 10;', 'javascript', CellKind.Code, [], textModelService), true); assert.deepEqual(viewModel.getTrackedRange(trackedId!), { start: 1, diff --git a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts index 7f43e90969f..7953687f0ce 100644 --- a/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/test/testNotebookEditor.ts @@ -12,13 +12,13 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { Range } from 'vs/editor/common/core/range'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { EditorModel } from 'vs/workbench/common/editor'; -import { ICellRange, ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { ICellRange, ICellViewModel, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, INotebookDeltaDecoration, INotebookEditorCreationOptions, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { NotebookEventDispatcher } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; import { CellViewModel, IModelDecorationsChangeAccessor, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, CellUri, INotebookEditorModel, IProcessedOutput, NotebookCellMetadata, INotebookKernelInfo, IInsetRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; import { NotImplementedError } from 'vs/base/common/errors'; @@ -39,17 +39,18 @@ export class TestCell extends NotebookCellTextModel { constructor( public viewType: string, handle: number, - public source: string[], + public source: string, language: string, cellKind: CellKind, outputs: IProcessedOutput[], modelService: ITextModelService ) { - super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, modelService); + super(CellUri.generate(URI.parse('test:///fake/notebook'), handle), handle, source, language, cellKind, outputs, undefined, { transientMetadata: {}, transientOutputs: false }, modelService); } } export class TestNotebookEditor implements INotebookEditor { + isEmbedded = false; private _isDisposed = false; get isDisposed() { @@ -59,10 +60,15 @@ export class TestNotebookEditor implements INotebookEditor { get viewModel() { return undefined; } + creationOptions: INotebookEditorCreationOptions = { isEmbedded: false }; constructor( ) { } + setOptions(options: NotebookEditorOptions | undefined): Promise { + throw new Error('Method not implemented.'); + } + hideInset(output: IProcessedOutput): void { throw new Error('Method not implemented.'); } @@ -71,6 +77,7 @@ export class TestNotebookEditor implements INotebookEditor { onDidChangeAvailableKernels: Event = new Emitter().event; onDidChangeActiveCell: Event = new Emitter().event; onDidScroll = new Emitter().event; + onWillDispose = new Emitter().event; uri?: URI | undefined; textModel?: NotebookTextModel | undefined; @@ -255,7 +262,7 @@ export class TestNotebookEditor implements INotebookEditor { // throw new Error('Method not implemented.'); return; } - createInset(cell: CellViewModel, output: IProcessedOutput, shadowContent: string, offset: number): Promise { + createInset(cell: CellViewModel, output: IInsetRenderOutput, offset: number): Promise { return Promise.resolve(); } removeInset(output: IProcessedOutput): void { @@ -277,6 +284,10 @@ export class TestNotebookEditor implements INotebookEditor { throw new Error('Method not implemented.'); } + deltaCellDecorations(oldDecorations: string[], newDecorations: INotebookDeltaDecoration[]): string[] { + throw new Error('Method not implemented.'); + } + deltaCellOutputContainerClassNames(cellId: string, added: string[], removed: string[]): void { throw new Error('Method not implemented.'); } @@ -371,15 +382,21 @@ export function setupInstantiationService() { return instantiationService; } -export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string[], string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { +export function withTestNotebook(instantiationService: TestInstantiationService, blukEditService: IBulkEditService, undoRedoService: IUndoRedoService, cells: [string, string, CellKind, IProcessedOutput[], NotebookCellMetadata][], callback: (editor: TestNotebookEditor, viewModel: NotebookViewModel, textModel: NotebookTextModel) => void) { const textModelService = instantiationService.get(ITextModelService); const viewType = 'notebook'; const editor = new TestNotebookEditor(); const notebook = new NotebookTextModel(0, viewType, false, URI.parse('test'), undoRedoService, textModelService); - notebook.cells = cells.map((cell, index) => { - return new NotebookCellTextModel(notebook.uri, index, cell[0], cell[1], cell[2], cell[3], cell[4], textModelService); - }); + notebook.initialize(cells.map(cell => { + return { + source: cell[0], + language: cell[1], + cellKind: cell[2], + outputs: cell[3], + metadata: cell[4] + }; + })); const model = new NotebookEditorTestModel(notebook); const eventDispatcher = new NotebookEventDispatcher(); const viewModel = new NotebookViewModel(viewType, model.notebook, eventDispatcher, null, instantiationService, blukEditService, undoRedoService); diff --git a/src/vs/workbench/contrib/output/browser/output.contribution.ts b/src/vs/workbench/contrib/output/browser/output.contribution.ts index 9a131ab24ef..472df16c4f5 100644 --- a/src/vs/workbench/contrib/output/browser/output.contribution.ts +++ b/src/vs/workbench/contrib/output/browser/output.contribution.ts @@ -322,7 +322,7 @@ Registry.as(ConfigurationExtensions.Configuration).regis type: 'boolean', description: nls.localize('output.smartScroll.enabled', "Enable/disable the ability of smart scrolling in the output view. Smart scrolling allows you to lock scrolling automatically when you click in the output view and unlocks when you click in the last line."), default: true, - scope: ConfigurationScope.APPLICATION, + scope: ConfigurationScope.WINDOW, tags: ['output'] } } diff --git a/src/vs/workbench/contrib/output/browser/outputView.ts b/src/vs/workbench/contrib/output/browser/outputView.ts index 2e5bd2cc7eb..337746000d6 100644 --- a/src/vs/workbench/contrib/output/browser/outputView.ts +++ b/src/vs/workbench/contrib/output/browser/outputView.ts @@ -13,7 +13,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { AbstractTextResourceEditor } from 'vs/workbench/browser/parts/editor/textResourceEditor'; import { OUTPUT_VIEW_ID, IOutputService, CONTEXT_IN_OUTPUT, IOutputChannel, CONTEXT_ACTIVE_LOG_OUTPUT, CONTEXT_OUTPUT_SCROLL_LOCK } from 'vs/workbench/contrib/output/common/output'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; @@ -146,7 +146,7 @@ export class OutputViewPane extends ViewPane { this.channelId = channel.id; const descriptor = this.outputService.getChannelDescriptor(channel.id); CONTEXT_ACTIVE_LOG_OUTPUT.bindTo(this.contextKeyService).set(!!descriptor?.file && descriptor?.log); - this.editorPromise = this.editor.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: true }), CancellationToken.None) + this.editorPromise = this.editor.setInput(this.createInput(channel), EditorOptions.create({ preserveFocus: true }), Object.create(null), CancellationToken.None) .then(() => this.editor); } @@ -228,7 +228,7 @@ export class OutputEditor extends AbstractTextResourceEditor { return channel ? nls.localize('outputViewWithInputAriaLabel', "{0}, Output panel", channel.label) : nls.localize('outputViewAriaLabel', "Output panel"); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { const focus = !(options && options.preserveFocus); if (input.matches(this.input)) { return; @@ -238,7 +238,7 @@ export class OutputEditor extends AbstractTextResourceEditor { // Dispose previous input (Output panel is not a workbench editor) this.input.dispose(); } - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); if (focus) { this.focus(); } diff --git a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts index fc4533b84ea..fc27aa4c3ba 100644 --- a/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/keybindingsEditor.ts @@ -14,8 +14,8 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { IAction, Action, Separator } from 'vs/base/common/actions'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { KeybindingsEditorModel, IKeybindingItemEntry, IListEntry, KEYBINDING_ENTRY_TEMPLATE_ID } from 'vs/workbench/services/preferences/common/keybindingsEditorModel'; @@ -58,7 +58,7 @@ interface ColumnItem { const oddRowBackgroundColor = new Color(new RGBA(130, 130, 130, 0.04)); -export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorPane { +export class KeybindingsEditor extends EditorPane implements IKeybindingsEditorPane { static readonly ID: string = 'workbench.editor.keybindings'; @@ -138,9 +138,9 @@ export class KeybindingsEditor extends BaseEditor implements IKeybindingsEditorP this.createBody(keybindingsEditorElement); } - setInput(input: KeybindingsEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: KeybindingsEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.keybindingsEditorContextKey.set(true); - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => this.render(!!(options && options.preserveFocus))); } diff --git a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css index 02f464009af..8b5017293dc 100644 --- a/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css +++ b/src/vs/workbench/contrib/preferences/browser/media/settingsEditor2.css @@ -175,14 +175,14 @@ .settings-editor > .settings-body .settings-tree-container .setting-toolbar-container { position: absolute; - left: -32px; + left: -22px; top: 11px; bottom: 0px; width: 26px; } .settings-editor > .settings-body .settings-tree-container .monaco-list-row .mouseover .setting-toolbar-container > .monaco-toolbar .codicon, -.settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-item-contents.focused .setting-toolbar-container > .monaco-toolbar .codicon, +.settings-editor > .settings-body .settings-tree-container .monaco-list-row.focused .setting-item-contents .setting-toolbar-container > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container:hover > .monaco-toolbar .codicon, .settings-editor > .settings-body .settings-tree-container .monaco-list-row .setting-toolbar-container > .monaco-toolbar .active .codicon { opacity: 1; @@ -283,15 +283,34 @@ max-width: 1000px; margin: auto; box-sizing: border-box; - padding-left: 219px; - padding-right: 20px; + padding-left: 204px; + padding-right: 5px; overflow: visible; } +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { + content: ' '; + position: absolute; + left: 0px; + right: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::before, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::before { + top: 0px; +} + +.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label::after, +.settings-editor > .settings-body > .settings-tree-container .setting-item-contents::after { + bottom: 0px; +} + .settings-editor > .settings-body > .settings-tree-container .setting-item-contents { position: relative; - padding-top: 12px; - padding-bottom: 18px; + padding: 12px 15px 18px; white-space: normal; } @@ -299,11 +318,9 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - display: inline-block; - /* size to contents for hover to show context button */ + display: inline-block; /* size to contents for hover to show context button */ } - .settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-modified-indicator { display: none; } @@ -315,7 +332,7 @@ width: 6px; border-left-width: 2px; border-left-style: solid; - left: -9px; + left: 5px; top: 15px; bottom: 16px; } @@ -528,12 +545,18 @@ } .settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { + display: inline-block; margin: 0px; font-weight: 600; + height: 100%; + box-sizing: border-box; + padding: 10px; + padding-left: 15px; + width: 100%; + position: relative; } .settings-editor > .settings-body > .settings-tree-container .settings-group-level-1 { - padding-top: 23px; font-size: 24px; } @@ -542,10 +565,6 @@ font-size: 20px; } -.settings-editor > .settings-body > .settings-tree-container .settings-group-level-1.settings-group-first { - padding-top: 7px; -} - .settings-editor.search-mode > .settings-body .settings-toc-container .monaco-list-row .settings-toc-count { display: block; } diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 5bfbe8133ab..42a9848038a 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -12,7 +12,7 @@ import * as nls from 'vs/nls'; import { Action2, MenuId, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; +import { InputFocusedContext, IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; @@ -40,6 +40,7 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { DefaultPreferencesEditorInput, KeybindingsEditorInput, PreferencesEditorInput, SettingsEditor2Input } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { AbstractSideBySideEditorInputFactory } from 'vs/workbench/browser/parts/editor/editor.contribution'; +import { WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService'; const SETTINGS_EDITOR_COMMAND_SEARCH = 'settings.action.search'; @@ -50,6 +51,8 @@ const SETTINGS_EDITOR_COMMAND_EDIT_FOCUSED_SETTING = 'settings.action.editFocuse const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focusSettingsFromSearch'; const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; const SETTINGS_EDITOR_COMMAND_FOCUS_TOC = 'settings.action.focusTOC'; +const SETTINGS_EDITOR_COMMAND_FOCUS_TOC2 = 'settings.action.focusTOC2'; +const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingControl'; const SETTINGS_EDITOR_COMMAND_SWITCH_TO_JSON = 'settings.switchToJSON'; const SETTINGS_EDITOR_COMMAND_FILTER_MODIFIED = 'settings.filterByModified'; @@ -507,6 +510,14 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } return null; } + + function settingsEditorFocusSearch(accessor: ServicesAccessor) { + const preferencesEditor = getPreferencesEditor(accessor); + if (preferencesEditor) { + preferencesEditor.focusSearch(); + } + } + registerAction2(class extends Action2 { constructor() { super({ @@ -521,12 +532,24 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon }); } - run(accessor: ServicesAccessor) { - const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor) { - preferencesEditor.focusSearch(); - } + run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_SEARCH, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS), + keybinding: { + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib, + when: null + }, + title: nls.localize('settings.focusSearch', "Focus settings search") + }); } + + run(accessor: ServicesAccessor) { settingsEditorFocusSearch(accessor); } }); registerAction2(class extends Action2 { @@ -691,16 +714,76 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon constructor() { super({ id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC, - precondition: CONTEXT_SETTINGS_EDITOR, + keybinding: [ + { + primary: KeyCode.Escape, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate()), + }, + { + primary: KeyCode.LeftArrow, + weight: KeybindingWeight.WorkbenchContrib, + when: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), InputFocusedContext.negate()) + }], title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") }); } run(accessor: ServicesAccessor): void { const preferencesEditor = getPreferencesEditor(accessor); - if (preferencesEditor instanceof SettingsEditor2) { - preferencesEditor.focusTOC(); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; } + + if (document.activeElement?.classList.contains('monaco-list')) { + preferencesEditor.focusTOC(); + } else { + preferencesEditor.focusSettings(); + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL, + precondition: ContextKeyExpr.and(CONTEXT_SETTINGS_EDITOR, CONTEXT_TOC_ROW_FOCUS.negate(), WorkbenchListFocusContextKey), + keybinding: { + primary: KeyCode.Enter, + weight: KeybindingWeight.WorkbenchContrib, + }, + title: nls.localize('settings.focusSettingControl', "Focus setting control") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; + } + + if (document.activeElement?.classList.contains('monaco-list')) { + preferencesEditor.focusSettings(true); + } + } + }); + + registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_TOC2, + + title: nls.localize('settings.focusSettingsTOC', "Focus settings TOC tree") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + if (!(preferencesEditor instanceof SettingsEditor2)) { + return; + } + + preferencesEditor.focusTOC(); } }); diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts index b0401d43adc..e8bb2984293 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesEditor.ts @@ -40,9 +40,9 @@ import { attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorInput, EditorOptions, IEditorControl } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorControl, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel'; import { DefaultSettingsRenderer, FolderSettingsRenderer, IPreferencesRenderer, UserSettingsRenderer, WorkspaceSettingsRenderer } from 'vs/workbench/contrib/preferences/browser/preferencesRenderers'; import { SearchWidget, SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; @@ -53,7 +53,7 @@ import { IFilterResult, IPreferencesService, ISetting, ISettingsEditorModel, ISe import { DefaultPreferencesEditorInput, PreferencesEditorInput } from 'vs/workbench/services/preferences/common/preferencesEditorInput'; import { DefaultSettingsEditorModel, SettingsEditorModel } from 'vs/workbench/services/preferences/common/preferencesModels'; -export class PreferencesEditor extends BaseEditor { +export class PreferencesEditor extends EditorPane { static readonly ID: string = 'workbench.editor.preferencesEditor'; @@ -75,7 +75,7 @@ export class PreferencesEditor extends BaseEditor { get minimumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.minimumWidth : 0; } get maximumWidth(): number { return this.sideBySidePreferencesWidget ? this.sideBySidePreferencesWidget.maximumWidth : Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -151,14 +151,14 @@ export class PreferencesEditor extends BaseEditor { this.preferencesRenderers.editFocusedPreference(); } - setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, token: CancellationToken): Promise { + setInput(newInput: EditorInput, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.defaultSettingsEditorContextKey.set(true); this.defaultSettingsJSONEditorContextKey.set(true); if (options && options.query) { this.focusSearch(options.query); } - return super.setInput(newInput, options, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, token)); + return super.setInput(newInput, options, context, token).then(() => this.updateInput(newInput as PreferencesEditorInput, options, context, token)); } layout(dimension: DOM.Dimension): void { @@ -204,8 +204,8 @@ export class PreferencesEditor extends BaseEditor { super.setEditorVisible(visible, group); } - private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { + private updateInput(newInput: PreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + return this.sideBySidePreferencesWidget.setInput(newInput.secondary, newInput.primary, options, context, token).then(({ defaultPreferencesRenderer, editablePreferencesRenderer }) => { if (token.isCancellationRequested) { return; } @@ -762,7 +762,7 @@ class SideBySidePreferencesWidget extends Widget { private defaultPreferencesHeader: HTMLElement; private defaultPreferencesEditor: DefaultPreferencesEditor; - private editablePreferencesEditor: BaseEditor | null = null; + private editablePreferencesEditor: EditorPane | null = null; private defaultPreferencesEditorContainer: HTMLElement; private editablePreferencesEditorContainer: HTMLElement; @@ -837,12 +837,12 @@ class SideBySidePreferencesWidget extends Widget { this._register(focusTracker.onDidFocus(() => this._onFocus.fire())); } - setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { + setInput(defaultPreferencesEditorInput: DefaultPreferencesEditorInput, editablePreferencesEditorInput: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise<{ defaultPreferencesRenderer?: IPreferencesRenderer, editablePreferencesRenderer?: IPreferencesRenderer; }> { this.getOrCreateEditablePreferencesEditor(editablePreferencesEditorInput); this.settingsTargetsWidget.settingsTarget = this.getSettingsTarget(editablePreferencesEditorInput.resource!); return Promise.all([ - this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, token), - this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, token) + this.updateInput(this.defaultPreferencesEditor, defaultPreferencesEditorInput, DefaultSettingsEditorContribution.ID, editablePreferencesEditorInput.resource!, options, context, token), + this.updateInput(this.editablePreferencesEditor!, editablePreferencesEditorInput, SettingsEditorContribution.ID, defaultPreferencesEditorInput.resource!, options, context, token) ]) .then(([defaultPreferencesRenderer, editablePreferencesRenderer]) => { if (token.isCancellationRequested) { @@ -906,7 +906,7 @@ class SideBySidePreferencesWidget extends Widget { } } - private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): BaseEditor { + private getOrCreateEditablePreferencesEditor(editorInput: EditorInput): EditorPane { if (this.editablePreferencesEditor) { return this.editablePreferencesEditor; } @@ -920,8 +920,8 @@ class SideBySidePreferencesWidget extends Widget { return editor; } - private updateInput(editor: BaseEditor, input: EditorInput, editorContributionId: string, associatedPreferencesModelUri: URI, options: EditorOptions | undefined, token: CancellationToken): Promise | undefined> { - return editor.setInput(input, options, token) + private updateInput(editor: EditorPane, input: EditorInput, editorContributionId: string, associatedPreferencesModelUri: URI, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise | undefined> { + return editor.setInput(input, options, context, token) .then(() => { if (token.isCancellationRequested) { return undefined; @@ -1025,8 +1025,8 @@ export class DefaultPreferencesEditor extends BaseTextEditor { return options; } - setInput(input: DefaultPreferencesEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - return super.setInput(input, options, token) + setInput(input: DefaultPreferencesEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + return super.setInput(input, options, context, token) .then(() => this.input!.resolve() .then(editorModel => { if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index a31a0fa0f9f..77b764417e7 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -39,12 +39,12 @@ import { attachButtonStyler, attachStylerCallback } from 'vs/platform/theme/comm import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IUserDataAutoSyncService, IUserDataSyncService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { IEditorMemento, IEditorPane } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; +import { IEditorMemento, IEditorOpenContext, IEditorPane } from 'vs/workbench/common/editor'; import { attachSuggestEnabledInputBoxStyler, SuggestEnabledInput } from 'vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput'; import { SettingsTarget, SettingsTargetsWidget } from 'vs/workbench/contrib/preferences/browser/preferencesWidgets'; import { commonlyUsedData, tocData } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; -import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers } from 'vs/workbench/contrib/preferences/browser/settingsTree'; +import { AbstractSettingRenderer, ISettingLinkClickEvent, ISettingOverrideClickEvent, resolveExtensionsSettings, resolveSettingsTree, SettingsTree, SettingTreeRenderers, updateSettingTreeTabOrder } from 'vs/workbench/contrib/preferences/browser/settingsTree'; import { ISettingsEditorViewState, parseQuery, SearchResultIdx, SearchResultModel, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeModel, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; import { settingsTextInputBorder } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { createTOCIterator, TOCTree, TOCTreeModel } from 'vs/workbench/contrib/preferences/browser/tocTree'; @@ -76,7 +76,7 @@ const searchBoxLabel = localize('SearchSettings.AriaLabel', "Search settings"); const SETTINGS_AUTOSAVE_NOTIFIED_KEY = 'hasNotifiedOfSettingsAutosave'; const SETTINGS_EDITOR_STATE_KEY = 'settingsEditorState'; -export class SettingsEditor2 extends BaseEditor { +export class SettingsEditor2 extends EditorPane { static readonly ID: string = 'workbench.editor.settings2'; private static NUM_INSTANCES: number = 0; @@ -150,6 +150,7 @@ export class SettingsEditor2 extends BaseEditor { private editorMemento: IEditorMemento; private tocFocusedElement: SettingsTreeGroupElement | null = null; + private treeFocusedElement: SettingsTreeElement | null = null; private settingsTreeScrollTop = 0; private dimension!: DOM.Dimension; @@ -201,7 +202,7 @@ export class SettingsEditor2 extends BaseEditor { get minimumWidth(): number { return 375; } get maximumWidth(): number { return Number.POSITIVE_INFINITY; } - // these setters need to exist because this extends from BaseEditor + // these setters need to exist because this extends from EditorPane set minimumWidth(value: number) { /*noop*/ } set maximumWidth(value: number) { /*noop*/ } @@ -234,9 +235,9 @@ export class SettingsEditor2 extends BaseEditor { this.updateStyles(); } - setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: SettingsEditor2Input, options: SettingsEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.inSettingsEditorContextKey.set(true); - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => timeout(0)) // Force setInput to be async .then(() => { // Don't block setInput on render (which can trigger an async search) @@ -347,7 +348,8 @@ export class SettingsEditor2 extends BaseEditor { } } - focusSettings(): void { + focusSettings(focusSettingInput = false): void { + // TODO@roblourens is this in the right place? // Update ARIA global labels const labelElement = this.settingsAriaExtraLabelsContainer.querySelector('#settings_aria_more_actions_shortcut_label'); if (labelElement) { @@ -357,9 +359,18 @@ export class SettingsEditor2 extends BaseEditor { } } - const firstFocusable = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); - if (firstFocusable) { - (firstFocusable).focus(); + const focused = this.settingsTree.getFocus(); + if (!focused.length) { + this.settingsTree.focusFirst(); + } + + this.settingsTree.domFocus(); + + if (focusSettingInput) { + const controlInFocusedRow = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); + if (controlInFocusedRow) { + (controlInFocusedRow).focus(); + } } } @@ -509,6 +520,11 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTree.reveal(elements[0], sourceTop); + // We need to shift focus from the setting that contains the link to the setting that's + // linked. Clicking on the link sets focus on the setting that contains the link, + // which is why we need the setTimeout + setTimeout(() => this.settingsTree.setFocus([elements[0]]), 50); + const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), evt.targetKey); if (domElements && domElements[0]) { const control = domElements[0].querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); @@ -569,48 +585,7 @@ export class SettingsEditor2 extends BaseEditor { })); this.createTOC(bodyContainer); - - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.scrollTop > 0) { - const firstElement = this.settingsTree.firstVisibleElement; - - if (typeof firstElement !== 'undefined') { - this.settingsTree.reveal(firstElement, 0.1); - } - - return true; - } - } else { - const firstControl = this.settingsTree.getHTMLElement().querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); - if (firstControl) { - (firstControl).focus(); - } - } - - return false; - }, - 'settings list focus helper'); - this.createSettingsTree(bodyContainer); - - this.createFocusSink( - bodyContainer, - e => { - if (DOM.findParentWithClass(e.relatedTarget, 'settings-editor-tree')) { - if (this.settingsTree.scrollTop < this.settingsTree.scrollHeight) { - const lastElement = this.settingsTree.lastVisibleElement; - this.settingsTree.reveal(lastElement, 0.9); - return true; - } - } - - return false; - }, - 'settings list focus helper' - ); } private addCtrlAInterceptor(container: HTMLElement): void { @@ -628,19 +603,6 @@ export class SettingsEditor2 extends BaseEditor { })); } - private createFocusSink(container: HTMLElement, callback: (e: any) => boolean, label: string): HTMLElement { - const listFocusSink = DOM.append(container, $('.settings-tree-focus-sink')); - listFocusSink.setAttribute('aria-label', label); - listFocusSink.tabIndex = 0; - this._register(DOM.addDisposableListener(listFocusSink, 'focus', (e: any) => { - if (e.relatedTarget && callback(e)) { - e.relatedTarget.focus(); - } - })); - - return listFocusSink; - } - private createTOC(parent: HTMLElement): void { this.tocTreeModel = this.instantiationService.createInstance(TOCTreeModel, this.viewState); this.tocTreeContainer = DOM.append(parent, $('.settings-toc-container')); @@ -668,6 +630,7 @@ export class SettingsEditor2 extends BaseEditor { } } else if (element && (!e.browserEvent || !(e.browserEvent).fromScroll)) { this.settingsTree.reveal(element, 0); + this.settingsTree.setFocus([element]); } })); @@ -717,7 +680,6 @@ export class SettingsEditor2 extends BaseEditor { this.settingsTreeContainer, this.viewState, this.settingRenderers.allRenderers)); - this.settingsTree.getHTMLElement().attributes.removeNamedItem('tabindex'); this._register(this.settingsTree.onDidScroll(() => { if (this.settingsTree.scrollTop === this.settingsTreeScrollTop) { @@ -725,6 +687,7 @@ export class SettingsEditor2 extends BaseEditor { } this.settingsTreeScrollTop = this.settingsTree.scrollTop; + updateSettingTreeTabOrder(this.settingsTreeContainer); // setTimeout because calling setChildren on the settingsTree can trigger onDidScroll, so it fires when // setChildren has called on the settings tree but not the toc tree yet, so their rendered elements are out of sync @@ -732,6 +695,20 @@ export class SettingsEditor2 extends BaseEditor { this.updateTreeScrollSync(); }, 0); })); + + // There is no different select state in the settings tree + this._register(this.settingsTree.onDidChangeFocus(e => { + const element = e.elements[0]; + if (this.treeFocusedElement === element) { + return; + } + + this.treeFocusedElement = element; + this.settingsTree.setSelection(element ? [element] : []); + + // Wait for rendering to complete + setTimeout(() => updateSettingTreeTabOrder(this.settingsTreeContainer), 0); + })); } private notifyNoSaveNeeded() { @@ -966,7 +943,7 @@ export class SettingsEditor2 extends BaseEditor { } const groups = this.defaultSettingsEditorModel.settingsGroups.slice(1); // Without commonlyUsed - const dividedGroups = collections.groupBy(groups, g => g.contributedByExtension ? 'extension' : 'core'); + const dividedGroups = collections.groupBy(groups, g => g.extensionInfo ? 'extension' : 'core'); const settingsResult = resolveSettingsTree(tocData, dividedGroups.core); const resolvedSettingsRoot = settingsResult.tree; diff --git a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts index dc5a734b6f6..df568849718 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsTree.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsTree.ts @@ -5,7 +5,6 @@ import { BrowserFeatures } from 'vs/base/browser/canIUse'; import * as DOM from 'vs/base/browser/dom'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { renderMarkdown } from 'vs/base/browser/markdownRenderer'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { alert as ariaAlert } from 'vs/base/browser/ui/aria/aria'; @@ -16,7 +15,7 @@ import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { DefaultStyleController } from 'vs/base/browser/ui/list/listWidget'; import { ISelectOptionItem, SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar'; -import { IObjectTreeOptions, ObjectTree } from 'vs/base/browser/ui/tree/objectTree'; +import { IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree'; import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel'; import { ITreeFilter, ITreeModel, ITreeNode, ITreeRenderer, TreeFilterResult, TreeVisibility } from 'vs/base/browser/ui/tree/tree'; import { Action, IAction, Separator } from 'vs/base/common/actions'; @@ -43,7 +42,7 @@ import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticip import { getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge'; import { ITOCEntry } from 'vs/workbench/contrib/preferences/browser/settingsLayout'; import { ISettingsEditorViewState, settingKeyToDisplayFormat, SettingsTreeElement, SettingsTreeGroupChild, SettingsTreeGroupElement, SettingsTreeNewExtensionsElement, SettingsTreeSettingElement } from 'vs/workbench/contrib/preferences/browser/settingsTreeModels'; -import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsHeaderForeground, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; +import { ExcludeSettingWidget, ISettingListChangeEvent, IListDataItem, ListSettingWidget, settingsNumberInputBackground, settingsNumberInputBorder, settingsNumberInputForeground, settingsSelectBackground, settingsSelectBorder, settingsSelectForeground, settingsSelectListBorder, settingsTextInputBackground, settingsTextInputBorder, settingsTextInputForeground, ObjectSettingWidget, IObjectDataItem, IObjectEnumOption, ObjectValue, IObjectValueSuggester, IObjectKeySuggester, focusedRowBackground, focusedRowBorder, settingsHeaderForeground, rowHoverBackground } from 'vs/workbench/contrib/preferences/browser/settingsWidgets'; import { SETTINGS_EDITOR_COMMAND_SHOW_CONTEXT_MENU } from 'vs/workbench/contrib/preferences/common/preferences'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { ISetting, ISettingsGroup, SettingValueType } from 'vs/workbench/services/preferences/common/preferences'; @@ -53,6 +52,9 @@ import { Codicon } from 'vs/base/common/codicons'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IList } from 'vs/base/browser/ui/tree/indexTreeModel'; +import { IListService, WorkbenchObjectTree } from 'vs/platform/list/browser/listService'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; const $ = DOM.$; @@ -451,6 +453,45 @@ export interface ISettingOverrideClickEvent { targetKey: string; } +function removeChildrenFromTabOrder(node: Element): void { + const focusableElements = node.querySelectorAll(` + [tabindex="0"], + input:not([tabindex="-1"]), + select:not([tabindex="-1"]), + textarea:not([tabindex="-1"]), + a:not([tabindex="-1"]), + button:not([tabindex="-1"]), + area:not([tabindex="-1"]) + `); + + focusableElements.forEach(element => { + element.setAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR, 'true'); + element.setAttribute('tabindex', '-1'); + }); +} + +function addChildrenToTabOrder(node: Element): void { + const focusableElements = node.querySelectorAll( + `[${AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR}="true"]` + ); + + focusableElements.forEach(element => { + element.removeAttribute(AbstractSettingRenderer.ELEMENT_FOCUSABLE_ATTR); + element.setAttribute('tabindex', '0'); + }); +} + +export function updateSettingTreeTabOrder(container: Element): void { + const allRows = [...container.querySelectorAll(AbstractSettingRenderer.ALL_ROWS_SELECTOR)]; + const focusedRow = allRows.find(row => row.classList.contains('focused')); + + allRows.forEach(removeChildrenFromTabOrder); + + if (isDefined(focusedRow)) { + addChildrenToTabOrder(focusedRow); + } +} + export abstract class AbstractSettingRenderer extends Disposable implements ITreeRenderer { /** To override */ abstract get templateId(): string; @@ -459,9 +500,11 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre static readonly CONTROL_SELECTOR = '.' + AbstractSettingRenderer.CONTROL_CLASS; static readonly CONTENTS_CLASS = 'setting-item-contents'; static readonly CONTENTS_SELECTOR = '.' + AbstractSettingRenderer.CONTENTS_CLASS; + static readonly ALL_ROWS_SELECTOR = '.monaco-list-row'; static readonly SETTING_KEY_ATTR = 'data-key'; static readonly SETTING_ID_ATTR = 'data-id'; + static readonly ELEMENT_FOCUSABLE_ATTR = 'data-focusable'; private readonly _onDidClickOverrideElement = this._register(new Emitter()); readonly onDidClickOverrideElement: Event = this._onDidClickOverrideElement.event; @@ -607,7 +650,7 @@ export abstract class AbstractSettingRenderer extends Disposable implements ITre private fixToolbarIcon(toolbar: ToolBar): void { const button = toolbar.getElement().querySelector('.codicon-toolbar-more'); if (button) { - (button).tabIndex = -1; + (button).tabIndex = 0; // change icon from ellipsis to gear (button).classList.add('codicon-gear'); @@ -1248,6 +1291,15 @@ export class SettingTextRenderer extends AbstractSettingRenderer implements ITre })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + inputBox.inputElement.tabIndex = 0; + + // TODO@9at8: listWidget filters out all key events from input boxes, so we need to come up with a better way + // Disable ArrowUp and ArrowDown behaviour in favor of list navigation + common.toDispose.add(DOM.addStandardDisposableListener(inputBox.inputElement, DOM.EventType.KEY_DOWN, e => { + if (e.equals(KeyCode.UpArrow) || e.equals(KeyCode.DownArrow)) { + e.preventDefault(); + } + })); const template: ISettingTextItemTemplate = { ...common, @@ -1300,6 +1352,7 @@ export class SettingEnumRenderer extends AbstractSettingRenderer implements ITre const selectElement = common.controlElement.querySelector('select'); if (selectElement) { selectElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + selectElement.tabIndex = 0; } common.toDispose.add( @@ -1392,6 +1445,7 @@ export class SettingNumberRenderer extends AbstractSettingRenderer implements IT })); common.toDispose.add(inputBox); inputBox.inputElement.classList.add(AbstractSettingRenderer.CONTROL_CLASS); + inputBox.inputElement.tabIndex = 0; const template: ISettingNumberItemTemplate = { ...common, @@ -1504,13 +1558,6 @@ export class SettingBoolRenderer extends AbstractSettingRenderer implements ITre // Prevent clicks from being handled by list toDispose.add(DOM.addDisposableListener(controlElement, 'mousedown', (e: IMouseEvent) => e.stopPropagation())); - - toDispose.add(DOM.addStandardDisposableListener(controlElement, 'keydown', (e: StandardKeyboardEvent) => { - if (e.keyCode === KeyCode.Escape) { - e.browserEvent.stopPropagation(); - } - })); - toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_ENTER, e => container.classList.add('mouseover'))); toDispose.add(DOM.addDisposableListener(titleElement, DOM.EventType.MOUSE_LEAVE, e => container.classList.remove('mouseover'))); @@ -1834,11 +1881,7 @@ class SettingsTreeDelegate extends CachedListVirtualDelegate extends ObjectTreeModel { } } -export class SettingsTree extends ObjectTree { +export class SettingsTree extends WorkbenchObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, renderers: ITreeRenderer[], + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, @IThemeService themeService: IThemeService, @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, @IInstantiationService instantiationService: IInstantiationService, ) { super('SettingsTree', container, new SettingsTreeDelegate(), renderers, { + horizontalScrolling: false, supportDynamicHeights: true, identityProvider: { getId(e) { @@ -1875,9 +1923,6 @@ export class SettingsTree extends ObjectTree { } }, accessibilityProvider: { - getWidgetRole() { - return 'form'; - }, getAriaLabel() { // TODO@roblourens https://github.com/microsoft/vscode/issues/95862 return ''; @@ -1889,9 +1934,16 @@ export class SettingsTree extends ObjectTree { styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), filter: instantiationService.createInstance(SettingsTreeFilter, viewState), smoothScrolling: configurationService.getValue('workbench.list.smoothScrolling'), - }); + multipleSelectionSupport: false, + }, + contextKeyService, + listService, + themeService, + configurationService, + keybindingService, + accessibilityService, + ); - this.disposables.clear(); this.disposables.add(registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const activeBorderColor = theme.getColor(focusBorder); if (activeBorderColor) { @@ -1930,6 +1982,26 @@ export class SettingsTree extends ObjectTree { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item.invalid-input .setting-item-control .monaco-inputbox.idle { outline-width: 0; border-style:solid; border-width: 1px; border-color: ${invalidInputBorder}; }`); } + const focusedRowBackgroundColor = theme.getColor(focusedRowBackground); + if (focusedRowBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .setting-item-contents, + .settings-editor > .settings-body > .settings-tree-container .monaco-list-row.focused .settings-group-title-label { background-color: ${focusedRowBackgroundColor}; }`); + } + + const rowHoverBackgroundColor = theme.getColor(rowHoverBackground); + if (rowHoverBackgroundColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row .setting-item-contents:hover, + .settings-editor > .settings-body > .settings-tree-container .monaco-list-row .settings-group-title-label:hover { background-color: ${rowHoverBackgroundColor}; }`); + } + + const focusedRowBorderColor = theme.getColor(focusedRowBorder); + if (focusedRowBorderColor) { + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::before, + .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .setting-item-contents::after { border-top: 1px solid ${focusedRowBorderColor} }`); + collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::before, + .settings-editor > .settings-body > .settings-tree-container .monaco-list:focus-within .monaco-list-row.focused .settings-group-title-label::after { border-top: 1px solid ${focusedRowBorderColor} }`); + } + const headerForegroundColor = theme.getColor(settingsHeaderForeground); if (headerForegroundColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .settings-group-title-label { color: ${headerForegroundColor}; }`); @@ -1940,6 +2012,12 @@ export class SettingsTree extends ObjectTree { if (focusBorderColor) { collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .setting-item-contents .setting-item-markdown a:focus { outline-color: ${focusBorderColor} }`); } + + // const listActiveSelectionBackgroundColor = theme.getColor(listActiveSelectionBackground); + // if (listActiveSelectionBackgroundColor) { + // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .setting-item-contents .setting-item-title { background-color: ${listActiveSelectionBackgroundColor}; }`); + // collector.addRule(`.settings-editor > .settings-body > .settings-tree-container .monaco-list-row.selected .settings-group-title-label { background-color: ${listActiveSelectionBackgroundColor}; }`); + // } })); this.getHTMLElement().classList.add('settings-editor-tree'); diff --git a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts index d7f85b56922..5eaab66e56c 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsWidgets.ts @@ -16,7 +16,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import 'vs/css!./media/settingsWidgets'; import { localize } from 'vs/nls'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { foreground, inputBackground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder } from 'vs/platform/theme/common/colorRegistry'; +import { foreground, inputBorder, inputForeground, listActiveSelectionBackground, listActiveSelectionForeground, listHoverBackground, listHoverForeground, listInactiveSelectionBackground, listInactiveSelectionForeground, registerColor, selectBackground, selectBorder, selectForeground, textLinkForeground, textPreformatForeground, editorWidgetBorder, textLinkActiveForeground, simpleCheckboxBackground, simpleCheckboxForeground, simpleCheckboxBorder, listFocusBackground, transparent, focusBorder } from 'vs/platform/theme/common/colorRegistry'; import { attachButtonStyler, attachInputBoxStyler, attachSelectBoxStyler } from 'vs/platform/theme/common/styler'; import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { disposableTimeout } from 'vs/base/common/async'; @@ -25,6 +25,7 @@ import { preferencesEditIcon } from 'vs/workbench/contrib/preferences/browser/pr import { SelectBox } from 'vs/base/browser/ui/selectBox/selectBox'; import { isIOS } from 'vs/base/common/platform'; import { BrowserFeatures } from 'vs/base/browser/canIUse'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; const $ = DOM.$; export const settingsHeaderForeground = registerColor('settings.headerForeground', { light: '#444444', dark: '#e7e7e7', hc: '#ffffff' }, localize('headerForeground', "The foreground color for a section header or active title.")); @@ -46,15 +47,33 @@ export const settingsCheckboxForeground = registerColor('settings.checkboxForegr export const settingsCheckboxBorder = registerColor('settings.checkboxBorder', { dark: simpleCheckboxBorder, light: simpleCheckboxBorder, hc: simpleCheckboxBorder }, localize('settingsCheckboxBorder', "Settings editor checkbox border.")); // Text control colors -export const settingsTextInputBackground = registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); +export const settingsTextInputBackground = settingsSelectBackground; //registerColor('settings.textInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('textInputBoxBackground', "Settings editor text input box background.")); export const settingsTextInputForeground = registerColor('settings.textInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('textInputBoxForeground', "Settings editor text input box foreground.")); export const settingsTextInputBorder = registerColor('settings.textInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('textInputBoxBorder', "Settings editor text input box border.")); // Number control colors -export const settingsNumberInputBackground = registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); +export const settingsNumberInputBackground = settingsSelectBackground; // registerColor('settings.numberInputBackground', { dark: inputBackground, light: inputBackground, hc: inputBackground }, localize('numberInputBoxBackground', "Settings editor number input box background.")); export const settingsNumberInputForeground = registerColor('settings.numberInputForeground', { dark: inputForeground, light: inputForeground, hc: inputForeground }, localize('numberInputBoxForeground', "Settings editor number input box foreground.")); export const settingsNumberInputBorder = registerColor('settings.numberInputBorder', { dark: inputBorder, light: inputBorder, hc: inputBorder }, localize('numberInputBoxBorder', "Settings editor number input box border.")); +export const focusedRowBackground = registerColor('settings.focusedRowBackground', { + dark: transparent(PANEL_BORDER, .4), + light: transparent(listFocusBackground, .4), + hc: null +}, localize('focusedRowBackground', "The background color of a cell when the row is focused.")); + +export const rowHoverBackground = registerColor('notebook.rowHoverBackground', { + dark: transparent(focusedRowBackground, .5), + light: transparent(focusedRowBackground, .7), + hc: null +}, localize('notebook.rowHoverBackground', "The background color of a row when the row is hovered.")); + +export const focusedRowBorder = registerColor('notebook.focusedRowBorder', { + dark: Color.white.transparent(0.12), + light: Color.black.transparent(0.12), + hc: focusBorder +}, localize('notebook.focusedRowBorder', "The color of the row's top and bottom border when the row is focused.")); + registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => { const checkboxBackgroundColor = theme.getColor(settingsCheckboxBackground); if (checkboxBackgroundColor) { @@ -527,7 +546,7 @@ export class ListSettingWidget extends AbstractListSettingWidget valueInput.element.classList.add('setting-list-valueInput'); this.listDisposables.add(attachInputBoxStyler(valueInput, this.themeService, { - inputBackground: settingsTextInputBackground, + inputBackground: settingsSelectBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -546,7 +565,7 @@ export class ListSettingWidget extends AbstractListSettingWidget siblingInput.element.classList.add('setting-list-siblingInput'); this.listDisposables.add(siblingInput); this.listDisposables.add(attachInputBoxStyler(siblingInput, this.themeService, { - inputBackground: settingsTextInputBackground, + inputBackground: settingsSelectBackground, inputForeground: settingsTextInputForeground, inputBorder: settingsTextInputBorder })); @@ -908,7 +927,7 @@ export class ObjectSettingWidget extends AbstractListSettingWidget { +export class TOCTree extends WorkbenchObjectTree { constructor( container: HTMLElement, viewState: ISettingsEditorViewState, + @IContextKeyService contextKeyService: IContextKeyService, + @IListService listService: IListService, @IThemeService themeService: IThemeService, - @IInstantiationService instantiationService: IInstantiationService + @IConfigurationService configurationService: IConfigurationService, + @IKeybindingService keybindingService: IKeybindingService, + @IAccessibilityService accessibilityService: IAccessibilityService, + @IInstantiationService instantiationService: IInstantiationService, ) { // test open mode const filter = instantiationService.createInstance(SettingsTreeFilter, viewState); - const options: IObjectTreeOptions = { + const options: IWorkbenchObjectTreeOptions = { filter, multipleSelectionSupport: false, identityProvider: { @@ -207,13 +216,23 @@ export class TOCTree extends ObjectTree { }, styleController: id => new DefaultStyleController(DOM.createStyleSheet(container), id), accessibilityProvider: instantiationService.createInstance(SettingsAccessibilityProvider), - collapseByDefault: true + collapseByDefault: true, + horizontalScrolling: false }; - super('SettingsTOC', container, + super( + 'SettingsTOC', + container, new TOCTreeDelegate(), [new TOCRenderer()], - options); + options, + contextKeyService, + listService, + themeService, + configurationService, + keybindingService, + accessibilityService, + ); this.disposables.add(attachStyler(themeService, { listBackground: editorBackground, diff --git a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts index 9e2ae84e031..895c3df7b10 100644 --- a/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts +++ b/src/vs/workbench/contrib/quickaccess/browser/quickAccess.contribution.ts @@ -95,10 +95,10 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { MenuRegistry.appendMenuItem(MenuId.EditorContext, { group: 'z_commands', + when: EditorContextKeys.editorSimpleInput.toNegated(), command: { id: ShowAllCommandsAction.ID, title: localize('commandPalette', "Command Palette..."), - precondition: EditorContextKeys.editorSimpleInput.toNegated() }, order: 1 }); diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index e9a398cc506..0446e6aa795 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -53,7 +53,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { Event } from 'vs/base/common/event'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { RemoteWindowActiveIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; +import { RemoteStatusIndicator } from 'vs/workbench/contrib/remote/browser/remoteIndicator'; import { inQuickPickContextKeyValue } from 'vs/workbench/browser/quickaccess'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal'; @@ -838,4 +838,4 @@ class RemoteAgentConnectionStatusListener implements IWorkbenchContribution { const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RemoteAgentConnectionStatusListener, LifecyclePhase.Eventually); -workbenchContributionsRegistry.registerWorkbenchContribution(RemoteWindowActiveIndicator, LifecyclePhase.Starting); +workbenchContributionsRegistry.registerWorkbenchContribution(RemoteStatusIndicator, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts index 7aec94c4f7e..fb51919e9b1 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts +++ b/src/vs/workbench/contrib/remote/browser/remoteIndicator.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, dispose } from 'vs/base/common/lifecycle'; import { MenuId, IMenuService, MenuItemAction, IMenu, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; @@ -21,88 +21,112 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { IHostService } from 'vs/workbench/services/host/browser/host'; -import { RemoteConnectionState, Deprecated_RemoteAuthorityContext } from 'vs/workbench/browser/contextkeys'; +import { RemoteConnectionState } from 'vs/workbench/browser/contextkeys'; import { isWeb } from 'vs/base/common/platform'; import { once } from 'vs/base/common/functional'; -const WINDOW_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; -const CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; -const SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command +export class RemoteStatusIndicator extends Disposable implements IWorkbenchContribution { -export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenchContribution { + private static REMOTE_ACTIONS_COMMAND_ID = 'workbench.action.remote.showMenu'; + private static CLOSE_REMOTE_COMMAND_ID = 'workbench.action.remote.close'; + private static SHOW_CLOSE_REMOTE_COMMAND_ID = !isWeb; // web does not have a "Close Remote" command - private windowIndicatorEntry: IStatusbarEntryAccessor | undefined; - private windowCommandMenu: IMenu; - private hasWindowActions: boolean = false; - private remoteAuthority: string | undefined; + private remoteStatusEntry: IStatusbarEntryAccessor | undefined; + + private remoteMenu = this._register(this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService)); + private hasRemoteActions = false; + + private remoteAuthority = this.environmentService.configuration.remoteAuthority; private connectionState: 'initializing' | 'connected' | 'disconnected' | undefined = undefined; + private connectionStateContextKey = RemoteConnectionState.bindTo(this.contextKeyService); constructor( @IStatusbarService private readonly statusbarService: IStatusbarService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, @ILabelService private readonly labelService: ILabelService, @IContextKeyService private contextKeyService: IContextKeyService, @IMenuService private menuService: IMenuService, @IQuickInputService private readonly quickInputService: IQuickInputService, @ICommandService private readonly commandService: ICommandService, - @IExtensionService extensionService: IExtensionService, - @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IRemoteAuthorityResolverService remoteAuthorityResolverService: IRemoteAuthorityResolverService, - @IHostService hostService: IHostService + @IExtensionService private readonly extensionService: IExtensionService, + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IRemoteAuthorityResolverService private readonly remoteAuthorityResolverService: IRemoteAuthorityResolverService, + @IHostService private readonly hostService: IHostService ) { super(); - this.windowCommandMenu = this.menuService.createMenu(MenuId.StatusBarWindowIndicatorMenu, this.contextKeyService); - this._register(this.windowCommandMenu); + // Set initial connection state + if (this.remoteAuthority) { + this.connectionState = 'initializing'; + this.connectionStateContextKey.set(this.connectionState); + } + this.registerActions(); + this.registerListeners(); + + this.updateWhenInstalledExtensionsRegistered(); + this.updateRemoteStatusIndicator(); + } + + private registerActions(): void { const category = { value: nls.localize('remote.category', "Remote"), original: 'Remote' }; + + // Show Remote Menu const that = this; registerAction2(class extends Action2 { constructor() { super({ - id: WINDOW_ACTIONS_COMMAND_ID, + id: RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID, category, title: { value: nls.localize('remote.showMenu', "Show Remote Menu"), original: 'Show Remote Menu' }, f1: true, }); } - run = () => that.showIndicatorActions(that.windowCommandMenu); + run = () => that.showRemoteMenu(that.remoteMenu); }); - this.remoteAuthority = environmentService.configuration.remoteAuthority; - Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(this.remoteAuthority || ''); + // Close Remote Connection + if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { + registerAction2(class extends Action2 { + constructor() { + super({ + id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, + category, + title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, + f1: true + }); + } + run = () => that.remoteAuthority && that.hostService.openWindow({ forceReuseWindow: true }); + }); + MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { + group: '6_close', + command: { + id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, + title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") + }, + order: 3.5 + }); + } + } + + private registerListeners(): void { + + // Menu changes + this._register(this.remoteMenu.onDidChange(() => this.updateRemoteActions())); + + // Update indicator when formatter changes as it may have an impact on the remote label + this._register(this.labelService.onDidChangeFormatters(() => this.updateRemoteStatusIndicator())); + + // Update based on remote indicator changes if any + const remoteIndicator = this.environmentService.options?.windowIndicator; + if (remoteIndicator) { + this._register(remoteIndicator.onDidChange(() => this.updateRemoteStatusIndicator())); + } + + // Listen to changes of the connection if (this.remoteAuthority) { - - if (SHOW_CLOSE_REMOTE_COMMAND_ID) { - registerAction2(class extends Action2 { - constructor() { - super({ - id: CLOSE_REMOTE_COMMAND_ID, - category, - title: { value: nls.localize('remote.close', "Close Remote Connection"), original: 'Close Remote Connection' }, - f1: true - }); - } - run = () => that.remoteAuthority && hostService.openWindow({ forceReuseWindow: true }); - }); - - MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, { - group: '6_close', - command: { - id: CLOSE_REMOTE_COMMAND_ID, - title: nls.localize({ key: 'miCloseRemote', comment: ['&& denotes a mnemonic'] }, "Close Re&&mote Connection") - }, - order: 3.5 - }); - } - - // Pending entry until extensions are ready - this.renderWindowIndicator('$(sync~spin) ' + nls.localize('host.open', "Opening Remote..."), undefined, WINDOW_ACTIONS_COMMAND_ID); - this.connectionState = 'initializing'; - RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); - - const connection = remoteAgentService.getConnection(); + const connection = this.remoteAgentService.getConnection(); if (connection) { this._register(connection.onDidStateChange((e) => { switch (e.type) { @@ -119,72 +143,106 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc })); } } + } - extensionService.whenInstalledExtensionsRegistered().then(_ => { - if (this.remoteAuthority) { - this._register(this.labelService.onDidChangeFormatters(e => this.updateWindowIndicator())); - remoteAuthorityResolverService.resolveAuthority(this.remoteAuthority).then(() => this.setDisconnected(false), () => this.setDisconnected(true)); - } - this._register(this.windowCommandMenu.onDidChange(e => this.updateWindowActions())); - this.updateWindowIndicator(); - }); + private async updateWhenInstalledExtensionsRegistered(): Promise { + await this.extensionService.whenInstalledExtensionsRegistered(); + + const remoteAuthority = this.remoteAuthority; + if (remoteAuthority) { + + // Try to resolve the authority to figure out connection state + (async () => { + try { + await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority); + + this.setDisconnected(false); + } catch (error) { + this.setDisconnected(true); + } + })(); + } + + this.updateRemoteStatusIndicator(); } private setDisconnected(isDisconnected: boolean): void { const newState = isDisconnected ? 'disconnected' : 'connected'; if (this.connectionState !== newState) { this.connectionState = newState; - RemoteConnectionState.bindTo(this.contextKeyService).set(this.connectionState); - Deprecated_RemoteAuthorityContext.bindTo(this.contextKeyService).set(isDisconnected ? `disconnected/${this.remoteAuthority!}` : this.remoteAuthority!); - this.updateWindowIndicator(); + this.connectionStateContextKey.set(this.connectionState); + + this.updateRemoteStatusIndicator(); } } - private updateWindowIndicator(): void { - const windowActionCommand = (this.remoteAuthority || this.windowCommandMenu.getActions().length) ? WINDOW_ACTIONS_COMMAND_ID : undefined; - if (this.remoteAuthority) { + private updateRemoteActions() { + const newHasWindowActions = this.remoteMenu.getActions().length > 0; + if (newHasWindowActions !== this.hasRemoteActions) { + this.hasRemoteActions = newHasWindowActions; + + this.updateRemoteStatusIndicator(); + } + } + + private updateRemoteStatusIndicator(): void { + + // Remote indicator: show if provided via options + const remoteIndicator = this.environmentService.options?.windowIndicator; + if (remoteIndicator) { + this.renderRemoteStatusIndicator(remoteIndicator.label, remoteIndicator.tooltip, remoteIndicator.command); + } + + // Remote Authority: show connection state + else if (this.remoteAuthority) { const hostLabel = this.labelService.getHostLabel(REMOTE_HOST_SCHEME, this.remoteAuthority) || this.remoteAuthority; - if (this.connectionState !== 'disconnected') { - this.renderWindowIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel), windowActionCommand); - } else { - this.renderWindowIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from")} ${hostLabel}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel), windowActionCommand); - } - } else { - if (windowActionCommand) { - this.renderWindowIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a remote window"), windowActionCommand); - } else if (this.windowIndicatorEntry) { - this.windowIndicatorEntry.dispose(); - this.windowIndicatorEntry = undefined; + switch (this.connectionState) { + case 'initializing': + this.renderRemoteStatusIndicator(`$(sync~spin) ${nls.localize('host.open', "Opening Remote...")}`, nls.localize('host.open', "Opening Remote...")); + break; + case 'disconnected': + this.renderRemoteStatusIndicator(`$(alert) ${nls.localize('disconnectedFrom', "Disconnected from {0}", hostLabel)}`, nls.localize('host.tooltipDisconnected', "Disconnected from {0}", hostLabel)); + break; + default: + this.renderRemoteStatusIndicator(`$(remote) ${hostLabel}`, nls.localize('host.tooltip', "Editing on {0}", hostLabel)); } } + + // Remote Extensions Installed: offer the indicator to show actions + else if (this.remoteMenu.getActions().length > 0) { + this.renderRemoteStatusIndicator(`$(remote)`, nls.localize('noHost.tooltip', "Open a Remote Window")); + } + + // No Remote Extensions: hide status indicator + else { + dispose(this.remoteStatusEntry); + this.remoteStatusEntry = undefined; + } } - private updateWindowActions() { - const newHasWindowActions = this.windowCommandMenu.getActions().length > 0; - if (newHasWindowActions !== this.hasWindowActions) { - this.hasWindowActions = newHasWindowActions; - this.updateWindowIndicator(); + private renderRemoteStatusIndicator(text: string, tooltip?: string, command?: string): void { + const name = nls.localize('remoteHost', "Remote Host"); + if (typeof command !== 'string' && this.remoteMenu.getActions().length > 0) { + command = RemoteStatusIndicator.REMOTE_ACTIONS_COMMAND_ID; } - } - private renderWindowIndicator(text: string, tooltip?: string, command?: string): void { const properties: IStatusbarEntry = { backgroundColor: themeColorFromId(STATUS_BAR_HOST_NAME_BACKGROUND), color: themeColorFromId(STATUS_BAR_HOST_NAME_FOREGROUND), - ariaLabel: nls.localize('remote', "Remote"), + ariaLabel: name, text, tooltip, command }; - if (this.windowIndicatorEntry) { - this.windowIndicatorEntry.update(properties); + + if (this.remoteStatusEntry) { + this.remoteStatusEntry.update(properties); } else { - this.windowIndicatorEntry = this.statusbarService.addEntry(properties, 'status.host', nls.localize('status.host', "Remote Host"), StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); + this.remoteStatusEntry = this.statusbarService.addEntry(properties, 'status.host', name, StatusbarAlignment.LEFT, Number.MAX_VALUE /* first entry */); } } - private showIndicatorActions(menu: IMenu) { - + private showRemoteMenu(menu: IMenu) { const actions = menu.getActions(); const items: (IQuickPickItem | IQuickPickSeparator)[] = []; @@ -192,6 +250,7 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc if (items.length) { items.push({ type: 'separator' }); } + for (let action of actionGroup[1]) { if (action instanceof MenuItemAction) { let label = typeof action.item.title === 'string' ? action.item.title : action.item.title.value; @@ -199,6 +258,7 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc const category = typeof action.item.category === 'string' ? action.item.category : action.item.category.value; label = nls.localize('cat.title', "{0}: {1}", category, label); } + items.push({ type: 'item', id: action.item.id, @@ -208,13 +268,14 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc } } - if (SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { + if (RemoteStatusIndicator.SHOW_CLOSE_REMOTE_COMMAND_ID && this.remoteAuthority) { if (items.length) { items.push({ type: 'separator' }); } + items.push({ type: 'item', - id: CLOSE_REMOTE_COMMAND_ID, + id: RemoteStatusIndicator.CLOSE_REMOTE_COMMAND_ID, label: nls.localize('closeRemote.title', 'Close Remote Connection') }); } @@ -227,8 +288,10 @@ export class RemoteWindowActiveIndicator extends Disposable implements IWorkbenc if (selectedItems.length === 1) { this.commandService.executeCommand(selectedItems[0].id!); } + quickPick.hide(); })); + quickPick.show(); } } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 32683bf8418..3b398442e30 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -37,7 +37,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { URI } from 'vs/base/common/uri'; -import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { isLocalhost, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -163,18 +163,34 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { }); } + private mapHasTunnel(map: Map, host: string, port: number): boolean { + if (!isLocalhost(host)) { + return map.has(MakeAddress(host, port)); + } + + const stringAddress = MakeAddress('localhost', port); + if (map.has(stringAddress)) { + return true; + } + const numberAddress = MakeAddress('127.0.0.1', port); + if (map.has(numberAddress)) { + return true; + } + return false; + } + get candidates(): TunnelItem[] { const candidates: TunnelItem[] = []; this._candidates.forEach(value => { - let key = MakeAddress(value.host, value.port); - if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) { + if (!this.mapHasTunnel(this.model.forwarded, value.host, value.port) && + !this.mapHasTunnel(this.model.detected, value.host, value.port)) { // The host:port hasn't been forwarded or detected. However, if the candidate is 0.0.0.0, // also check that the port hasn't already been forwarded with localhost, and vice versa. // For example: no need to show 0.0.0.0:3000 as a candidate if localhost:3000 is already forwarded. const otherHost = value.host === '0.0.0.0' ? 'localhost' : (value.host === 'localhost' ? '0.0.0.0' : undefined); if (otherHost) { - key = MakeAddress(otherHost, value.port); - if (this.model.forwarded.has(key) || this.model.detected.has(key)) { + if (this.mapHasTunnel(this.model.forwarded, otherHost, value.port) || + this.mapHasTunnel(this.model.detected, otherHost, value.port)) { return; } } @@ -411,11 +427,11 @@ class TunnelItem implements ITunnelItem { get label(): string { if (this.name) { return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name); - } else if (this.localAddress && (this.remoteHost !== 'localhost')) { + } else if (this.localAddress && !isLocalhost(this.remoteHost)) { return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0}:{1} \u2192 {2}", this.remoteHost, this.remotePort, this.localAddress); } else if (this.localAddress) { return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} \u2192 {1}", this.remotePort, this.localAddress); - } else if (this.remoteHost !== 'localhost') { + } else if (!isLocalhost(this.remoteHost)) { return nls.localize('remote.tunnelsView.forwardedPortLabel4', "{0}:{1}", this.remoteHost, this.remotePort); } else { return nls.localize('remote.tunnelsView.forwardedPortLabel5', "{0}", this.remotePort); diff --git a/src/vs/workbench/contrib/remote/browser/urlFinder.ts b/src/vs/workbench/contrib/remote/browser/urlFinder.ts index e72dc1fdeb0..d0ee06fd48b 100644 --- a/src/vs/workbench/contrib/remote/browser/urlFinder.ts +++ b/src/vs/workbench/contrib/remote/browser/urlFinder.ts @@ -60,7 +60,7 @@ export class UrlFinder extends Disposable { if (!isNaN(port) && Number.isInteger(port) && port > 0 && port <= 65535) { // normalize the host name let host = serverUrl.hostname; - if (host !== '0.0.0.0') { + if (host !== '0.0.0.0' && host !== '127.0.0.1') { host = 'localhost'; } this._onDidMatchLocalUrl.fire({ port, host }); diff --git a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts index 450448112bb..14550a5bcb7 100644 --- a/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts +++ b/src/vs/workbench/contrib/scm/browser/dirtydiffDecorator.ts @@ -982,7 +982,7 @@ function compareChanges(a: IChange, b: IChange): number { return a.originalEndLineNumber - b.originalEndLineNumber; } -function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number { +export function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => number { return (a, b) => { const aIsParent = isEqualOrParent(uri, a.rootUri!); const bIsParent = isEqualOrParent(uri, b.rootUri!); @@ -999,6 +999,22 @@ function createProviderComparer(uri: URI): (a: ISCMProvider, b: ISCMProvider) => }; } +export async function getOriginalResource(scmService: ISCMService, uri: URI): Promise { + const providers = scmService.repositories.map(r => r.provider); + const rootedProviders = providers.filter(p => !!p.rootUri); + + rootedProviders.sort(createProviderComparer(uri)); + + const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); + + if (result) { + return result; + } + + const nonRootedProviders = providers.filter(p => !p.rootUri); + return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); +} + export class DirtyDiffModel extends Disposable { private _originalModel: IResolvedTextFileEditorModel | null = null; @@ -1155,19 +1171,7 @@ export class DirtyDiffModel extends Disposable { } const uri = this._model.resource; - const providers = this.scmService.repositories.map(r => r.provider); - const rootedProviders = providers.filter(p => !!p.rootUri); - - rootedProviders.sort(createProviderComparer(uri)); - - const result = await first(rootedProviders.map(p => () => p.getOriginalResource(uri))); - - if (result) { - return result; - } - - const nonRootedProviders = providers.filter(p => !p.rootUri); - return first(nonRootedProviders.map(p => () => p.getOriginalResource(uri))); + return getOriginalResource(this.scmService, uri); } findNextClosestChange(lineNumber: number, inclusive = true): number { diff --git a/src/vs/workbench/contrib/scm/browser/menus.ts b/src/vs/workbench/contrib/scm/browser/menus.ts index aff10c40551..4fd31d3a776 100644 --- a/src/vs/workbench/contrib/scm/browser/menus.ts +++ b/src/vs/workbench/contrib/scm/browser/menus.ts @@ -180,6 +180,7 @@ export class SCMRepositoryMenus implements ISCMRepositoryMenus, IDisposable { ) { this.contextKeyService = contextKeyService.createScoped(); this.contextKeyService.createKey('scmProvider', provider.contextValue); + this.contextKeyService.createKey('scmProviderRootUri', provider.rootUri?.toString()); this.contextKeyService.createKey('scmProviderHasRootUri', !!provider.rootUri); const serviceCollection = new ServiceCollection([IContextKeyService, this.contextKeyService]); diff --git a/src/vs/workbench/contrib/search/browser/replaceService.ts b/src/vs/workbench/contrib/search/browser/replaceService.ts index 90023f40b24..ba13946ada5 100644 --- a/src/vs/workbench/contrib/search/browser/replaceService.ts +++ b/src/vs/workbench/contrib/search/browser/replaceService.ts @@ -18,10 +18,9 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; +import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService'; import { Range } from 'vs/editor/common/core/range'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { mergeSort } from 'vs/base/common/arrays'; @@ -101,8 +100,8 @@ export class ReplaceService implements IReplaceService { replace(files: FileMatch[], progress?: IProgress): Promise; replace(match: FileMatchOrMatch, progress?: IProgress, resource?: URI): Promise; async replace(arg: any, progress: IProgress | undefined = undefined, resource: URI | null = null): Promise { - const edits: WorkspaceTextEdit[] = this.createEdits(arg, resource); - await this.bulkEditorService.apply({ edits }, { progress }); + const edits = this.createEdits(arg, resource); + await this.bulkEditorService.apply(edits, { progress }); return Promise.all(edits.map(e => this.textFileService.files.get(e.resource)?.save())); } @@ -162,15 +161,15 @@ export class ReplaceService implements IReplaceService { const modelEdits: IIdentifiedSingleEditOperation[] = []; for (const resourceEdit of resourceEdits) { modelEdits.push(EditOperation.replaceMove( - Range.lift(resourceEdit.edit.range), - resourceEdit.edit.text) + Range.lift(resourceEdit.textEdit.range), + resourceEdit.textEdit.text) ); } replaceModel.pushEditOperations([], mergeSort(modelEdits, (a, b) => Range.compareRangesUsingStarts(a.range, b.range)), () => []); } - private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): WorkspaceTextEdit[] { - const edits: WorkspaceTextEdit[] = []; + private createEdits(arg: FileMatchOrMatch | FileMatch[], resource: URI | null = null): ResourceTextEdit[] { + const edits: ResourceTextEdit[] = []; if (arg instanceof Match) { const match = arg; @@ -193,15 +192,11 @@ export class ReplaceService implements IReplaceService { return edits; } - private createEdit(match: Match, text: string, resource: URI | null = null): WorkspaceTextEdit { + private createEdit(match: Match, text: string, resource: URI | null = null): ResourceTextEdit { const fileMatch: FileMatch = match.parent(); - const resourceEdit: WorkspaceTextEdit = { - resource: resource !== null ? resource : fileMatch.resource, - edit: { - range: match.range(), - text: text - } - }; - return resourceEdit; + return new ResourceTextEdit( + resource ?? fileMatch.resource, + { range: match.range(), text }, undefined, undefined + ); } } diff --git a/src/vs/workbench/contrib/search/browser/searchActions.ts b/src/vs/workbench/contrib/search/browser/searchActions.ts index 01d1831bb0a..c5eedd23793 100644 --- a/src/vs/workbench/contrib/search/browser/searchActions.ts +++ b/src/vs/workbench/contrib/search/browser/searchActions.ts @@ -683,6 +683,8 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { static readonly LABEL = nls.localize('match.replace.label', "Replace"); + static runQ = Promise.resolve(); + constructor(private viewer: WorkbenchObjectTree, private element: Match, private viewlet: SearchView, @IReplaceService private readonly replaceService: IReplaceService, @IKeybindingService keyBindingService: IKeybindingService, @@ -691,26 +693,24 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { super(Constants.ReplaceActionId, appendKeyBindingLabel(ReplaceAction.LABEL, keyBindingService.lookupKeybinding(Constants.ReplaceActionId), keyBindingService), searchReplaceIcon.classNames); } - run(): Promise { + async run(): Promise { this.enabled = false; - return this.element.parent().replace(this.element).then(() => { - const elementToFocus = this.getElementToFocusAfterReplace(); - if (elementToFocus) { - this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); - } + await this.element.parent().replace(this.element); + const elementToFocus = this.getElementToFocusAfterReplace(); + if (elementToFocus) { + this.viewer.setFocus([elementToFocus], getSelectionKeyboardEvent()); + } - return this.getElementToShowReplacePreview(elementToFocus); - }).then(elementToShowReplacePreview => { - this.viewer.domFocus(); + const elementToShowReplacePreview = this.getElementToShowReplacePreview(elementToFocus); + this.viewer.domFocus(); - const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; - if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile()) { - this.viewlet.open(this.element, true); - } else { - this.replaceService.openReplacePreview(elementToShowReplacePreview, true); - } - }); + const useReplacePreview = this.configurationService.getValue().search.useReplacePreview; + if (!useReplacePreview || !elementToShowReplacePreview || this.hasToOpenFile()) { + this.viewlet.open(this.element, true); + } else { + this.replaceService.openReplacePreview(elementToShowReplacePreview, true); + } } private getElementToFocusAfterReplace(): RenderableMatch { @@ -740,11 +740,11 @@ export class ReplaceAction extends AbstractSearchAndReplaceAction { return elementToFocus!; } - private async getElementToShowReplacePreview(elementToFocus: RenderableMatch): Promise { + private getElementToShowReplacePreview(elementToFocus: RenderableMatch): Match | null { if (this.hasSameParent(elementToFocus)) { return elementToFocus; } - const previousElement = await this.getPreviousElementAfterRemoved(this.viewer, this.element); + const previousElement = this.getPreviousElementAfterRemoved(this.viewer, this.element); if (this.hasSameParent(previousElement)) { return previousElement; } diff --git a/src/vs/workbench/contrib/search/common/searchModel.ts b/src/vs/workbench/contrib/search/common/searchModel.ts index e5bbd712833..983355a2396 100644 --- a/src/vs/workbench/contrib/search/common/searchModel.ts +++ b/src/vs/workbench/contrib/search/common/searchModel.ts @@ -360,9 +360,12 @@ export class FileMatch extends Disposable implements IFileMatch { this._onChange.fire({ didRemove: true }); } - replace(toReplace: Match): Promise { - return this.replaceService.replace(toReplace) - .then(() => this.updatesMatchesForLineAfterReplace(toReplace.range().startLineNumber, false)); + private replaceQ = Promise.resolve(); + async replace(toReplace: Match): Promise { + return this.replaceQ = this.replaceQ.finally(async () => { + await this.replaceService.replace(toReplace); + this.updatesMatchesForLineAfterReplace(toReplace.range().startLineNumber, false); + }); } setSelectedMatch(match: Match | null): void { diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts index 533d6fe56fb..4c1c7205324 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditor.ts @@ -37,7 +37,7 @@ import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { BaseTextEditor } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions } from 'vs/workbench/common/editor'; +import { EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/contrib/search/browser/patternInputWidget'; import { SearchWidget } from 'vs/workbench/contrib/search/browser/searchWidget'; import { InputBoxFocusedKey } from 'vs/workbench/contrib/search/common/constants'; @@ -280,6 +280,14 @@ export class SearchEditor extends BaseTextEditor { } } + setQuery(query: string) { + this.queryEditorWidget.searchInput.setValue(query); + } + + selectQuery() { + this.queryEditorWidget.searchInput.select(); + } + toggleWholeWords() { this.queryEditorWidget.searchInput.setWholeWords(!this.queryEditorWidget.searchInput.getWholeWords()); this.triggerSearch({ resetCursor: false }); @@ -548,10 +556,10 @@ export class SearchEditor extends BaseTextEditor { return this._input as SearchEditorInput; } - async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + async setInput(newInput: SearchEditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { this.saveViewState(); - await super.setInput(newInput, options, token); + await super.setInput(newInput, options, context, token); if (token.isCancellationRequested) { return; } const { body, config } = await newInput.getModels(); diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts index 0863b0b1c71..5b366415b8a 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorActions.ts @@ -148,7 +148,8 @@ export const openNewSearchEditor = if (existing && args.location === 'reuse') { const input = existing.editor as SearchEditorInput; editor = assertIsDefined(await assertIsDefined(editorGroupsService.getGroup(existing.groupId)).openEditor(input)) as SearchEditor; - editor.focusSearchInput(); + if (selected) { editor.setQuery(selected); } + else { editor.selectQuery(); } } else { const input = instantiationService.invokeFunction(getOrMakeSearchEditorInput, { config: args, text: '' }); editor = await editorService.openEditor(input, { pinned: true }, toSide ? SIDE_GROUP : ACTIVE_GROUP) as SearchEditor; @@ -161,6 +162,8 @@ export const openNewSearchEditor = ) { editor.triggerSearch({ focusResults: args.focusResults !== false }); } + + if (args.focusResults === false) { editor.focusSearchInput(); } }; export const createEditorFromSearchResult = diff --git a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts index c884a84be2b..da6858a833e 100644 --- a/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts +++ b/src/vs/workbench/contrib/searchEditor/browser/searchEditorInput.ts @@ -112,7 +112,7 @@ export class SearchEditorInput extends EditorInput { const workingCopyAdapter = new class implements IWorkingCopy { readonly resource = input.modelUri; get name() { return input.getName(); } - readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : 0; + readonly capabilities = input.isUntitled() ? WorkingCopyCapabilities.Untitled : WorkingCopyCapabilities.None; readonly onDidChangeDirty = input.onDidChangeDirty; readonly onDidChangeContent = input.onDidChangeContent; isDirty(): boolean { return input.isDirty(); } diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index e757824eb43..883e2ffcb85 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -1528,7 +1528,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (this._taskSystem?.isTaskVisible(executeResult.task)) { const message = nls.localize('TaskSystem.activeSame.noBackground', 'The task \'{0}\' is already active.', executeResult.task.getQualifiedLabel()); let lastInstance = this.getTaskSystem().getLastInstance(executeResult.task) ?? executeResult.task; - this.notificationService.prompt(Severity.Info, message, + this.notificationService.prompt(Severity.Warning, message, [{ label: nls.localize('terminateTask', "Terminate Task"), run: () => this.terminate(lastInstance) @@ -2606,7 +2606,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer } if (buildTasks.length === 1) { this.tryResolveTask(buildTasks[0]).then(resolvedTask => { - this.run(resolvedTask).then(undefined, reason => { + this.run(resolvedTask, undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); }); @@ -2617,7 +2617,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { let { defaults, users } = this.splitPerGroupType(tasks); if (defaults.length === 1) { - this.run(defaults[0]).then(undefined, reason => { + this.run(defaults[0], undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); return; @@ -2641,7 +2641,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.runConfigureDefaultBuildTask(); return; } - this.run(task, { attachProblemMatcher: true }).then(undefined, reason => { + this.run(task, { attachProblemMatcher: true }, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); }); @@ -2667,7 +2667,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { let { defaults, users } = this.splitPerGroupType(tasks); if (defaults.length === 1) { - this.run(defaults[0]).then(undefined, reason => { + this.run(defaults[0], undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); return; @@ -2691,7 +2691,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer this.runConfigureTasks(); return; } - this.run(task).then(undefined, reason => { + this.run(task, undefined, TaskRunSource.User).then(undefined, reason => { // eat the error, it has already been surfaced to the user and we don't care about it here }); }); @@ -2963,7 +2963,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (tasks.length > 0) { tasks = tasks.sort((a, b) => a._label.localeCompare(b._label)); for (let task of tasks) { - entries.push({ label: task._label, task, description: this.getTaskDescription(task) }); + entries.push({ label: task._label, task, description: this.getTaskDescription(task), detail: this.showDetail() ? task.configurationProperties.detail : undefined }); if (!ContributedTask.is(task)) { needsCreateOrOpen = false; } @@ -3059,7 +3059,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (selectedTask) { selectedEntry = { label: nls.localize('TaskService.defaultBuildTaskExists', '{0} is already marked as the default build task', selectedTask.getQualifiedLabel()), - task: selectedTask + task: selectedTask, + detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined }; } this.showIgnoredFoldersMessage().then(() => { @@ -3110,7 +3111,8 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer if (selectedTask) { selectedEntry = { label: nls.localize('TaskService.defaultTestTaskExists', '{0} is already marked as the default test task.', selectedTask.getQualifiedLabel()), - task: selectedTask + task: selectedTask, + detail: this.showDetail() ? selectedTask.configurationProperties.detail : undefined }; } diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 4866dd1e823..c17c12d9b76 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -8,11 +8,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ITaskService, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; import { forEach } from 'vs/base/common/collections'; -import { RunOnOptions, Task, TaskRunSource } from 'vs/workbench/contrib/tasks/common/tasks'; +import { RunOnOptions, Task, TaskRunSource, TASKS_CATEGORY } from 'vs/workbench/contrib/tasks/common/tasks'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { Action } from 'vs/base/common/actions'; import { IQuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { Action2 } from 'vs/platform/actions/common/actions'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; const ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE = 'tasks.run.allowAutomatic'; @@ -132,27 +133,29 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut } -export class ManageAutomaticTaskRunning extends Action { +export class ManageAutomaticTaskRunning extends Action2 { public static readonly ID = 'workbench.action.tasks.manageAutomaticRunning'; public static readonly LABEL = nls.localize('workbench.action.tasks.manageAutomaticRunning', "Manage Automatic Tasks in Folder"); - constructor( - id: string, label: string, - @IStorageService private readonly storageService: IStorageService, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label); + constructor() { + super({ + id: ManageAutomaticTaskRunning.ID, + title: ManageAutomaticTaskRunning.LABEL, + category: TASKS_CATEGORY + }); } - public async run(): Promise { + public async run(accessor: ServicesAccessor): Promise { + const quickInputService = accessor.get(IQuickInputService); + const storageService = accessor.get(IStorageService); const allowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.allowAutomaticTasks', "Allow Automatic Tasks in Folder") }; const disallowItem: IQuickPickItem = { label: nls.localize('workbench.action.tasks.disallowAutomaticTasks', "Disallow Automatic Tasks in Folder") }; - const value = await this.quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); + const value = await quickInputService.pick([allowItem, disallowItem], { canPickMany: false }); if (!value) { return; } - this.storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, value === allowItem, StorageScope.WORKSPACE); + storageService.store(ARE_AUTOMATIC_TASKS_ALLOWED_IN_WORKSPACE, value === allowItem, StorageScope.WORKSPACE); } } diff --git a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts index 6418a23ec93..25e5462bf69 100644 --- a/src/vs/workbench/contrib/tasks/browser/task.contribution.ts +++ b/src/vs/workbench/contrib/tasks/browser/task.contribution.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Disposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { MenuRegistry, MenuId, SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { ProblemMatcherRegistry } from 'vs/workbench/contrib/tasks/common/problemMatcher'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -20,11 +20,10 @@ import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatus import { IOutputChannelRegistry, Extensions as OutputExt } from 'vs/workbench/services/output/common/output'; -import { TaskEvent, TaskEventKind, TaskGroup, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; -import { ITaskService } from 'vs/workbench/contrib/tasks/common/taskService'; +import { TaskEvent, TaskEventKind, TaskGroup, TASKS_CATEGORY, TASK_RUNNING_STATE } from 'vs/workbench/contrib/tasks/common/tasks'; +import { ITaskService, ProcessExecutionSupportedContext, ShellExecutionSupportedContext } from 'vs/workbench/contrib/tasks/common/taskService'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWorkbenchContribution } from 'vs/workbench/common/contributions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; import { RunAutomaticTasks, ManageAutomaticTaskRunning } from 'vs/workbench/contrib/tasks/browser/runAutomaticTasks'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; @@ -36,14 +35,22 @@ import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'v import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/platform/quickinput/common/quickAccess'; import { TasksQuickAccessProvider } from 'vs/workbench/contrib/tasks/browser/tasksQuickAccess'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -let tasksCategory = { value: nls.localize('tasksCategory', "Tasks"), original: 'Tasks' }; +const SHOW_TASKS_COMMANDS_CONTEXT = ContextKeyExpr.or(ShellExecutionSupportedContext, ProcessExecutionSupportedContext); const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchRegistry.registerWorkbenchContribution(RunAutomaticTasks, LifecyclePhase.Eventually); -const actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ManageAutomaticTaskRunning), 'Tasks: Manage Automatic Tasks in Folder', tasksCategory.value); +registerAction2(ManageAutomaticTaskRunning); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ManageAutomaticTaskRunning.ID, + title: ManageAutomaticTaskRunning.LABEL, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); export class TaskStatusBarContributions extends Disposable implements IWorkbenchContribution { private runningTasksStatusItem: IStatusbarEntryAccessor | undefined; @@ -160,7 +167,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.runTask', title: nls.localize({ key: 'miRunTask', comment: ['&& denotes a mnemonic'] }, "&&Run Task...") }, - order: 1 + order: 1, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -169,7 +177,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.build', title: nls.localize({ key: 'miBuildTask', comment: ['&& denotes a mnemonic'] }, "Run &&Build Task...") }, - order: 2 + order: 2, + when: SHOW_TASKS_COMMANDS_CONTEXT }); // Manage Tasks @@ -180,7 +189,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.showTasks', title: nls.localize({ key: 'miRunningTask', comment: ['&& denotes a mnemonic'] }, "Show Runnin&&g Tasks...") }, - order: 1 + order: 1, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -190,7 +200,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.restartTask', title: nls.localize({ key: 'miRestartTask', comment: ['&& denotes a mnemonic'] }, "R&&estart Running Task...") }, - order: 2 + order: 2, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -200,7 +211,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.terminate', title: nls.localize({ key: 'miTerminateTask', comment: ['&& denotes a mnemonic'] }, "&&Terminate Task...") }, - order: 3 + order: 3, + when: SHOW_TASKS_COMMANDS_CONTEXT }); // Configure Tasks @@ -210,7 +222,8 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.configureTaskRunner', title: nls.localize({ key: 'miConfigureTask', comment: ['&& denotes a mnemonic'] }, "&&Configure Tasks...") }, - order: 1 + order: 1, + when: SHOW_TASKS_COMMANDS_CONTEXT }); MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { @@ -219,31 +232,123 @@ MenuRegistry.appendMenuItem(MenuId.MenubarTerminalMenu, { id: 'workbench.action.tasks.configureDefaultBuildTask', title: nls.localize({ key: 'miConfigureBuildTask', comment: ['&& denotes a mnemonic'] }, "Configure De&&fault Build Task...") }, - order: 2 + order: 2, + when: SHOW_TASKS_COMMANDS_CONTEXT }); -MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({ +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: 'workbench.action.tasks.openWorkspaceFileTasks', title: { value: nls.localize('workbench.action.tasks.openWorkspaceFileTasks', "Open Workspace Tasks"), original: 'Open Workspace Tasks' }, - category: tasksCategory + category: TASKS_CATEGORY }, - when: WorkbenchStateContext.isEqualTo('workspace') -})); + when: ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace'), SHOW_TASKS_COMMANDS_CONTEXT) +}); -MenuRegistry.addCommand({ id: ConfigureTaskAction.ID, title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.showLog', title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.runTask', title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.reRunTask', title: { value: nls.localize('ReRunTaskAction.label', "Rerun Last Task"), original: 'Rerun Last Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.restartTask', title: { value: nls.localize('RestartTaskAction.label', "Restart Running Task"), original: 'Restart Running Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.showTasks', title: { value: nls.localize('ShowTasksAction.label', "Show Running Tasks"), original: 'Show Running Tasks' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.terminate', title: { value: nls.localize('TerminateAction.label', "Terminate Task"), original: 'Terminate Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.build', title: { value: nls.localize('BuildAction.label', "Run Build Task"), original: 'Run Build Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.test', title: { value: nls.localize('TestAction.label', "Run Test Task"), original: 'Run Test Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.configureDefaultBuildTask', title: { value: nls.localize('ConfigureDefaultBuildTask.label', "Configure Default Build Task"), original: 'Configure Default Build Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.configureDefaultTestTask', title: { value: nls.localize('ConfigureDefaultTestTask.label', "Configure Default Test Task"), original: 'Configure Default Test Task' }, category: tasksCategory }); -MenuRegistry.addCommand({ id: 'workbench.action.tasks.openUserTasks', title: { value: nls.localize('workbench.action.tasks.openUserTasks', "Open User Tasks"), original: 'Open User Tasks' }, category: tasksCategory }); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: ConfigureTaskAction.ID, + title: { value: ConfigureTaskAction.TEXT, original: 'Configure Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.showLog', + title: { value: nls.localize('ShowLogAction.label', "Show Task Log"), original: 'Show Task Log' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.runTask', + title: { value: nls.localize('RunTaskAction.label', "Run Task"), original: 'Run Task' }, + category: TASKS_CATEGORY + } +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.reRunTask', + title: { value: nls.localize('ReRunTaskAction.label', "Rerun Last Task"), original: 'Rerun Last Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.restartTask', + title: { value: nls.localize('RestartTaskAction.label', "Restart Running Task"), original: 'Restart Running Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.showTasks', + title: { value: nls.localize('ShowTasksAction.label', "Show Running Tasks"), original: 'Show Running Tasks' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.terminate', + title: { value: nls.localize('TerminateAction.label', "Terminate Task"), original: 'Terminate Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.build', + title: { value: nls.localize('BuildAction.label', "Run Build Task"), original: 'Run Build Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.test', + title: { value: nls.localize('TestAction.label', "Run Test Task"), original: 'Run Test Task' }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.configureDefaultBuildTask', + title: { + value: nls.localize('ConfigureDefaultBuildTask.label', "Configure Default Build Task"), + original: 'Configure Default Build Task' + }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.configureDefaultTestTask', + title: { + value: nls.localize('ConfigureDefaultTestTask.label', "Configure Default Test Task"), + original: 'Configure Default Test Task' + }, + category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); +MenuRegistry.appendMenuItem(MenuId.CommandPalette, { + command: { + id: 'workbench.action.tasks.openUserTasks', + title: { + value: nls.localize('workbench.action.tasks.openUserTasks', "Open User Tasks"), + original: 'Open User Tasks' + }, category: TASKS_CATEGORY + }, + when: SHOW_TASKS_COMMANDS_CONTEXT +}); // MenuRegistry.addCommand( { id: 'workbench.action.tasks.rebuild', title: nls.localize('RebuildAction.label', 'Run Rebuild Task'), category: tasksCategory }); // MenuRegistry.addCommand( { id: 'workbench.action.tasks.clean', title: nls.localize('CleanAction.label', 'Run Clean Task'), category: tasksCategory }); diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index f6993a12c3c..e35865f75fc 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -81,12 +81,12 @@ class InstanceManager { class VariableResolver { - constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, private _values: Map, private _service: IConfigurationResolverService | undefined) { + constructor(public workspaceFolder: IWorkspaceFolder | undefined, public taskSystemInfo: TaskSystemInfo | undefined, public readonly values: Map, private _service: IConfigurationResolverService | undefined) { } resolve(value: string): string { return value.replace(/\$\{(.*?)\}/g, (match: string, variable: string) => { // Strip out the ${} because the map contains them variables without those characters. - let result = this._values.get(match.substring(2, match.length - 1)); + let result = this.values.get(match.substring(2, match.length - 1)); if ((result !== undefined) && (result !== null)) { return result; } @@ -840,7 +840,7 @@ export class TerminalTaskSystem implements ITaskSystem { }, (_error) => { // The process never got ready. Need to think how to handle this. }); - this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task, terminal.id)); + this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Start, task, terminal.id, resolver.values)); const mapKey = task.getMapKey(); this.busyTasks[mapKey] = task; this._onDidStateChange.fire(TaskEvent.create(TaskEventKind.Active, task)); @@ -1331,6 +1331,22 @@ export class TerminalTaskSystem implements ITaskSystem { this.collectCommandVariables(variables, task.command, task); } this.collectMatcherVariables(variables, task.configurationProperties.problemMatchers); + + if (task.command.runtime === RuntimeType.CustomExecution && CustomTask.is(task)) { + this.collectDefinitionVariables(variables, task._source.config.element); + } + } + + private collectDefinitionVariables(variables: Set, definition: any): void { + for (const key in definition) { + if (Types.isString(definition[key])) { + this.collectVariables(variables, definition[key]); + } else if (Types.isArray(definition[key])) { + definition[key].forEach((element: any) => this.collectDefinitionVariables(variables, element)); + } else if (Types.isObject(definition[key])) { + this.collectDefinitionVariables(variables, definition[key]); + } + } } private collectCommandVariables(variables: Set, command: CommandConfiguration, task: CustomTask | ContributedTask): void { diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index ec40a24a548..a88910f481f 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -19,6 +19,7 @@ import { ConfigurationTarget } from 'vs/platform/configuration/common/configurat import { USER_TASKS_GROUP_KEY } from 'vs/workbench/contrib/tasks/common/taskService'; export const TASK_RUNNING_STATE = new RawContextKey('taskRunning', false); +export const TASKS_CATEGORY = { value: nls.localize('tasksCategory', "Tasks"), original: 'Tasks' }; export enum ShellQuoting { /** @@ -1066,6 +1067,7 @@ export interface TaskEvent { exitCode?: number; terminalId?: number; __task?: Task; + resolvedVariables?: Map; } export const enum TaskRunSource { @@ -1077,10 +1079,10 @@ export const enum TaskRunSource { export namespace TaskEvent { export function create(kind: TaskEventKind.ProcessStarted | TaskEventKind.ProcessEnded, task: Task, processIdOrExitCode?: number): TaskEvent; - export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number): TaskEvent; + export function create(kind: TaskEventKind.Start, task: Task, terminalId?: number, resolvedVariables?: Map): TaskEvent; export function create(kind: TaskEventKind.DependsOnStarted | TaskEventKind.Start | TaskEventKind.Active | TaskEventKind.Inactive | TaskEventKind.Terminated | TaskEventKind.End, task: Task): TaskEvent; export function create(kind: TaskEventKind.Changed): TaskEvent; - export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number): TaskEvent { + export function create(kind: TaskEventKind, task?: Task, processIdOrExitCodeOrTerminalId?: number, resolvedVariables?: Map): TaskEvent { if (task) { let result: TaskEvent = { kind: kind, @@ -1095,6 +1097,7 @@ export namespace TaskEvent { }; if (kind === TaskEventKind.Start) { result.terminalId = processIdOrExitCodeOrTerminalId; + result.resolvedVariables = resolvedVariables; } else if (kind === TaskEventKind.ProcessStarted) { result.processId = processIdOrExitCodeOrTerminalId; } else if (kind === TaskEventKind.ProcessEnded) { diff --git a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts index 9297f97fd2a..e39db0a24d6 100644 --- a/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts +++ b/src/vs/workbench/contrib/terminal/browser/links/terminalLinkHelpers.ts @@ -54,6 +54,12 @@ export function convertLinkRangeToBuffer(lines: IBufferLine[], bufferWidth: numb const startLineOffset = (y === startWrappedLineCount - 1 ? startOffset : 0); let lineOffset = 0; const line = lines[y]; + // Sanity check for line, apparently this can happen but it's not clear under what + // circumstances this happens. Continue on, skipping the remainder of start offset if this + // happens to minimize impact. + if (!line) { + break; + } for (let x = start; x < Math.min(bufferWidth, lineLength + lineOffset + startLineOffset); x++) { const cell = line.getCell(x)!; const width = cell.getWidth(); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 01fbd5bded8..7739e56ef72 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -1128,9 +1128,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { if (!reset) { // HACK: Force initialText to be non-falsy for reused terminals such that the - // conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to hang - // in Windows 10 1903 so we only want to use it when something is definitely written to the - // terminal. + // conptyInheritCursor flag is passed to the node-pty, this flag can cause a Window to stop + // responding in Windows 10 1903 so we only want to use it when something is definitely written + // to the terminal. shell.initialText = ' '; } diff --git a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts index 20c3551c8f7..2ad5db5eb26 100644 --- a/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts +++ b/src/vs/workbench/contrib/terminal/common/terminalEnvironment.ts @@ -93,7 +93,7 @@ export function shouldSetLangEnvVariable(env: platform.IProcessEnvironment, dete return true; } if (detectLocale === 'auto') { - return !env['LANG'] || env['LANG'].search(/\.UTF\-8$/) === -1; + return !env['LANG'] || (env['LANG'].search(/\.UTF\-8$/) === -1 && env['LANG'].search(/\.utf8$/) === -1); } return false; // 'off' } diff --git a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts index a007d9aa027..f3a9e3da81c 100644 --- a/src/vs/workbench/contrib/terminal/node/terminalProcess.ts +++ b/src/vs/workbench/contrib/terminal/node/terminalProcess.ts @@ -5,7 +5,7 @@ import * as path from 'vs/base/common/path'; import * as platform from 'vs/base/common/platform'; -import * as pty from 'node-pty'; +import type * as pty from 'node-pty'; import * as fs from 'fs'; import { Event, Emitter } from 'vs/base/common/event'; import { getWindowsBuildNumber } from 'vs/workbench/contrib/terminal/node/terminal'; @@ -90,7 +90,7 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess } try { - this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); + await this.setupPtyProcess(this._shellLaunchConfig, this._ptyOptions); return undefined; } catch (err) { this._logService.trace('IPty#spawn native exception', err); @@ -136,10 +136,10 @@ export class TerminalProcess extends Disposable implements ITerminalChildProcess return undefined; } - private setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): void { + private async setupPtyProcess(shellLaunchConfig: IShellLaunchConfig, options: pty.IPtyForkOptions): Promise { const args = shellLaunchConfig.args || []; this._logService.trace('IPty#spawn', shellLaunchConfig.executable, args, options); - const ptyProcess = pty.spawn(shellLaunchConfig.executable!, args, options); + const ptyProcess = (await import('node-pty')).spawn(shellLaunchConfig.executable!, args, options); this._ptyProcess = ptyProcess; this._processStartupComplete = new Promise(c => { this.onProcessReady(() => c()); diff --git a/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts b/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts index 78d6cee648e..2ced826aa57 100644 --- a/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts +++ b/src/vs/workbench/contrib/terminal/test/node/terminalEnvironment.test.ts @@ -45,16 +45,22 @@ suite('Workbench - TerminalEnvironment', () => { test('auto', () => { assert.equal(shouldSetLangEnvVariable({}, 'auto'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'auto'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'auto'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'auto'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'auto'), false); }); test('off', () => { assert.equal(shouldSetLangEnvVariable({}, 'off'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'off'), false); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'off'), false); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'off'), false); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'off'), false); }); test('on', () => { assert.equal(shouldSetLangEnvVariable({}, 'on'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US' }, 'on'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf' }, 'on'), true); + assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.utf8' }, 'on'), true); assert.equal(shouldSetLangEnvVariable({ LANG: 'en-US.UTF-8' }, 'on'), true); }); }); diff --git a/src/vs/workbench/contrib/timeline/common/timelineService.ts b/src/vs/workbench/contrib/timeline/common/timelineService.ts index 2d2d5e76d5a..1e6dde4eb70 100644 --- a/src/vs/workbench/contrib/timeline/common/timelineService.ts +++ b/src/vs/workbench/contrib/timeline/common/timelineService.ts @@ -27,7 +27,6 @@ export class TimelineService implements ITimelineService { private readonly _onDidChangeUri = new Emitter(); readonly onDidChangeUri: Event = this._onDidChangeUri.event; - private excludedSources: Set; private readonly hasProviderContext: IContextKey; private readonly providers = new Map(); private readonly providerSubscriptions = new Map(); @@ -39,16 +38,6 @@ export class TimelineService implements ITimelineService { @IContextKeyService protected contextKeyService: IContextKeyService, ) { this.hasProviderContext = TimelineHasProviderContext.bindTo(this.contextKeyService); - - this.excludedSources = new Set(configurationService.getValue('timeline.excludeSources')); - configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('timeline.excludeSources')) { - this.excludedSources = new Set(this.configurationService.getValue('timeline.excludeSources')); - - this.updateHasProviderContext(); - } - }, this); - this.updateHasProviderContext(); // let source = 'fast-source'; @@ -271,12 +260,6 @@ export class TimelineService implements ITimelineService { } private updateHasProviderContext() { - if (this.providers.size === 0) { - this.hasProviderContext.set(false); - return; - } - - const hasProviders = [...this.providers.keys()].some(id => !this.excludedSources.has(id)); - this.hasProviderContext.set(hasProviders); + this.hasProviderContext.set(this.providers.size !== 0); } } diff --git a/src/vs/workbench/contrib/update/browser/update.ts b/src/vs/workbench/contrib/update/browser/update.ts index 5d590de2e93..8c45b22c10a 100644 --- a/src/vs/workbench/contrib/update/browser/update.ts +++ b/src/vs/workbench/contrib/update/browser/update.ts @@ -14,7 +14,6 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IUpdateService, State as UpdateState, StateType, IUpdate } from 'vs/platform/update/common/update'; -import * as semver from 'semver-umd'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -131,7 +130,7 @@ export class ProductContribution implements IWorkbenchContribution { @IHostService hostService: IHostService, @IProductService productService: IProductService ) { - hostService.hadLastFocus().then(hadLastFocus => { + hostService.hadLastFocus().then(async hadLastFocus => { if (!hadLastFocus) { return; } @@ -160,6 +159,7 @@ export class ProductContribution implements IWorkbenchContribution { } // should we show the new license? + const semver = await import('semver-umd'); if (productService.licenseUrl && lastVersion && semver.satisfies(lastVersion, '<1.0.0') && semver.satisfies(productService.version, '>=1.0.0')) { notificationService.info(nls.localize('licenseChanged', "Our license terms have changed, please click [here]({0}) to go through them.", productService.licenseUrl)); } @@ -435,7 +435,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu group: '6_update', command: { id: 'update.downloadNow', - title: nls.localize('download update', "Download Update") + title: nls.localize('download update_1', "Download Update (1)") }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.AvailableForDownload) }); @@ -456,7 +456,7 @@ export class UpdateContribution extends Disposable implements IWorkbenchContribu group: '6_update', command: { id: 'update.install', - title: nls.localize('installUpdate...', "Install Update...") + title: nls.localize('installUpdate...', "Install Update... (1)") }, when: CONTEXT_UPDATE_STATE.isEqualTo(StateType.Downloaded) }); diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts index 9b326f949d5..2c3b7295c73 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataAutoSyncService.ts @@ -36,7 +36,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { this._register(Event.debounce(Event.any( Event.map(hostService.onDidChangeFocus, () => 'windowFocus'), instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync, - ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true))); + ), (last, source) => last ? [...last, source] : [source], 1000)(sources => this.triggerSync(sources, true, false))); } } diff --git a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts index 049be0714d3..d8625ab699d 100644 --- a/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts +++ b/src/vs/workbench/contrib/userDataSync/browser/userDataSync.ts @@ -326,7 +326,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo if (isEqual(this.userDataSyncStoreManagementService.userDataSyncStore?.url, this.userDataSyncStoreManagementService.userDataSyncStore?.insidersUrl)) { this.notificationService.notify({ severity: Severity.Info, - message: localize('switched to insiders', "Settings sync now uses a separate service, more information is available in the [release notes](command:update.showCurrentReleaseNotes)."), + message: localize('switched to insiders', "Settings sync now uses a separate service, more information is available in the [release notes](https://code.visualstudio.com/updates/v1_48#_settings-sync)."), }); } return; @@ -1031,7 +1031,7 @@ export class UserDataSyncWorkbenchContribution extends Disposable implements IWo }); } run(accessor: ServicesAccessor): Promise { - return that.userDataAutoSyncService.triggerSync([syncNowCommand.id], false); + return that.userDataAutoSyncService.triggerSync([syncNowCommand.id], false, true); } })); } diff --git a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts index 35fde76ad97..20ebeff1c5d 100644 --- a/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/workbench/contrib/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -29,11 +29,11 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i ) { super(storageService, environmentService, userDataSyncStoreManagementService); this.channel = sharedProcessService.getChannel('userDataAutoSync'); - this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerSync([source], true))); + this._register(instantiationService.createInstance(UserDataSyncTrigger).onDidTriggerSync(source => this.triggerSync([source], true, false))); } - triggerSync(sources: string[], hasToLimitSync: boolean): Promise { - return this.channel.call('triggerSync', [sources, hasToLimitSync]); + triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { + return this.channel.call('triggerSync', [sources, hasToLimitSync, disableCache]); } turnOn(): Promise { diff --git a/src/vs/workbench/contrib/views/browser/treeView.ts b/src/vs/workbench/contrib/views/browser/treeView.ts index a44f72ae44c..0a6fa487993 100644 --- a/src/vs/workbench/contrib/views/browser/treeView.ts +++ b/src/vs/workbench/contrib/views/browser/treeView.ts @@ -766,7 +766,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer('explorer.decorations'); templateData.resourceLabel.setResource({ name: label, description, resource: resource ? resource : URI.parse('missing:_icon_resource') }, { fileKind: this.getFileKind(node), - title, + title: undefined, hideIcon: !!iconUrl, fileDecorations, extraClasses: ['custom-view-tree-node-item-resourceLabel'], @@ -775,7 +775,7 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - await resolvableNode.resolve(); - const tooltip = resolvableNode.tooltip ?? label; + if (node instanceof ResolvableTreeItem) { + await node.resolve(); + } + const tooltip = node.tooltip ?? label; if (isHovering && tooltip) { if (!hoverOptions) { const target: IHoverTarget = { diff --git a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts index 0291f0c605d..b9dcaf3f964 100644 --- a/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/baseWebviewElement.ts @@ -74,15 +74,14 @@ export abstract class BaseWebview extends Disposable { protected get element(): T | undefined { return this._element; } private _focused: boolean | undefined; - protected get focused(): boolean { return !!this._focused; } + public get isFocused(): boolean { return !!this._focused; } private _state: WebviewState.State = new WebviewState.Initializing([]); protected content: WebviewContent; constructor( - // TODO: matb, this should not be protected. The only reason it needs to be is that the base class ends up using it in the call to createElement - protected readonly id: string, + public readonly id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, public readonly extension: WebviewExtensionDescription | undefined, diff --git a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts index ca03dc90b08..7d2046676ff 100644 --- a/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts +++ b/src/vs/workbench/contrib/webview/browser/dynamicWebviewEditorOverlay.ts @@ -39,7 +39,7 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv private _findWidgetVisible: IContextKey; public constructor( - private readonly id: string, + public readonly id: string, initialOptions: WebviewOptions, initialContentOptions: WebviewContentOptions, public readonly extension: WebviewExtensionDescription | undefined, @@ -55,6 +55,10 @@ export class DynamicWebviewEditorOverlay extends Disposable implements WebviewOv this._findWidgetVisible = KEYBINDING_CONTEXT_WEBVIEW_FIND_WIDGET_VISIBLE.bindTo(_contextKeyService); } + public get isFocused() { + return !!this._webview.value?.isFocused; + } + private readonly _onDispose = this._register(new Emitter()); public onDispose = this._onDispose.event; diff --git a/src/vs/workbench/contrib/webview/browser/pre/main.js b/src/vs/workbench/contrib/webview/browser/pre/main.js index 630d5d93422..8cd6211afde 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/main.js +++ b/src/vs/workbench/contrib/webview/browser/pre/main.js @@ -514,7 +514,7 @@ }, 0); } - if (host.fakeLoad) { + if (host.fakeLoad && false) { // On Safari for iframes with scripts disabled, the `DOMContentLoaded` never seems to be fired. // Use polling instead. const interval = setInterval(() => { diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 969dd6728bc..4df30f8be7b 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -86,6 +86,9 @@ export interface IDataLinkClickEvent { } export interface Webview extends IDisposable { + + readonly id: string; + html: string; contentOptions: WebviewContentOptions; localResourcesRoot: URI[]; @@ -93,6 +96,8 @@ export interface Webview extends IDisposable { initialScrollProgress: number; state: string | undefined; + readonly isFocused: boolean; + readonly onDidFocus: Event; readonly onDidBlur: Event; readonly onDidClickLink: Event; diff --git a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts index 2b4b56fa148..5777d6d5c5b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewEditor.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewEditor.ts @@ -11,9 +11,9 @@ import { isWeb } from 'vs/base/common/platform'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { IEditorDropService } from 'vs/workbench/services/editor/browser/editorDropService'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, IEditorOpenContext } from 'vs/workbench/common/editor'; import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -21,7 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService'; -export class WebviewEditor extends BaseEditor { +export class WebviewEditor extends EditorPane { public static readonly ID = 'WebviewEditor'; @@ -107,7 +107,7 @@ export class WebviewEditor extends BaseEditor { super.clearInput(); } - public async setInput(input: EditorInput, options: EditorOptions, token: CancellationToken): Promise { + public async setInput(input: EditorInput, options: EditorOptions, context: IEditorOpenContext, token: CancellationToken): Promise { if (input.matches(this.input)) { return; } @@ -117,7 +117,7 @@ export class WebviewEditor extends BaseEditor { this.webview.release(this); } - await super.setInput(input, options, token); + await super.setInput(input, options, context, token); await input.resolve(); if (token.isCancellationRequested) { diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 792d98a57bb..993b0dfc59a 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -54,7 +54,7 @@ export class IFrameWebview extends BaseWebview implements Web this._register(this.on(WebviewMessageChannels.loadResource, (entry: any) => { const rawPath = entry.path; const normalizedPath = decodeURIComponent(rawPath); - const uri = URI.parse(normalizedPath.replace(/^\/(\w+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); + const uri = URI.parse(normalizedPath.replace(/^\/([\w\-]+)\/(.+)$/, (_, scheme, path) => scheme + ':/' + path)); this.loadResource(rawPath, uri); })); diff --git a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts index 6e7570d370a..03d68b88996 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/iframeWebviewElement.ts @@ -122,7 +122,7 @@ export class ElectronIframeWebview extends IFrameWebview { // Workaround this by debouncing the focus and making sure we are not focused on an input // when we try to re-focus. this._focusDelayer.trigger(async () => { - if (!this.focused || !this.element) { + if (!this.isFocused || !this.element) { return; } diff --git a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts index 7421ee686ff..2ef9b4d1bcf 100644 --- a/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/electron-browser/webviewElement.ts @@ -166,7 +166,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme this._myLogService.debug(`Webview(${this.id}): dom-ready`); // Workaround for https://github.com/electron/electron/issues/14474 - if (this.element && (this.focused || document.activeElement === this.element)) { + if (this.element && (this.isFocused || document.activeElement === this.element)) { this.element.blur(); this.element.focus(); } @@ -312,7 +312,7 @@ export class ElectronWebviewBasedWebview extends BaseWebview impleme // Workaround this by debouncing the focus and making sure we are not focused on an input // when we try to re-focus. this._focusDelayer.trigger(async () => { - if (!this.focused || !this.element) { + if (!this.isFocused || !this.element) { return; } diff --git a/extensions/vscode-web-playground/extension.webpack.config.js b/src/vs/workbench/contrib/webviewView/browser/webviewView.contribution.ts similarity index 57% rename from extensions/vscode-web-playground/extension.webpack.config.js rename to src/vs/workbench/contrib/webviewView/browser/webviewView.contribution.ts index 45600607fc5..9dd05114160 100644 --- a/extensions/vscode-web-playground/extension.webpack.config.js +++ b/src/vs/workbench/contrib/webviewView/browser/webviewView.contribution.ts @@ -3,15 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -//@ts-check +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWebviewViewService, WebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; -'use strict'; - -const withDefaults = require('../shared.webpack.config'); - -module.exports = withDefaults({ - context: __dirname, - entry: { - extension: './src/extension.ts' - } -}); +registerSingleton(IWebviewViewService, WebviewViewService, true); diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts new file mode 100644 index 00000000000..31ce142a161 --- /dev/null +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewPane.ts @@ -0,0 +1,175 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Emitter } from 'vs/base/common/event'; +import { toDisposable } from 'vs/base/common/lifecycle'; +import { setImmediate } from 'vs/base/common/platform'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IProgressService } from 'vs/platform/progress/common/progress'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; +import { IViewDescriptorService } from 'vs/workbench/common/views'; +import { IWebviewService, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { IWebviewViewService } from 'vs/workbench/contrib/webviewView/browser/webviewViewService'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; + +declare const ResizeObserver: any; + +const webviewStateKey = 'webviewState'; + +export class WebviewViewPane extends ViewPane { + + private _webview?: WebviewOverlay; + private _activated = false; + + private _container?: HTMLElement; + private _resizeObserver?: any; + + private readonly memento: Memento; + private readonly viewState: MementoObject; + + constructor( + options: IViewletViewOptions, + @IKeybindingService keybindingService: IKeybindingService, + @IContextMenuService contextMenuService: IContextMenuService, + @IConfigurationService configurationService: IConfigurationService, + @IContextKeyService contextKeyService: IContextKeyService, + @IViewDescriptorService viewDescriptorService: IViewDescriptorService, + @IInstantiationService instantiationService: IInstantiationService, + @IOpenerService openerService: IOpenerService, + @IThemeService themeService: IThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IExtensionService private readonly extensionService: IExtensionService, + @IProgressService private readonly progressService: IProgressService, + @IWebviewService private readonly webviewService: IWebviewService, + @IWebviewViewService private readonly webviewViewService: IWebviewViewService, + ) { + super({ ...options, titleMenuId: MenuId.ViewTitle }, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); + + this.memento = new Memento(`webviewView.${this.id}`, storageService); + this.viewState = this.memento.getMemento(StorageScope.WORKSPACE); + + this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); + this.updateTreeVisibility(); + } + + private readonly _onDidChangeVisibility = this._register(new Emitter()); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; + + private readonly _onDispose = this._register(new Emitter()); + readonly onDispose = this._onDispose.event; + + dispose() { + this._onDispose.fire(); + + super.dispose(); + } + + focus(): void { + super.focus(); + this._webview?.focus(); + } + + renderBody(container: HTMLElement): void { + super.renderBody(container); + + this._container = container; + + if (!this._resizeObserver) { + this._resizeObserver = new ResizeObserver(() => { + setImmediate(() => { + if (this._container) { + this._webview?.layoutWebviewOverElement(this._container); + } + }); + }); + + this._register(toDisposable(() => { + this._resizeObserver.disconnect(); + })); + this._resizeObserver.observe(container); + } + } + + public saveState() { + if (this._webview) { + this.viewState[webviewStateKey] = this._webview.state; + } + + this.memento.saveMemento(); + super.saveState(); + } + + protected layoutBody(height: number, width: number): void { + super.layoutBody(height, width); + + if (!this._webview) { + return; + } + + if (this._container) { + this._webview.layoutWebviewOverElement(this._container, { width, height }); + } + } + + private updateTreeVisibility() { + if (this.isBodyVisible()) { + this.activate(); + this._webview?.claim(this); + } else { + this._webview?.release(this); + } + } + + private activate() { + if (!this._activated) { + this._activated = true; + + const webviewId = `webviewView-${this.id.replace(/[^a-z0-9]/gi, '-')}`.toLowerCase(); + const webview = this.webviewService.createWebviewOverlay(webviewId, {}, {}, undefined); + webview.state = this.viewState['webviewState']; + this._webview = webview; + + this._register(toDisposable(() => { + this._webview?.release(this); + })); + + this._register(webview.onDidUpdateState(() => { + this.viewState[webviewStateKey] = webview.state; + })); + + const source = this._register(new CancellationTokenSource()); + + this.withProgress(async () => { + await this.extensionService.activateByEvent(`onView:${this.id}`); + + let self = this; + await this.webviewViewService.resolve(this.id, { + webview, + onDidChangeVisibility: this.onDidChangeBodyVisibility, + onDispose: this.onDispose, + get title() { return self.title; }, + set title(value: string) { self.updateTitle(value); } + }, source.token); + }); + } + } + + private async withProgress(task: () => Promise): Promise { + return this.progressService.withProgress({ location: this.id, delay: 500 }, task); + } +} diff --git a/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts new file mode 100644 index 00000000000..5de370ac42d --- /dev/null +++ b/src/vs/workbench/contrib/webviewView/browser/webviewViewService.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Event } from 'vs/base/common/event'; +import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; + +export const IWebviewViewService = createDecorator('webviewViewService'); + +export interface WebviewView { + title?: string; + + readonly webview: WebviewOverlay; + + readonly onDidChangeVisibility: Event; + readonly onDispose: Event; +} + +export interface IWebviewViewResolver { + resolve(webviewView: WebviewView, cancellation: CancellationToken): Promise; +} + +export interface IWebviewViewService { + + readonly _serviceBrand: undefined; + + register(type: string, resolver: IWebviewViewResolver): IDisposable; + + resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise; +} + +export class WebviewViewService extends Disposable implements IWebviewViewService { + + readonly _serviceBrand: undefined; + + private readonly _views = new Map(); + + private readonly _awaitingRevival = new Map void }>(); + + constructor() { + super(); + } + + register(viewType: string, resolver: IWebviewViewResolver): IDisposable { + if (this._views.has(viewType)) { + throw new Error(`View resolver already registered for ${viewType}`); + } + + this._views.set(viewType, resolver); + + const pending = this._awaitingRevival.get(viewType); + if (pending) { + resolver.resolve(pending.webview, CancellationToken.None).then(() => { + this._awaitingRevival.delete(viewType); + pending.resolve(); + }); + } + + return toDisposable(() => { + this._views.delete(viewType); + }); + } + + resolve(viewType: string, webview: WebviewView, cancellation: CancellationToken): Promise { + const resolver = this._views.get(viewType); + if (!resolver) { + if (this._awaitingRevival.has(viewType)) { + throw new Error('View already awaiting revival'); + } + + let resolve: () => void; + const p = new Promise(r => resolve = r); + this._awaitingRevival.set(viewType, { webview, resolve: resolve! }); + return p; + } + + return resolver.resolve(webview, cancellation); + } +} + diff --git a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts index 6378a16dddf..67c86cd6a89 100644 --- a/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts +++ b/src/vs/workbench/contrib/welcome/page/browser/welcomePage.ts @@ -7,7 +7,7 @@ import 'vs/css!./welcomePage'; import 'vs/workbench/contrib/welcome/page/browser/vs_code_welcome_page'; import { URI } from 'vs/base/common/uri'; import * as strings from 'vs/base/common/strings'; -import { ICommandService } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import * as arrays from 'vs/base/common/arrays'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -31,7 +31,7 @@ import { splitName } from 'vs/base/common/labels'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { registerColor, focusBorder, textLinkForeground, textLinkActiveForeground, foreground, descriptionForeground, contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; -import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; +import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService, VIEWLET_ID } from 'vs/workbench/contrib/extensions/common/extensions'; import { IEditorInputFactory, EditorInput } from 'vs/workbench/common/editor'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { TimeoutTimer } from 'vs/base/common/async'; @@ -46,6 +46,7 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IProductService } from 'vs/platform/product/common/productService'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; const configurationKey = 'workbench.startupEditor'; const oldConfigurationKey = 'workbench.welcome.enabled'; @@ -221,6 +222,16 @@ const extensionPackStrings: Strings = { extensionNotFound: localize('welcomePage.extensionPackNotFound', "Support for {0} with id {1} could not be found."), }; +CommandsRegistry.registerCommand('workbench.extensions.action.showAzureExtensions', accessor => { + const viewletService = accessor.get(IViewletService); + return viewletService.openViewlet(VIEWLET_ID, true) + .then(viewlet => viewlet?.getViewPaneContainer() as IExtensionsViewPaneContainer) + .then(viewlet => { + viewlet.search('@sort:installs azure '); + viewlet.focus(); + }); +}); + /* __GDPR__ "installKeymap" : { "${include}": [ diff --git a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts index 4d73d5db9cf..ac8ba77a324 100644 --- a/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts +++ b/src/vs/workbench/contrib/welcome/walkThrough/browser/walkThroughPart.ts @@ -10,8 +10,8 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorMemento, IEditorOpenContext } from 'vs/workbench/common/editor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { WalkThroughInput } from 'vs/workbench/contrib/welcome/walkThrough/browser/walkThroughInput'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -55,7 +55,7 @@ interface IWalkThroughEditorViewState { viewState: IViewState; } -export class WalkThroughPart extends BaseEditor { +export class WalkThroughPart extends EditorPane { static readonly ID: string = 'workbench.editor.walkThroughPart'; @@ -262,7 +262,7 @@ export class WalkThroughPart extends BaseEditor { this.scrollbar.setScrollPosition({ scrollTop: scrollPosition.scrollTop + scrollDimensions.height }); } - setInput(input: WalkThroughInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + setInput(input: WalkThroughInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { if (this.input instanceof WalkThroughInput) { this.saveTextEditorViewState(this.input); } @@ -270,7 +270,7 @@ export class WalkThroughPart extends BaseEditor { this.contentDisposables = dispose(this.contentDisposables); this.content.innerText = ''; - return super.setInput(input, options, token) + return super.setInput(input, options, context, token) .then(() => { return input.resolve(); }) diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 17ace9f6abe..3749645b62c 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -451,10 +451,10 @@ export class NativeWindow extends Disposable { return (await this.remoteAuthorityResolverService.resolveAuthority(remoteAuthority)).authority; } } : undefined; - const tunnel = await this.tunnelService.openTunnel(addressProvider, undefined, portMappingRequest.port); + const tunnel = await this.tunnelService.openTunnel(addressProvider, portMappingRequest.address, portMappingRequest.port); if (tunnel) { return { - resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }), + resolved: uri.with({ authority: tunnel.localAddress }), dispose: () => tunnel.dispose(), }; } diff --git a/src/vs/workbench/electron-sandbox/desktop.contribution.ts b/src/vs/workbench/electron-sandbox/desktop.contribution.ts index f608c5ff52d..61102025606 100644 --- a/src/vs/workbench/electron-sandbox/desktop.contribution.ts +++ b/src/vs/workbench/electron-sandbox/desktop.contribution.ts @@ -258,9 +258,9 @@ import { IJSONSchema } from 'vs/base/common/jsonSchema'; 'window.autoDetectHighContrast': { 'type': 'boolean', 'default': true, - 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if Windows is using a high contrast theme, and to dark theme when switching away from a Windows high contrast theme."), + 'description': nls.localize('autoDetectHighContrast', "If enabled, will automatically change to high contrast theme if the OS is using a high contrast theme, and to dark theme when switching away from a high contrast theme."), 'scope': ConfigurationScope.APPLICATION, - 'included': isWindows + 'included': isWindows || isMacintosh }, 'window.doubleClickIconToClose': { 'type': 'boolean', diff --git a/src/vs/workbench/electron-sandbox/desktop.main.ts b/src/vs/workbench/electron-sandbox/desktop.main.ts new file mode 100644 index 00000000000..9b4c13f9441 --- /dev/null +++ b/src/vs/workbench/electron-sandbox/desktop.main.ts @@ -0,0 +1,201 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { zoomLevelToZoomFactor } from 'vs/platform/windows/common/windows'; +import { importEntries, mark } from 'vs/base/common/performance'; +import { Workbench } from 'vs/workbench/browser/workbench'; +import { setZoomLevel, setZoomFactor, setFullscreen } from 'vs/base/browser/browser'; +import { domContentLoaded, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom'; +import { URI } from 'vs/base/common/uri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ILogService } from 'vs/platform/log/common/log'; +import { Schemas } from 'vs/base/common/network'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { FileService } from 'vs/platform/files/common/fileService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { RemoteFileSystemProvider } from 'vs/workbench/services/remote/common/remoteAgentFileSystemChannel'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider'; +import { IProductService } from 'vs/platform/product/common/productService'; +import product from 'vs/platform/product/common/product'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { IElectronService, ElectronService } from 'vs/platform/electron/electron-sandbox/electron'; +import { SimpleConfigurationService, simpleFileSystemProvider, SimpleLogService, SimpleRemoteAgentService, SimpleRemoteAuthorityResolverService, SimpleResourceIdentityService, SimpleSignService, SimpleStorageService, SimpleWorkspaceService } from 'vs/workbench/electron-sandbox/sandbox.simpleservices'; +import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; + +class DesktopMain extends Disposable { + + private readonly environmentService = new BrowserWorkbenchEnvironmentService({ + logsPath: URI.file('logs-path'), + workspaceId: '' + }); + + constructor(private configuration: any /*INativeWindowConfiguration*/) { + super(); + + this.init(); + } + + private init(): void { + + // Setup perf + importEntries(this.configuration.perfEntries); + + // Browser config + const zoomLevel = this.configuration.zoomLevel || 0; + setZoomFactor(zoomLevelToZoomFactor(zoomLevel)); + setZoomLevel(zoomLevel, true /* isTrusted */); + setFullscreen(!!this.configuration.fullscreen); + } + + async open(): Promise { + const services = await this.initServices(); + + await domContentLoaded(); + mark('willStartWorkbench'); + + // Create Workbench + const workbench = new Workbench(document.body, services.serviceCollection, services.logService); + + // Listeners + this.registerListeners(workbench, services.storageService); + + // Startup + workbench.startup(); + + // Logging + services.logService.trace('workbench configuration', JSON.stringify(this.environmentService.configuration)); + } + + private registerListeners(workbench: Workbench, storageService: SimpleStorageService): void { + + // Layout + this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench))); + + // Workbench Lifecycle + this._register(workbench.onShutdown(() => this.dispose())); + this._register(workbench.onWillShutdown(event => event.join(storageService.close()))); + } + + private onWindowResize(e: Event, retry: boolean, workbench: Workbench): void { + if (e.target === window) { + if (window.document && window.document.body && window.document.body.clientWidth === 0) { + // TODO@Ben this is an electron issue on macOS when simple fullscreen is enabled + // where for some reason the window clientWidth is reported as 0 when switching + // between simple fullscreen and normal screen. In that case we schedule the layout + // call at the next animation frame once, in the hope that the dimensions are + // proper then. + if (retry) { + scheduleAtNextAnimationFrame(() => this.onWindowResize(e, false, workbench)); + } + return; + } + + workbench.layout(); + } + } + + private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: SimpleStorageService }> { + const serviceCollection = new ServiceCollection(); + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // NOTE: DO NOT ADD ANY OTHER SERVICE INTO THE COLLECTION HERE. + // CONTRIBUTE IT VIA WORKBENCH.DESKTOP.MAIN.TS AND registerSingleton(). + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + // Main Process + const mainProcessService = this._register(new MainProcessService(this.configuration.windowId)); + serviceCollection.set(IMainProcessService, mainProcessService); + + // Environment + serviceCollection.set(IWorkbenchEnvironmentService, this.environmentService); + + // Product + const productService: IProductService = { _serviceBrand: undefined, ...product }; + serviceCollection.set(IProductService, productService); + + // Log + const logService = new SimpleLogService(); + serviceCollection.set(ILogService, logService); + + // Remote + const remoteAuthorityResolverService = new SimpleRemoteAuthorityResolverService(); + serviceCollection.set(IRemoteAuthorityResolverService, remoteAuthorityResolverService); + + // Sign + const signService = new SimpleSignService(); + serviceCollection.set(ISignService, signService); + + // Remote Agent + const remoteAgentService = new SimpleRemoteAgentService(); + serviceCollection.set(IRemoteAgentService, remoteAgentService); + + // Electron + const electronService = new ElectronService(this.configuration.windowId, mainProcessService) as IElectronService; + serviceCollection.set(IElectronService, electronService); + + // Files + const fileService = this._register(new FileService(logService)); + serviceCollection.set(IFileService, fileService); + + fileService.registerProvider(Schemas.file, simpleFileSystemProvider); + + // User Data Provider + fileService.registerProvider(Schemas.userData, new FileUserDataProvider(URI.file('user-home'), this.environmentService.backupHome, simpleFileSystemProvider, this.environmentService, logService)); + + const connection = remoteAgentService.getConnection(); + if (connection) { + const remoteFileSystemProvider = this._register(new RemoteFileSystemProvider(remoteAgentService)); + fileService.registerProvider(Schemas.vscodeRemote, remoteFileSystemProvider); + } + + const resourceIdentityService = new SimpleResourceIdentityService(); + serviceCollection.set(IResourceIdentityService, resourceIdentityService); + + const services = await Promise.all([ + this.createWorkspaceService().then(service => { + + // Workspace + serviceCollection.set(IWorkspaceContextService, service); + + // Configuration + serviceCollection.set(IConfigurationService, new SimpleConfigurationService()); + + return service; + }), + + this.createStorageService().then(service => { + + // Storage + serviceCollection.set(IStorageService, service); + + return service; + }) + ]); + + return { serviceCollection, logService, storageService: services[1] }; + } + + private async createWorkspaceService(): Promise { + return new SimpleWorkspaceService(); + } + + private async createStorageService(): Promise { + return new SimpleStorageService(); + } +} + +export function main(configuration: any /*INativeWindowConfiguration*/): Promise { + const workbench = new DesktopMain(configuration); + + return workbench.open(); +} diff --git a/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts new file mode 100644 index 00000000000..596d7b2a8a9 --- /dev/null +++ b/src/vs/workbench/electron-sandbox/sandbox.simpleservices.ts @@ -0,0 +1,907 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* eslint-disable code-no-standalone-editor */ +/* eslint-disable code-import-patterns */ + +import { ConsoleLogService } from 'vs/platform/log/common/log'; +import { IResourceIdentityService } from 'vs/platform/resource/common/resourceIdentityService'; +import { ISignService } from 'vs/platform/sign/common/sign'; +import { hash } from 'vs/base/common/hash'; +import { URI } from 'vs/base/common/uri'; +import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider'; +import { IRemoteAuthorityResolverService, IRemoteConnectionData, ResolvedAuthority, ResolvedOptions, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { Event } from 'vs/base/common/event'; +import { IRemoteAgentConnection, IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IDiagnosticInfoOptions, IDiagnosticInfo } from 'vs/platform/diagnostics/common/diagnostics'; +import { IAddressProvider, ISocketFactory } from 'vs/platform/remote/common/remoteAgentConnection'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; +import { ITelemetryData, ITelemetryInfo, ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { BrowserSocketFactory } from 'vs/platform/remote/browser/browserSocketFactory'; +import { ExtensionIdentifier, ExtensionType, IExtension, IExtensionDescription, IExtensionManifest } from 'vs/platform/extensions/common/extensions'; +import { SimpleConfigurationService as BaseSimpleConfigurationService } from 'vs/editor/standalone/browser/simpleServices'; +import { InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; +import { ITextSnapshot } from 'vs/editor/common/model'; +import { IExtensionService, NullExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ClassifiedEvent, GDPRClassification, StrictPropertyChecker } from 'vs/platform/telemetry/common/gdprTypings'; +import { IKeyboardLayoutInfo, IKeymapService, ILinuxKeyboardLayoutInfo, ILinuxKeyboardMapping, IMacKeyboardLayoutInfo, IMacKeyboardMapping, IWindowsKeyboardLayoutInfo, IWindowsKeyboardMapping } from 'vs/workbench/services/keybinding/common/keymapInfo'; +import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding'; +import { DispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig'; +import { IKeyboardMapper } from 'vs/workbench/services/keybinding/common/keyboardMapper'; +import { ChordKeybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; +import { ScanCodeBinding } from 'vs/base/common/scanCode'; +import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding'; +import { isWindows, OperatingSystem, OS } from 'vs/base/common/platform'; +import { IPathService } from 'vs/workbench/services/path/common/pathService'; +import { posix, win32 } from 'vs/base/common/path'; +import { IConfirmation, IConfirmationResult, IDialogOptions, IDialogService, IShowResult } from 'vs/platform/dialogs/common/dialogs'; +import Severity from 'vs/base/common/severity'; +import { IWebviewService, WebviewContentOptions, WebviewElement, WebviewExtensionDescription, WebviewIcons, WebviewOptions, WebviewOverlay } from 'vs/workbench/contrib/webview/browser/webview'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { AbstractTextFileService } from 'vs/workbench/services/textfile/browser/textFileService'; +import { EnablementState, ExtensionRecommendationReason, IExtensionManagementServer, IExtensionManagementServerService, IExtensionRecommendation } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; +import { LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; +import { IGrammar, ITextMateService } from 'vs/workbench/services/textMate/common/textMateService'; +import { AccessibilitySupport, IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { ITunnelProvider, ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IManualSyncTask, IResourcePreview, ISyncResourceHandle, ISyncTask, IUserDataAutoSyncService, IUserDataSyncService, IUserDataSyncStore, IUserDataSyncStoreManagementService, SyncResource, SyncStatus, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncAccount, IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; +import { AbstractTimerService, IStartupMetrics, ITimerService, Writeable } from 'vs/workbench/services/timer/browser/timerService'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; +import { ISingleFolderWorkspaceIdentifier, IWorkspaceFolderCreationData, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { ITaskProvider, ITaskService, ITaskSummary, ProblemMatcherRunOptions, Task, TaskFilter, TaskTerminateResponse, WorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService'; +import { Action } from 'vs/base/common/actions'; +import { LinkedMap } from 'vs/base/common/map'; +import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { CustomTask, ContributedTask, InMemoryTask, TaskRunSource, ConfiguringTask, TaskIdentifier, TaskSorter } from 'vs/workbench/contrib/tasks/common/tasks'; +import { TaskSystemInfo } from 'vs/workbench/contrib/tasks/common/taskSystem'; +import { IExtensionManagementService, ILocalExtension, IGalleryExtension, IReportedExtension, IGalleryMetadata, IExtensionIdentifier, IExtensionTipsService, IConfigBasedExtensionTip, IExecutableBasedExtensionTip, IWorkspaceTips } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IWorkspaceTagsService, Tags } from 'vs/workbench/contrib/tags/common/workspaceTags'; +import { AsbtractOutputChannelModelService, IOutputChannelModelService } from 'vs/workbench/services/output/common/outputChannelModel'; +import { Color, RGBA } from 'vs/base/common/color'; +import { joinPath } from 'vs/base/common/resources'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; + +//#region Workspace + +export const workspaceResource = URI.file(isWindows ? '\\simpleWorkspace' : '/simpleWorkspace'); + +export class SimpleWorkspaceService implements IWorkspaceContextService { + + declare readonly _serviceBrand: undefined; + + readonly onDidChangeWorkspaceName = Event.None; + readonly onDidChangeWorkspaceFolders = Event.None; + readonly onDidChangeWorkbenchState = Event.None; + + private readonly workspace: IWorkspace; + + constructor() { + this.workspace = { id: '4064f6ec-cb38-4ad0-af64-ee6467e63c82', folders: [new WorkspaceFolder({ uri: workspaceResource, name: '', index: 0 })] }; + } + + async getCompleteWorkspace(): Promise { return this.getWorkspace(); } + + getWorkspace(): IWorkspace { return this.workspace; } + + getWorkbenchState(): WorkbenchState { + if (this.workspace) { + if (this.workspace.configuration) { + return WorkbenchState.WORKSPACE; + } + return WorkbenchState.FOLDER; + } + return WorkbenchState.EMPTY; + } + + getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { return resource && resource.scheme === workspaceResource.scheme ? this.workspace.folders[0] : null; } + isInsideWorkspace(resource: URI): boolean { return resource && resource.scheme === workspaceResource.scheme; } + isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { return true; } +} + +//#endregion + + +//#region Configuration + +export class SimpleStorageService extends InMemoryStorageService { } + +//#endregion + + +//#region Configuration + +export class SimpleConfigurationService extends BaseSimpleConfigurationService { } + +//#endregion + + +//#region Logger + +export class SimpleLogService extends ConsoleLogService { } + +export class SimpleSignService implements ISignService { + + declare readonly _serviceBrand: undefined; + + async sign(value: string): Promise { return value; } +} + +//#endregion + + +//#region Files + +class SimpleFileSystemProvider extends InMemoryFileSystemProvider { } + +export const simpleFileSystemProvider = new SimpleFileSystemProvider(); + +function createFile(parent: string, name: string, content: string = ''): void { + simpleFileSystemProvider.writeFile(joinPath(workspaceResource, parent, name), VSBuffer.fromString(content).buffer, { create: true, overwrite: true }); +} + +function createFolder(name: string): void { + simpleFileSystemProvider.mkdir(joinPath(workspaceResource, name)); +} + +createFolder(''); +createFolder('src'); +createFolder('test'); + +createFile('', '.gitignore', `out +node_modules +.vscode-test/ +*.vsix +`); + +createFile('', '.vscodeignore', `.vscode/** +.vscode-test/** +out/test/** +src/** +.gitignore +vsc-extension-quickstart.md +**/tsconfig.json +**/tslint.json +**/*.map +**/*.ts`); + +createFile('', 'CHANGELOG.md', `# Change Log +All notable changes to the "test-ts" extension will be documented in this file. + +Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. + +## [Unreleased] +- Initial release`); +createFile('', 'package.json', `{ + "name": "test-ts", + "displayName": "test-ts", + "description": "", + "version": "0.0.1", + "engines": { + "vscode": "^1.31.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onCommand:extension.helloWorld" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "extension.helloWorld", + "title": "Hello World" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "npm run compile && node ./node_modules/vscode/bin/test" + }, + "devDependencies": { + "typescript": "^3.3.1", + "vscode": "^1.1.28", + "tslint": "^5.12.1", + "@types/node": "^8.10.25", + "@types/mocha": "^2.2.42" + } +} +`); + +createFile('', 'tsconfig.json', `{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} +`); + +createFile('', 'tslint.json', `{ + "rules": { + "no-string-throw": true, + "no-unused-expression": true, + "no-duplicate-variable": true, + "curly": true, + "class-name": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": true + }, + "defaultSeverity": "warning" +} +`); + +createFile('src', 'extension.ts', `// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; + +// this method is called when your extension is activated +// your extension is activated the very first time the command is executed +export function activate(context: vscode.ExtensionContext) { + + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "test-ts" is now active!'); + + // The command has been defined in the package.json file + // Now provide the implementation of the command with registerCommand + // The commandId parameter must match the command field in package.json + let disposable = vscode.commands.registerCommand('extension.helloWorld', () => { + // The code you place here will be executed every time your command is executed + + // Display a message box to the user + vscode.window.showInformationMessage('Hello World!'); + }); + + context.subscriptions.push(disposable); +} + +// this method is called when your extension is deactivated +export function deactivate() {} +`); + +createFile('test', 'extension.test.ts', `// +// Note: This example test is leveraging the Mocha test framework. +// Please refer to their documentation on https://mochajs.org/ for help. +// + +// The module 'assert' provides assertion methods from node +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +// import * as vscode from 'vscode'; +// import * as myExtension from '../extension'; + +// Defines a Mocha test suite to group tests of similar kind together +suite("Extension Tests", function () { + + // Defines a Mocha unit test + test("Something 1", function() { + assert.equal(-1, [1, 2, 3].indexOf(5)); + assert.equal(-1, [1, 2, 3].indexOf(0)); + }); +});`); + +createFile('test', 'index.ts', `// +// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING +// +// This file is providing the test runner to use when running extension tests. +// By default the test runner in use is Mocha based. +// +// You can provide your own test runner if you want to override it by exporting +// a function run(testRoot: string, clb: (error:Error) => void) that the extension +// host can call to run the tests. The test runner is expected to use console.log +// to report the results back to the caller. When the tests are finished, return +// a possible error to the callback or null if none. + +import * as testRunner from 'vscode/lib/testrunner'; + +// You can directly control Mocha options by configuring the test runner below +// 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.) + useColors: true // colored output from test results +}); + +module.exports = testRunner;`); + +//#endregion + + +//#region Resource Identity + +export class SimpleResourceIdentityService implements IResourceIdentityService { + + declare readonly _serviceBrand: undefined; + + async resolveResourceIdentity(resource: URI): Promise { return hash(resource.toString()).toString(16); } +} + +//#endregion + + +//#region Remote + +export class SimpleRemoteAuthorityResolverService implements IRemoteAuthorityResolverService { + + declare readonly _serviceBrand: undefined; + + onDidChangeConnectionData: Event = Event.None; + resolveAuthority(authority: string): Promise { throw new Error('Method not implemented.'); } + getConnectionData(authority: string): IRemoteConnectionData | null { return null; } + _clearResolvedAuthority(authority: string): void { } + _setResolvedAuthority(resolvedAuthority: ResolvedAuthority, resolvedOptions?: ResolvedOptions): void { } + _setResolvedAuthorityError(authority: string, err: any): void { } + _setAuthorityConnectionToken(authority: string, connectionToken: string): void { } +} + +export class SimpleRemoteAgentService implements IRemoteAgentService { + + declare readonly _serviceBrand: undefined; + + socketFactory: ISocketFactory = new BrowserSocketFactory(null); + + getConnection(): IRemoteAgentConnection | null { return null; } + async getEnvironment(bail?: boolean): Promise { return null; } + async getDiagnosticInfo(options: IDiagnosticInfoOptions): Promise { return undefined; } + async disableTelemetry(): Promise { } + async logTelemetry(eventName: string, data?: ITelemetryData): Promise { } + async flushTelemetry(): Promise { } + async getRawEnvironment(): Promise { return null; } + async scanExtensions(skipExtensions?: ExtensionIdentifier[]): Promise { return []; } +} + +//#endregion + + +//#region Backup File + +class SimpleBackupFileService implements IBackupFileService { + + declare readonly _serviceBrand: undefined; + + async hasBackups(): Promise { return false; } + async discardResourceBackup(resource: URI): Promise { } + async discardAllWorkspaceBackups(): Promise { } + toBackupResource(resource: URI): URI { return resource; } + hasBackupSync(resource: URI, versionId?: number): boolean { return false; } + async getBackups(): Promise { return []; } + async resolve(resource: URI): Promise | undefined> { return undefined; } + async backup(resource: URI, content?: ITextSnapshot, versionId?: number, meta?: T): Promise { } + async discardBackup(resource: URI): Promise { } + async discardBackups(): Promise { } +} + +registerSingleton(IBackupFileService, SimpleBackupFileService); + +//#endregion + + +//#region Extensions + +class SimpleExtensionService extends NullExtensionService { } + +registerSingleton(IExtensionService, SimpleExtensionService); + +//#endregion + + +//#region Extensions Workbench (TODO@sandbox TODO@ben remove when 'semver-umd' can be loaded) + +class SimpleExtensionsWorkbenchService implements IExtensionsWorkbenchService { + + declare readonly _serviceBrand: undefined; + + onChange = Event.None; + + local = []; + installed = []; + outdated = []; + + queryGallery(...args: any[]): any { throw new Error('Method not implemented.'); } + install(...args: any[]): any { throw new Error('Method not implemented.'); } + queryLocal(server?: IExtensionManagementServer): Promise { throw new Error('Method not implemented.'); } + canInstall(extension: any): boolean { throw new Error('Method not implemented.'); } + uninstall(extension: any): Promise { throw new Error('Method not implemented.'); } + installVersion(extension: any, version: string): Promise { throw new Error('Method not implemented.'); } + reinstall(extension: any): Promise { throw new Error('Method not implemented.'); } + setEnablement(extensions: any | any[], enablementState: EnablementState): Promise { throw new Error('Method not implemented.'); } + open(extension: any, options?: { sideByside?: boolean | undefined; preserveFocus?: boolean | undefined; pinned?: boolean | undefined; }): Promise { throw new Error('Method not implemented.'); } + checkForUpdates(): Promise { throw new Error('Method not implemented.'); } + isExtensionIgnoredToSync(extension: any): boolean { throw new Error('Method not implemented.'); } + toggleExtensionIgnoredToSync(extension: any): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(IExtensionsWorkbenchService, SimpleExtensionsWorkbenchService); + +//#endregion + +//#region Telemetry + +class SimpleTelemetryService implements ITelemetryService { + + declare readonly _serviceBrand: undefined; + + readonly sendErrorTelemetry = false; + readonly isOptedIn = false; + + async publicLog(eventName: string, data?: ITelemetryData, anonymizeFilePaths?: boolean): Promise { } + async publicLog2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyChecker, 'Type of classified event does not match event properties'>, anonymizeFilePaths?: boolean): Promise { } + async publicLogError(errorEventName: string, data?: ITelemetryData): Promise { } + async publicLogError2 = never, T extends GDPRClassification = never>(eventName: string, data?: StrictPropertyChecker, 'Type of classified event does not match event properties'>): Promise { } + setEnabled(value: boolean): void { } + setExperimentProperty(name: string, value: string): void { } + async getTelemetryInfo(): Promise { + return { + instanceId: 'someValue.instanceId', + sessionId: 'someValue.sessionId', + machineId: 'someValue.machineId' + }; + } +} + +registerSingleton(ITelemetryService, SimpleTelemetryService); + +//#endregion + + +//#region Keymap Service + +class SimpleKeyboardMapper implements IKeyboardMapper { + dumpDebugInfo(): string { return ''; } + resolveKeybinding(keybinding: ChordKeybinding): ResolvedKeybinding[] { return []; } + resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding { + let keybinding = new SimpleKeybinding( + keyboardEvent.ctrlKey, + keyboardEvent.shiftKey, + keyboardEvent.altKey, + keyboardEvent.metaKey, + keyboardEvent.keyCode + ).toChord(); + return new USLayoutResolvedKeybinding(keybinding, OS); + } + resolveUserBinding(firstPart: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[] { return []; } +} + +class SimpleKeymapService implements IKeymapService { + + declare readonly _serviceBrand: undefined; + + onDidChangeKeyboardMapper = Event.None; + getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper { return new SimpleKeyboardMapper(); } + getCurrentKeyboardLayout(): (IWindowsKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | (ILinuxKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | (IMacKeyboardLayoutInfo & { isUserKeyboardLayout?: boolean | undefined; isUSStandard?: true | undefined; }) | null { return null; } + getAllKeyboardLayouts(): IKeyboardLayoutInfo[] { return []; } + getRawKeyboardMapping(): IWindowsKeyboardMapping | ILinuxKeyboardMapping | IMacKeyboardMapping | null { return null; } + validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void { } +} + +registerSingleton(IKeymapService, SimpleKeymapService); + +//#endregion + + +//#region Path + +class SimplePathService implements IPathService { + + declare readonly _serviceBrand: undefined; + + readonly resolvedUserHome = URI.file('user-home'); + readonly path = Promise.resolve(OS === OperatingSystem.Windows ? win32 : posix); + + async fileURI(path: string): Promise { return URI.file(path); } + async userHome(options?: { preferLocal: boolean; }): Promise { return this.resolvedUserHome; } +} + +registerSingleton(IPathService, SimplePathService); + +//#endregion + + +//#region Dialog + +class SimpleDialogService implements IDialogService { + + declare readonly _serviceBrand: undefined; + + async confirm(confirmation: IConfirmation): Promise { return { confirmed: false }; } + async show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise { return { choice: 1 }; } + async about(): Promise { } +} + +registerSingleton(IDialogService, SimpleDialogService); + +//#endregion + + +//#region Webview + +class SimpleWebviewService implements IWebviewService { + declare readonly _serviceBrand: undefined; + + createWebviewElement(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewElement { throw new Error('Method not implemented.'); } + createWebviewOverlay(id: string, options: WebviewOptions, contentOptions: WebviewContentOptions, extension: WebviewExtensionDescription | undefined): WebviewOverlay { throw new Error('Method not implemented.'); } + setIcons(id: string, value: WebviewIcons | undefined): void { } +} + +registerSingleton(IWebviewService, SimpleWebviewService); + +//#endregion + + +//#region Textfiles + +class SimpleTextFileService extends AbstractTextFileService { + declare readonly _serviceBrand: undefined; +} + +registerSingleton(ITextFileService, SimpleTextFileService); + +//#endregion + + +//#region extensions management + +class SimpleExtensionManagementServerService implements IExtensionManagementServerService { + + declare readonly _serviceBrand: undefined; + + readonly localExtensionManagementServer = null; + readonly remoteExtensionManagementServer = null; + readonly webExtensionManagementServer = null; + + getExtensionManagementServer(extension: IExtension): IExtensionManagementServer | null { return null; } +} + +registerSingleton(IExtensionManagementServerService, SimpleExtensionManagementServerService); + +//#endregion + + +//#region Textmate + +TokenizationRegistry.setColorMap([null!, new Color(new RGBA(212, 212, 212, 1)), new Color(new RGBA(30, 30, 30, 1))]); + +class SimpleTextMateService implements ITextMateService { + + declare readonly _serviceBrand: undefined; + + readonly onDidEncounterLanguage: Event = Event.None; + + async createGrammar(modeId: string): Promise { return null; } + startDebugMode(printFn: (str: string) => void, onStop: () => void): void { } +} + +registerSingleton(ITextMateService, SimpleTextMateService); + +//#endregion + + +//#region Accessibility + +class SimpleAccessibilityService implements IAccessibilityService { + + declare readonly _serviceBrand: undefined; + + onDidChangeScreenReaderOptimized = Event.None; + + isScreenReaderOptimized(): boolean { return false; } + async alwaysUnderlineAccessKeys(): Promise { return false; } + setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } + getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } +} + +registerSingleton(IAccessibilityService, SimpleAccessibilityService); + +//#endregion + + +//#region Tunnel + +class SimpleTunnelService implements ITunnelService { + + declare readonly _serviceBrand: undefined; + + tunnels: Promise = Promise.resolve([]); + + onTunnelOpened = Event.None; + onTunnelClosed = Event.None; + + openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined { return undefined; } + async closeTunnel(remoteHost: string, remotePort: number): Promise { } + setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { return Disposable.None; } +} + +registerSingleton(ITunnelService, SimpleTunnelService); + +//#endregion + + +//#region User Data Sync + +class SimpleUserDataSyncService implements IUserDataSyncService { + + declare readonly _serviceBrand: undefined; + + onDidChangeStatus = Event.None; + onDidChangeConflicts = Event.None; + onDidChangeLocal = Event.None; + onSyncErrors = Event.None; + onDidChangeLastSyncTime = Event.None; + onDidResetRemote = Event.None; + onDidResetLocal = Event.None; + + status: SyncStatus = SyncStatus.Idle; + conflicts: [SyncResource, IResourcePreview[]][] = []; + lastSyncTime = undefined; + + createSyncTask(): Promise { throw new Error('Method not implemented.'); } + createManualSyncTask(): Promise { throw new Error('Method not implemented.'); } + + async replace(uri: URI): Promise { } + async reset(): Promise { } + async resetRemote(): Promise { } + async resetLocal(): Promise { } + async hasLocalData(): Promise { return false; } + async hasPreviouslySynced(): Promise { return false; } + async resolveContent(resource: URI): Promise { return null; } + async accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise { } + async getLocalSyncResourceHandles(resource: SyncResource): Promise { return []; } + async getRemoteSyncResourceHandles(resource: SyncResource): Promise { return []; } + async getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI; comparableResource: URI; }[]> { return []; } + async getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise { return undefined; } +} + +registerSingleton(IUserDataSyncService, SimpleUserDataSyncService); + +//#endregion + + +//#region User Data Sync Account + +class SimpleUserDataSyncAccountService implements IUserDataSyncAccountService { + + declare readonly _serviceBrand: undefined; + + onTokenFailed = Event.None; + onDidChangeAccount = Event.None; + + account: IUserDataSyncAccount | undefined = undefined; + + async updateAccount(account: IUserDataSyncAccount | undefined): Promise { } +} + +registerSingleton(IUserDataSyncAccountService, SimpleUserDataSyncAccountService); + +//#endregion + + +//#region User Data Auto Sync Account + +class SimpleUserDataAutoSyncAccountService implements IUserDataAutoSyncService { + + declare readonly _serviceBrand: undefined; + + onError = Event.None; + onDidChangeEnablement = Event.None; + + isEnabled(): boolean { return false; } + canToggleEnablement(): boolean { return false; } + async turnOn(): Promise { } + async turnOff(everywhere: boolean): Promise { } + async triggerSync(sources: string[], hasToLimitSync: boolean, disableCache: boolean): Promise { } +} + +registerSingleton(IUserDataAutoSyncService, SimpleUserDataAutoSyncAccountService); + +//#endregion + + +//#region User Data Sync Store Management + +class SimpleIUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService { + + declare readonly _serviceBrand: undefined; + + userDataSyncStore: IUserDataSyncStore | undefined = undefined; + + async switch(type: UserDataSyncStoreType): Promise { } + + async getPreviousUserDataSyncStore(): Promise { return undefined; } +} + +registerSingleton(IUserDataSyncStoreManagementService, SimpleIUserDataSyncStoreManagementService); + +//#endregion + + +//#region Timer + +class SimpleTimerService extends AbstractTimerService { + protected _isInitialStartup(): boolean { return true; } + protected _didUseCachedData(): boolean { return false; } + protected async _getWindowCount(): Promise { return 1; } + protected async _extendStartupInfo(info: Writeable): Promise { } +} + +registerSingleton(ITimerService, SimpleTimerService); + +//#endregion + + +//#region Workspace Editing + +class SimpleWorkspaceEditingService implements IWorkspaceEditingService { + + declare readonly _serviceBrand: undefined; + + async addFolders(folders: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { } + async removeFolders(folders: URI[], donotNotifyError?: boolean): Promise { } + async updateFolders(index: number, deleteCount?: number, foldersToAdd?: IWorkspaceFolderCreationData[], donotNotifyError?: boolean): Promise { } + async enterWorkspace(path: URI): Promise { } + async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise { } + async saveAndEnterWorkspace(path: URI): Promise { } + async copyWorkspaceSettings(toWorkspace: IWorkspaceIdentifier): Promise { } + async pickNewWorkspacePath(): Promise { return undefined!; } +} + +registerSingleton(IWorkspaceEditingService, SimpleWorkspaceEditingService); + +//#endregion + + +//#region Task + +class SimpleTaskService implements ITaskService { + + declare readonly _serviceBrand: undefined; + + onDidStateChange = Event.None; + supportsMultipleTaskExecutions = false; + + configureAction(): Action { throw new Error('Method not implemented.'); } + build(): Promise { throw new Error('Method not implemented.'); } + runTest(): Promise { throw new Error('Method not implemented.'); } + run(task: CustomTask | ContributedTask | InMemoryTask | undefined, options?: ProblemMatcherRunOptions): Promise { throw new Error('Method not implemented.'); } + inTerminal(): boolean { throw new Error('Method not implemented.'); } + isActive(): Promise { throw new Error('Method not implemented.'); } + getActiveTasks(): Promise { throw new Error('Method not implemented.'); } + getBusyTasks(): Promise { throw new Error('Method not implemented.'); } + restart(task: Task): void { throw new Error('Method not implemented.'); } + terminate(task: Task): Promise { throw new Error('Method not implemented.'); } + terminateAll(): Promise { throw new Error('Method not implemented.'); } + tasks(filter?: TaskFilter): Promise { throw new Error('Method not implemented.'); } + taskTypes(): string[] { throw new Error('Method not implemented.'); } + getWorkspaceTasks(runSource?: TaskRunSource): Promise> { throw new Error('Method not implemented.'); } + readRecentTasks(): Promise<(CustomTask | ContributedTask | InMemoryTask | ConfiguringTask)[]> { throw new Error('Method not implemented.'); } + getTask(workspaceFolder: string | IWorkspace | IWorkspaceFolder, alias: string | TaskIdentifier, compareId?: boolean): Promise { throw new Error('Method not implemented.'); } + tryResolveTask(configuringTask: ConfiguringTask): Promise { throw new Error('Method not implemented.'); } + getTasksForGroup(group: string): Promise { throw new Error('Method not implemented.'); } + getRecentlyUsedTasks(): LinkedMap { throw new Error('Method not implemented.'); } + migrateRecentTasks(tasks: Task[]): Promise { throw new Error('Method not implemented.'); } + createSorter(): TaskSorter { throw new Error('Method not implemented.'); } + getTaskDescription(task: CustomTask | ContributedTask | InMemoryTask | ConfiguringTask): string | undefined { throw new Error('Method not implemented.'); } + canCustomize(task: CustomTask | ContributedTask): boolean { throw new Error('Method not implemented.'); } + customize(task: CustomTask | ContributedTask | ConfiguringTask, properties?: {}, openConfig?: boolean): Promise { throw new Error('Method not implemented.'); } + openConfig(task: CustomTask | ConfiguringTask | undefined): Promise { throw new Error('Method not implemented.'); } + registerTaskProvider(taskProvider: ITaskProvider, type: string): IDisposable { throw new Error('Method not implemented.'); } + registerTaskSystem(scheme: string, taskSystemInfo: TaskSystemInfo): void { throw new Error('Method not implemented.'); } + registerSupportedExecutions(custom?: boolean, shell?: boolean, process?: boolean): void { throw new Error('Method not implemented.'); } + setJsonTasksSupported(areSuppored: Promise): void { throw new Error('Method not implemented.'); } + extensionCallbackTaskComplete(task: Task, result: number | undefined): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(ITaskService, SimpleTaskService); + +//#endregion + + +//#region Extension Management + +class SimpleExtensionManagementService implements IExtensionManagementService { + + declare readonly _serviceBrand: undefined; + + onInstallExtension = Event.None; + onDidInstallExtension = Event.None; + onUninstallExtension = Event.None; + onDidUninstallExtension = Event.None; + + async zip(extension: ILocalExtension): Promise { throw new Error('Method not implemented.'); } + async unzip(zipLocation: URI): Promise { throw new Error('Method not implemented.'); } + async getManifest(vsix: URI): Promise { throw new Error('Method not implemented.'); } + async install(vsix: URI, isMachineScoped?: boolean): Promise { throw new Error('Method not implemented.'); } + async canInstall(extension: IGalleryExtension): Promise { throw new Error('Method not implemented.'); } + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { throw new Error('Method not implemented.'); } + async uninstall(extension: ILocalExtension, force?: boolean): Promise { } + async reinstallFromGallery(extension: ILocalExtension): Promise { } + async getInstalled(type?: ExtensionType): Promise { return []; } + async getExtensionsReport(): Promise { return []; } + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { throw new Error('Method not implemented.'); } +} + +registerSingleton(IExtensionManagementService, SimpleExtensionManagementService); + +//#endregion + + +//#region Extension Tips + +class SimpleExtensionTipsService implements IExtensionTipsService { + + declare readonly _serviceBrand: undefined; + + onRecommendationChange = Event.None; + + getAllRecommendationsWithReason(): { [id: string]: { reasonId: ExtensionRecommendationReason; reasonText: string; }; } { return Object.create(null); } + getFileBasedRecommendations(): IExtensionRecommendation[] { return []; } + async getOtherRecommendations(): Promise { return []; } + async getWorkspaceRecommendations(): Promise { return []; } + getKeymapRecommendations(): IExtensionRecommendation[] { return []; } + toggleIgnoredRecommendation(extensionId: string, shouldIgnore: boolean): void { } + getAllIgnoredRecommendations(): { global: string[]; workspace: string[]; } { return Object.create(null); } + async getConfigBasedTips(folder: URI): Promise { return []; } + async getImportantExecutableBasedTips(): Promise { return []; } + async getOtherExecutableBasedTips(): Promise { return []; } + async getAllWorkspacesTips(): Promise { return []; } +} + +registerSingleton(IExtensionTipsService, SimpleExtensionTipsService); + +//#endregion + + +//#region Workspace Tags + +class SimpleWorkspaceTagsService implements IWorkspaceTagsService { + + declare readonly _serviceBrand: undefined; + + async getTags(): Promise { return Object.create(null); } + getTelemetryWorkspaceId(workspace: IWorkspace, state: WorkbenchState): string | undefined { return undefined; } + async getHashedRemotesFromUri(workspaceUri: URI, stripEndingDotGit?: boolean): Promise { return []; } +} + +registerSingleton(IWorkspaceTagsService, SimpleWorkspaceTagsService); + +//#endregion + + +//#region Output Channel + +class SimpleOutputChannelModelService extends AsbtractOutputChannelModelService { + declare readonly _serviceBrand: undefined; +} + +registerSingleton(IOutputChannelModelService, SimpleOutputChannelModelService); + +//#endregion diff --git a/src/vs/workbench/services/authentication/browser/authenticationService.ts b/src/vs/workbench/services/authentication/browser/authenticationService.ts index 9741fd8fbcf..8791faa4681 100644 --- a/src/vs/workbench/services/authentication/browser/authenticationService.ts +++ b/src/vs/workbench/services/authentication/browser/authenticationService.ts @@ -16,9 +16,29 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { isString } from 'vs/base/common/types'; export function getAuthenticationProviderActivationEvent(id: string): string { return `onAuthenticationRequest:${id}`; } +export type AuthenticationSessionInfo = { readonly id: string, readonly accessToken: string, readonly providerId: string }; +export async function getCurrentAuthenticationSessionInfo(environmentService: IWorkbenchEnvironmentService, productService: IProductService): Promise { + if (environmentService.options?.credentialsProvider) { + const authenticationSessionValue = await environmentService.options.credentialsProvider.getPassword(`${productService.urlProtocol}.login`, 'account'); + if (authenticationSessionValue) { + const authenticationSessionInfo: AuthenticationSessionInfo = JSON.parse(authenticationSessionValue); + if (authenticationSessionInfo + && isString(authenticationSessionInfo.id) + && isString(authenticationSessionInfo.accessToken) + && isString(authenticationSessionInfo.providerId) + ) { + return authenticationSessionInfo; + } + } + } + return undefined; +} + export const IAuthenticationService = createDecorator('IAuthenticationService'); export interface IAuthenticationService { diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 747f5506106..650b4c9fad3 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -22,12 +22,14 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { ConfigurationEditingService, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; import { WorkspaceConfiguration, FolderConfiguration, RemoteUserConfiguration, UserConfiguration } from 'vs/workbench/services/configuration/browser/configuration'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; -import { IJSONSchema } from 'vs/base/common/jsonSchema'; +import { IJSONSchema, IJSONSchemaMap } from 'vs/base/common/jsonSchema'; import { isEqual, dirname } from 'vs/base/common/resources'; import { mark } from 'vs/base/common/performance'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; export class WorkspaceService extends Disposable implements IConfigurationService, IWorkspaceContextService { @@ -73,6 +75,13 @@ export class WorkspaceService extends Disposable implements IConfigurationServic ) { super(); + const configurationRegistry = Registry.as(Extensions.Configuration); + // register defaults before creating default configuration model + // so that the model is not required to be updated after registering + if (environmentService.options?.configurationDefaults) { + configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); + } + this.completeWorkspaceBarrier = new Barrier(); this.defaultConfiguration = new DefaultConfigurationModel(); this.configurationCache = configurationCache; @@ -94,11 +103,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic }); })); - const configurationRegistry = Registry.as(Extensions.Configuration); - if (environmentService.options?.configurationDefaults) { - configurationRegistry.registerDefaultConfigurations([environmentService.options.configurationDefaults]); - } - this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas())); this._register(configurationRegistry.onDidUpdateConfiguration(configurationProperties => this.onDefaultConfigurationChanged(configurationProperties))); this.workspaceEditingQueue = new Queue(); @@ -423,7 +427,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } private initializeConfiguration(): Promise { - this.registerConfigurationSchemas(); return this.initializeUserConfiguration() .then(({ local, remote }) => this.loadConfiguration(local, remote)); } @@ -498,7 +501,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic private onDefaultConfigurationChanged(keys: string[]): void { this.defaultConfiguration = new DefaultConfigurationModel(); - this.registerConfigurationSchemas(); if (this.workspace) { const previousData = this._configuration.toData(); const change = this._configuration.compareAndUpdateDefaultConfiguration(this.defaultConfiguration, keys); @@ -525,30 +527,6 @@ export class WorkspaceService extends Disposable implements IConfigurationServic } } - private registerConfigurationSchemas(): void { - if (this.workspace) { - const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); - const defaultSettingsSchema: IJSONSchema = { additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const userSettingsSchema: IJSONSchema = this.remoteUserConfiguration ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; - const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - - jsonRegistry.registerSchema(defaultSettingsSchemaId, defaultSettingsSchema); - jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); - jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); - - if (WorkbenchState.WORKSPACE === this.getWorkbenchState()) { - const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; - jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); - } else { - jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); - jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); - } - } - } - private onLocalUserConfigurationChanged(userConfiguration: ConfigurationModel): void { const previous = { data: this._configuration.toData(), workspace: this.workspace }; const change = this._configuration.compareAndUpdateLocalUserConfiguration(userConfiguration); @@ -774,3 +752,45 @@ export class WorkspaceService extends Disposable implements IConfigurationServic return null; } } + +class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution { + constructor( + @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + ) { + super(); + this.registerConfigurationSchemas(); + const configurationRegistry = Registry.as(Extensions.Configuration); + this._register(configurationRegistry.onDidUpdateConfiguration(e => this.registerConfigurationSchemas())); + this._register(configurationRegistry.onDidSchemaChange(e => this.registerConfigurationSchemas())); + } + + private registerConfigurationSchemas(): void { + const jsonRegistry = Registry.as(JSONExtensions.JSONContribution); + const allSettingsSchema: IJSONSchema = { properties: allSettings.properties, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + const userSettingsSchema: IJSONSchema = this.workbenchEnvironmentService.configuration.remoteAuthority ? { properties: { ...applicationSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true } : allSettingsSchema; + const machineSettingsSchema: IJSONSchema = { properties: { ...machineSettings.properties, ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + const workspaceSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...windowSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + + jsonRegistry.registerSchema(defaultSettingsSchemaId, { + properties: Object.keys(allSettings.properties).reduce((result, key) => { result[key] = { ...allSettings.properties[key], deprecationMessage: undefined }; return result; }, {}), + patternProperties: Object.keys(allSettings.patternProperties).reduce((result, key) => { result[key] = { ...allSettings.patternProperties[key], deprecationMessage: undefined }; return result; }, {}), + additionalProperties: true, + allowTrailingCommas: true, + allowComments: true + }); + jsonRegistry.registerSchema(userSettingsSchemaId, userSettingsSchema); + jsonRegistry.registerSchema(machineSettingsSchemaId, machineSettingsSchema); + + if (WorkbenchState.WORKSPACE === this.workspaceContextService.getWorkbenchState()) { + const folderSettingsSchema: IJSONSchema = { properties: { ...machineOverridableSettings.properties, ...resourceSettings.properties }, patternProperties: allSettings.patternProperties, additionalProperties: true, allowTrailingCommas: true, allowComments: true }; + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, folderSettingsSchema); + } else { + jsonRegistry.registerSchema(workspaceSettingsSchemaId, workspaceSettingsSchema); + jsonRegistry.registerSchema(folderSettingsSchemaId, workspaceSettingsSchema); + } + } +} + +Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterConfigurationSchemasContribution, LifecyclePhase.Restored); diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 37ef711911b..d748536584c 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -353,8 +353,9 @@ export class ConfigurationEditingService { } } return nls.localize('errorInvalidConfigurationFolder', "Unable to write into folder settings. Please open the '{0}' folder settings to correct errors/warnings in it and try again.", workspaceFolderName); + default: + return ''; } - return ''; } case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY: { if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { @@ -379,8 +380,9 @@ export class ConfigurationEditingService { } } return nls.localize('errorConfigurationFileDirtyFolder', "Unable to write into folder settings because the file is dirty. Please save the '{0}' folder settings file first and then try again.", workspaceFolderName); + default: + return ''; } - return ''; } case ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_MODIFIED_SINCE: if (operation.workspaceStandAloneConfigurationKey === TASKS_CONFIGURATION_KEY) { @@ -412,8 +414,9 @@ export class ConfigurationEditingService { return nls.localize('workspaceTarget', "Workspace Settings"); case EditableConfigurationTarget.WORKSPACE_FOLDER: return nls.localize('folderTarget', "Folder Settings"); + default: + return ''; } - return ''; } private getEdits(model: ITextModel, edit: IConfigurationEditOperation): Edit[] { diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index 5091446162d..6d2f43ace73 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -39,6 +39,7 @@ 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 { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache'; +import { ConfigurationCache as BrowserConfigurationCache } from 'vs/workbench/services/configuration/browser/configurationCache'; import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; import { IConfigurationCache } from 'vs/workbench/services/configuration/common/configuration'; import { SignService } from 'vs/platform/sign/browser/signService'; @@ -50,6 +51,7 @@ import { timeout } from 'vs/base/common/async'; import { VSBuffer } from 'vs/base/common/buffer'; import { DisposableStore } from 'vs/base/common/lifecycle'; import product from 'vs/platform/product/common/product'; +import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -2059,6 +2061,46 @@ suite('WorkspaceConfigurationService - Remote Folder', () => { }); +suite('ConfigurationService - Configuration Defaults', () => { + + const disposableStore: DisposableStore = new DisposableStore(); + + suiteSetup(() => { + Registry.as(ConfigurationExtensions.Configuration).registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.defaultOverridesSetting': { + 'type': 'string', + 'default': 'isSet', + }, + } + }); + }); + + teardown(() => { + disposableStore.clear(); + }); + + test('when default value is not overriden', () => { + const testObject = createConfiurationService({}); + assert.deepEqual(testObject.getValue('configurationService.defaultOverridesSetting'), 'isSet'); + }); + + test('when default value is overriden', () => { + const testObject = createConfiurationService({ 'configurationService.defaultOverridesSetting': 'overriddenValue' }); + assert.deepEqual(testObject.getValue('configurationService.defaultOverridesSetting'), 'overriddenValue'); + }); + + function createConfiurationService(configurationDefaults: Record): IConfigurationService { + const remoteAgentService = (workbenchInstantiationService()).createInstance(RemoteAgentService); + const environmentService = new BrowserWorkbenchEnvironmentService({ logsPath: URI.file(''), workspaceId: '', configurationDefaults }); + const fileService = new FileService(new NullLogService()); + return disposableStore.add(new WorkspaceService({ configurationCache: new BrowserConfigurationCache() }, environmentService, fileService, remoteAgentService)); + } + +}); + function getWorkspaceId(configPath: URI): string { let workspaceConfigPath = configPath.scheme === Schemas.file ? originalFSPath(configPath) : configPath.toString(); if (!isLinux) { diff --git a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts index 42b3431dc41..7fc15ac3615 100644 --- a/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts +++ b/src/vs/workbench/services/configurationResolver/browser/configurationResolverService.ts @@ -264,7 +264,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR if (!Types.isString(info.description)) { missingAttribute('description'); } - const inputOptions: IInputOptions = { prompt: info.description }; + const inputOptions: IInputOptions = { prompt: info.description, ignoreFocusLost: true }; if (info.default) { inputOptions.value = info.default; } @@ -310,7 +310,7 @@ export abstract class BaseConfigurationResolverService extends AbstractVariableR picks.push(item); } }); - const pickOptions: IPickOptions = { placeHolder: info.description, matchOnDetail: true }; + const pickOptions: IPickOptions = { placeHolder: info.description, matchOnDetail: true, ignoreFocusLost: true }; return this.quickInputService.pick(picks, pickOptions, undefined).then(resolvedInput => { if (resolvedInput) { return resolvedInput.value; diff --git a/src/vs/workbench/services/credentials/browser/credentialsService.ts b/src/vs/workbench/services/credentials/browser/credentialsService.ts index 5d64ecf1d2f..c95b04b5ad1 100644 --- a/src/vs/workbench/services/credentials/browser/credentialsService.ts +++ b/src/vs/workbench/services/credentials/browser/credentialsService.ts @@ -3,21 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; +import { ICredentialsProvider, ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { find } from 'vs/base/common/arrays'; -export interface ICredentialsProvider { - getPassword(service: string, account: string): Promise; - setPassword(service: string, account: string, password: string): Promise; - - deletePassword(service: string, account: string): Promise; - - findPassword(service: string): Promise; - findCredentials(service: string): Promise>; -} - export class BrowserCredentialsService implements ICredentialsService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index 88800a2c196..2f64b62a888 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -747,9 +747,9 @@ export class EditorService extends Disposable implements EditorServiceImpl { //#region replaceEditors() - replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; - replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; - replaceEditors(editors: Array, group: IEditorGroup | GroupIdentifier): Promise { + async replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; + async replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; + async replaceEditors(editors: Array, group: IEditorGroup | GroupIdentifier): Promise { const typedEditors: IEditorReplacement[] = []; editors.forEach(replaceEditorArg => { @@ -776,8 +776,6 @@ export class EditorService extends Disposable implements EditorServiceImpl { if (targetGroup) { return targetGroup.replaceEditors(typedEditors); } - - return Promise.resolve(); } //#endregion 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 bec876bc4d3..817616f1ccd 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { EditorActivation } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorInput, EditorsOrder, SideBySideEditorInput } from 'vs/workbench/common/editor'; import { workbenchInstantiationService, TestServiceAccessor, registerTestEditor, TestFileEditorInput } from 'vs/workbench/test/browser/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; @@ -386,7 +386,7 @@ suite('EditorService', () => { test('delegate', function (done) { const instantiationService = workbenchInstantiationService(); - class MyEditor extends BaseEditor { + class MyEditor extends EditorPane { constructor(id: string) { super(id, undefined!, new TestThemeService(), new TestStorageService()); diff --git a/src/vs/workbench/services/environment/browser/environmentService.ts b/src/vs/workbench/services/environment/browser/environmentService.ts index ba2701ec54d..819607be0c1 100644 --- a/src/vs/workbench/services/environment/browser/environmentService.ts +++ b/src/vs/workbench/services/environment/browser/environmentService.ts @@ -14,7 +14,6 @@ import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api'; import product from 'vs/platform/product/common/product'; import { memoize } from 'vs/base/common/decorators'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { LIGHT } from 'vs/platform/theme/common/themeService'; import { parseLineAndColumnAware } from 'vs/base/common/extpath'; export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguration { @@ -78,10 +77,6 @@ export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguratio get highContrast() { return false; // could investigate to detect high contrast theme automatically } - - get defaultThemeType() { - return LIGHT; - } } interface IBrowserWorkbenchEnvironmentConstructionOptions extends IWorkbenchConstructionOptions { diff --git a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts index 8a5016a8f51..b9af68bd937 100644 --- a/src/vs/workbench/services/experiment/electron-browser/experimentService.ts +++ b/src/vs/workbench/services/experiment/electron-browser/experimentService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as platform from 'vs/base/common/platform'; -import { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client'; +import type { IKeyValueStorage, IExperimentationTelemetry, IExperimentationFilterProvider, ExperimentationService as TASClient } from 'tas-client'; import { MementoObject, Memento } from 'vs/workbench/common/memento'; import { IProductService } from 'vs/platform/product/common/productService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -172,7 +172,7 @@ export class ExperimentService implements ITASExperimentService { @IConfigurationService private configurationService: IConfigurationService, ) { - if (this.productService.tasConfig && this.experimentsEnabled) { + if (this.productService.tasConfig && this.experimentsEnabled && this.telemetryService.isOptedIn) { this.tasClient = this.setupTASClient(); } } @@ -206,7 +206,7 @@ export class ExperimentService implements ITASExperimentService { const telemetry = new ExperimentServiceTelemetry(this.telemetryService); const tasConfig = this.productService.tasConfig!; - const tasClient = new TASClient({ + const tasClient = new (await import('tas-client')).ExperimentationService({ filterProviders: [filterProvider], telemetry: telemetry, storageKey: storageKey, diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts index 34647803e9b..12c949d67d2 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagement.ts @@ -128,6 +128,7 @@ export interface IExtensionRecommendationsService { getAllRecommendationsWithReason(): IStringDictionary; getFileBasedRecommendations(): IExtensionRecommendation[]; + getExeBasedRecommendations(exe?: string): Promise<{ important: IExtensionRecommendation[], others: IExtensionRecommendation[] }>; getImportantRecommendations(): Promise; getConfigBasedRecommendations(): Promise; getOtherRecommendations(): Promise; diff --git a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts index 388d6996081..dfee9ed2d2a 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementServerService.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; @@ -15,6 +14,7 @@ import { isWeb } from 'vs/base/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { WebExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/webExtensionManagementService'; import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { WebRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -31,7 +31,7 @@ export class ExtensionManagementServerService implements IExtensionManagementSer ) { const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new ExtensionManagementChannelClient(remoteAgentConnection!.getChannel('extensions')); + const extensionManagementService = instantiationService.createInstance(WebRemoteExtensionManagementService, remoteAgentConnection!.getChannel('extensions')); 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 33eb56db3c2..a982b3ecc58 100644 --- a/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/extensionManagementService.ts @@ -189,6 +189,15 @@ export class ExtensionManagementService extends Disposable implements IExtension return Promise.reject('No Servers'); } + async canInstall(gallery: IGalleryExtension): Promise { + for (const server of this.servers) { + if (await server.extensionManagementService.canInstall(gallery)) { + return true; + } + } + return false; + } + async installFromGallery(gallery: IGalleryExtension): Promise { // Only local server, install without any checks diff --git a/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts new file mode 100644 index 00000000000..908ce5a2ab1 --- /dev/null +++ b/src/vs/workbench/services/extensionManagement/common/remoteExtensionManagementService.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IChannel } from 'vs/base/parts/ipc/common/ipc'; +import { IExtensionManagementService, IGalleryExtension, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { canExecuteOnWorkspace } 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'; + +export class WebRemoteExtensionManagementService extends ExtensionManagementChannelClient implements IExtensionManagementService { + + constructor( + channel: IChannel, + @IExtensionGalleryService protected readonly galleryService: IExtensionGalleryService, + @IConfigurationService protected readonly configurationService: IConfigurationService, + @IProductService protected readonly productService: IProductService + ) { + super(channel); + } + + async canInstall(extension: IGalleryExtension): Promise { + const manifest = await this.galleryService.getManifest(extension, CancellationToken.None); + return !!manifest && canExecuteOnWorkspace(manifest, this.productService, this.configurationService); + } + +} diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts index 98a423df368..7282f156199 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionManagementService.ts @@ -11,6 +11,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ILogService } from 'vs/platform/log/common/log'; import { Disposable } from 'vs/base/common/lifecycle'; +import { localize } from 'vs/nls'; export class WebExtensionManagementService extends Disposable implements IExtensionManagementService { @@ -40,7 +41,14 @@ export class WebExtensionManagementService extends Disposable implements IExtens return Promise.all(extensions.map(e => this.toLocalExtension(e))); } + async canInstall(gallery: IGalleryExtension): Promise { + return !!gallery.properties.webExtension; + } + async installFromGallery(gallery: IGalleryExtension): Promise { + if (!(await this.canInstall(gallery))) { + throw new Error(localize('non web extension', "Cannot install because {0} is not a web extension", gallery.displayName)); + } this.logService.info('Installing extension:', gallery.identifier.id); this._onInstallExtension.fire({ identifier: gallery.identifier, gallery }); try { diff --git a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts index 2faf33d9c22..2e8d7fa49b0 100644 --- a/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts +++ b/src/vs/workbench/services/extensionManagement/common/webExtensionsScannerService.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as semver from 'semver-umd'; import { IBuiltinExtensionsScannerService, IScannedExtension, ExtensionType, IExtensionIdentifier, ITranslatedScannedExtension } from 'vs/platform/extensions/common/extensions'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IWebExtensionsScannerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -225,6 +224,7 @@ export class WebExtensionsScannerService extends Disposable implements IWebExten } private async scanUserExtensions(): Promise { + const semver = await import('semver-umd'); let userExtensions = await this.readUserExtensions(); const byExtension: IUserExtension[][] = groupByExtension(userExtensions, e => e.identifier); userExtensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.version, b.version))[0]); diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts index 0ac506857c7..fb573f0618f 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionManagementServerService.ts @@ -5,7 +5,6 @@ import { localize } from 'vs/nls'; import { Schemas } from 'vs/base/common/network'; -import { IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { ExtensionManagementChannelClient } from 'vs/platform/extensionManagement/common/extensionManagementIpc'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -13,12 +12,10 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IChannel } from 'vs/base/parts/ipc/common/ipc'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { ILogService } from 'vs/platform/log/common/log'; -import { RemoteExtensionManagementChannelClient } from 'vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IProductService } from 'vs/platform/product/common/productService'; +import { DesktopRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService'; import { ILabelService } from 'vs/platform/label/common/label'; import { IExtension } from 'vs/platform/extensions/common/extensions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export class ExtensionManagementServerService implements IExtensionManagementServerService { @@ -31,11 +28,8 @@ export class ExtensionManagementServerService implements IExtensionManagementSer constructor( @ISharedProcessService sharedProcessService: ISharedProcessService, + @IInstantiationService instantiationService: IInstantiationService, @IRemoteAgentService remoteAgentService: IRemoteAgentService, - @IExtensionGalleryService galleryService: IExtensionGalleryService, - @IConfigurationService configurationService: IConfigurationService, - @IProductService productService: IProductService, - @ILogService logService: ILogService, @ILabelService labelService: ILabelService, ) { const localExtensionManagementService = new ExtensionManagementChannelClient(sharedProcessService.getChannel('extensions')); @@ -43,7 +37,7 @@ export class ExtensionManagementServerService implements IExtensionManagementSer this._localExtensionManagementServer = { extensionManagementService: localExtensionManagementService, id: 'local', label: localize('local', "Local") }; const remoteAgentConnection = remoteAgentService.getConnection(); if (remoteAgentConnection) { - const extensionManagementService = new RemoteExtensionManagementChannelClient(remoteAgentConnection.getChannel('extensions'), this.localExtensionManagementServer.extensionManagementService, galleryService, logService, configurationService, productService); + const extensionManagementService = instantiationService.createInstance(DesktopRemoteExtensionManagementService, remoteAgentConnection.getChannel('extensions'), this.localExtensionManagementServer); this.remoteExtensionManagementServer = { id: 'remote', extensionManagementService, diff --git a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts b/src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts similarity index 86% rename from src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts rename to src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts index b11f3e7f7fa..749f01c3bc9 100644 --- a/src/vs/workbench/services/extensions/electron-browser/remoteExtensionManagementIpc.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/remoteExtensionManagementService.ts @@ -12,28 +12,30 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { ILogService } from 'vs/platform/log/common/log'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { prefersExecuteOnUI } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { isNonEmptyArray, toArray } from 'vs/base/common/arrays'; +import { isNonEmptyArray } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; 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 { generateUuid } from 'vs/base/common/uuid'; import { joinPath } from 'vs/base/common/resources'; +import { WebRemoteExtensionManagementService } from 'vs/workbench/services/extensionManagement/common/remoteExtensionManagementService'; +import { IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -export class RemoteExtensionManagementChannelClient extends ExtensionManagementChannelClient { +export class DesktopRemoteExtensionManagementService extends WebRemoteExtensionManagementService implements IExtensionManagementService { - declare readonly _serviceBrand: undefined; + private readonly localExtensionManagementService: IExtensionManagementService; constructor( channel: IChannel, - private readonly localExtensionManagementService: IExtensionManagementService, - private readonly galleryService: IExtensionGalleryService, - private readonly logService: ILogService, - private readonly configurationService: IConfigurationService, - private readonly productService: IProductService + localExtensionManagementServer: IExtensionManagementServer, + @ILogService private readonly logService: ILogService, + @IExtensionGalleryService galleryService: IExtensionGalleryService, + @IConfigurationService configurationService: IConfigurationService, + @IProductService productService: IProductService ) { - super(channel); + super(channel, galleryService, configurationService, productService); + this.localExtensionManagementService = localExtensionManagementServer.extensionManagementService; } async install(vsix: URI): Promise { @@ -101,14 +103,14 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC const result = new Map(); const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])]; await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, true, token); - return toArray(result.values()); + return [...result.values()]; } private async getAllWorkspaceDependenciesAndPackedExtensions(manifest: IExtensionManifest, token: CancellationToken): Promise { const result = new Map(); const extensions = [...(manifest.extensionPack || []), ...(manifest.extensionDependencies || [])]; await this.getDependenciesAndPackedExtensionsRecursively(extensions, result, false, token); - return toArray(result.values()); + return [...result.values()]; } private async getDependenciesAndPackedExtensionsRecursively(toGet: string[], result: Map, uiExtension: boolean, token: CancellationToken): Promise { diff --git a/src/vs/workbench/services/extensions/browser/extensionService.ts b/src/vs/workbench/services/extensions/browser/extensionService.ts index d0710e77fa2..9e979d28691 100644 --- a/src/vs/workbench/services/extensions/browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/browser/extensionService.ts @@ -24,6 +24,8 @@ import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browse import { Schemas } from 'vs/base/common/network'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver'; +import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { IUserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; export class ExtensionService extends AbstractExtensionService implements IExtensionService { @@ -43,6 +45,8 @@ export class ExtensionService extends AbstractExtensionService implements IExten @IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService, @IConfigurationService private readonly _configService: IConfigurationService, @IWebExtensionsScannerService private readonly _webExtensionsScannerService: IWebExtensionsScannerService, + @ILifecycleService private readonly _lifecycleService: ILifecycleService, + @IUserDataInitializationService private readonly _userDataInitializationService: IUserDataInitializationService, ) { super( instantiationService, @@ -56,7 +60,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten this._runningLocation = new Map(); - this._initialize(); + // Initialize extensions first and do it only after workbench is ready + this._lifecycleService.when(LifecyclePhase.Ready).then(async () => { + await this._userDataInitializationService.initializeExtensions(this._instantiationService); + this._initialize(); + }); + this._initFetchFileSystem(); } diff --git a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts index 446f74eff84..c89897ab69c 100644 --- a/src/vs/workbench/services/extensions/common/abstractExtensionService.ts +++ b/src/vs/workbench/services/extensions/common/abstractExtensionService.ts @@ -15,7 +15,7 @@ import { BetterMergeId } from 'vs/platform/extensionManagement/common/extensionM import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost } from 'vs/workbench/services/extensions/common/extensions'; +import { ActivationTimes, ExtensionPointContribution, IExtensionService, IExtensionsStatus, IMessage, IWillActivateEvent, IResponsiveStateChangeEvent, toExtension, IExtensionHost, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionMessageCollector, ExtensionPoint, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry'; import { ResponsiveState } from 'vs/workbench/services/extensions/common/rpcProtocol'; @@ -186,7 +186,7 @@ export abstract class AbstractExtensionService extends Disposable implements IEx this._startExtensionHosts(false, Array.from(this._allRequestedActivateEvents.keys())); } - public activateByEvent(activationEvent: string): Promise { + public activateByEvent(activationEvent: string, activationKind: ActivationKind = ActivationKind.Normal): Promise { if (this._installedExtensionsReady.isOpen()) { // Extensions have been scanned and interpreted @@ -198,20 +198,25 @@ export abstract class AbstractExtensionService extends Disposable implements IEx return NO_OP_VOID_PROMISE; } - return this._activateByEvent(activationEvent); + return this._activateByEvent(activationEvent, activationKind); } else { // Extensions have not been scanned yet. // Record the fact that this activationEvent was requested (in case of a restart) this._allRequestedActivateEvents.add(activationEvent); - return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent)); + if (activationKind === ActivationKind.Immediate) { + // Do not wait for the normal start-up of the extension host(s) + return this._activateByEvent(activationEvent, activationKind); + } + + return this._installedExtensionsReady.wait().then(() => this._activateByEvent(activationEvent, activationKind)); } } - private _activateByEvent(activationEvent: string): Promise { + private _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { const result = Promise.all( - this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent)) + this._extensionHostManagers.map(extHostManager => extHostManager.activateByEvent(activationEvent, activationKind)) ).then(() => { }); this._onWillActivateByEvent.fire({ event: activationEvent, diff --git a/src/vs/workbench/services/extensions/common/extensionHostManager.ts b/src/vs/workbench/services/extensions/common/extensionHostManager.ts index 484444e968d..d531ae94a20 100644 --- a/src/vs/workbench/services/extensions/common/extensionHostManager.ts +++ b/src/vs/workbench/services/extensions/common/extensionHostManager.ts @@ -21,7 +21,7 @@ import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StopWatch } from 'vs/base/common/stopwatch'; import { VSBuffer } from 'vs/base/common/buffer'; -import { IExtensionHost, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions'; +import { IExtensionHost, ExtensionHostKind, ActivationKind } from 'vs/workbench/services/extensions/common/extensions'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; // Enable to see detailed message communication between window and extension host @@ -48,6 +48,7 @@ export class ExtensionHostManager extends Disposable { */ private _proxy: Promise<{ value: ExtHostExtensionServiceShape; } | null> | null; private _resolveAuthorityAttempt: number; + private _hasStarted = false; constructor( extensionHost: IExtensionHost, @@ -65,6 +66,7 @@ export class ExtensionHostManager extends Disposable { this.onDidExit = this._extensionHost.onExit; this._proxy = this._extensionHost.start()!.then( (protocol) => { + this._hasStarted = true; return { value: this._createExtensionHostCustomers(protocol) }; }, (err) => { @@ -74,7 +76,7 @@ export class ExtensionHostManager extends Disposable { } ); this._proxy.then(() => { - initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent)); + initialActivationEvents.forEach((activationEvent) => this.activateByEvent(activationEvent, ActivationKind.Normal)); this._register(registerLatencyTestProvider({ measure: () => this.measure() })); @@ -217,14 +219,18 @@ export class ExtensionHostManager extends Disposable { return proxy.$activate(extension, reason); } - public activateByEvent(activationEvent: string): Promise { + public activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { + if (activationKind === ActivationKind.Immediate && !this._hasStarted) { + return Promise.resolve(); + } + if (!this._cachedActivationEvents.has(activationEvent)) { - this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent)); + this._cachedActivationEvents.set(activationEvent, this._activateByEvent(activationEvent, activationKind)); } return this._cachedActivationEvents.get(activationEvent)!; } - private async _activateByEvent(activationEvent: string): Promise { + private async _activateByEvent(activationEvent: string, activationKind: ActivationKind): Promise { if (!this._proxy) { return; } @@ -234,7 +240,7 @@ export class ExtensionHostManager extends Disposable { // i.e. the extension host could not be started return; } - return proxy.value.$activateByEvent(activationEvent); + return proxy.value.$activateByEvent(activationEvent, activationKind); } public async getInspectPort(tryEnableInspector: boolean): Promise { diff --git a/src/vs/workbench/services/extensions/common/extensions.ts b/src/vs/workbench/services/extensions/common/extensions.ts index 652a2603156..83fb70f44b2 100644 --- a/src/vs/workbench/services/extensions/common/extensions.ts +++ b/src/vs/workbench/services/extensions/common/extensions.ts @@ -140,6 +140,11 @@ export interface IResponsiveStateChangeEvent { isResponsive: boolean; } +export const enum ActivationKind { + Normal = 0, + Immediate = 1 +} + export interface IExtensionService { readonly _serviceBrand: undefined; @@ -177,8 +182,15 @@ export interface IExtensionService { /** * Send an activation event and activate interested extensions. + * + * This will wait for the normal startup of the extension host(s). + * + * In extraordinary circumstances, if the activation event needs to activate + * one or more extensions before the normal startup is finished, then you can use + * `ActivationKind.Immediate`. Please do not use this flag unless really necessary + * and you understand all consequences. */ - activateByEvent(activationEvent: string): Promise; + activateByEvent(activationEvent: string, activationKind?: ActivationKind): Promise; /** * An promise that resolves when the installed extensions are registered after diff --git a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts index 637a617c76c..976d2b1cc19 100644 --- a/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts +++ b/src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts @@ -139,11 +139,9 @@ suite('RPCProtocol', () => { let p = bProxy.$m(4, tokenSource.token); p.then((res: number) => { assert.equal(res, 7); - done(null); }, (err) => { assert.fail('should not receive error'); - done(); - }); + }).finally(done); tokenSource.cancel(); }); @@ -153,11 +151,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.fail('unexpected'); - done(null); }, (err) => { assert.equal(err.message, 'nope'); - done(null); - }); + }).finally(done); }); test('error promise', function (done) { @@ -166,11 +162,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.fail('unexpected'); - done(null); }, (err) => { assert.equal(err, undefined); - done(null); - }); + }).finally(done); }); test('issue #60450: Converting circular structure to JSON', function (done) { @@ -181,11 +175,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.equal(res, null); - done(null); }, (err) => { assert.fail('unexpected'); - done(null); - }); + }).finally(done); }); test('issue #72798: null errors are hard to digest', function (done) { @@ -195,11 +187,9 @@ suite('RPCProtocol', () => { }; bProxy.$m(4, 1).then((res) => { assert.fail('unexpected'); - done(null); }, (err) => { assert.equal(err.what, 'what'); - done(null); - }); + }).finally(done); }); test('undefined arguments arrive as null', function () { diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index b1213ff2ce7..d8ff60045c5 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -33,9 +33,8 @@ export function positionToString(position: Position): string { case Position.LEFT: return 'left'; case Position.RIGHT: return 'right'; case Position.BOTTOM: return 'bottom'; + default: return 'bottom'; } - - return 'bottom'; } const positionsByString: { [key: string]: Position } = { diff --git a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts index 7071d3d1de0..883af93ee8e 100644 --- a/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts +++ b/src/vs/workbench/services/lifecycle/electron-sandbox/lifecycleService.ts @@ -135,16 +135,16 @@ export class NativeLifecycleService extends AbstractLifecycleService { let message: string; switch (reason) { case ShutdownReason.CLOSE: - message = localize('errorClose', "An unexpected error prevented the window from closing ({0}).", toErrorMessage(error)); + message = localize('errorClose', "An unexpected error was thrown while attempting to close the window ({0}).", toErrorMessage(error)); break; case ShutdownReason.QUIT: - message = localize('errorQuit', "An unexpected error prevented the application from closing ({0}).", toErrorMessage(error)); + message = localize('errorQuit', "An unexpected error was thrown while attempting to quit the application ({0}).", toErrorMessage(error)); break; case ShutdownReason.RELOAD: - message = localize('errorReload', "An unexpected error prevented the window from reloading ({0}).", toErrorMessage(error)); + message = localize('errorReload', "An unexpected error was thrown while attempting to reload the window ({0}).", toErrorMessage(error)); break; case ShutdownReason.LOAD: - message = localize('errorLoad', "An unexpected error prevented the window from changing it's workspace ({0}).", toErrorMessage(error)); + message = localize('errorLoad', "An unexpected error was thrown while attempting to change the workspace of the window ({0}).", toErrorMessage(error)); break; } diff --git a/src/vs/workbench/services/path/common/pathService.ts b/src/vs/workbench/services/path/common/pathService.ts index c2e1b21bd5a..8d877ba3746 100644 --- a/src/vs/workbench/services/path/common/pathService.ts +++ b/src/vs/workbench/services/path/common/pathService.ts @@ -9,7 +9,7 @@ import { URI } from 'vs/base/common/uri'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; -export const IPathService = createDecorator('path'); +export const IPathService = createDecorator('pathService'); /** * Provides access to path related properties that will match the diff --git a/src/vs/workbench/services/preferences/common/preferences.ts b/src/vs/workbench/services/preferences/common/preferences.ts index 0d414997afc..0033c6bd263 100644 --- a/src/vs/workbench/services/preferences/common/preferences.ts +++ b/src/vs/workbench/services/preferences/common/preferences.ts @@ -40,7 +40,7 @@ export interface ISettingsGroup { title: string; titleRange: IRange; sections: ISettingsSection[]; - contributedByExtension: boolean; + extensionInfo?: IConfigurationExtensionInfo; } export interface ISettingsSection { diff --git a/src/vs/workbench/services/preferences/common/preferencesModels.ts b/src/vs/workbench/services/preferences/common/preferencesModels.ts index 1172b5b0b32..16ff41f32e4 100644 --- a/src/vs/workbench/services/preferences/common/preferencesModels.ts +++ b/src/vs/workbench/services/preferences/common/preferencesModels.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { flatten, tail, find, coalesce } from 'vs/base/common/arrays'; +import { flatten, tail, coalesce } from 'vs/base/common/arrays'; import { IStringDictionary } from 'vs/base/common/collections'; import { Emitter, Event } from 'vs/base/common/event'; import { JSONVisitor, visit } from 'vs/base/common/json'; import { Disposable, IReference } from 'vs/base/common/lifecycle'; -import { assign } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { IRange, Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -198,7 +197,7 @@ export class SettingsEditorModel extends AbstractSettingsModel implements ISetti }], title: modelGroup.title, titleRange: modelGroup.titleRange, - contributedByExtension: !!modelGroup.contributedByExtension + extensionInfo: modelGroup.extensionInfo }; } @@ -291,7 +290,7 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }; - setting.range = assign(setting.range, { + setting.range = Object.assign(setting.range, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); @@ -357,11 +356,11 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting!.overrides![overrideSetting!.overrides!.length - 1]; if (setting) { const valueEndPosition = model.getPositionAt(offset + length); - setting.valueRange = assign(setting.valueRange, { + setting.valueRange = Object.assign(setting.valueRange, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); - setting.range = assign(setting.range, { + setting.range = Object.assign(setting.range, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); @@ -393,11 +392,11 @@ function parse(model: ITextModel, isSettingsProperty: (currentProperty: string, const setting = previousParents.length === settingsPropertyIndex + 1 ? settings[settings.length - 1] : overrideSetting!.overrides![overrideSetting!.overrides!.length - 1]; if (setting) { const valueEndPosition = model.getPositionAt(offset + length); - setting.valueRange = assign(setting.valueRange, { + setting.valueRange = Object.assign(setting.valueRange, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); - setting.range = assign(setting.range, { + setting.range = Object.assign(setting.range, { endLineNumber: valueEndPosition.lineNumber, endColumn: valueEndPosition.column }); @@ -558,16 +557,16 @@ export class DefaultSettings extends Disposable { seenSettings = seenSettings ? seenSettings : {}; let title = config.title; if (!title) { - const configWithTitleAndSameId = find(configurations, c => (c.id === config.id) && c.title); + const configWithTitleAndSameId = configurations.find(c => (c.id === config.id) && c.title); if (configWithTitleAndSameId) { title = configWithTitleAndSameId.title; } } if (title) { if (!settingsGroup) { - settingsGroup = find(result, g => g.title === title); + settingsGroup = result.find(g => g.title === title && g.extensionInfo?.id === config.extensionInfo?.id); if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: title || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.extensionInfo }; + settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: title || '', titleRange: nullRange, range: nullRange, extensionInfo: config.extensionInfo }; result.push(settingsGroup); } } else { @@ -576,7 +575,7 @@ export class DefaultSettings extends Disposable { } if (config.properties) { if (!settingsGroup) { - settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: config.id || '', titleRange: nullRange, range: nullRange, contributedByExtension: !!config.extensionInfo }; + settingsGroup = { sections: [{ settings: [] }], id: config.id || '', title: config.id || '', titleRange: nullRange, range: nullRange, extensionInfo: config.extensionInfo }; result.push(settingsGroup); } const configurationSettings: ISetting[] = []; diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index a8232c97d59..99a8e1489c2 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -48,15 +48,8 @@ export interface Tunnel { closeable?: boolean; } -function ToLocalHost(host: string): string { - if (host === '127.0.0.1') { - host = 'localhost'; - } - return host; -} - export function MakeAddress(host: string, port: number): string { - return ToLocalHost(host) + ':' + port; + return host + ':' + port; } export class TunnelModel extends Disposable { @@ -218,7 +211,7 @@ export class TunnelModel extends Disposable { const nullIndex = value.detail.indexOf('\0'); const detail = value.detail.substr(0, nullIndex > 0 ? nullIndex : value.detail.length).trim(); return { - host: ToLocalHost(value.host), + host: value.host, port: value.port, detail }; diff --git a/src/vs/workbench/services/request/browser/requestService.ts b/src/vs/workbench/services/request/browser/requestService.ts index b9315a52ea2..16aa984a3f3 100644 --- a/src/vs/workbench/services/request/browser/requestService.ts +++ b/src/vs/workbench/services/request/browser/requestService.ts @@ -10,8 +10,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { RequestChannelClient } from 'vs/platform/request/common/requestIpc'; import { IRemoteAgentService, IRemoteAgentConnection } from 'vs/workbench/services/remote/common/remoteAgentService'; import { RequestService } from 'vs/platform/request/browser/requestService'; -import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { IRequestService } from 'vs/platform/request/common/request'; export class BrowserRequestService extends RequestService { @@ -44,5 +42,3 @@ export class BrowserRequestService extends RequestService { return connection.withChannel('request', channel => RequestChannelClient.request(channel, options, token)); } } - -registerSingleton(IRequestService, BrowserRequestService, true); diff --git a/src/vs/workbench/services/search/common/replace.ts b/src/vs/workbench/services/search/common/replace.ts index 9dd7567231c..66b67d620bb 100644 --- a/src/vs/workbench/services/search/common/replace.ts +++ b/src/vs/workbench/services/search/common/replace.ts @@ -13,6 +13,7 @@ export class ReplacePattern { private _replacePattern: string; private _hasParameters: boolean = false; private _regExp: RegExp; + private _caseOpsRegExp: RegExp; constructor(replaceString: string, searchPatternInfo: IPatternInfo) constructor(replaceString: string, parseParameters: boolean, regEx: RegExp) @@ -37,6 +38,8 @@ export class ReplacePattern { if (this._regExp.global) { this._regExp = strings.createRegExp(this._regExp.source, true, { matchCase: !this._regExp.ignoreCase, wholeWord: false, multiline: this._regExp.multiline, global: false }); } + + this._caseOpsRegExp = new RegExp(/([^\\]*?)((?:\\[uUlL])+?|)(\$[0-9]+)(.*?)/g); } get hasParameters(): boolean { @@ -60,10 +63,10 @@ export class ReplacePattern { const match = this._regExp.exec(text); if (match) { if (this.hasParameters) { + const replaceString = this.replaceWithCaseOperations(text, this._regExp, this.buildReplaceString(match, preserveCase)); if (match[0] === text) { - return text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); + return replaceString; } - const replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase)); return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length)); } return this.buildReplaceString(match, preserveCase); @@ -72,6 +75,84 @@ export class ReplacePattern { return null; } + /** + * replaceWithCaseOperations applies case operations to relevant replacement strings and applies + * the affected $N arguments. It then passes unaffected $N arguments through to string.replace(). + * + * \u => upper-cases one character in a match. + * \U => upper-cases ALL remaining characters in a match. + * \l => lower-cases one character in a match. + * \L => lower-cases ALL remaining characters in a match. + */ + private replaceWithCaseOperations(text: string, regex: RegExp, replaceString: string): string { + // Short-circuit the common path. + if (!/\\[uUlL]/.test(replaceString)) { + return text.replace(regex, replaceString); + } + // Store the values of the search parameters. + const firstMatch = regex.exec(text); + if (firstMatch === null) { + return text.replace(regex, replaceString); + } + + let patMatch: RegExpExecArray | null; + let newReplaceString = ''; + let lastIndex = 0; + let lastMatch = ''; + // For each annotated $N, perform text processing on the parameters and perform the substitution. + while ((patMatch = this._caseOpsRegExp.exec(replaceString)) !== null) { + lastIndex = patMatch.index; + const fullMatch = patMatch[0]; + lastMatch = fullMatch; + let caseOps = patMatch[2]; // \u, \l\u, etc. + const money = patMatch[3]; // $1, $2, etc. + + if (!caseOps) { + newReplaceString += fullMatch; + continue; + } + const replacement = firstMatch[parseInt(money.slice(1))]; + if (!replacement) { + newReplaceString += fullMatch; + continue; + } + const replacementLen = replacement.length; + + newReplaceString += patMatch[1]; // prefix + caseOps = caseOps.replace(/\\/g, ''); + let i = 0; + for (; i < caseOps.length; i++) { + switch (caseOps[i]) { + case 'U': + newReplaceString += replacement.slice(i).toUpperCase(); + i = replacementLen; + break; + case 'u': + newReplaceString += replacement[i].toUpperCase(); + break; + case 'L': + newReplaceString += replacement.slice(i).toLowerCase(); + i = replacementLen; + break; + case 'l': + newReplaceString += replacement[i].toLowerCase(); + break; + } + } + // Append any remaining replacement string content not covered by case operations. + if (i < replacementLen) { + newReplaceString += replacement.slice(i); + } + + newReplaceString += patMatch[4]; // suffix + } + + // Append any remaining trailing content after the final regex match. + newReplaceString += replaceString.slice(lastIndex + lastMatch.length); + + return text.replace(regex, newReplaceString); + } + public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string { if (preserveCase) { return buildReplaceStringWithCasePreserved(matches, this._replacePattern); diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 006b8fec0df..a22fd8ba3b9 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -504,7 +504,7 @@ export function spreadGlobComponents(globArg: string): string[] { export function unicodeEscapesToPCRE2(pattern: string): string { // Match \u1234 - const unicodePattern = /((?:[^\\]|^)(?:\\\\)*)\\u([a-z0-9]{4})/g; + const unicodePattern = /((?:[^\\]|^)(?:\\\\)*)\\u([a-z0-9]{4})/gi; while (pattern.match(unicodePattern)) { pattern = pattern.replace(unicodePattern, `$1\\x{$2}`); @@ -512,7 +512,7 @@ export function unicodeEscapesToPCRE2(pattern: string): string { // Match \u{1234} // \u with 5-6 characters will be left alone because \x only takes 4 characters. - const unicodePatternWithBraces = /((?:[^\\]|^)(?:\\\\)*)\\u\{([a-z0-9]{4})\}/g; + const unicodePatternWithBraces = /((?:[^\\]|^)(?:\\\\)*)\\u\{([a-z0-9]{4})\}/gi; while (pattern.match(unicodePatternWithBraces)) { pattern = pattern.replace(unicodePatternWithBraces, `$1\\x{$2}`); } diff --git a/src/vs/workbench/services/search/test/common/replace.test.ts b/src/vs/workbench/services/search/test/common/replace.test.ts index 7180e0fb43e..3c18ccb9e14 100644 --- a/src/vs/workbench/services/search/test/common/replace.test.ts +++ b/src/vs/workbench/services/search/test/common/replace.test.ts @@ -140,6 +140,12 @@ suite('Replace Pattern test', () => { assert.equal('cat ()', actual); }); + test('case operations', () => { + let testObject = new ReplacePattern('a\\u$1l\\u\\l\\U$2M$3n', { pattern: 'a(l)l(good)m(e)n', isRegExp: true }); + let actual = testObject.getReplaceString('allgoodmen'); + assert.equal('aLlGoODMen', actual); + }); + test('get replace string for no matches', () => { let testObject = new ReplacePattern('hello', { pattern: 'bla', isRegExp: true }); let actual = testObject.getReplaceString('foo'); diff --git a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts index 05fc07f6214..6c7d8296a2d 100644 --- a/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts +++ b/src/vs/workbench/services/search/test/node/ripgrepTextSearchEngine.test.ts @@ -20,6 +20,7 @@ suite('RipgrepTextSearchEngine', () => { assert.equal(unicodeEscapesToPCRE2('\\u{1234}'), '\\x{1234}'); assert.equal(unicodeEscapesToPCRE2('\\u{1234}\\u{0001}'), '\\x{1234}\\x{0001}'); assert.equal(unicodeEscapesToPCRE2('foo\\u{1234}bar'), 'foo\\x{1234}bar'); + assert.equal(unicodeEscapesToPCRE2('[\\u00A0-\\u00FF]'), '[\\x{00A0}-\\x{00FF}]'); assert.equal(unicodeEscapesToPCRE2('foo\\u{123456}7bar'), 'foo\\u{123456}7bar'); assert.equal(unicodeEscapesToPCRE2('\\u123'), '\\u123'); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 1bb7ac6975d..721d459f8d4 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -19,7 +19,7 @@ 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 } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopyService, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { ILabelService } from 'vs/platform/label/common/label'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; @@ -65,7 +65,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil //#endregion - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.labelService.getUriLabel(this.resource)); diff --git a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts index 05dfa205077..19add1a54b2 100644 --- a/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts +++ b/src/vs/workbench/services/textfile/electron-browser/nativeTextFileService.ts @@ -126,7 +126,7 @@ export class NativeTextFileService extends AbstractTextFileService { } try { - return super.write(resource, value, options); + return await super.write(resource, value, options); } catch (error) { // In case of permission denied, we need to check for readonly diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index e537e332668..3e5529f99c5 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -33,6 +33,7 @@ import { updateColorThemeConfigurationSchemas, updateFileIconThemeConfigurationS import { ProductIconThemeData, DEFAULT_PRODUCT_ICON_THEME_ID } from 'vs/workbench/services/themes/browser/productIconThemeData'; import { registerProductIconThemeSchemas } from 'vs/workbench/services/themes/common/productIconThemeSchema'; import { ILogService } from 'vs/platform/log/common/log'; +import { isWeb } from 'vs/base/common/platform'; // implementation @@ -102,8 +103,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { @ILogService private readonly logService: ILogService ) { this.container = layoutService.container; - const defaultThemeType = environmentService.configuration.defaultThemeType || DARK; - this.settings = new ThemeConfiguration(configurationService, defaultThemeType); + this.settings = new ThemeConfiguration(configurationService); this.colorThemeRegistry = new ThemeRegistry(extensionService, colorThemesExtPoint, ColorThemeData.fromExtensionTheme); this.colorThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this)); @@ -128,7 +128,13 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { themeData = ColorThemeData.createUnloadedThemeForThemeType(HIGH_CONTRAST); } if (!themeData) { - themeData = ColorThemeData.createUnloadedThemeForThemeType(defaultThemeType); + const initialColorTheme = environmentService.options?.initialColorTheme; + if (initialColorTheme) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(initialColorTheme.themeType, initialColorTheme.colors); + } + } + if (!themeData) { + themeData = ColorThemeData.createUnloadedThemeForThemeType(isWeb ? LIGHT : DARK); } themeData.setCustomizations(this.settings); this.applyTheme(themeData, undefined, true); diff --git a/src/vs/workbench/services/themes/common/colorThemeData.ts b/src/vs/workbench/services/themes/common/colorThemeData.ts index 50bbe176e9e..b8c41191476 100644 --- a/src/vs/workbench/services/themes/common/colorThemeData.ts +++ b/src/vs/workbench/services/themes/common/colorThemeData.ts @@ -550,15 +550,20 @@ export class ColorThemeData implements IWorkbenchColorTheme { // constructors - static createUnloadedThemeForThemeType(themeType: ThemeType): ColorThemeData { - return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType)); + static createUnloadedThemeForThemeType(themeType: ThemeType, colorMap?: { [id: string]: string }): ColorThemeData { + return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType), colorMap); } - static createUnloadedTheme(id: string): ColorThemeData { + static createUnloadedTheme(id: string, colorMap?: { [id: string]: string }): ColorThemeData { let themeData = new ColorThemeData(id, '', '__' + id); themeData.isLoaded = false; themeData.themeTokenColors = []; themeData.watch = false; + if (colorMap) { + for (let id in colorMap) { + themeData.colorMap[id] = Color.fromHex(colorMap[id]); + } + } return themeData; } diff --git a/src/vs/workbench/services/themes/common/themeConfiguration.ts b/src/vs/workbench/services/themes/common/themeConfiguration.ts index 4bacbe49d3d..cd4ad6203f5 100644 --- a/src/vs/workbench/services/themes/common/themeConfiguration.ts +++ b/src/vs/workbench/services/themes/common/themeConfiguration.ts @@ -14,7 +14,7 @@ import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry' import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry'; import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IColorCustomizations, ITokenColorCustomizations, IWorkbenchProductIconTheme, ISemanticTokenColorCustomizations, IExperimentalSemanticTokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { ThemeType, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; +import { isMacintosh, isWeb, isWindows } from 'vs/base/common/platform'; const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+'; const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+'; @@ -33,7 +33,7 @@ const colorThemeSettingEnumDescriptions: string[] = []; const colorThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."), - default: DEFAULT_THEME_DARK_SETTING_VALUE, + default: isWeb ? DEFAULT_THEME_LIGHT_SETTING_VALUE : DEFAULT_THEME_DARK_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), @@ -60,6 +60,7 @@ const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { default: DEFAULT_THEME_HC_SETTING_VALUE, enum: colorThemeSettingEnum, enumDescriptions: colorThemeSettingEnumDescriptions, + included: isWindows || isMacintosh, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { @@ -110,6 +111,7 @@ const themeSettingsConfiguration: IConfigurationNode = { [ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema } }; +configurationRegistry.registerConfiguration(themeSettingsConfiguration); function tokenGroupSettings(description: string): IJSONSchema { return { @@ -231,19 +233,7 @@ export function updateProductIconThemeConfigurationSchemas(themes: IWorkbenchPro export class ThemeConfiguration { - constructor(private configurationService: IConfigurationService, themeType: ThemeType) { - switch (themeType) { - case LIGHT: - colorThemeSettingSchema.default = DEFAULT_THEME_LIGHT_SETTING_VALUE; - break; - case HIGH_CONTRAST: - colorThemeSettingSchema.default = DEFAULT_THEME_HC_SETTING_VALUE; - break; - default: - colorThemeSettingSchema.default = DEFAULT_THEME_DARK_SETTING_VALUE; - break; - } - configurationRegistry.registerConfiguration(themeSettingsConfiguration); + constructor(private configurationService: IConfigurationService) { } public get colorTheme(): string { diff --git a/src/vs/workbench/services/userData/browser/userDataInit.ts b/src/vs/workbench/services/userData/browser/userDataInit.ts new file mode 100644 index 00000000000..d3fc176f380 --- /dev/null +++ b/src/vs/workbench/services/userData/browser/userDataInit.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { AbstractInitializer } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { ExtensionsInitializer } from 'vs/platform/userDataSync/common/extensionsSync'; +import { GlobalStateInitializer } from 'vs/platform/userDataSync/common/globalStateSync'; +import { KeybindingsInitializer } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { SettingsInitializer } from 'vs/platform/userDataSync/common/settingsSync'; +import { SnippetsInitializer } from 'vs/platform/userDataSync/common/snippetsSync'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { UserDataSyncStoreClient } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { CONFIGURATION_SYNC_STORE_KEY, IUserDataSyncStoreClient, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { URI } from 'vs/base/common/uri'; +import { getCurrentAuthenticationSessionInfo } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getSyncAreaLabel } from 'vs/workbench/services/userDataSync/common/userDataSync'; +import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { isWeb } from 'vs/base/common/platform'; + +export const IUserDataInitializationService = createDecorator('IUserDataInitializationService'); +export interface IUserDataInitializationService { + _serviceBrand: any; + + initializeRequiredResources(): Promise; + initializeOtherResources(): Promise; + initializeExtensions(instantiationService: IInstantiationService): Promise; +} + +export class UserDataInitializationService implements IUserDataInitializationService { + + _serviceBrand: any; + + private readonly initialized: SyncResource[] = []; + + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + @IFileService private readonly fileService: IFileService, + @IStorageService private readonly storageService: IStorageService, + @IProductService private readonly productService: IProductService, + @IRequestService private readonly requestService: IRequestService, + @ILogService private readonly logService: ILogService + ) { } + + private _userDataSyncStoreClientPromise: Promise | undefined; + private createUserDataSyncStoreClient(): Promise { + if (!this._userDataSyncStoreClientPromise) { + this._userDataSyncStoreClientPromise = (async (): Promise => { + if (!isWeb) { + this.logService.trace(`Skipping initializing user data in desktop`); + return; + } + + if (!this.environmentService.options?.enableSyncByDefault) { + this.logService.trace(`Skipping initializing user data as sync is not enabled by default`); + return; + } + + if (!this.storageService.isNew(StorageScope.GLOBAL)) { + this.logService.trace(`Skipping initializing user data as application was opened before`); + return; + } + + if (!this.storageService.isNew(StorageScope.WORKSPACE)) { + this.logService.trace(`Skipping initializing user data as workspace was opened before`); + return; + } + + const userDataSyncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY]; + if (!userDataSyncStore) { + this.logService.trace(`Skipping initializing user data as sync service is not provided`); + return; + } + + if (!this.environmentService.options?.credentialsProvider) { + this.logService.trace(`Skipping initializing user data as credentials provider is not provided`); + return; + } + + let authenticationSession; + try { + authenticationSession = await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService); + } catch (error) { + this.logService.error(error); + } + if (!authenticationSession) { + this.logService.trace(`Skipping initializing user data as authentication session is not set`); + return; + } + + const userDataSyncStoreClient = new UserDataSyncStoreClient(URI.parse(userDataSyncStore.url), this.productService, this.requestService, this.logService, this.environmentService, this.fileService, this.storageService); + userDataSyncStoreClient.setAuthToken(authenticationSession.accessToken, authenticationSession.providerId); + return userDataSyncStoreClient; + })(); + } + + return this._userDataSyncStoreClientPromise; + } + + async initializeRequiredResources(): Promise { + return this.initialize([SyncResource.Settings, SyncResource.GlobalState]); + } + + async initializeOtherResources(): Promise { + return this.initialize([SyncResource.Keybindings, SyncResource.Snippets]); + } + + async initializeExtensions(instantiationService: IInstantiationService): Promise { + return this.initialize([SyncResource.Extensions], instantiationService); + } + + private async initialize(syncResources: SyncResource[], instantiationService?: IInstantiationService): Promise { + const userDataSyncStoreClient = await this.createUserDataSyncStoreClient(); + if (!userDataSyncStoreClient) { + return; + } + + await Promise.all(syncResources.map(async syncResource => { + try { + if (this.initialized.includes(syncResource)) { + this.logService.info(`${getSyncAreaLabel(syncResource)} initialized already.`); + return; + } + this.initialized.push(syncResource); + this.logService.trace(`Initializing ${getSyncAreaLabel(syncResource)}`); + const initializer = this.createSyncResourceInitializer(syncResource, instantiationService); + const userData = await userDataSyncStoreClient.read(syncResource, null); + await initializer.initialize(userData); + this.logService.info(`Initialized ${getSyncAreaLabel(syncResource)}`); + } catch (error) { + this.logService.info(`Error while initializing ${getSyncAreaLabel(syncResource)}`); + this.logService.error(error); + } + })); + } + + private createSyncResourceInitializer(syncResource: SyncResource, instantiationService?: IInstantiationService): AbstractInitializer { + switch (syncResource) { + case SyncResource.Settings: return new SettingsInitializer(this.fileService, this.environmentService, this.logService); + case SyncResource.Keybindings: return new KeybindingsInitializer(this.fileService, this.environmentService, this.logService); + case SyncResource.Snippets: return new SnippetsInitializer(this.fileService, this.environmentService, this.logService); + case SyncResource.GlobalState: return new GlobalStateInitializer(this.storageService, this.fileService, this.environmentService, this.logService); + case SyncResource.Extensions: + if (!instantiationService) { + throw new Error('Instantiation Service is required to initialize extension'); + } + return instantiationService.createInstance(ExtensionsInitializer); + } + } + +} + +class InitializeOtherResourcesContribution implements IWorkbenchContribution { + constructor(@IUserDataInitializationService userDataInitializeService: IUserDataInitializationService) { + userDataInitializeService.initializeOtherResources(); + } +} + +if (isWeb) { + const workbenchRegistry = Registry.as(Extensions.Workbench); + workbenchRegistry.registerWorkbenchContribution(InitializeOtherResourcesContribution, LifecyclePhase.Restored); +} diff --git a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts index bdac6f7a6c6..72286eeed77 100644 --- a/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts +++ b/src/vs/workbench/services/userDataSync/browser/userDataSyncWorkbenchService.ts @@ -11,7 +11,7 @@ import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/edi import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { Emitter, Event } from 'vs/base/common/event'; import { flatten, equals } from 'vs/base/common/arrays'; -import { getAuthenticationProviderActivationEvent, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { getAuthenticationProviderActivationEvent, getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount'; import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage'; @@ -153,8 +153,9 @@ export class UserDataSyncWorkbenchService extends Disposable implements IUserDat } private async initialize(): Promise { - if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) { - this.currentSessionId = this.environmentService.options.authenticationSessionId; + const authenticationSession = this.environmentService.options?.credentialsProvider ? await getCurrentAuthenticationSessionInfo(this.environmentService, this.productService) : undefined; + if (this.currentSessionId === undefined && this.useWorkbenchSessionId && (authenticationSession?.id || this.environmentService.options?.authenticationSessionId)) { + this.currentSessionId = authenticationSession?.id || this.environmentService.options?.authenticationSessionId; this.useWorkbenchSessionId = false; } diff --git a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts index ba56c701fec..e5f96509de8 100644 --- a/src/vs/workbench/services/workingCopy/common/workingCopyService.ts +++ b/src/vs/workbench/services/workingCopy/common/workingCopyService.ts @@ -14,6 +14,11 @@ import { ITextSnapshot } from 'vs/editor/common/model'; 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 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 50fbeff1806..97840f97145 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, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService'; +import { IWorkingCopy, IWorkingCopyBackup, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { URI } from 'vs/base/common/uri'; import { Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; @@ -23,7 +23,7 @@ export class TestWorkingCopy extends Disposable implements IWorkingCopy { private readonly _onDispose = this._register(new Emitter()); readonly onDispose = this._onDispose.event; - readonly capabilities = 0; + readonly capabilities = WorkingCopyCapabilities.None; readonly name = basename(this.resource); diff --git a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts index b8e9555996f..42ea107dbc7 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebook.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebook.test.ts @@ -18,6 +18,8 @@ import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { nullExtensionDescription } from 'vs/workbench/services/extensions/common/extensions'; import { isEqual } from 'vs/base/common/resources'; +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { generateUuid } from 'vs/base/common/uuid'; suite('NotebookCell#Document', function () { @@ -43,7 +45,12 @@ suite('NotebookCell#Document', function () { }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }, new NullLogService()); + const extHostStoragePaths = new class extends mock() { + workspaceValue() { + return URI.from({ scheme: 'test', path: generateUuid() }); + } + }; + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }, new NullLogService(), extHostStoragePaths); let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { // async openNotebook() { } }); @@ -89,26 +96,26 @@ suite('NotebookCell#Document', function () { test('cell document is vscode.TextDocument', async function () { - assert.strictEqual(notebook.cells.length, 2); + assert.strictEqual(notebook.notebookDocument.cells.length, 2); - const [c1, c2] = notebook.cells; + const [c1, c2] = notebook.notebookDocument.cells; const d1 = extHostDocuments.getDocument(c1.uri); assert.ok(d1); assert.equal(d1.languageId, c1.language); assert.equal(d1.version, 1); - assert.ok(d1.notebook === notebook); + assert.ok(d1.notebook === notebook.notebookDocument); const d2 = extHostDocuments.getDocument(c2.uri); assert.ok(d2); assert.equal(d2.languageId, c2.language); assert.equal(d2.version, 1); - assert.ok(d2.notebook === notebook); + assert.ok(d2.notebook === notebook.notebookDocument); }); test('cell document goes when notebook closes', async function () { const cellUris: string[] = []; - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { assert.ok(extHostDocuments.getDocument(cell.uri)); cellUris.push(cell.uri.toString()); } @@ -153,7 +160,7 @@ suite('NotebookCell#Document', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 2, uri: CellUri.generate(notebookUri, 2), @@ -181,7 +188,7 @@ suite('NotebookCell#Document', function () { const docs: vscode.TextDocument[] = []; const addData: IModelAddedData[] = []; - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { const doc = extHostDocuments.getDocument(cell.uri); assert.ok(doc); assert.equal(extHostDocuments.getDocument(cell.uri).isClosed, false); @@ -203,14 +210,14 @@ suite('NotebookCell#Document', function () { extHostDocumentsAndEditors.$acceptDocumentsAndEditorsDelta({ removedDocuments: docs.map(d => d.uri) }); // notebook is still open -> cell documents stay open - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { assert.ok(extHostDocuments.getDocument(cell.uri)); assert.equal(extHostDocuments.getDocument(cell.uri).isClosed, false); } // close notebook -> docs are closed extHostNotebooks.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); - for (let cell of notebook.cells) { + for (let cell of notebook.notebookDocument.cells) { assert.throws(() => extHostDocuments.getDocument(cell.uri)); } for (let doc of docs) { @@ -218,9 +225,27 @@ suite('NotebookCell#Document', function () { } }); + test('cell document goes when cell is removed', async function () { + + assert.equal(notebook.notebookDocument.cells.length, 2); + const [cell1, cell2] = notebook.notebookDocument.cells; + + extHostNotebooks.$acceptModelChanged(notebook.uri, { + kind: NotebookCellsChangeType.ModelChange, + versionId: 2, + changes: [[0, 1, []]] + }); + + assert.equal(notebook.notebookDocument.cells.length, 1); + assert.equal(cell1.document.isClosed, true); // ref still alive! + assert.equal(cell2.document.isClosed, false); + + assert.throws(() => extHostDocuments.getDocument(cell1.uri)); + }); + test('cell document knows notebook', function () { - for (let cells of notebook.cells) { - assert.equal(cells.document.notebook === notebook, true); + for (let cells of notebook.notebookDocument.cells) { + assert.equal(cells.document.notebook === notebook.notebookDocument, true); } }); }); diff --git a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts index 237c1a30e4a..764eee4540c 100644 --- a/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts +++ b/src/vs/workbench/test/browser/api/extHostNotebookConcatDocument.test.ts @@ -19,7 +19,8 @@ import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/common/workbenchTestServices'; import { MainContext, MainThreadCommandsShape, MainThreadNotebookShape } from 'vs/workbench/api/common/extHost.protocol'; import { DisposableStore } from 'vs/base/common/lifecycle'; - +import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths'; +import { generateUuid } from 'vs/base/common/uuid'; suite('NotebookConcatDocument', function () { @@ -44,7 +45,12 @@ suite('NotebookConcatDocument', function () { }); extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); - extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }, new NullLogService()); + const extHostStoragePaths = new class extends mock() { + workspaceValue() { + return URI.from({ scheme: 'test', path: generateUuid() }); + } + }; + extHostNotebooks = new ExtHostNotebookController(rpcProtocol, new ExtHostCommands(rpcProtocol, new NullLogService()), extHostDocumentsAndEditors, { isExtensionDevelopmentDebug: false, webviewCspSource: '', webviewResourceRoot: '' }, new NullLogService(), extHostStoragePaths); let reg = extHostNotebooks.registerNotebookContentProvider(nullExtensionDescription, 'test', new class extends mock() { // async openNotebook() { } }); @@ -82,7 +88,7 @@ suite('NotebookConcatDocument', function () { }); test('empty', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assert.equal(doc.getText(), ''); assert.equal(doc.version, 0); @@ -119,7 +125,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: cellUri1, @@ -140,9 +146,9 @@ suite('NotebookConcatDocument', function () { }); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assert.equal(doc.contains(cellUri1), true); assert.equal(doc.contains(cellUri2), true); @@ -153,7 +159,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -174,27 +180,27 @@ suite('NotebookConcatDocument', function () { }); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped }); test('location, position mapping, cell changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); // UPDATE 1 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -205,19 +211,19 @@ suite('NotebookConcatDocument', function () { outputs: [], }]]] }); - assert.equal(notebook.cells.length, 1 + 1); + assert.equal(notebook.notebookDocument.cells.length, 1 + 1); assert.equal(doc.version, 1); assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12)), false); // clamped // UPDATE 2 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[1, 0, [{ handle: 2, uri: CellUri.generate(notebook.uri, 2), @@ -229,37 +235,37 @@ suite('NotebookConcatDocument', function () { }]]] }); - assert.equal(notebook.cells.length, 1 + 2); + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); assert.equal(doc.version, 2); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); - assertLocation(doc, new Position(5, 11), new Location(notebook.cells[1].uri, new Position(2, 11))); - assertLocation(doc, new Position(5, 12), new Location(notebook.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(5, 11), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11))); + assertLocation(doc, new Position(5, 12), new Location(notebook.notebookDocument.cells[1].uri, new Position(2, 11)), false); // don't check identity because position will be clamped // UPDATE 3 (remove cell #2 again) extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[1, 1, []]] }); - assert.equal(notebook.cells.length, 1 + 1); + assert.equal(notebook.notebookDocument.cells.length, 1 + 1); assert.equal(doc.version, 3); assertLines(doc, 'Hello', 'World', 'Hello World!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[0].uri, new Position(2, 12)), false); // clamped + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12)), false); // clamped }); test('location, position mapping, cell-document changes', function () { - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); // UPDATE 1 extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -278,21 +284,21 @@ suite('NotebookConcatDocument', function () { outputs: [], }]]] }); - assert.equal(notebook.cells.length, 1 + 2); + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); assert.equal(doc.version, 1); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(0, 0), new Location(notebook.cells[0].uri, new Position(0, 0))); - assertLocation(doc, new Position(2, 2), new Location(notebook.cells[0].uri, new Position(2, 2))); - assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 12))); - assertLocation(doc, new Position(4, 0), new Location(notebook.cells[1].uri, new Position(1, 0))); - assertLocation(doc, new Position(4, 3), new Location(notebook.cells[1].uri, new Position(1, 3))); + assertLocation(doc, new Position(0, 0), new Location(notebook.notebookDocument.cells[0].uri, new Position(0, 0))); + assertLocation(doc, new Position(2, 2), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 2))); + assertLocation(doc, new Position(2, 12), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 12))); + assertLocation(doc, new Position(4, 0), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 0))); + assertLocation(doc, new Position(4, 3), new Location(notebook.notebookDocument.cells[1].uri, new Position(1, 3))); // offset math let cell1End = doc.offsetAt(new Position(2, 12)); assert.equal(doc.positionAt(cell1End).isEqual(new Position(2, 12)), true); - extHostDocuments.$acceptModelChanged(notebook.cells[0].uri, { + extHostDocuments.$acceptModelChanged(notebook.notebookDocument.cells[0].uri, { versionId: 0, eol: '\n', changes: [{ @@ -303,7 +309,7 @@ suite('NotebookConcatDocument', function () { }] }, false); assertLines(doc, 'Hello', 'World', 'Hi World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocation(doc, new Position(2, 12), new Location(notebook.cells[0].uri, new Position(2, 9)), false); + assertLocation(doc, new Position(2, 12), new Location(notebook.notebookDocument.cells[0].uri, new Position(2, 9)), false); assert.equal(doc.positionAt(cell1End).isEqual(new Position(3, 2)), true); @@ -313,7 +319,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -333,9 +339,9 @@ suite('NotebookConcatDocument', function () { }]]] }); - const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); - const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'fooLang'); - const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, 'barLang'); + const mixedDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); + const fooLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, 'fooLang'); + const barLangDoc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, 'barLang'); assertLines(mixedDoc, 'fooLang-document', 'barLang-document'); assertLines(fooLangDoc, 'fooLang-document'); @@ -343,7 +349,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[2, 0, [{ handle: 3, uri: CellUri.generate(notebook.uri, 3), @@ -377,7 +383,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -397,9 +403,9 @@ suite('NotebookConcatDocument', function () { }]]] }); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assertOffsetAtPosition(doc, 0, { line: 0, character: 0 }); @@ -430,7 +436,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -450,24 +456,24 @@ suite('NotebookConcatDocument', function () { }]]] }); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); - assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.cells[0].uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.cells[0].uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.cells[0].uri, line: 2, character: 12 }); - assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.cells[1].uri, line: 0, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.cells[1].uri, line: 2, character: 0 }); - assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.cells[1].uri, line: 2, character: 11 }); + assertLocationAtPosition(doc, { line: 0, character: 0 }, { uri: notebook.notebookDocument.cells[0].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 0 }, { uri: notebook.notebookDocument.cells[0].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 2, character: 12 }, { uri: notebook.notebookDocument.cells[0].uri, line: 2, character: 12 }); + assertLocationAtPosition(doc, { line: 3, character: 0 }, { uri: notebook.notebookDocument.cells[1].uri, line: 0, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 0 }, { uri: notebook.notebookDocument.cells[1].uri, line: 2, character: 0 }); + assertLocationAtPosition(doc, { line: 5, character: 11 }, { uri: notebook.notebookDocument.cells[1].uri, line: 2, character: 11 }); }); test('getText(range)', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -487,9 +493,9 @@ suite('NotebookConcatDocument', function () { }]]] }); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); assert.equal(doc.getText(new Range(0, 0, 0, 0)), ''); @@ -501,7 +507,7 @@ suite('NotebookConcatDocument', function () { extHostNotebooks.$acceptModelChanged(notebookUri, { kind: NotebookCellsChangeType.ModelChange, - versionId: notebook.versionId + 1, + versionId: notebook.notebookDocument.version + 1, changes: [[0, 0, [{ handle: 1, uri: CellUri.generate(notebook.uri, 1), @@ -521,9 +527,9 @@ suite('NotebookConcatDocument', function () { }]]] }); - assert.equal(notebook.cells.length, 1 + 2); // markdown and code + assert.equal(notebook.notebookDocument.cells.length, 1 + 2); // markdown and code - let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook, undefined); + let doc = new ExtHostNotebookConcatDocument(extHostNotebooks, extHostDocuments, notebook.notebookDocument, undefined); assertLines(doc, 'Hello', 'World', 'Hello World!', 'Hallo', 'Welt', 'Hallo Welt!'); diff --git a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts index 4d4a90b1ec1..a1778a00358 100644 --- a/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTextEditors.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; -import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { MainContext, MainThreadTextEditorsShape, IWorkspaceEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { URI } from 'vs/base/common/uri'; import { mock } from 'vs/base/test/common/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors'; -import { WorkspaceTextEdit } from 'vs/editor/common/modes'; import { NullLogService } from 'vs/platform/log/common/log'; +import { assertType } from 'vs/base/common/types'; suite('ExtHostTextEditors.applyWorkspaceEdit', () => { @@ -40,7 +40,7 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { EOL: '\n', }] }); - editors = new ExtHostEditors(rpcProtocol, documentsAndEditors); + editors = new ExtHostEditors(rpcProtocol, documentsAndEditors, null!); }); test('uses version id if document available', async () => { @@ -48,7 +48,9 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.equal((workspaceResourceEdits.edits[0]).modelVersionId, 1337); + const [first] = workspaceResourceEdits.edits; + assertType(first._type === WorkspaceEditType.Text); + assert.equal(first.modelVersionId, 1337); }); test('does not use version id if document is not available', async () => { @@ -56,7 +58,9 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); await editors.applyWorkspaceEdit(edit); assert.equal(workspaceResourceEdits.edits.length, 1); - assert.ok(typeof (workspaceResourceEdits.edits[0]).modelVersionId === 'undefined'); + const [first] = workspaceResourceEdits.edits; + assertType(first._type === WorkspaceEditType.Text); + assert.ok(typeof first.modelVersionId === 'undefined'); }); }); diff --git a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts index 69494b5d834..9ba5a4e9f1b 100644 --- a/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTreeViews.test.ts @@ -203,7 +203,8 @@ suite('ExtHostTreeView', function () { assert.deepEqual(actuals, ['1/a', '1/b']); return testObject.$getChildren('testNodeWithIdTreeProvider', '1/a') .then(() => testObject.$getChildren('testNodeWithIdTreeProvider', '1/b')) - .then(() => { assert.fail('Should fail with duplicate id'); done(); }, () => done()); + .then(() => assert.fail('Should fail with duplicate id')) + .finally(done); }); }); onDidChangeTreeNode.fire(undefined); diff --git a/src/vs/workbench/test/browser/api/extHostTypes.test.ts b/src/vs/workbench/test/browser/api/extHostTypes.test.ts index 92e9616c1a8..08e6159df75 100644 --- a/src/vs/workbench/test/browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/browser/api/extHostTypes.test.ts @@ -384,21 +384,21 @@ suite('ExtHostTypes', function () { edit.replace(URI.parse('foo:a'), new types.Range(2, 1, 2, 1), 'bar'); edit.replace(URI.parse('foo:b'), new types.Range(3, 1, 3, 1), 'bazz'); - const all = edit.allEntries(); + const all = edit._allEntries(); assert.equal(all.length, 4); const [first, second, third, fourth] = all; - assertType(first._type === 2); + assertType(first._type === types.FileEditType.Text); assert.equal(first.uri.toString(), 'foo:a'); - assertType(second._type === 1); + assertType(second._type === types.FileEditType.File); assert.equal(second.from!.toString(), 'foo:a'); assert.equal(second.to!.toString(), 'foo:b'); - assertType(third._type === 2); + assertType(third._type === types.FileEditType.Text); assert.equal(third.uri.toString(), 'foo:a'); - assertType(fourth._type === 2); + assertType(fourth._type === types.FileEditType.Text); assert.equal(fourth.uri.toString(), 'foo:b'); }); @@ -408,11 +408,11 @@ suite('ExtHostTypes', function () { edit.insert(uri, new types.Position(0, 0), 'Hello'); edit.insert(uri, new types.Position(0, 0), 'Foo'); - assert.equal(edit.allEntries().length, 2); - let [first, second] = edit.allEntries(); + assert.equal(edit._allEntries().length, 2); + let [first, second] = edit._allEntries(); - assertType(first._type === 2); - assertType(second._type === 2); + assertType(first._type === types.FileEditType.Text); + assertType(second._type === types.FileEditType.Text); assert.equal(first.edit.newText, 'Hello'); assert.equal(second.edit.newText, 'Foo'); }); diff --git a/src/vs/workbench/test/browser/api/extHostWebview.test.ts b/src/vs/workbench/test/browser/api/extHostWebview.test.ts index f74b2998d27..577b61c1747 100644 --- a/src/vs/workbench/test/browser/api/extHostWebview.test.ts +++ b/src/vs/workbench/test/browser/api/extHostWebview.test.ts @@ -3,33 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as vscode from 'vscode'; import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; -import { MainThreadWebviews } from 'vs/workbench/api/browser/mainThreadWebview'; -import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; -import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; -import { mock } from 'vs/base/test/common/mock'; -import { SingleProxyRPCProtocol } from './testRPCProtocol'; -import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; -import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { MainThreadWebviewManager } from 'vs/workbench/api/browser/mainThreadWebviewManager'; import { IExtHostContext } from 'vs/workbench/api/common/extHost.protocol'; import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview'; +import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels'; +import { EditorViewColumn } from 'vs/workbench/api/common/shared/editor'; +import type * as vscode from 'vscode'; +import { SingleProxyRPCProtocol } from './testRPCProtocol'; suite('ExtHostWebview', () => { let rpcProtocol: (IExtHostRpcService & IExtHostContext) | undefined; - let extHostDocuments: ExtHostDocuments | undefined; setup(() => { const shape = createNoopMainThreadWebviews(); rpcProtocol = SingleProxyRPCProtocol(shape); - - const extHostDocumentsAndEditors = new ExtHostDocumentsAndEditors(rpcProtocol, new NullLogService()); - extHostDocuments = new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors); }); test('Cannot register multiple serializers for the same view type', async () => { @@ -39,7 +34,9 @@ suite('ExtHostWebview', () => { webviewCspSource: '', webviewResourceRoot: '', isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); let lastInvokedDeserializer: vscode.WebviewPanelSerializer | undefined = undefined; @@ -54,20 +51,20 @@ suite('ExtHostWebview', () => { const serializerA = new NoopSerializer(); const serializerB = new NoopSerializer(); - const serializerARegistration = extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerA); + const serializerARegistration = extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerA); - await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerA); assert.throws( - () => extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerB), + () => extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB), 'Should throw when registering two serializers for the same view'); serializerARegistration.dispose(); - extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializerB); + extHostWebviewPanels.registerWebviewPanelSerializer(extension, viewType, serializerB); - await extHostWebviews.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); + await extHostWebviewPanels.$deserializeWebviewPanel('x', viewType, 'title', {}, 0 as EditorViewColumn, {}); assert.strictEqual(lastInvokedDeserializer, serializerB); }); @@ -76,8 +73,11 @@ suite('ExtHostWebview', () => { webviewCspSource: '', webviewResourceRoot: 'vscode-resource://{{resource}}', isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); assert.strictEqual( webview.webview.asWebviewUri(URI.parse('file:///Users/codey/file.html')).toString(), @@ -115,8 +115,11 @@ suite('ExtHostWebview', () => { webviewCspSource: '', webviewResourceRoot: `https://{{uuid}}.webview.contoso.com/commit/{{resource}}`, isExtensionDevelopmentDebug: false, - }, undefined, new NullLogService(), NullApiDeprecationService, extHostDocuments!); - const webview = extHostWebviews.createWebviewPanel({} as any, 'type', 'title', 1, {}); + }, undefined, new NullLogService(), NullApiDeprecationService); + + const extHostWebviewPanels = new ExtHostWebviewPanels(rpcProtocol!, extHostWebviews, undefined); + + const webview = extHostWebviewPanels.createWebviewPanel({} as any, 'type', 'title', 1, {}); function stripEndpointUuid(input: string) { return input.replace(/^https:\/\/[^\.]+?\./, ''); @@ -156,7 +159,7 @@ suite('ExtHostWebview', () => { function createNoopMainThreadWebviews() { - return new class extends mock() { + return new class extends mock() { $createWebviewPanel() { /* noop */ } $registerSerializer() { /* noop */ } $unregisterSerializer() { /* noop */ } diff --git a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts index d0f65701a26..b413ec9fa5d 100644 --- a/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/browser/api/mainThreadEditors.test.ts @@ -10,7 +10,7 @@ import { TestConfigurationService } from 'vs/platform/configuration/test/common/ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestCodeEditorService } from 'vs/editor/test/browser/editorTestServices'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape, IWorkspaceTextEditDto, WorkspaceEditType } from 'vs/workbench/api/common/extHost.protocol'; import { mock } from 'vs/base/test/common/mock'; import { Event } from 'vs/base/common/event'; import { MainThreadTextEditors } from 'vs/workbench/api/browser/mainThreadEditors'; @@ -20,7 +20,7 @@ import { Position } from 'vs/editor/common/core/position'; import { IModelService } from 'vs/editor/common/services/modelService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { TestFileService, TestEditorService, TestEditorGroupsService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; -import { BulkEditService } from 'vs/workbench/services/bulkEdit/browser/bulkEditService'; +import { BulkEditService } from 'vs/workbench/contrib/bulkEdit/browser/bulkEditService'; import { NullLogService, ILogService } from 'vs/platform/log/common/log'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { IReference, ImmortalReference } from 'vs/base/common/lifecycle'; @@ -170,6 +170,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); let workspaceResourceEdit: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -191,6 +192,7 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); let workspaceResourceEdit1: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -199,6 +201,7 @@ suite('MainThreadEditors', () => { } }; let workspaceResourceEdit2: IWorkspaceTextEditDto = { + _type: WorkspaceEditType.Text, resource: resource, modelVersionId: model.getVersionId(), edit: { @@ -221,9 +224,9 @@ suite('MainThreadEditors', () => { test(`applyWorkspaceEdit with only resource edit`, () => { return editors.$tryApplyWorkspaceEdit({ edits: [ - { oldUri: resource, newUri: resource, options: undefined }, - { oldUri: undefined, newUri: resource, options: undefined }, - { oldUri: resource, newUri: undefined, options: undefined } + { _type: WorkspaceEditType.File, oldUri: resource, newUri: resource, options: undefined }, + { _type: WorkspaceEditType.File, oldUri: undefined, newUri: resource, options: undefined }, + { _type: WorkspaceEditType.File, oldUri: resource, newUri: undefined, options: undefined } ] }).then((result) => { assert.equal(result, true); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts index 1e618eb1943..5c399799eaf 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroups.test.ts @@ -464,8 +464,9 @@ suite('Workbench editor groups', () => { // Active && Pinned const input1 = input(); - const openedEditor = group.openEditor(input1, { active: true, pinned: true }); + const { editor: openedEditor, isNew } = group.openEditor(input1, { active: true, pinned: true }); assert.equal(openedEditor, input1); + assert.equal(isNew, true); assert.equal(group.count, 1); assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); @@ -575,11 +576,13 @@ suite('Workbench editor groups', () => { const input3 = input('3'); // Pinned and Active - let openedEditor = group.openEditor(input1, { pinned: true, active: true }); - assert.equal(openedEditor, input1); + let openedEditorResult = group.openEditor(input1, { pinned: true, active: true }); + assert.equal(openedEditorResult.editor, input1); + assert.equal(openedEditorResult.isNew, true); - openedEditor = group.openEditor(input1Copy, { pinned: true, active: true }); // opening copy of editor should still return existing one - assert.equal(openedEditor, input1); + openedEditorResult = group.openEditor(input1Copy, { pinned: true, active: true }); // opening copy of editor should still return existing one + assert.equal(openedEditorResult.editor, input1); + assert.equal(openedEditorResult.isNew, false); group.openEditor(input2, { pinned: true, active: true }); group.openEditor(input3, { pinned: true, active: true }); @@ -1145,7 +1148,7 @@ suite('Workbench editor groups', () => { // [] -> /index.html/ const indexHtml = input('index.html'); - let openedEditor = group.openEditor(indexHtml); + let openedEditor = group.openEditor(indexHtml).editor; assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); @@ -1154,7 +1157,7 @@ suite('Workbench editor groups', () => { // /index.html/ -> /index.html/ const sameIndexHtml = input('index.html'); - openedEditor = group.openEditor(sameIndexHtml); + openedEditor = group.openEditor(sameIndexHtml).editor; assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); @@ -1163,7 +1166,7 @@ suite('Workbench editor groups', () => { // /index.html/ -> /style.css/ const styleCss = input('style.css'); - openedEditor = group.openEditor(styleCss); + openedEditor = group.openEditor(styleCss).editor; assert.equal(openedEditor, styleCss); assert.equal(group.activeEditor, styleCss); assert.equal(group.previewEditor, styleCss); @@ -1172,7 +1175,7 @@ suite('Workbench editor groups', () => { // /style.css/ -> [/style.css/, test.js] const testJs = input('test.js'); - openedEditor = group.openEditor(testJs, { active: true, pinned: true }); + openedEditor = group.openEditor(testJs, { active: true, pinned: true }).editor; assert.equal(openedEditor, testJs); assert.equal(group.previewEditor, styleCss); assert.equal(group.activeEditor, testJs); diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts similarity index 97% rename from src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts rename to src/vs/workbench/test/browser/parts/editor/editorPane.test.ts index 82a0abe7d2a..15b7aba94ce 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorPane.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane, EditorMemento } from 'vs/workbench/browser/parts/editor/editorPane'; import { EditorInput, EditorOptions, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as Platform from 'vs/platform/registry/common/platform'; @@ -27,7 +27,7 @@ const NullThemeService = new TestThemeService(); let EditorRegistry: IEditorRegistry = Platform.Registry.as(Extensions.Editors); let EditorInputRegistry: IEditorInputFactoryRegistry = Platform.Registry.as(EditorExtensions.EditorInputFactories); -export class MyEditor extends BaseEditor { +export class MyEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyEditor', NullTelemetryService, NullThemeService, new TestStorageService()); @@ -38,7 +38,7 @@ export class MyEditor extends BaseEditor { createEditor(): any { } } -export class MyOtherEditor extends BaseEditor { +export class MyOtherEditor extends EditorPane { constructor(@ITelemetryService telemetryService: ITelemetryService) { super('myOtherEditor', NullTelemetryService, NullThemeService, new TestStorageService()); @@ -96,9 +96,9 @@ class MyOtherInput extends EditorInput { } class MyResourceEditorInput extends ResourceEditorInput { } -suite('Workbench base editor', () => { +suite('Workbench EditorPane', () => { - test('BaseEditor API', async () => { + test('EditorPane API', async () => { let e = new MyEditor(NullTelemetryService); let input = new MyOtherInput(); let options = new EditorOptions(); @@ -106,7 +106,7 @@ suite('Workbench base editor', () => { assert(!e.isVisible()); assert(!e.input); - await e.setInput(input, options, CancellationToken.None); + await e.setInput(input, options, Object.create(null), CancellationToken.None); assert.strictEqual(input, e.input); const group = new TestEditorGroupView(1); e.setVisible(true, group); diff --git a/src/vs/workbench/test/browser/workbenchTestServices.ts b/src/vs/workbench/test/browser/workbenchTestServices.ts index 8840867e739..0ee2a6325ba 100644 --- a/src/vs/workbench/test/browser/workbenchTestServices.ts +++ b/src/vs/workbench/test/browser/workbenchTestServices.ts @@ -10,7 +10,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, IEditorIdentifier, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, IEditorInput, IEditorPane, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder, IFileEditorInput, IEditorInputFactoryRegistry, IEditorInputFactory, Extensions as EditorExtensions, ISaveOptions, IMoveResult, ITextEditorPane, ITextDiffEditorPane, IVisibleEditorPane, IEditorOpenContext } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import { IBackupFileService, IResolvedBackup } from 'vs/workbench/services/backup/common/backup'; @@ -91,7 +91,7 @@ import { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Registry } from 'vs/platform/registry/common/platform'; -import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; import { CancellationToken } from 'vs/base/common/cancellation'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; @@ -1068,12 +1068,12 @@ export class TestEditorInput extends EditorInput { } export function registerTestEditor(id: string, inputs: SyncDescriptor[], factoryInputId?: string): IDisposable { - class TestEditorControl extends BaseEditor { + class TestEditor extends EditorPane { constructor() { super(id, NullTelemetryService, new TestThemeService(), new TestStorageService()); } - async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { - super.setInput(input, options, token); + async setInput(input: EditorInput, options: EditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + super.setInput(input, options, context, token); await input.resolve(); } @@ -1085,7 +1085,7 @@ export function registerTestEditor(id: string, inputs: SyncDescriptor(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, id, 'Test Editor Control'), inputs)); + disposables.add(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditor, id, 'Test Editor Control'), inputs)); if (factoryInputId) { diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 6d6d66e1381..6b38f0bcc3c 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -55,7 +55,6 @@ import 'vs/workbench/browser/parts/views/viewsService'; import 'vs/platform/undoRedo/common/undoRedoService'; import 'vs/workbench/services/uriIdentity/common/uriIdentityService'; import 'vs/workbench/services/extensions/browser/extensionUrlHandler'; -import 'vs/workbench/services/bulkEdit/browser/bulkEditService'; import 'vs/workbench/services/keybinding/common/keybindingEditing'; import 'vs/workbench/services/decorations/browser/decorationsService'; import 'vs/workbench/services/progress/browser/progressService'; @@ -165,7 +164,8 @@ import 'vs/workbench/contrib/files/browser/files.contribution'; import 'vs/workbench/contrib/backup/common/backup.contribution'; // bulkEdit -import 'vs/workbench/contrib/bulkEdit/browser/bulkEdit.contribution'; +import 'vs/workbench/contrib/bulkEdit/browser/bulkEditService'; +import 'vs/workbench/contrib/bulkEdit/browser/preview/bulkEdit.contribution'; // Search import 'vs/workbench/contrib/search/browser/search.contribution'; @@ -199,6 +199,7 @@ import 'vs/workbench/contrib/url/browser/url.contribution'; // Webview import 'vs/workbench/contrib/webview/browser/webview.contribution'; +import 'vs/workbench/contrib/webviewView/browser/webviewView.contribution'; import 'vs/workbench/contrib/customEditor/browser/customEditor.contribution'; // Extensions Management diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index d52fd0e9c7a..b436aed9f7a 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -69,12 +69,14 @@ import { ITunnelService } from 'vs/platform/remote/common/tunnel'; import { TunnelService } from 'vs/platform/remote/node/tunnelService'; import { ITimerService } from 'vs/workbench/services/timer/browser/timerService'; import { TimerService } from 'vs/workbench/services/timer/electron-browser/timerService'; +import { IUserDataInitializationService, UserDataInitializationService } from 'vs/workbench/services/userData/browser/userDataInit'; registerSingleton(ICredentialsService, KeytarCredentialsService, true); registerSingleton(IUserDataSyncStoreManagementService, UserDataSyncStoreManagementService); registerSingleton(IUserDataAutoSyncService, UserDataAutoSyncService); registerSingleton(ITunnelService, TunnelService); registerSingleton(ITimerService, TimerService); +registerSingleton(IUserDataInitializationService, UserDataInitializationService); //#endregion diff --git a/src/vs/workbench/workbench.desktop.sandbox.main.ts b/src/vs/workbench/workbench.desktop.sandbox.main.ts new file mode 100644 index 00000000000..1d2f2a4150d --- /dev/null +++ b/src/vs/workbench/workbench.desktop.sandbox.main.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +// ####################################################################### +// ### ### +// ### !!! PLEASE ADD COMMON IMPORTS INTO WORKBENCH.COMMON.MAIN.TS !!! ### +// ### ### +// ####################################################################### + + +//#region --- workbench common & sandbox + +import 'vs/workbench/workbench.sandbox.main'; + +//#endregion + + +//#region --- workbench actions + + +//#endregion + + +//#region --- workbench (desktop main) + +import 'vs/workbench/electron-sandbox/desktop.main'; + +//#endregion + + +//#region --- workbench services + + +//#endregion + + +//#region --- workbench contributions + + +//#endregion diff --git a/src/vs/workbench/workbench.web.api.ts b/src/vs/workbench/workbench.web.api.ts index ce168ccdd0a..4386dfc2857 100644 --- a/src/vs/workbench/workbench.web.api.ts +++ b/src/vs/workbench/workbench.web.api.ts @@ -8,7 +8,6 @@ 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 { ICredentialsProvider } from 'vs/workbench/services/credentials/browser/credentialsService'; import { IExtensionManifest } from 'vs/platform/extensions/common/extensions'; import { IURLCallbackProvider } from 'vs/workbench/services/url/browser/urlService'; import { LogLevel } from 'vs/platform/log/common/log'; @@ -19,6 +18,7 @@ import { IWorkspaceProvider, IWorkspace } from 'vs/workbench/services/host/brows import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IProductConfiguration } from 'vs/platform/product/common/productService'; import { mark } from 'vs/base/common/performance'; +import { ICredentialsProvider } from 'vs/platform/credentials/common/credentials'; interface IResourceUriProvider { (uri: URI): URI; @@ -121,6 +121,41 @@ interface IHomeIndicator { title: string; } +interface IWindowIndicator { + + /** + * Triggering this event will cause the window indicator to update. + */ + onDidChange: Event; + + /** + * Label of the window indicator may include octicons + * e.g. `$(remote) label` + */ + label: string; + + /** + * Tooltip of the window indicator should not include + * octicons and be descriptive. + */ + tooltip: string; + + /** + * If provided, overrides the default command that + * is executed when clicking on the window indicator. + */ + command?: string; +} + +interface IInitialColorTheme { + themeType: 'light' | 'dark' | 'hc'; + + /** + * a list of workbench colors + */ + colors?: { [colorId: string]: string }; +} + interface IDefaultSideBarLayout { visible?: boolean; containers?: ({ @@ -204,6 +239,11 @@ interface IWorkbenchConstructionOptions { */ readonly connectionToken?: string; + /** + * Session id of the current authenticated user + */ + readonly authenticationSessionId?: string; + /** * An endpoint to serve iframe content ("webview") from. This is required * to provide full security isolation from the workbench host. @@ -231,6 +271,11 @@ interface IWorkbenchConstructionOptions { */ readonly tunnelProvider?: ITunnelProvider; + /** + * Endpoints to be used for proxying authentication code exchange calls in the browser. + */ + readonly codeExchangeProxyEndpoints?: { [providerId: string]: string } + //#endregion @@ -247,11 +292,6 @@ interface IWorkbenchConstructionOptions { */ userDataProvider?: IFileSystemProvider; - /** - * Session id of the current authenticated user - */ - readonly authenticationSessionId?: string; - /** * Enables user data sync by default and syncs into the current authenticated user account using the provided [authenticationSessionId}(#authenticationSessionId). */ @@ -345,6 +385,20 @@ interface IWorkbenchConstructionOptions { */ readonly productConfiguration?: Partial; + /** + * Optional override for properties of the window indicator in the status bar. + */ + readonly windowIndicator?: IWindowIndicator; + + /** + * Specifies the default theme type (LIGHT, DARK..) and allows to provide initial colors that are shown + * until the color theme that is specified in the settings (`editor.colorTheme`) is loaded and applied. + * Once there are persisted colors from a last run these will be used. + * + * The idea is that the colors match the main colors from the theme defined in the `configurationDefaults`. + */ + readonly initialColorTheme?: IInitialColorTheme; + //#endregion @@ -360,11 +414,6 @@ interface IWorkbenchConstructionOptions { */ readonly driver?: boolean; - /** - * Endpoints to be used for proxying authentication code exchange calls in the browser. - */ - readonly codeExchangeProxyEndpoints?: { [providerId: string]: string } - //#endregion } @@ -504,6 +553,7 @@ export { // Branding IHomeIndicator, IProductConfiguration, + IWindowIndicator, // Default layout IDefaultView, diff --git a/src/vs/workbench/workbench.web.main.ts b/src/vs/workbench/workbench.web.main.ts index 0669178db4c..e6452bb627c 100644 --- a/src/vs/workbench/workbench.web.main.ts +++ b/src/vs/workbench/workbench.web.main.ts @@ -46,7 +46,6 @@ import 'vs/workbench/services/workspaces/browser/workspaceEditingService'; import 'vs/workbench/services/dialogs/browser/dialogService'; import 'vs/workbench/services/dialogs/browser/fileDialogService'; import 'vs/workbench/services/host/browser/browserHostService'; -import 'vs/workbench/services/request/browser/requestService'; import 'vs/workbench/services/lifecycle/browser/lifecycleService'; import 'vs/workbench/services/clipboard/browser/clipboardService'; import 'vs/workbench/services/extensionResourceLoader/browser/extensionResourceLoaderService'; @@ -131,4 +130,7 @@ import 'vs/workbench/contrib/welcome/telemetryOptOut/browser/telemetryOptOut.con // Issues import 'vs/workbench/contrib/issue/browser/issue.web.contribution'; +// Extensions Management (// TODO@sandbox TODO@ben move back into common/extensions.contribution.ts when 'semver-umd' can be loaded) +import 'vs/workbench/contrib/extensions/browser/extensions.web.contribution'; + //#endregion diff --git a/test/integration/browser/src/index.ts b/test/integration/browser/src/index.ts index 91bdd3a8322..5ce67dd0548 100644 --- a/test/integration/browser/src/index.ts +++ b/test/integration/browser/src/index.ts @@ -29,7 +29,9 @@ if (optimist.argv.help) { const width = 1200; const height = 800; -async function runTestsInBrowser(browserType: 'chromium' | 'firefox' | 'webkit', endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { +type BrowserType = 'chromium' | 'firefox' | 'webkit'; + +async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWithStringQuery, server: cp.ChildProcess): Promise { const args = process.platform === 'linux' && browserType === 'chromium' ? ['--no-sandbox'] : undefined; // disable sandbox to run chrome on certain Linux distros const browser = await playwright[browserType].launch({ headless: !Boolean(optimist.argv.debug), args }); const context = await browser.newContext(); @@ -78,7 +80,7 @@ function pkill(pid: number): Promise { }); } -async function launchServer(): Promise<{ endpoint: url.UrlWithStringQuery, server: cp.ChildProcess }> { +async function launchServer(browserType: BrowserType): Promise<{ endpoint: url.UrlWithStringQuery, server: cp.ChildProcess }> { // Ensure a tmp user-data-dir is used for the tests const tmpDir = tmp.dirSync({ prefix: 't' }); @@ -89,6 +91,7 @@ async function launchServer(): Promise<{ endpoint: url.UrlWithStringQuery, serve const env = { VSCODE_AGENT_FOLDER: userDataDir, + VSCODE_BROWSER: browserType, ...process.env }; @@ -130,7 +133,7 @@ async function launchServer(): Promise<{ endpoint: url.UrlWithStringQuery, serve }); } -launchServer().then(async ({ endpoint, server }) => { +launchServer(optimist.argv.browser).then(async ({ endpoint, server }) => { return runTestsInBrowser(optimist.argv.browser, endpoint, server); }, error => { console.error(error); diff --git a/test/smoke/test/index.js b/test/smoke/test/index.js index 5e33b701fa3..a2d16f5e306 100644 --- a/test/smoke/test/index.js +++ b/test/smoke/test/index.js @@ -7,13 +7,14 @@ const path = require('path'); const Mocha = require('mocha'); const minimist = require('minimist'); -const suite = 'Smoke Tests'; - const [, , ...args] = process.argv; const opts = minimist(args, { + boolean: 'web', string: ['f', 'g'] }); +const suite = opts['web'] ? 'Browser Smoke Tests' : 'Smoke Tests'; + const options = { color: true, timeout: 60000, @@ -27,7 +28,7 @@ if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { reporterEnabled: 'spec, mocha-junit-reporter', mochaJunitReporterReporterOptions: { testsuitesTitle: `${suite} ${process.platform}`, - mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) } }; } diff --git a/test/unit/browser/index.js b/test/unit/browser/index.js index a2640613c6e..ba30a81e7b5 100644 --- a/test/unit/browser/index.js +++ b/test/unit/browser/index.js @@ -45,7 +45,7 @@ const withReporter = (function () { new MochaJUnitReporter(runner, { reporterOptions: { testsuitesTitle: `${argv.tfs} ${process.platform}`, - mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${browserType}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${browserType}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined } }); } diff --git a/test/unit/electron/index.js b/test/unit/electron/index.js index 440d8812b8f..6685ff8f713 100644 --- a/test/unit/electron/index.js +++ b/test/unit/electron/index.js @@ -143,7 +143,7 @@ app.on('ready', () => { new MochaJUnitReporter(runner, { reporterOptions: { testsuitesTitle: `${argv.tfs} ${process.platform}`, - mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined + mochaFile: process.env.BUILD_ARTIFACTSTAGINGDIRECTORY ? path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${argv.tfs.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) : undefined } }); } else { diff --git a/yarn.lock b/yarn.lock index b760c375232..07c68d7b74f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2744,10 +2744,10 @@ electron-to-chromium@^1.2.7: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.27.tgz#78ecb8a399066187bb374eede35d9c70565a803d" integrity sha1-eOy4o5kGYYe7N07t412ccFZagD0= -electron@9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-9.2.0.tgz#d9fc8c8c9e5109669c366bd7b9ba83b06095d7a4" - integrity sha512-4ecZ3rcGg//Gk4fAK3Jo61T+uh36JhU6HHR/PTujQqQiBw1g4tNPd4R2hGGth2d+7FkRIs5GdRNef7h64fQEMw== +electron@9.2.1: + version "9.2.1" + resolved "https://registry.yarnpkg.com/electron/-/electron-9.2.1.tgz#54ef574e1af4ae967b5efa94312f1b6458d44a02" + integrity sha512-ZsetaQjXB8+9/EFW1FnfK4ukpkwXCxMEaiKiUZhZ0ZLFlLnFCpe0Bg4vdDf7e4boWGcnlgN1jAJpBw7w0eXuqA== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -6237,10 +6237,10 @@ native-is-elevated@0.4.1: resolved "https://registry.yarnpkg.com/native-is-elevated/-/native-is-elevated-0.4.1.tgz#f6391aafb13441f5b949b39ae0b466b06e7f3986" integrity sha512-2vBXCXCXYKLDjP0WzrXs/AFjDb2njPR31EbGiZ1mR2fMJg211xClK1Xm19RXve35kvAL4dBKOFGCMIyc2+pPsw== -native-keymap@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.1.2.tgz#9773313f619d4c2b66b452cf036310a145523b59" - integrity sha512-n+oe+sxaauCFxomkl9Xrw1iUp88jTamMaGJSHNSGZ8rkIN9N+Wi6KIvBO8x3nmFxLI27KWu1d8IrLBxFKPNQag== +native-keymap@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/native-keymap/-/native-keymap-2.2.0.tgz#940aeb4ae05776dd44dbd9a80dba5342fd49fb8c" + integrity sha512-rWT9mf5f4vMGluXoIoxKSZy76fcVgMvk5jC4meBaOP2GfMJAI7Obtdzpa1Fa1qZCBtZa+OAYV8vlc8dKPOhUNw== native-watchdog@1.3.0: version "1.3.0" @@ -9369,10 +9369,10 @@ typescript@^2.6.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.6.2.tgz#3c5b6fd7f6de0914269027f03c0946758f7673a4" integrity sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q= -typescript@^4.0.1-rc: - version "4.0.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.1-rc.tgz#8adc78223eae56fe71d906a5fa90c3543b07a677" - integrity sha512-TCkspT3dSKOykbzS3/WSK7pqU2h1d/lEO6i45Afm5Y3XNAEAo8YXTG3kHOQk/wFq/5uPyO1+X8rb/Q+g7UsxJw== +typescript@^4.1.0-dev.20200824: + version "4.1.0-dev.20200824" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.1.0-dev.20200824.tgz#34c92d9b6e5124600658c0d4e9b8c125beaf577d" + integrity sha512-hTJfocmebnMKoqRw/xs3bL61z87XXtvOUwYtM7zaCX9mAvnfdo1x1bzQlLZAsvdzRIgAHPJQYbqYHKygWkDw6g== uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3"