diff --git a/.eslintrc.json b/.eslintrc.json index 1c420f93df5..65054e9b4a4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -63,13 +63,18 @@ "browser": [ "common" ], - "electron-main": [ + "electron-sandbox": [ "common", - "node" + "browser" ], "electron-browser": [ "common", "browser", + "node", + "electron-sandbox" + ], + "electron-main": [ + "common", "node" ] } @@ -104,6 +109,14 @@ "**/vs/base/{common,browser}/**" ] }, + { + "target": "**/vs/base/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/vs/base/node/**", "restrictions": [ @@ -149,13 +162,22 @@ "*" // node modules ] }, + { + "target": "**/vs/base/parts/*/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/*/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/vs/base/parts/*/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/vs/base/{common,browser,node,electron-browser}/**", - "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", + "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -185,6 +207,7 @@ "vs/nls", "**/vs/base/common/**", "**/vs/base/parts/*/common/**", + "**/vs/base/test/common/**", "**/vs/platform/*/common/**", "**/vs/platform/*/test/common/**" ] @@ -209,14 +232,24 @@ "*" // node modules ] }, + { + "target": "**/vs/platform/*/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/*/{common,browser,electron-sandbox}/**", + "**/vs/platform/*/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/vs/platform/*/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/vs/base/{common,browser,node}/**", - "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", - "**/vs/platform/*/{common,browser,node,electron-browser}/**", + "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -420,18 +453,34 @@ "**/vs/**/{common,worker}/**" ] }, + { + "target": "**/vs/workbench/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/*/{common,browser,electron-sandbox}/**", + "**/vs/platform/*/{common,browser,electron-sandbox}/**", + "**/vs/editor/{common,browser,electron-sandbox}/**", + "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention + "**/vs/workbench/{common,browser,electron-sandbox}/**", + "**/vs/workbench/api/{common,browser,electron-sandbox}/**", + "**/vs/workbench/services/*/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/vs/workbench/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/vs/base/{common,browser,node,electron-browser}/**", - "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", - "**/vs/platform/*/{common,browser,node,electron-browser}/**", - "**/vs/editor/{common,browser,node,electron-browser}/**", + "**/vs/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/{common,browser,node,electron-sandbox,electron-browser}/**", "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention - "**/vs/workbench/{common,browser,node,electron-browser,api}/**", - "**/vs/workbench/services/*/{common,browser,node,electron-browser}/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/*/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -443,7 +492,7 @@ "**/vs/base/**", "**/vs/platform/**", "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-browser}/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", "vs/workbench/contrib/files/common/editors/fileEditorInput", "**/vs/workbench/services/**", "**/vs/workbench/test/**", @@ -507,16 +556,30 @@ "*" // node modules ] }, + { + "target": "**/vs/workbench/services/**/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker,electron-sandbox}/**", + "**/vs/platform/**/{common,browser,electron-sandbox}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,electron-sandbox}/**", + "**/vs/workbench/api/{common,browser,electron-sandbox}/**", + "**/vs/workbench/services/**/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/vs/workbench/services/**/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker,node,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-browser}/**", + "**/vs/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-browser,api}/**", - "**/vs/workbench/services/**/{common,browser,node,electron-browser}/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -529,7 +592,7 @@ "**/vs/base/**", "**/vs/platform/**", "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-browser}/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", "**/vs/workbench/services/**", "**/vs/workbench/contrib/**", "**/vs/workbench/test/**", @@ -623,17 +686,32 @@ "*" // node modules ] }, + { + "target": "**/vs/workbench/contrib/**/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/vs/base/**/{common,browser,worker,electron-sandbox}/**", + "**/vs/platform/**/{common,browser,electron-sandbox}/**", + "**/vs/editor/**", + "**/vs/workbench/{common,browser,electron-sandbox}/**", + "**/vs/workbench/api/{common,browser,electron-sandbox}/**", + "**/vs/workbench/services/**/{common,browser,electron-sandbox}/**", + "**/vs/workbench/contrib/**/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/vs/workbench/contrib/**/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/vs/base/**/{common,browser,worker,node,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-browser}/**", + "**/vs/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/vs/editor/**", - "**/vs/workbench/{common,browser,node,electron-browser,api}/**", - "**/vs/workbench/services/**/{common,browser,node,electron-browser}/**", - "**/vs/workbench/contrib/**/{common,browser,node,electron-browser}/**", + "**/vs/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/contrib/**/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -653,10 +731,10 @@ "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/vs/base/**/{common,browser,node,electron-browser}/**", - "**/vs/base/parts/**/{common,browser,node,electron-browser}/**", - "**/vs/platform/**/{common,browser,node,electron-browser}/**", - "**/vs/code/**/{common,browser,node,electron-browser}/**", + "**/vs/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/code/**/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -684,6 +762,54 @@ "*" // node modules ] }, + { + "target": "**/src/vs/workbench/workbench.common.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser}/**" + ] + }, + { + "target": "**/src/vs/workbench/workbench.web.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser}/**", + "**/vs/base/parts/**/{common,browser}/**", + "**/vs/platform/**/{common,browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser}/**", + "**/vs/workbench/workbench.common.main" + ] + }, + { + "target": "**/src/vs/workbench/workbench.sandbox.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser,electron-sandbox}/**", + "**/vs/base/parts/**/{common,browser,electron-sandbox}/**", + "**/vs/platform/**/{common,browser,electron-sandbox}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser,electron-sandbox}/**", + "**/vs/workbench/workbench.common.main" + ] + }, + { + "target": "**/src/vs/workbench/workbench.desktop.main.ts", + "restrictions": [ + "vs/nls", + "**/vs/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/editor/**", + "**/vs/workbench/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/vs/workbench/workbench.common.main", + "**/vs/workbench/workbench.sandbox.main" + ] + }, { "target": "**/extensions/**", "restrictions": "**/*" diff --git a/.github/workflows/author-verified.yml b/.github/workflows/author-verified.yml index e78602ddd11..3e0e8b6425b 100644 --- a/.github/workflows/author-verified.yml +++ b/.github/workflows/author-verified.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v16 + ref: v18 path: ./actions - name: Install Actions if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') @@ -31,6 +31,7 @@ jobs: if: github.event_name != 'issues' || contains(github.event.issue.labels.*.name, 'author-verification-requested') uses: ./actions/author-verified with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} requestVerificationComment: "This bug has been fixed in to the latest release of [VS Code Insiders](https://code.visualstudio.com/insiders/)!\n\n@${author}, you can help us out by commenting `/verified` if things are now working as expected.\n\nIf things still don't seem right, please ensure you're on version ${commit} of Insiders (today's or later - you can use `Help: About` in the command pallette to check), and leave a comment letting us know what isn't working as expected.\n\nHappy Coding!" pendingReleaseLabel: awaiting-insiders-release authorVerificationRequestedLabel: author-verification-requested diff --git a/.github/workflows/classifier-apply.yml b/.github/workflows/classifier-apply.yml index 38e0aabad7f..a881de9e501 100644 --- a/.github/workflows/classifier-apply.yml +++ b/.github/workflows/classifier-apply.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v2 with: repository: 'microsoft/vscode-github-triage-actions' - ref: v16 + ref: v18 path: ./actions - name: Install Actions run: npm install --production --prefix ./actions diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 127af00a002..e11044ee792 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: v16 + ref: v18 - name: Install Actions run: npm install --production --prefix ./actions - name: Run Commands diff --git a/.github/workflows/english-please.yml b/.github/workflows/english-please.yml index 241fe7b413d..1e18239a5cf 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: v16 + ref: v18 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 3beed1f0e28..d14c721c0f3 100644 --- a/.github/workflows/feature-request.yml +++ b/.github/workflows/feature-request.yml @@ -17,7 +17,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v16 + ref: v18 - 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/locker.yml b/.github/workflows/locker.yml index e870f6bcb17..b7ad214939e 100644 --- a/.github/workflows/locker.yml +++ b/.github/workflows/locker.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v16 + ref: v18 - 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 d82efcf97de..9693bab593a 100644 --- a/.github/workflows/needs-more-info-closer.yml +++ b/.github/workflows/needs-more-info-closer.yml @@ -13,7 +13,7 @@ jobs: with: repository: 'microsoft/vscode-github-triage-actions' path: ./actions - ref: v16 + ref: v18 - 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 57fe3b3b988..241e75e670d 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: v16 + ref: v18 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 a8f501c4656..9d1be512929 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: v16 + ref: v18 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 new file mode 100644 index 00000000000..83eb1657edf --- /dev/null +++ b/.github/workflows/release-pipeline-labeler.yml @@ -0,0 +1,31 @@ +name: "Release Pipeline Labeler" +on: + issues: + types: [closed] + schedule: + - cron: 20 14 * * * # 4:20pm Zurich + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout Actions + uses: actions/checkout@v2 + with: + repository: 'microsoft/vscode-github-triage-actions' + ref: v18 + path: ./actions + - name: Checkout Repo + if: github.event_name != 'issues' + uses: actions/checkout@v2 + with: + path: ./repo + fetch-depth: 0 + - name: Install Actions + run: npm install --production --prefix ./actions + - name: "Run Release Pipeline Labeler" + uses: ./actions/release-pipeline + with: + token: ${{secrets.VSCODE_ISSUE_TRIAGE_BOT_PAT}} + notYetReleasedLabel: unreleased + insidersReleasedLabel: insiders-released diff --git a/.github/workflows/test-plan-item-validator.yml b/.github/workflows/test-plan-item-validator.yml index be7d119fa3b..154189c4d6c 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: v16 + ref: v18 - 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 496b3cbf6e3..cfd41590211 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,10 +20,7 @@ "port": 5870, "outFiles": [ "${workspaceFolder}/out/**/*.js" - ], - "presentation": { - "hidden": true - } + ] }, { "type": "pwa-chrome", @@ -173,7 +170,7 @@ "${workspaceFolder}/out/**/*.js" ], "presentation": { - "group": "6_tests", + "group": "5_tests", "order": 6 } }, @@ -220,10 +217,7 @@ "type": "node", "request": "launch", "name": "VS Code (Web)", - "runtimeExecutable": "yarn", - "runtimeArgs": [ - "web" - ], + "program": "${workspaceFolder}/scripts/code-web.js", "presentation": { "group": "0_vscode", "order": 2 @@ -277,7 +271,7 @@ { "type": "node", "request": "launch", - "name": "HTML Unit Tests", + "name": "HTML Server Unit Tests", "program": "${workspaceFolder}/extensions/html-language-features/server/test/index.js", "stopOnEntry": false, "cwd": "${workspaceFolder}/extensions/html-language-features/server", @@ -289,6 +283,21 @@ "order": 10 } }, + { + "type": "node", + "request": "launch", + "name": "CSS Server Unit Tests", + "program": "${workspaceFolder}/extensions/css-language-features/server/test/index.js", + "stopOnEntry": false, + "cwd": "${workspaceFolder}/extensions/css-language-features/server", + "outFiles": [ + "${workspaceFolder}/extensions/css-language-features/server/out/**/*.js" + ], + "presentation": { + "group": "5_tests", + "order": 10 + } + }, { "type": "extensionHost", "request": "launch", @@ -313,7 +322,7 @@ "name": "TypeScript Extension Tests", "runtimeExecutable": "${execPath}", "args": [ - "${workspaceFolder}/extensions/typescript-language-features/test-fixtures", + "${workspaceFolder}/extensions/typescript-language-features/test-workspace", "--extensionDevelopmentPath=${workspaceFolder}/extensions/typescript-language-features", "--extensionTestsPath=${workspaceFolder}/extensions/typescript-language-features/out/test" ], diff --git a/.yarnrc b/.yarnrc index d86b284e83e..00b2ebda693 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "7.2.4" +target "7.3.0" runtime "electron" diff --git a/build/azure-pipelines/darwin/entitlements.plist b/build/azure-pipelines/darwin/app-entitlements.plist similarity index 84% rename from build/azure-pipelines/darwin/entitlements.plist rename to build/azure-pipelines/darwin/app-entitlements.plist index be8b7163da7..90031d937be 100644 --- a/build/azure-pipelines/darwin/entitlements.plist +++ b/build/azure-pipelines/darwin/app-entitlements.plist @@ -6,8 +6,6 @@ com.apple.security.cs.allow-unsigned-executable-memory - com.apple.security.cs.disable-library-validation - com.apple.security.cs.allow-dyld-environment-variables diff --git a/build/azure-pipelines/darwin/helper-entitlements.plist b/build/azure-pipelines/darwin/helper-entitlements.plist deleted file mode 100644 index 123d12a53e9..00000000000 --- a/build/azure-pipelines/darwin/helper-entitlements.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.cs.disable-library-validation - - - diff --git a/build/azure-pipelines/darwin/helper-gpu-entitlements.plist b/build/azure-pipelines/darwin/helper-gpu-entitlements.plist index 777b3abd95e..4efe1ce508f 100644 --- a/build/azure-pipelines/darwin/helper-gpu-entitlements.plist +++ b/build/azure-pipelines/darwin/helper-gpu-entitlements.plist @@ -4,7 +4,5 @@ com.apple.security.cs.allow-jit - com.apple.security.cs.disable-library-validation - diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 8419e3388ad..ea286ef1418 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -162,21 +162,13 @@ steps: - script: | set -e - APP_ROOT=$(agent.builddirectory)/VSCode-darwin - APP_NAME="`ls $APP_ROOT | head -n 1`" - HELPER_APP_NAME="`echo $APP_NAME | sed -e 's/^Visual Studio //;s/\.app$//'`" - APP_FRAMEWORK_PATH="$APP_ROOT/$APP_NAME/Contents/Frameworks" security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain security default-keychain -s $(agent.tempdirectory)/buildagent.keychain security unlock-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain echo "$(macos-developer-certificate)" | base64 -D > $(agent.tempdirectory)/cert.p12 security import $(agent.tempdirectory)/cert.p12 -k $(agent.tempdirectory)/buildagent.keychain -P "$(macos-developer-certificate-key)" -T /usr/bin/codesign security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k pwd $(agent.tempdirectory)/buildagent.keychain - codesign -s 99FM488X57 --deep --force --options runtime --entitlements build/azure-pipelines/darwin/entitlements.plist "$APP_ROOT"/*.app - codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper.app" - codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-gpu-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (GPU).app" - codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-plugin-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (Plugin).app" - codesign -s 99FM488X57 --force --options runtime --entitlements build/azure-pipelines/darwin/helper-renderer-entitlements.plist "$APP_FRAMEWORK_PATH/$HELPER_APP_NAME Helper (Renderer).app" + DEBUG=electron-osx-sign* node build/darwin/sign.js displayName: Set Hardened Entitlements - script: | diff --git a/build/azure-pipelines/mixin.js b/build/azure-pipelines/mixin.js index efb7d4d1ca9..8d441e783a9 100644 --- a/build/azure-pipelines/mixin.js +++ b/build/azure-pipelines/mixin.js @@ -12,6 +12,8 @@ const es = require('event-stream'); const vfs = require('vinyl-fs'); const fancyLog = require('fancy-log'); const ansiColors = require('ansi-colors'); +const fs = require('fs'); +const path = require('path'); function main() { const quality = process.env['VSCODE_QUALITY']; @@ -21,7 +23,7 @@ function main() { return; } - const productJsonFilter = filter('product.json', { restore: true }); + const productJsonFilter = filter(f => f.relative === 'product.json', { restore: true }); fancyLog(ansiColors.blue('[mixin]'), `Mixing in sources:`); return vfs @@ -29,7 +31,32 @@ function main() { .pipe(filter(f => !f.isDirectory())) .pipe(productJsonFilter) .pipe(buffer()) - .pipe(json(o => Object.assign({}, require('../product.json'), o))) + .pipe(json(o => { + const ossProduct = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'product.json'), 'utf8')); + let builtInExtensions = ossProduct.builtInExtensions; + + if (Array.isArray(o.builtInExtensions)) { + fancyLog(ansiColors.blue('[mixin]'), 'Overwriting built-in extensions:', o.builtInExtensions.map(e => e.name)); + + builtInExtensions = o.builtInExtensions; + } else if (o.builtInExtensions) { + const include = o.builtInExtensions['include'] || []; + const exclude = o.builtInExtensions['exclude'] || []; + + fancyLog(ansiColors.blue('[mixin]'), 'OSS built-in extensions:', builtInExtensions.map(e => e.name)); + fancyLog(ansiColors.blue('[mixin]'), 'Including built-in extensions:', include.map(e => e.name)); + fancyLog(ansiColors.blue('[mixin]'), 'Excluding built-in extensions:', exclude); + + builtInExtensions = builtInExtensions.filter(ext => !include.find(e => e.name === ext.name) && !exclude.find(name => name === ext.name)); + builtInExtensions = [...builtInExtensions, ...include]; + + fancyLog(ansiColors.blue('[mixin]'), 'Final built-in extensions:', builtInExtensions.map(e => e.name)); + } else { + fancyLog(ansiColors.blue('[mixin]'), 'Inheriting OSS built-in extensions', builtInExtensions.map(e => e.name)); + } + + return { ...o, builtInExtensions }; + })) .pipe(productJsonFilter.restore) .pipe(es.mapSync(function (f) { fancyLog(ansiColors.blue('[mixin]'), f.relative, ansiColors.green('✔︎')); @@ -38,4 +65,4 @@ function main() { .pipe(vfs.dest('.')); } -main(); \ No newline at end of file +main(); diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index a98b5f4f77e..7b6d2bcbbde 100644 --- a/build/azure-pipelines/product-build.yml +++ b/build/azure-pipelines/product-build.yml @@ -36,6 +36,17 @@ jobs: steps: - template: win32/product-build-win32.yml +- job: WindowsARM64 + condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_WIN32_ARM64'], 'true')) + pool: + vmImage: VS2017-Win2016 + variables: + VSCODE_ARCH: arm64 + dependsOn: + - Compile + steps: + - template: win32/product-build-win32-arm64.yml + - job: Linux condition: and(succeeded(), eq(variables['VSCODE_COMPILE_ONLY'], 'false'), eq(variables['VSCODE_BUILD_LINUX'], 'true')) pool: diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index c3db41e80d5..db6524be03b 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -72,29 +72,6 @@ steps: vstsFeed: 'npm-vscode' condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), ne(variables['CacheRestored'], 'true')) -- script: | - set -e - yarn generate-github-config - displayName: Generate GitHub config - env: - OSS_GITHUB_ID: "a5d3c261b032765a78de" - OSS_GITHUB_SECRET: $(oss-github-client-secret) - INSIDERS_GITHUB_ID: "31f02627809389d9f111" - INSIDERS_GITHUB_SECRET: $(insiders-github-client-secret) - STABLE_GITHUB_ID: "baa8a44b5e861d918709" - STABLE_GITHUB_SECRET: $(stable-github-client-secret) - EXPLORATION_GITHUB_ID: "94e8376d3a90429aeaea" - EXPLORATION_GITHUB_SECRET: $(exploration-github-client-secret) - VSO_GITHUB_ID: "3d4be8f37a0325b5817d" - VSO_GITHUB_SECRET: $(vso-github-client-secret) - VSO_PPE_GITHUB_ID: "eabf35024dc2e891a492" - VSO_PPE_GITHUB_SECRET: $(vso-ppe-github-client-secret) - VSO_DEV_GITHUB_ID: "84383ebd8a7c5f5efc5c" - VSO_DEV_GITHUB_SECRET: $(vso-dev-github-client-secret) - GITHUB_APP_ID: "Iv1.ae51e546bef24ff1" - GITHUB_APP_SECRET: $(github-app-client-secret) - condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), ne(variables['CacheRestored'], 'true')) - - script: | set -e yarn postinstall diff --git a/build/azure-pipelines/win32/product-build-win32-arm64.yml b/build/azure-pipelines/win32/product-build-win32-arm64.yml new file mode 100644 index 00000000000..01be34aa9a8 --- /dev/null +++ b/build/azure-pipelines/win32/product-build-win32-arm64.yml @@ -0,0 +1,190 @@ +steps: +- powershell: | + mkdir .build -ea 0 + "$env:BUILD_SOURCEVERSION" | Out-File -Encoding ascii -NoNewLine .build\commit + "$env:VSCODE_QUALITY" | Out-File -Encoding ascii -NoNewLine .build\quality + displayName: Prepare cache flag + +- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + inputs: + keyfile: 'build/.cachesalt, .build/commit, .build/quality' + targetfolder: '.build, out-build, out-vscode-min, out-vscode-reh-min, out-vscode-reh-web-min' + vstsFeed: 'npm-vscode' + platformIndependent: true + alias: 'Compilation' + +- powershell: | + $ErrorActionPreference = "Stop" + exit 1 + displayName: Check RestoreCache + condition: and(succeeded(), ne(variables['CacheRestored-Compilation'], 'true')) + +- task: NodeTool@0 + inputs: + versionSpec: "12.13.0" + +- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.x" + +- task: UsePythonVersion@0 + inputs: + versionSpec: '2.x' + addToPath: true + +- task: AzureKeyVault@1 + displayName: 'Azure Key Vault: Get Secrets' + inputs: + azureSubscription: 'vscode-builds-subscription' + KeyVaultName: vscode + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + "machine github.com`nlogin vscode`npassword $(github-distro-mixin-password)" | Out-File "$env:USERPROFILE\_netrc" -Encoding ASCII + + exec { git config user.email "vscode@microsoft.com" } + exec { git config user.name "VSCode" } + + mkdir .build -ea 0 + "$(VSCODE_ARCH)" | Out-File -Encoding ascii -NoNewLine .build\arch + displayName: Prepare tooling + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { git remote add distro "https://github.com/$(VSCODE_MIXIN_REPO).git" } + exec { git fetch distro } + exec { git merge $(node -p "require('./package.json').distro") } + displayName: Merge distro + +- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 + inputs: + keyfile: 'build/.cachesalt, .build/arch, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' + targetfolder: '**/node_modules, !**/node_modules/**/node_modules' + vstsFeed: 'npm-vscode' + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:npm_config_arch="$(VSCODE_ARCH)" + $env:CHILD_CONCURRENCY="1" + exec { yarn --frozen-lockfile } + displayName: Install dependencies + condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + +- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 + inputs: + keyfile: 'build/.cachesalt, .build/arch, .yarnrc, remote/.yarnrc, **/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' + targetfolder: '**/node_modules, !**/node_modules/**/node_modules' + vstsFeed: 'npm-vscode' + condition: and(succeeded(), ne(variables['CacheRestored'], 'true')) + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { yarn postinstall } + displayName: Run postinstall scripts + condition: and(succeeded(), eq(variables['CacheRestored'], 'true')) + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + exec { node build/azure-pipelines/mixin } + displayName: Mix in quality + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + exec { yarn gulp "vscode-win32-$env:VSCODE_ARCH-min-ci" } + exec { yarn gulp "vscode-win32-$env:VSCODE_ARCH-code-helper" } + exec { yarn gulp "vscode-win32-$env:VSCODE_ARCH-inno-updater" } + displayName: Build + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '$(agent.builddirectory)/VSCode-win32-$(VSCODE_ARCH)' + Pattern: '*.dll,*.exe,*.node' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolSign", + "parameters": [ + { + "parameterName": "OpusName", + "parameterValue": "VS Code" + }, + { + "parameterName": "OpusInfo", + "parameterValue": "https://code.visualstudio.com/" + }, + { + "parameterName": "Append", + "parameterValue": "/as" + }, + { + "parameterName": "FileDigest", + "parameterValue": "/fd \"SHA256\"" + }, + { + "parameterName": "PageHash", + "parameterValue": "/NPH" + }, + { + "parameterName": "TimeStamp", + "parameterValue": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + }, + { + "keyCode": "CP-230012", + "operationSetCode": "SigntoolVerify", + "parameters": [ + { + "parameterName": "VerifyAll", + "parameterValue": "/all" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + +- task: NuGetCommand@2 + displayName: Install ESRPClient.exe + inputs: + restoreSolution: 'build\azure-pipelines\win32\ESRPClient\packages.config' + feedsToUse: config + nugetConfigPath: 'build\azure-pipelines\win32\ESRPClient\NuGet.config' + externalFeedCredentials: 3fc0b7f7-da09-4ae7-a9c8-d69824b1819b + restoreDirectory: packages + +- task: ESRPImportCertTask@1 + displayName: Import ESRP Request Signing Certificate + inputs: + ESRP: 'ESRP CodeSign' + +- powershell: | + $ErrorActionPreference = "Stop" + .\build\azure-pipelines\win32\import-esrp-auth-cert.ps1 -AuthCertificateBase64 $(esrp-auth-certificate) -AuthCertificateKey $(esrp-auth-certificate-key) + displayName: Import ESRP Auth Certificate + +- powershell: | + . build/azure-pipelines/win32/exec.ps1 + $ErrorActionPreference = "Stop" + $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(vscode-storage-key)" + $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" + $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" + .\build\azure-pipelines\win32\publish.ps1 + displayName: Publish + +- task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 + displayName: 'Component Detection' + continueOnError: true diff --git a/build/azure-pipelines/win32/publish.ps1 b/build/azure-pipelines/win32/publish.ps1 index ee98fc946ec..a225f9d5fdf 100644 --- a/build/azure-pipelines/win32/publish.ps1 +++ b/build/azure-pipelines/win32/publish.ps1 @@ -16,16 +16,21 @@ $ServerZip = "$Repo\.build\vscode-server-win32-$Arch.zip" $Build = "$Root\VSCode-win32-$Arch" # Create server archive -exec { xcopy $LegacyServer $Server /H /E /I } -exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $ServerZip $Server -r } +if ("$Arch" -ne "arm64") { + exec { xcopy $LegacyServer $Server /H /E /I } + exec { .\node_modules\7zip\7zip-lite\7z.exe a -tzip $ServerZip $Server -r } +} # get version $PackageJson = Get-Content -Raw -Path "$Build\resources\app\package.json" | ConvertFrom-Json $Version = $PackageJson.version -$AssetPlatform = if ("$Arch" -eq "ia32") { "win32" } else { "win32-x64" } +$AssetPlatform = if ("$Arch" -eq "ia32") { "win32" } else { "win32-$Arch" } exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-archive" archive "VSCode-win32-$Arch-$Version.zip" $Zip } exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform" setup "VSCodeSetup-$Arch-$Version.exe" $SystemExe } exec { node build/azure-pipelines/common/createAsset.js "$AssetPlatform-user" setup "VSCodeUserSetup-$Arch-$Version.exe" $UserExe } -exec { node build/azure-pipelines/common/createAsset.js "server-$AssetPlatform" archive "vscode-server-win32-$Arch.zip" $ServerZip } + +if ("$Arch" -ne "arm64") { + exec { node build/azure-pipelines/common/createAsset.js "server-$AssetPlatform" archive "vscode-server-win32-$Arch.zip" $ServerZip } +} diff --git a/build/builtin/main.js b/build/builtin/main.js index b094a67cac5..93cef8cbdad 100644 --- a/build/builtin/main.js +++ b/build/builtin/main.js @@ -10,11 +10,11 @@ const path = require('path'); let window = null; app.once('ready', () => { - window = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, webviewTag: true } }); + window = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, webviewTag: true, enableWebSQL: false } }); window.setMenuBarVisibility(false); window.loadURL(url.format({ pathname: path.join(__dirname, 'index.html'), protocol: 'file:', slashes: true })); // window.webContents.openDevTools(); window.once('closed', () => window = null); }); -app.on('window-all-closed', () => app.quit()); \ No newline at end of file +app.on('window-all-closed', () => app.quit()); diff --git a/build/darwin/sign.js b/build/darwin/sign.js new file mode 100644 index 00000000000..b8eb9fc7525 --- /dev/null +++ b/build/darwin/sign.js @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +Object.defineProperty(exports, "__esModule", { value: true }); +const codesign = require("electron-osx-sign"); +const path = require("path"); +const util = require("../lib/util"); +const product = require("../../product.json"); +async function main() { + const buildDir = process.env['AGENT_BUILDDIRECTORY']; + const tempDir = process.env['AGENT_TEMPDIRECTORY']; + if (!buildDir) { + throw new Error('$AGENT_BUILDDIRECTORY not set'); + } + if (!tempDir) { + throw new Error('$AGENT_TEMPDIRECTORY not set'); + } + const baseDir = path.dirname(__dirname); + const appRoot = path.join(buildDir, 'VSCode-darwin'); + const appName = product.nameLong + '.app'; + const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); + const helperAppBaseName = product.nameShort; + const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; + const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; + const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; + const defaultOpts = { + app: path.join(appRoot, appName), + platform: 'darwin', + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + hardenedRuntime: true, + 'pre-auto-entitlements': false, + 'pre-embed-provisioning-profile': false, + keychain: path.join(tempDir, 'buildagent.keychain'), + version: util.getElectronVersion(), + identity: '99FM488X57', + 'gatekeeper-assess': false + }; + const appOpts = Object.assign(Object.assign({}, defaultOpts), { + // TODO(deepak1556): Incorrectly declared type in electron-osx-sign + ignore: (filePath) => { + return filePath.includes(gpuHelperAppName) || + filePath.includes(pluginHelperAppName) || + filePath.includes(rendererHelperAppName); + } }); + const gpuHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, gpuHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist') }); + const pluginHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, pluginHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist') }); + const rendererHelperOpts = Object.assign(Object.assign({}, defaultOpts), { app: path.join(appFrameworkPath, rendererHelperAppName), entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist') }); + await codesign.signAsync(gpuHelperOpts); + await codesign.signAsync(pluginHelperOpts); + await codesign.signAsync(rendererHelperOpts); + await codesign.signAsync(appOpts); +} +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts new file mode 100644 index 00000000000..ee5d2eeb17b --- /dev/null +++ b/build/darwin/sign.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as codesign from 'electron-osx-sign'; +import * as path from 'path'; +import * as util from '../lib/util'; +import * as product from '../../product.json'; + +async function main(): Promise { + const buildDir = process.env['AGENT_BUILDDIRECTORY']; + const tempDir = process.env['AGENT_TEMPDIRECTORY']; + + if (!buildDir) { + throw new Error('$AGENT_BUILDDIRECTORY not set'); + } + + if (!tempDir) { + throw new Error('$AGENT_TEMPDIRECTORY not set'); + } + + const baseDir = path.dirname(__dirname); + const appRoot = path.join(buildDir, 'VSCode-darwin'); + const appName = product.nameLong + '.app'; + const appFrameworkPath = path.join(appRoot, appName, 'Contents', 'Frameworks'); + const helperAppBaseName = product.nameShort; + const gpuHelperAppName = helperAppBaseName + ' Helper (GPU).app'; + const pluginHelperAppName = helperAppBaseName + ' Helper (Plugin).app'; + const rendererHelperAppName = helperAppBaseName + ' Helper (Renderer).app'; + + const defaultOpts: codesign.SignOptions = { + app: path.join(appRoot, appName), + platform: 'darwin', + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'app-entitlements.plist'), + hardenedRuntime: true, + 'pre-auto-entitlements': false, + 'pre-embed-provisioning-profile': false, + keychain: path.join(tempDir, 'buildagent.keychain'), + version: util.getElectronVersion(), + identity: '99FM488X57', + 'gatekeeper-assess': false + }; + + const appOpts = { + ...defaultOpts, + // TODO(deepak1556): Incorrectly declared type in electron-osx-sign + ignore: (filePath: string) => { + return filePath.includes(gpuHelperAppName) || + filePath.includes(pluginHelperAppName) || + filePath.includes(rendererHelperAppName); + } + }; + + const gpuHelperOpts: codesign.SignOptions = { + ...defaultOpts, + app: path.join(appFrameworkPath, gpuHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-gpu-entitlements.plist'), + }; + + const pluginHelperOpts: codesign.SignOptions = { + ...defaultOpts, + app: path.join(appFrameworkPath, pluginHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-plugin-entitlements.plist'), + }; + + const rendererHelperOpts: codesign.SignOptions = { + ...defaultOpts, + app: path.join(appFrameworkPath, rendererHelperAppName), + entitlements: path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + 'entitlements-inherit': path.join(baseDir, 'azure-pipelines', 'darwin', 'helper-renderer-entitlements.plist'), + }; + + await codesign.signAsync(gpuHelperOpts); + await codesign.signAsync(pluginHelperOpts); + await codesign.signAsync(rendererHelperOpts); + await codesign.signAsync(appOpts as any); +} + +if (require.main === module) { + main().catch(err => { + console.error(err); + process.exit(1); + }); +} diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 75c8413ae5d..0e2285bad00 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -59,6 +59,7 @@ const indentationFilter = [ // except specific folders '!test/automation/out/**', '!test/smoke/out/**', + '!extensions/typescript-language-features/test-workspace/**', '!extensions/vscode-api-tests/testWorkspace/**', '!extensions/vscode-api-tests/testWorkspace2/**', '!build/monaco/**', @@ -84,7 +85,7 @@ const indentationFilter = [ '!src/typings/**/*.d.ts', '!extensions/**/*.d.ts', '!**/*.{svg,exe,png,bmp,scpt,bat,cmd,cur,ttf,woff,eot,md,ps1,template,yaml,yml,d.ts.recipe,ico,icns,plist}', - '!build/{lib,download}/**/*.js', + '!build/{lib,download,darwin}/**/*.js', '!build/**/*.sh', '!build/azure-pipelines/**/*.js', '!build/azure-pipelines/**/*.config', diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 25b41bd3a91..5881bc66e91 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -66,6 +66,7 @@ const vscodeResources = [ 'out-build/vs/base/node/languagePacks.js', 'out-build/vs/base/node/{stdForkStart.js,terminateProcess.sh,cpuUsage.sh,ps.sh}', 'out-build/vs/base/browser/ui/codicons/codicon/**', + 'out-build/vs/base/parts/sandbox/electron-browser/preload.js', 'out-build/vs/workbench/browser/media/*-theme.css', 'out-build/vs/workbench/contrib/debug/**/*.json', 'out-build/vs/workbench/contrib/externalTerminal/**/*.scpt', @@ -154,6 +155,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op const out = sourceFolderName; const checksums = computeChecksums(out, [ + 'vs/base/parts/sandbox/electron-browser/preload.js', 'vs/workbench/workbench.desktop.main.js', 'vs/workbench/workbench.desktop.main.css', 'vs/workbench/services/extensions/node/extensionHostProcess.js', diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index b0956371451..d659b15d94b 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -65,6 +65,7 @@ function buildWin32Setup(arch, target) { return cb => { const ia32AppId = target === 'system' ? product.win32AppId : product.win32UserAppId; const x64AppId = target === 'system' ? product.win32x64AppId : product.win32x64UserAppId; + const arm64AppId = target === 'system' ? product.win32arm64AppId : product.win32arm64UserAppId; const sourcePath = buildPath(arch); const outputPath = setupDir(arch, target); @@ -88,12 +89,12 @@ function buildWin32Setup(arch, target) { ShellNameShort: product.win32ShellNameShort, AppMutex: product.win32MutexName, Arch: arch, - AppId: arch === 'ia32' ? ia32AppId : x64AppId, - IncompatibleTargetAppId: arch === 'ia32' ? product.win32AppId : product.win32x64AppId, - IncompatibleArchAppId: arch === 'ia32' ? x64AppId : ia32AppId, + AppId: { 'ia32': ia32AppId, 'x64': x64AppId, 'arm64': arm64AppId }[arch], + IncompatibleTargetAppId: { 'ia32': product.win32AppId, 'x64': product.win32x64AppId, 'arm64': product.win32arm64AppId }[arch], + IncompatibleArchAppId: { 'ia32': x64AppId, 'x64': ia32AppId, 'arm64': ia32AppId }[arch], AppUserId: product.win32AppUserModelId, - ArchitecturesAllowed: arch === 'ia32' ? '' : 'x64', - ArchitecturesInstallIn64BitMode: arch === 'ia32' ? '' : 'x64', + ArchitecturesAllowed: { 'ia32': '', 'x64': 'x64', 'arm64': '' }[arch], + ArchitecturesInstallIn64BitMode: { 'ia32': '', 'x64': 'x64', 'arm64': '' }[arch], SourceDir: sourcePath, RepoDir: repoPath, OutputDir: outputPath, @@ -112,8 +113,10 @@ function defineWin32SetupTasks(arch, target) { defineWin32SetupTasks('ia32', 'system'); defineWin32SetupTasks('x64', 'system'); +defineWin32SetupTasks('arm64', 'system'); defineWin32SetupTasks('ia32', 'user'); defineWin32SetupTasks('x64', 'user'); +defineWin32SetupTasks('arm64', 'user'); function archiveWin32Setup(arch) { return cb => { @@ -145,6 +148,7 @@ function updateIcon(executablePath) { gulp.task(task.define('vscode-win32-ia32-inno-updater', task.series(copyInnoUpdater('ia32'), updateIcon(path.join(buildPath('ia32'), 'tools', 'inno_updater.exe'))))); gulp.task(task.define('vscode-win32-x64-inno-updater', task.series(copyInnoUpdater('x64'), updateIcon(path.join(buildPath('x64'), 'tools', 'inno_updater.exe'))))); +gulp.task(task.define('vscode-win32-arm64-inno-updater', task.series(copyInnoUpdater('arm64'), updateIcon(path.join(buildPath('arm64'), 'tools', 'inno_updater.exe'))))); // CodeHelper.exe icon diff --git a/build/lib/electron.js b/build/lib/electron.js index abf6baab419..bb71f14c12d 100644 --- a/build/lib/electron.js +++ b/build/lib/electron.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.config = exports.getElectronVersion = void 0; +exports.config = void 0; const fs = require("fs"); const path = require("path"); const vfs = require("vinyl-fs"); @@ -16,12 +16,6 @@ const electron = require('gulp-atom-electron'); const root = path.dirname(path.dirname(__dirname)); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const commit = util.getVersion(root); -function getElectronVersion() { - const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); - const target = /^target "(.*)"$/m.exec(yarnrc)[1]; - return target; -} -exports.getElectronVersion = getElectronVersion; const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSync(path.join(root, product.darwinCredits), 'utf8')); function darwinBundleDocumentType(extensions, icon) { return { @@ -33,7 +27,7 @@ function darwinBundleDocumentType(extensions, icon) { }; } exports.config = { - version: getElectronVersion(), + version: util.getElectronVersion(), productAppName: product.nameLong, companyName: 'Microsoft Corporation', copyright: 'Copyright (C) 2019 Microsoft. All rights reserved', @@ -70,7 +64,7 @@ exports.config = { darwinBundleDocumentType(["vue"], 'resources/darwin/vue.icns'), darwinBundleDocumentType(["ascx", "csproj", "dtd", "wxi", "wxl", "wxs", "xml", "xaml"], 'resources/darwin/xml.icns'), darwinBundleDocumentType(["eyaml", "eyml", "yaml", "yml"], 'resources/darwin/yaml.icns'), - darwinBundleDocumentType(["clj", "cljs", "cljx", "clojure", "code-workspace", "coffee", "ctp", "dockerfile", "dot", "edn", "fs", "fsi", "fsscript", "fsx", "handlebars", "hbs", "lua", "m", "makefile", "ml", "mli", "pl", "pl6", "pm", "pm6", "pod", "pp", "properties", "psgi", "pug", "r", "rs", "rt", "svg", "svgz", "t", "txt", "vb", "xcodeproj", "xcworkspace"], 'resources/darwin/default.icns') + darwinBundleDocumentType(["clj", "cljs", "cljx", "clojure", "code-workspace", "coffee", "containerfile", "ctp", "dockerfile", "dot", "edn", "fs", "fsi", "fsscript", "fsx", "handlebars", "hbs", "lua", "m", "makefile", "ml", "mli", "pl", "pl6", "pm", "pm6", "pod", "pp", "properties", "psgi", "pug", "r", "rs", "rt", "svg", "svgz", "t", "txt", "vb", "xcodeproj", "xcworkspace"], 'resources/darwin/default.icns') ], darwinBundleURLTypes: [{ role: 'Viewer', @@ -100,7 +94,7 @@ function getElectron(arch) { }; } async function main(arch = process.arch) { - const version = getElectronVersion(); + const version = util.getElectronVersion(); const electronPath = path.join(root, '.build', 'electron'); const versionFile = path.join(electronPath, 'version'); const isUpToDate = fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf8') === `${version}`; diff --git a/build/lib/electron.ts b/build/lib/electron.ts index 86c7afcf312..e0beca78079 100644 --- a/build/lib/electron.ts +++ b/build/lib/electron.ts @@ -19,12 +19,6 @@ const root = path.dirname(path.dirname(__dirname)); const product = JSON.parse(fs.readFileSync(path.join(root, 'product.json'), 'utf8')); const commit = util.getVersion(root); -export function getElectronVersion(): string { - const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); - const target = /^target "(.*)"$/m.exec(yarnrc)![1]; - return target; -} - const darwinCreditsTemplate = product.darwinCredits && _.template(fs.readFileSync(path.join(root, product.darwinCredits), 'utf8')); function darwinBundleDocumentType(extensions: string[], icon: string) { @@ -38,7 +32,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string) { } export const config = { - version: getElectronVersion(), + version: util.getElectronVersion(), productAppName: product.nameLong, companyName: 'Microsoft Corporation', copyright: 'Copyright (C) 2019 Microsoft. All rights reserved', @@ -75,7 +69,7 @@ export const config = { darwinBundleDocumentType(["vue"], 'resources/darwin/vue.icns'), darwinBundleDocumentType(["ascx", "csproj", "dtd", "wxi", "wxl", "wxs", "xml", "xaml"], 'resources/darwin/xml.icns'), darwinBundleDocumentType(["eyaml", "eyml", "yaml", "yml"], 'resources/darwin/yaml.icns'), - darwinBundleDocumentType(["clj", "cljs", "cljx", "clojure", "code-workspace", "coffee", "ctp", "dockerfile", "dot", "edn", "fs", "fsi", "fsscript", "fsx", "handlebars", "hbs", "lua", "m", "makefile", "ml", "mli", "pl", "pl6", "pm", "pm6", "pod", "pp", "properties", "psgi", "pug", "r", "rs", "rt", "svg", "svgz", "t", "txt", "vb", "xcodeproj", "xcworkspace"], 'resources/darwin/default.icns') + darwinBundleDocumentType(["clj", "cljs", "cljx", "clojure", "code-workspace", "coffee", "containerfile", "ctp", "dockerfile", "dot", "edn", "fs", "fsi", "fsscript", "fsx", "handlebars", "hbs", "lua", "m", "makefile", "ml", "mli", "pl", "pl6", "pm", "pm6", "pod", "pp", "properties", "psgi", "pug", "r", "rs", "rt", "svg", "svgz", "t", "txt", "vb", "xcodeproj", "xcworkspace"], 'resources/darwin/default.icns') ], darwinBundleURLTypes: [{ role: 'Viewer', @@ -108,7 +102,7 @@ function getElectron(arch: string): () => NodeJS.ReadWriteStream { } async function main(arch = process.arch): Promise { - const version = getElectronVersion(); + const version = util.getElectronVersion(); const electronPath = path.join(root, '.build', 'electron'); const versionFile = path.join(electronPath, 'version'); const isUpToDate = fs.existsSync(versionFile) && fs.readFileSync(versionFile, 'utf8') === `${version}`; diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 930b13edabe..93a6992e411 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -342,6 +342,10 @@ "name": "vs/workbench/services/userDataSync", "project": "vscode-workbench" }, + { + "name": "vs/workbench/services/views", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/timeline", "project": "vscode-workbench" diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 663769dacea..f3cc2252b5e 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -130,6 +130,14 @@ const RULES = [ 'lib.dom.d.ts' // no DOM ] }, + // Electron (sandbox) + { + target: '**/vs/**/electron-sandbox/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, // Electron (renderer): skip { target: '**/vs/**/electron-browser/**', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 0dcedb8d4e3..508d2b986e9 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -143,6 +143,15 @@ const RULES = [ ] }, + // Electron (sandbox) + { + target: '**/vs/**/electron-sandbox/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, + // Electron (renderer): skip { target: '**/vs/**/electron-browser/**', diff --git a/build/lib/util.js b/build/lib/util.js index d42670e67a5..8b70b534b20 100644 --- a/build/lib/util.js +++ b/build/lib/util.js @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); -exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.getVersion = exports.ensureDir = exports.rreddir = exports.rimraf = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.incremental = void 0; +exports.getElectronVersion = exports.streamToPromise = exports.versionStringToNumber = exports.filter = exports.rebase = exports.getVersion = exports.ensureDir = exports.rreddir = exports.rimraf = exports.stripSourceMappingURL = exports.loadSourcemaps = exports.cleanNodeModules = exports.skipDirectories = exports.toFileUri = exports.setExecutableBit = exports.fixWin32DirectoryPermissions = exports.incremental = void 0; const es = require("event-stream"); const debounce = require("debounce"); const _filter = require("gulp-filter"); @@ -14,6 +14,7 @@ const fs = require("fs"); const _rimraf = require("rimraf"); const git = require("./git"); const VinylFile = require("vinyl"); +const root = path.dirname(path.dirname(__dirname)); const NoCancellationToken = { isCancellationRequested: () => false }; function incremental(streamProvider, initial, supportsCancellation) { const input = es.through(); @@ -255,3 +256,9 @@ function streamToPromise(stream) { }); } exports.streamToPromise = streamToPromise; +function getElectronVersion() { + const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); + const target = /^target "(.*)"$/m.exec(yarnrc)[1]; + return target; +} +exports.getElectronVersion = getElectronVersion; diff --git a/build/lib/util.ts b/build/lib/util.ts index 45b6c9e1b82..e379b47d8ed 100644 --- a/build/lib/util.ts +++ b/build/lib/util.ts @@ -18,6 +18,8 @@ import * as VinylFile from 'vinyl'; import { ThroughStream } from 'through'; import * as sm from 'source-map'; +const root = path.dirname(path.dirname(__dirname)); + export interface ICancellationToken { isCancellationRequested(): boolean; } @@ -318,3 +320,9 @@ export function streamToPromise(stream: NodeJS.ReadWriteStream): Promise { stream.on('end', () => c()); }); } + +export function getElectronVersion(): string { + const yarnrc = fs.readFileSync(path.join(root, '.yarnrc'), 'utf8'); + const target = /^target "(.*)"$/m.exec(yarnrc)![1]; + return target; +} diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 7a2320d8289..7b8b710636a 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -33,9 +33,10 @@ function yarnInstall(location, opts) { yarnInstall('extensions'); // node modules shared by all extensions -yarnInstall('remote'); // node modules used by vscode server - -yarnInstall('remote/web'); // node modules used by vscode web +if (!(process.platform === 'win32' && process.env['npm_config_arch'] === 'arm64')) { + yarnInstall('remote'); // node modules used by vscode server + yarnInstall('remote/web'); // node modules used by vscode web +} const allExtensionFolders = fs.readdirSync('extensions'); const extensions = allExtensionFolders.filter(e => { diff --git a/build/package.json b/build/package.json index ae605126424..ecff6c7a182 100644 --- a/build/package.json +++ b/build/package.json @@ -33,6 +33,7 @@ "@typescript-eslint/parser": "^2.12.0", "applicationinsights": "1.0.8", "azure-storage": "^2.1.0", + "electron-osx-sign": "^0.4.16", "github-releases": "^0.4.1", "gulp-bom": "^1.0.0", "gulp-sourcemaps": "^1.11.0", @@ -43,7 +44,7 @@ "minimist": "^1.2.3", "request": "^2.85.0", "terser": "4.3.8", - "typescript": "^3.9.1-rc", + "typescript": "^3.9.3", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/win32/code.iss b/build/win32/code.iss index 15293a0c5cf..49b2f255ce3 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -238,6 +238,13 @@ Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.confi Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\config.ico"; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\resources\app\resources\win32\default.ico"; Tasks: associatewithfiles +Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#ExeBasename}.exe"" ""%1"""; Tasks: associatewithfiles + Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles Root: {#SoftwareClassesRootKey}; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles @@ -1011,7 +1018,7 @@ begin #endif #if "user" == InstallTarget - #if "ia32" == Arch + #if "ia32" == Arch || "arm64" == Arch #define IncompatibleArchRootKey "HKLM32" #else #define IncompatibleArchRootKey "HKLM64" diff --git a/build/yarn.lock b/build/yarn.lock index 62326638698..8f615cb4e6c 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -557,6 +557,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.2.3: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + bcrypt-pbkdf@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz#63bc5dcb61331b92bc05fd528953c33462a06f8d" @@ -569,6 +574,11 @@ beeper@^1.0.0: resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak= +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + boolbase@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" @@ -615,11 +625,29 @@ browserify-mime@~1.2.9: resolved "https://registry.yarnpkg.com/browserify-mime/-/browserify-mime-1.2.9.tgz#aeb1af28de6c0d7a6a2ce40adb68ff18422af31f" integrity sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8= +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -717,6 +745,11 @@ commander@^2.8.1: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== +compare-version@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" + integrity sha1-AWLsLZNR9d3VmpICy6k1NmpyUIA= + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -805,7 +838,7 @@ debug-fabulous@0.0.X: lazy-debug-legacy "0.0.X" object-assign "4.1.0" -debug@2.X: +debug@2.X, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -918,6 +951,18 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" +electron-osx-sign@^0.4.16: + version "0.4.16" + resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.16.tgz#0be8e579b2d9fa4c12d2a21f063898294b3434aa" + integrity sha512-ziMWfc3NmQlwnWLW6EaZq8nH2BWVng/atX5GWsGwhexJYpdW6hsg//MkAfRTRx1kR3Veiqkeiog1ibkbA4x0rg== + dependencies: + bluebird "^3.5.0" + compare-version "^0.1.2" + debug "^2.6.8" + isbinaryfile "^3.0.2" + minimist "^1.2.0" + plist "^3.0.1" + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1488,6 +1533,13 @@ isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= +isbinaryfile@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80" + integrity sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw== + dependencies: + buffer-alloc "^1.2.0" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -1969,6 +2021,15 @@ picomatch@^2.0.5: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== +plist@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.1.tgz#a9b931d17c304e8912ef0ba3bdd6182baf2e1f8c" + integrity sha512-GpgvHHocGRyQm74b6FWEZZVRroHKE1I0/BTjAmySaohK+cUn+hZpbqXkc3KWgW3gQYkqcQej35FohcT0FRlkRQ== + dependencies: + base64-js "^1.2.3" + xmlbuilder "^9.0.7" + xmldom "0.1.x" + prettyjson@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prettyjson/-/prettyjson-1.2.1.tgz#fcffab41d19cab4dfae5e575e64246619b12d289" @@ -2458,10 +2519,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@^3.9.1-rc: - version "3.9.1-rc" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.1-rc.tgz#81d5a5a0a597e224b6e2af8dffb46524b2eaf5f3" - integrity sha512-+cPv8L2Vd4KidCotqi2wjegBZ5n47CDRUu/QiLVu2YbeXAz78hIfcai9ziBiNI6JTGTVwUqXRug2UZxDcxhvFw== +typescript@^3.9.3: + version "3.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" + integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== typical@^4.0.0: version "4.0.0" @@ -2661,11 +2722,21 @@ xmlbuilder@0.4.3: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-0.4.3.tgz#c4614ba74e0ad196e609c9272cd9e1ddb28a8a58" integrity sha1-xGFLp04K0ZbmCcknLNnh3bKKilg= +xmlbuilder@^9.0.7: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + xmlbuilder@~9.0.1: version "9.0.4" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" integrity sha1-UZy0ymhtAFqEINNJbz8MruzKWA8= +xmldom@0.1.x: + version "0.1.31" + resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" + integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== + xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" diff --git a/cgmanifest.json b/cgmanifest.json index 09478ca403e..f1ce13c7205 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "0552e0d5de46ffa3b481d741f1db5c779e201565" + "commitHash": "8f502de1dc5b6df4218a900d0857de7a40301d98" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "7.2.4" + "version": "7.3.0" }, { "component": { diff --git a/extensions/bat/test/colorize-results/test_bat.json b/extensions/bat/test/colorize-results/test_bat.json index 97155fafc8b..eb76b0e1d04 100644 --- a/extensions/bat/test/colorize-results/test_bat.json +++ b/extensions/bat/test/colorize-results/test_bat.json @@ -488,7 +488,7 @@ "t": "source.batchfile constant.character.escape.batchfile", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", "hc_black": "constant.character: #569CD6" @@ -510,7 +510,7 @@ "t": "source.batchfile constant.character.escape.batchfile", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "default: #D4D4D4", "light_vs": "default: #000000", "hc_black": "constant.character: #569CD6" diff --git a/extensions/coffeescript/test/colorize-results/test_coffee.json b/extensions/coffeescript/test/colorize-results/test_coffee.json index 7c72176431d..9647307f073 100644 --- a/extensions/coffeescript/test/colorize-results/test_coffee.json +++ b/extensions/coffeescript/test/colorize-results/test_coffee.json @@ -1555,7 +1555,7 @@ "t": "source.coffee string.regexp.multiline.coffee keyword.control.anchor.regexp", "r": { "dark_plus": "keyword.control.anchor.regexp: #DCDCAA", - "light_plus": "keyword.control.anchor.regexp: #FF0000", + "light_plus": "keyword.control.anchor.regexp: #EE0000", "dark_vs": "keyword.control: #569CD6", "light_vs": "keyword.control: #0000FF", "hc_black": "keyword.control: #C586C0" @@ -1605,4 +1605,4 @@ "hc_black": "string.regexp: #D16969" } } -] +] \ No newline at end of file diff --git a/extensions/cpp/test/colorize-results/test-78769_cpp.json b/extensions/cpp/test/colorize-results/test-78769_cpp.json index eb114a4f007..82846880603 100644 --- a/extensions/cpp/test/colorize-results/test-78769_cpp.json +++ b/extensions/cpp/test/colorize-results/test-78769_cpp.json @@ -191,7 +191,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -257,7 +257,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -389,7 +389,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp meta.block.struct.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -433,7 +433,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp meta.block.struct.cpp meta.body.struct.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -532,7 +532,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp meta.block.struct.cpp meta.body.struct.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -587,7 +587,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -719,7 +719,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -763,7 +763,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -862,7 +862,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp meta.function.definition.cpp meta.body.function.definition.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -906,7 +906,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -1027,7 +1027,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp meta.block.namespace.cpp meta.body.namespace.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -1071,7 +1071,7 @@ "t": "source.cpp meta.preprocessor.macro.cpp constant.character.escape.line-continuation.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "meta.preprocessor: #569CD6", "light_vs": "meta.preprocessor: #0000FF", "hc_black": "constant.character: #569CD6" @@ -1187,4 +1187,4 @@ "hc_black": "meta.preprocessor: #569CD6" } } -] +] \ No newline at end of file diff --git a/extensions/cpp/test/colorize-results/test_cpp.json b/extensions/cpp/test/colorize-results/test_cpp.json index 108895f8f65..10f6041bb00 100644 --- a/extensions/cpp/test/colorize-results/test_cpp.json +++ b/extensions/cpp/test/colorize-results/test_cpp.json @@ -2314,7 +2314,7 @@ "t": "source.cpp meta.function.definition.cpp meta.body.function.definition.cpp string.quoted.double.cpp constant.character.escape.cpp", "r": { "dark_plus": "constant.character.escape: #D7BA7D", - "light_plus": "constant.character.escape: #FF0000", + "light_plus": "constant.character.escape: #EE0000", "dark_vs": "string: #CE9178", "light_vs": "string: #A31515", "hc_black": "constant.character: #569CD6" @@ -2430,4 +2430,4 @@ "hc_black": "default: #FFFFFF" } } -] +] \ No newline at end of file diff --git a/extensions/css-language-features/client/src/browser/cssClientMain.ts b/extensions/css-language-features/client/src/browser/cssClientMain.ts new file mode 100644 index 00000000000..8b1d7205fcd --- /dev/null +++ b/extensions/css-language-features/client/src/browser/cssClientMain.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ExtensionContext } from 'vscode'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { startClient, LanguageClientConstructor } from '../cssClient'; +import { LanguageClient } from 'vscode-languageclient/browser'; + +declare const Worker: { + new(stringUrl: string): any; +}; +declare const TextDecoder: { + new(encoding?: string): { decode(buffer: ArrayBuffer): string; }; +}; + +// this method is called when vs code is activated +export function activate(context: ExtensionContext) { + const serverMain = context.asAbsolutePath('server/dist/browser/cssServerMain.js'); + try { + const worker = new Worker(serverMain); + const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { + return new LanguageClient(id, name, clientOptions, worker); + }; + + startClient(context, newLanguageClient, { TextDecoder }); + + } catch (e) { + console.log(e); + } +} diff --git a/extensions/css-language-features/client/src/browser/vscodeNlsShim.ts b/extensions/css-language-features/client/src/browser/vscodeNlsShim.ts new file mode 100644 index 00000000000..b4943fe0ee1 --- /dev/null +++ b/extensions/css-language-features/client/src/browser/vscodeNlsShim.ts @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface Options { + locale?: string; + cacheLanguageResolution?: boolean; +} +export interface LocalizeInfo { + key: string; + comment: string[]; +} +export interface LocalizeFunc { + (info: LocalizeInfo, message: string, ...args: any[]): string; + (key: string, message: string, ...args: any[]): string; +} +export interface LoadFunc { + (file?: string): LocalizeFunc; +} + +function format(message: string, args: any[]): string { + let result: string; + + if (args.length === 0) { + result = message; + } else { + result = message.replace(/\{(\d+)\}/g, (match, rest) => { + let index = rest[0]; + return typeof args[index] !== 'undefined' ? args[index] : match; + }); + } + return result; +} + +function localize(_key: string | LocalizeInfo, message: string, ...args: any[]): string { + return format(message, args); +} + +export function loadMessageBundle(_file?: string): LocalizeFunc { + return localize; +} + +export function config(_opt?: Options | string): LoadFunc { + return loadMessageBundle; +} diff --git a/extensions/css-language-features/client/src/cssMain.ts b/extensions/css-language-features/client/src/cssClient.ts similarity index 76% rename from extensions/css-language-features/client/src/cssMain.ts rename to extensions/css-language-features/client/src/cssClient.ts index beabfc68cad..4bfa95c8439 100644 --- a/extensions/css-language-features/client/src/cssMain.ts +++ b/extensions/css-language-features/client/src/cssClient.ts @@ -3,38 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as fs from 'fs'; -import * as path from 'path'; -import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, workspace, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList } from 'vscode'; -import { Disposable, LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, ProvideCompletionItemsSignature } from 'vscode-languageclient'; +import { commands, CompletionItem, CompletionItemKind, ExtensionContext, languages, Position, Range, SnippetString, TextEdit, window, TextDocument, CompletionContext, CancellationToken, ProviderResult, CompletionList } from 'vscode'; +import { Disposable, LanguageClientOptions, ProvideCompletionItemsSignature, NotificationType, CommonLanguageClient } from 'vscode-languageclient'; import * as nls from 'vscode-nls'; -import { getCustomDataPathsFromAllExtensions, getCustomDataPathsInAllWorkspaces } from './customData'; +import { getCustomDataSource } from './customData'; +import { RequestService, serveFileSystemRequests } from './requests'; + +namespace CustomDataChangedNotification { + export const type: NotificationType = new NotificationType('css/customDataChanged'); +} const localize = nls.loadMessageBundle(); -// this method is called when vs code is activated -export function activate(context: ExtensionContext) { +export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => CommonLanguageClient; - let serverMain = readJSONFile(context.asAbsolutePath('./server/package.json')).main; - let serverModule = context.asAbsolutePath(path.join('server', serverMain)); +export interface Runtime { + TextDecoder: { new(encoding?: string): { decode(buffer: ArrayBuffer): string; } }; + fs?: RequestService; +} - // The debug options for the server - let debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] }; +export function startClient(context: ExtensionContext, newLanguageClient: LanguageClientConstructor, runtime: Runtime) { - // If the extension is launch in debug mode the debug server options are use - // Otherwise the run options are used - let serverOptions: ServerOptions = { - run: { module: serverModule, transport: TransportKind.ipc }, - debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } - }; + const customDataSource = getCustomDataSource(context.subscriptions); let documentSelector = ['css', 'scss', 'less']; - let dataPaths = [ - ...getCustomDataPathsInAllWorkspaces(workspace.workspaceFolders), - ...getCustomDataPathsFromAllExtensions() - ]; - // Options to control the language client let clientOptions: LanguageClientOptions = { documentSelector, @@ -42,7 +35,7 @@ export function activate(context: ExtensionContext) { configurationSection: ['css', 'scss', 'less'] }, initializationOptions: { - dataPaths + handledSchemas: ['file'] }, middleware: { provideCompletionItem(document: TextDocument, position: Position, context: CompletionContext, token: CancellationToken, next: ProvideCompletionItemsSignature): ProviderResult { @@ -82,8 +75,17 @@ export function activate(context: ExtensionContext) { }; // Create the language client and start the client. - let client = new LanguageClient('css', localize('cssserver.name', 'CSS Language Server'), serverOptions, clientOptions); + let client = newLanguageClient('css', localize('cssserver.name', 'CSS Language Server'), clientOptions); client.registerProposedFeatures(); + client.onReady().then(() => { + + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); + customDataSource.onDidChange(() => { + client.sendNotification(CustomDataChangedNotification.type, customDataSource.uris); + }); + + serveFileSystemRequests(client, runtime); + }); let disposable = client.start(); // Push the disposable to the context's subscriptions so that the @@ -118,7 +120,7 @@ export function activate(context: ExtensionContext) { const regionCompletionRegExpr = /^(\s*)(\/(\*\s*(#\w*)?)?)?$/; return languages.registerCompletionItemProvider(documentSelector, { - provideCompletionItems(doc, pos) { + provideCompletionItems(doc: TextDocument, pos: Position) { let lineUntilPos = doc.getText(new Range(new Position(pos.line, 0), pos)); let match = lineUntilPos.match(regionCompletionRegExpr); if (match) { @@ -162,13 +164,3 @@ export function activate(context: ExtensionContext) { } } } - -function readJSONFile(location: string) { - try { - return JSON.parse(fs.readFileSync(location).toString()); - } catch (e) { - console.log(`Problems reading ${location}: ${e}`); - return {}; - } -} - diff --git a/extensions/css-language-features/client/src/customData.ts b/extensions/css-language-features/client/src/customData.ts index 92d9030a343..50540528d63 100644 --- a/extensions/css-language-features/client/src/customData.ts +++ b/extensions/css-language-features/client/src/customData.ts @@ -3,42 +3,78 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; -import { workspace, WorkspaceFolder, extensions } from 'vscode'; +import { workspace, extensions, Uri, EventEmitter, Disposable } from 'vscode'; +import { resolvePath, joinPath } from './requests'; -interface ExperimentalConfig { - customData?: string[]; - experimental?: { - customData?: string[]; +export function getCustomDataSource(toDispose: Disposable[]) { + let pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); + let pathsInExtensions = getCustomDataPathsFromAllExtensions(); + + const onChange = new EventEmitter(); + + toDispose.push(extensions.onDidChange(_ => { + const newPathsInExtensions = getCustomDataPathsFromAllExtensions(); + if (newPathsInExtensions.length !== pathsInExtensions.length || !newPathsInExtensions.every((val, idx) => val === pathsInExtensions[idx])) { + pathsInExtensions = newPathsInExtensions; + onChange.fire(); + } + })); + toDispose.push(workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('css.customData')) { + pathsInWorkspace = getCustomDataPathsInAllWorkspaces(); + onChange.fire(); + } + })); + + return { + get uris() { + return pathsInWorkspace.concat(pathsInExtensions); + }, + get onDidChange() { + return onChange.event; + } }; } -export function getCustomDataPathsInAllWorkspaces(workspaceFolders: readonly WorkspaceFolder[] | undefined): string[] { + +function getCustomDataPathsInAllWorkspaces(): string[] { + const workspaceFolders = workspace.workspaceFolders; + const dataPaths: string[] = []; if (!workspaceFolders) { return dataPaths; } - workspaceFolders.forEach(wf => { - const allCssConfig = workspace.getConfiguration(undefined, wf.uri); - const wfCSSConfig = allCssConfig.inspect('css'); - if (wfCSSConfig && wfCSSConfig.workspaceFolderValue && wfCSSConfig.workspaceFolderValue.customData) { - const customData = wfCSSConfig.workspaceFolderValue.customData; - if (Array.isArray(customData)) { - customData.forEach(t => { - if (typeof t === 'string') { - dataPaths.push(path.resolve(wf.uri.fsPath, t)); - } - }); + const collect = (paths: string[] | undefined, rootFolder: Uri) => { + if (Array.isArray(paths)) { + for (const path of paths) { + if (typeof path === 'string') { + dataPaths.push(resolvePath(rootFolder, path).toString()); + } } } - }); + }; + for (let i = 0; i < workspaceFolders.length; i++) { + const folderUri = workspaceFolders[i].uri; + const allCssConfig = workspace.getConfiguration('css', folderUri); + const customDataInspect = allCssConfig.inspect('customData'); + if (customDataInspect) { + collect(customDataInspect.workspaceFolderValue, folderUri); + if (i === 0) { + if (workspace.workspaceFile) { + collect(customDataInspect.workspaceValue, workspace.workspaceFile); + } + collect(customDataInspect.globalValue, folderUri); + } + } + + } return dataPaths; } -export function getCustomDataPathsFromAllExtensions(): string[] { +function getCustomDataPathsFromAllExtensions(): string[] { const dataPaths: string[] = []; for (const extension of extensions.all) { @@ -47,7 +83,7 @@ export function getCustomDataPathsFromAllExtensions(): string[] { if (contributes && contributes.css && contributes.css.customData && Array.isArray(contributes.css.customData)) { const relativePaths: string[] = contributes.css.customData; relativePaths.forEach(rp => { - dataPaths.push(path.resolve(extension.extensionPath, rp)); + dataPaths.push(joinPath(extension.extensionUri, rp).toString()); }); } } diff --git a/extensions/css-language-features/client/src/node/cssClientMain.ts b/extensions/css-language-features/client/src/node/cssClientMain.ts new file mode 100644 index 00000000000..350e0de02d5 --- /dev/null +++ b/extensions/css-language-features/client/src/node/cssClientMain.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getNodeFSRequestService } from './nodeFs'; +import { ExtensionContext, extensions } from 'vscode'; +import { startClient, LanguageClientConstructor } from '../cssClient'; +import { ServerOptions, TransportKind, LanguageClientOptions, LanguageClient } from 'vscode-languageclient/node'; +import { TextDecoder } from 'util'; + +// this method is called when vs code is activated +export function activate(context: ExtensionContext) { + + const clientMain = extensions.getExtension('vscode.css-language-features')?.packageJSON?.main; + const serverMain = clientMain?.replace('client', 'server').replace('cssClientMain', 'cssServerMain'); + if (!serverMain) { + throw new Error('Unable to compute CSS server module path. Client: ' + clientMain); + } + + const serverModule = context.asAbsolutePath(serverMain); + + // The debug options for the server + const debugOptions = { execArgv: ['--nolazy', '--inspect=6044'] }; + + // If the extension is launch in debug mode the debug server options are use + // Otherwise the run options are used + const serverOptions: ServerOptions = { + run: { module: serverModule, transport: TransportKind.ipc }, + debug: { module: serverModule, transport: TransportKind.ipc, options: debugOptions } + }; + + const newLanguageClient: LanguageClientConstructor = (id: string, name: string, clientOptions: LanguageClientOptions) => { + return new LanguageClient(id, name, serverOptions, clientOptions); + }; + + startClient(context, newLanguageClient, { fs: getNodeFSRequestService(), TextDecoder }); +} diff --git a/extensions/css-language-features/client/src/node/nodeFs.ts b/extensions/css-language-features/client/src/node/nodeFs.ts new file mode 100644 index 00000000000..c13ef2e1c08 --- /dev/null +++ b/extensions/css-language-features/client/src/node/nodeFs.ts @@ -0,0 +1,85 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import { Uri } from 'vscode'; +import { getScheme, RequestService, FileType } from '../requests'; + +export function getNodeFSRequestService(): RequestService { + function ensureFileUri(location: string) { + if (getScheme(location) !== 'file') { + throw new Error('fileRequestService can only handle file URLs'); + } + } + return { + getContent(location: string, encoding?: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const uri = Uri.parse(location); + fs.readFile(uri.fsPath, encoding, (err, buf) => { + if (err) { + return e(err); + } + c(buf.toString()); + + }); + }); + }, + stat(location: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const uri = Uri.parse(location); + fs.stat(uri.fsPath, (err, stats) => { + if (err) { + if (err.code === 'ENOENT') { + return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 }); + } else { + return e(err); + } + } + + let type = FileType.Unknown; + if (stats.isFile()) { + type = FileType.File; + } else if (stats.isDirectory()) { + type = FileType.Directory; + } else if (stats.isSymbolicLink()) { + type = FileType.SymbolicLink; + } + + c({ + type, + ctime: stats.ctime.getTime(), + mtime: stats.mtime.getTime(), + size: stats.size + }); + }); + }); + }, + readDirectory(location: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const path = Uri.parse(location).fsPath; + + fs.readdir(path, { withFileTypes: true }, (err, children) => { + if (err) { + return e(err); + } + c(children.map(stat => { + if (stat.isSymbolicLink()) { + return [stat.name, FileType.SymbolicLink]; + } else if (stat.isDirectory()) { + return [stat.name, FileType.Directory]; + } else if (stat.isFile()) { + return [stat.name, FileType.File]; + } else { + return [stat.name, FileType.Unknown]; + } + })); + }); + }); + } + }; +} diff --git a/extensions/css-language-features/client/src/requests.ts b/extensions/css-language-features/client/src/requests.ts new file mode 100644 index 00000000000..1b1e70b2d88 --- /dev/null +++ b/extensions/css-language-features/client/src/requests.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri, workspace } from 'vscode'; +import { RequestType, CommonLanguageClient } from 'vscode-languageclient'; +import { Runtime } from './cssClient'; + +export namespace FsContentRequest { + export const type: RequestType<{ uri: string; encoding?: string; }, string, any, any> = new RequestType('fs/content'); +} +export namespace FsStatRequest { + export const type: RequestType = new RequestType('fs/stat'); +} + +export namespace FsReadDirRequest { + export const type: RequestType = new RequestType('fs/readDir'); +} + +export function serveFileSystemRequests(client: CommonLanguageClient, runtime: Runtime) { + client.onRequest(FsContentRequest.type, (param: { uri: string; encoding?: string; }) => { + const uri = Uri.parse(param.uri); + if (uri.scheme === 'file' && runtime.fs) { + return runtime.fs.getContent(param.uri); + } + return workspace.fs.readFile(uri).then(buffer => { + return new runtime.TextDecoder(param.encoding).decode(buffer); + }); + }); + client.onRequest(FsReadDirRequest.type, (uriString: string) => { + const uri = Uri.parse(uriString); + if (uri.scheme === 'file' && runtime.fs) { + return runtime.fs.readDirectory(uriString); + } + return workspace.fs.readDirectory(uri); + }); + client.onRequest(FsStatRequest.type, (uriString: string) => { + const uri = Uri.parse(uriString); + if (uri.scheme === 'file' && runtime.fs) { + return runtime.fs.stat(uriString); + } + return workspace.fs.stat(uri); + }); +} + +export enum FileType { + /** + * The file type is unknown. + */ + Unknown = 0, + /** + * A regular file. + */ + File = 1, + /** + * A directory. + */ + Directory = 2, + /** + * A symbolic link to a file. + */ + SymbolicLink = 64 +} +export interface FileStat { + /** + * The type of the file, e.g. is a regular file, a directory, or symbolic link + * to a file. + */ + type: FileType; + /** + * The creation timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + ctime: number; + /** + * The modification timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + mtime: number; + /** + * The size in bytes. + */ + size: number; +} + +export interface RequestService { + getContent(uri: string, encoding?: string): Promise; + + stat(uri: string): Promise; + readDirectory(uri: string): Promise<[string, FileType][]>; +} + +export function getScheme(uri: string) { + return uri.substr(0, uri.indexOf(':')); +} + +export function dirname(uri: string) { + const lastIndexOfSlash = uri.lastIndexOf('/'); + return lastIndexOfSlash !== -1 ? uri.substr(0, lastIndexOfSlash) : ''; +} + +export function basename(uri: string) { + const lastIndexOfSlash = uri.lastIndexOf('/'); + return uri.substr(lastIndexOfSlash + 1); +} + +const Slash = '/'.charCodeAt(0); +const Dot = '.'.charCodeAt(0); + +export function isAbsolutePath(path: string) { + return path.charCodeAt(0) === Slash; +} + +export function resolvePath(uri: Uri, path: string): Uri { + if (isAbsolutePath(path)) { + return uri.with({ path: normalizePath(path.split('/')) }); + } + return joinPath(uri, path); +} + +export function normalizePath(parts: string[]): string { + const newParts: string[] = []; + for (const part of parts) { + if (part.length === 0 || part.length === 1 && part.charCodeAt(0) === Dot) { + // ignore + } else if (part.length === 2 && part.charCodeAt(0) === Dot && part.charCodeAt(1) === Dot) { + newParts.pop(); + } else { + newParts.push(part); + } + } + if (parts.length > 1 && parts[parts.length - 1].length === 0) { + newParts.push(''); + } + let res = newParts.join('/'); + if (parts[0].length === 0) { + res = '/' + res; + } + return res; +} + + +export function joinPath(uri: Uri, ...paths: string[]): Uri { + const parts = uri.path.split('/'); + for (let path of paths) { + parts.push(...path.split('/')); + } + return uri.with({ path: normalizePath(parts) }); +} diff --git a/extensions/css-language-features/extension-browser.webpack.config.js b/extensions/css-language-features/extension-browser.webpack.config.js new file mode 100644 index 00000000000..0cf30ec5dc0 --- /dev/null +++ b/extensions/css-language-features/extension-browser.webpack.config.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +//@ts-check + +'use strict'; + +const withDefaults = require('../shared.webpack.config'); +const path = require('path'); +const webpack = require('webpack'); + +const vscodeNlsReplacement = new webpack.NormalModuleReplacementPlugin( + /vscode\-nls\/lib\/main\.js/, + path.join(__dirname, 'client/out/browser/vscodeNlsShim.js') +); + +const clientConfig = withDefaults({ + target: 'webworker', + context: path.join(__dirname, 'client'), + entry: { + extension: './src/browser/cssClientMain.ts' + }, + output: { + filename: 'cssClientMain.js', + path: path.join(__dirname, 'client', 'dist', 'browser') + } +}); +clientConfig.plugins[1] = vscodeNlsReplacement; // replace nls bundler +clientConfig.module.rules[0].use.shift(); // remove nls loader + +const serverConfig = withDefaults({ + target: 'webworker', + context: path.join(__dirname, 'server'), + entry: { + extension: './src/browser/cssServerMain.ts', + }, + output: { + filename: 'cssServerMain.js', + path: path.join(__dirname, 'server', 'dist', 'browser'), + libraryTarget: 'var' + } +}); +serverConfig.plugins[1] = vscodeNlsReplacement; // replace nls bundler +serverConfig.module.rules[0].use.shift(); // remove nls loader + +module.exports = [clientConfig, serverConfig]; diff --git a/extensions/css-language-features/extension.webpack.config.js b/extensions/css-language-features/extension.webpack.config.js index dec7ad5afb4..a931210ab32 100644 --- a/extensions/css-language-features/extension.webpack.config.js +++ b/extensions/css-language-features/extension.webpack.config.js @@ -13,10 +13,10 @@ const path = require('path'); module.exports = withDefaults({ context: path.join(__dirname, 'client'), entry: { - extension: './src/cssMain.ts', + extension: './src/node/cssClientMain.ts', }, output: { - filename: 'cssMain.js', - path: path.join(__dirname, 'client', 'dist') + filename: 'cssClientMain.js', + path: path.join(__dirname, 'client', 'dist', 'node') } }); diff --git a/extensions/css-language-features/package.json b/extensions/css-language-features/package.json index 4c737601679..4d44ee6d7d5 100644 --- a/extensions/css-language-features/package.json +++ b/extensions/css-language-features/package.json @@ -15,7 +15,8 @@ "onLanguage:scss", "onCommand:_css.applyCodeAction" ], - "main": "./client/out/cssMain", + "main": "./client/out/node/cssClientMain", + "browser": "./client/dist/browser/cssClientMain", "enableProposedApi": true, "scripts": { "compile": "gulp compile-extension:css-language-features-client compile-extension:css-language-features-server", @@ -806,7 +807,7 @@ ] }, "dependencies": { - "vscode-languageclient": "^6.1.3", + "vscode-languageclient": "7.0.0-next.4", "vscode-nls": "^4.1.2" }, "devDependencies": { diff --git a/extensions/css-language-features/server/extension.webpack.config.js b/extensions/css-language-features/server/extension.webpack.config.js index 68b850b3773..531035f636c 100644 --- a/extensions/css-language-features/server/extension.webpack.config.js +++ b/extensions/css-language-features/server/extension.webpack.config.js @@ -13,10 +13,10 @@ const path = require('path'); module.exports = withDefaults({ context: path.join(__dirname), entry: { - extension: './src/cssServerMain.ts', + extension: './src/node/cssServerMain.ts', }, output: { filename: 'cssServerMain.js', - path: path.join(__dirname, 'dist') + path: path.join(__dirname, 'dist', 'node'), } }); diff --git a/extensions/css-language-features/server/package.json b/extensions/css-language-features/server/package.json index c142ff1e620..247b276dace 100644 --- a/extensions/css-language-features/server/package.json +++ b/extensions/css-language-features/server/package.json @@ -7,10 +7,12 @@ "engines": { "node": "*" }, - "main": "./out/cssServerMain", + "main": "./out/node/cssServerMain", + "browser": "./dist/browser/cssServerMain", "dependencies": { - "vscode-css-languageservice": "^4.1.2", - "vscode-languageserver": "^6.1.1" + "vscode-css-languageservice": "4.3.0-next.2", + "vscode-languageserver": "7.0.0-next.3", + "vscode-uri": "^2.1.2" }, "devDependencies": { "@types/mocha": "7.0.2", @@ -27,6 +29,6 @@ "install-service-local": "npm install ../../../../vscode-css-languageservice -f", "install-server-next": "yarn add vscode-languageserver@next", "install-server-local": "npm install ../../../../vscode-languageserver-node/server -f", - "test": "../../../node_modules/.bin/mocha" + "test": "node ./test/index.js" } } diff --git a/extensions/css-language-features/server/src/browser/cssServerMain.ts b/extensions/css-language-features/server/src/browser/cssServerMain.ts new file mode 100644 index 00000000000..13284fadcd9 --- /dev/null +++ b/extensions/css-language-features/server/src/browser/cssServerMain.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createConnection, BrowserMessageReader, BrowserMessageWriter } from 'vscode-languageserver/browser'; +import { startServer } from '../cssServer'; + +declare let self: any; + +const messageReader = new BrowserMessageReader(self); +const messageWriter = new BrowserMessageWriter(self); + +const connection = createConnection(messageReader, messageWriter); + +startServer(connection, {}); diff --git a/extensions/css-language-features/server/src/cssServer.ts b/extensions/css-language-features/server/src/cssServer.ts new file mode 100644 index 00000000000..42a88c9a5dc --- /dev/null +++ b/extensions/css-language-features/server/src/cssServer.ts @@ -0,0 +1,373 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { + Connection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind, NotificationType +} from 'vscode-languageserver/lib/common/api'; +import { URI } from 'vscode-uri'; +import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, TextDocument, Position } from 'vscode-css-languageservice'; +import { getLanguageModelCache } from './languageModelCache'; +import { formatError, runSafeAsync } from './utils/runner'; +import { getDocumentContext } from './utils/documentContext'; +import { fetchDataProviders } from './customData'; +import { RequestService, getRequestService } from './requests'; + +namespace CustomDataChangedNotification { + export const type: NotificationType = new NotificationType('css/customDataChanged'); +} + +export interface Settings { + css: LanguageSettings; + less: LanguageSettings; + scss: LanguageSettings; +} + +export interface RuntimeEnvironment { + file?: RequestService; + http?: RequestService +} + +export function startServer(connection: Connection, runtime: RuntimeEnvironment) { + + // Create a text document manager. + const documents = new TextDocuments(TextDocument); + // Make the text document manager listen on the connection + // for open, change and close text document events + documents.listen(connection); + + const stylesheets = getLanguageModelCache(10, 60, document => getLanguageService(document).parseStylesheet(document)); + documents.onDidClose(e => { + stylesheets.onDocumentRemoved(e.document); + }); + connection.onShutdown(() => { + stylesheets.dispose(); + }); + + let scopedSettingsSupport = false; + let foldingRangeLimit = Number.MAX_VALUE; + let workspaceFolders: WorkspaceFolder[]; + + let dataProvidersReady: Promise = Promise.resolve(); + + const languageServices: { [id: string]: LanguageService } = {}; + + const notReady = () => Promise.reject('Not Ready'); + let requestService: RequestService = { getContent: notReady, stat: notReady, readDirectory: notReady }; + + // After the server has started the client sends an initialize request. The server receives + // in the passed params the rootPath of the workspace plus the client capabilities. + connection.onInitialize((params: InitializeParams): InitializeResult => { + workspaceFolders = (params).workspaceFolders; + if (!Array.isArray(workspaceFolders)) { + workspaceFolders = []; + if (params.rootPath) { + workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() }); + } + } + + requestService = getRequestService(params.initializationOptions.handledSchemas || ['file'], connection, runtime); + + function getClientCapability(name: string, def: T) { + const keys = name.split('.'); + let c: any = params.capabilities; + for (let i = 0; c && i < keys.length; i++) { + if (!c.hasOwnProperty(keys[i])) { + return def; + } + c = c[keys[i]]; + } + return c; + } + const snippetSupport = !!getClientCapability('textDocument.completion.completionItem.snippetSupport', false); + scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); + foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); + + languageServices.css = getCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); + languageServices.scss = getSCSSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); + languageServices.less = getLESSLanguageService({ fileSystemProvider: requestService, clientCapabilities: params.capabilities }); + + const capabilities: ServerCapabilities = { + textDocumentSync: TextDocumentSyncKind.Incremental, + completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: ['/', '-'] } : undefined, + hoverProvider: true, + documentSymbolProvider: true, + referencesProvider: true, + definitionProvider: true, + documentHighlightProvider: true, + documentLinkProvider: { + resolveProvider: false + }, + codeActionProvider: true, + renameProvider: true, + colorProvider: {}, + foldingRangeProvider: true, + selectionRangeProvider: true + }; + return { capabilities }; + }); + + function getLanguageService(document: TextDocument) { + let service = languageServices[document.languageId]; + if (!service) { + connection.console.log('Document type is ' + document.languageId + ', using css instead.'); + service = languageServices['css']; + } + return service; + } + + let documentSettings: { [key: string]: Thenable } = {}; + // remove document settings on close + documents.onDidClose(e => { + delete documentSettings[e.document.uri]; + }); + function getDocumentSettings(textDocument: TextDocument): Thenable { + if (scopedSettingsSupport) { + let promise = documentSettings[textDocument.uri]; + if (!promise) { + const configRequestParam = { items: [{ scopeUri: textDocument.uri, section: textDocument.languageId }] }; + promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => s[0]); + documentSettings[textDocument.uri] = promise; + } + return promise; + } + return Promise.resolve(undefined); + } + + // The settings have changed. Is send on server activation as well. + connection.onDidChangeConfiguration(change => { + updateConfiguration(change.settings); + }); + + function updateConfiguration(settings: Settings) { + for (const languageId in languageServices) { + languageServices[languageId].configure((settings as any)[languageId]); + } + // reset all document settings + documentSettings = {}; + // Revalidate any open text documents + documents.all().forEach(triggerValidation); + } + + const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; + const validationDelayMs = 500; + + // The content of a text document has changed. This event is emitted + // when the text document first opened or when its content has changed. + documents.onDidChangeContent(change => { + triggerValidation(change.document); + }); + + // a document has closed: clear all diagnostics + documents.onDidClose(event => { + cleanPendingValidation(event.document); + connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); + }); + + function cleanPendingValidation(textDocument: TextDocument): void { + const request = pendingValidationRequests[textDocument.uri]; + if (request) { + clearTimeout(request); + delete pendingValidationRequests[textDocument.uri]; + } + } + + function triggerValidation(textDocument: TextDocument): void { + cleanPendingValidation(textDocument); + pendingValidationRequests[textDocument.uri] = setTimeout(() => { + delete pendingValidationRequests[textDocument.uri]; + validateTextDocument(textDocument); + }, validationDelayMs); + } + + function validateTextDocument(textDocument: TextDocument): void { + const settingsPromise = getDocumentSettings(textDocument); + Promise.all([settingsPromise, dataProvidersReady]).then(async ([settings]) => { + const stylesheet = stylesheets.get(textDocument); + const diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet, settings); + // Send the computed diagnostics to VSCode. + connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); + }, e => { + connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); + }); + } + + + function updateDataProviders(dataPaths: string[]) { + dataProvidersReady = fetchDataProviders(dataPaths, requestService).then(customDataProviders => { + for (const lang in languageServices) { + languageServices[lang].setDataProviders(true, customDataProviders); + } + }); + } + + connection.onCompletion((textDocumentPosition, token) => { + return runSafeAsync(async () => { + const document = documents.get(textDocumentPosition.textDocument.uri); + if (document) { + await dataProvidersReady; + const styleSheet = stylesheets.get(document); + const documentContext = getDocumentContext(document.uri, workspaceFolders); + return getLanguageService(document).doComplete2(document, textDocumentPosition.position, styleSheet, documentContext); + } + return null; + }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token); + }); + + connection.onHover((textDocumentPosition, token) => { + return runSafeAsync(async () => { + const document = documents.get(textDocumentPosition.textDocument.uri); + if (document) { + await dataProvidersReady; + const styleSheet = stylesheets.get(document); + return getLanguageService(document).doHover(document, textDocumentPosition.position, styleSheet); + } + return null; + }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token); + }); + + connection.onDocumentSymbol((documentSymbolParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(documentSymbolParams.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findDocumentSymbols(document, stylesheet); + } + return []; + }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); + }); + + connection.onDefinition((documentDefinitionParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(documentDefinitionParams.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findDefinition(document, documentDefinitionParams.position, stylesheet); + } + return null; + }, null, `Error while computing definitions for ${documentDefinitionParams.textDocument.uri}`, token); + }); + + connection.onDocumentHighlight((documentHighlightParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(documentHighlightParams.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findDocumentHighlights(document, documentHighlightParams.position, stylesheet); + } + return []; + }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token); + }); + + + connection.onDocumentLinks(async (documentLinkParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(documentLinkParams.textDocument.uri); + if (document) { + await dataProvidersReady; + const documentContext = getDocumentContext(document.uri, workspaceFolders); + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findDocumentLinks2(document, stylesheet, documentContext); + } + return []; + }, [], `Error while computing document links for ${documentLinkParams.textDocument.uri}`, token); + }); + + + connection.onReferences((referenceParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(referenceParams.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findReferences(document, referenceParams.position, stylesheet); + } + return []; + }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token); + }); + + connection.onCodeAction((codeActionParams, token) => { + return runSafeAsync(async () => { + const document = documents.get(codeActionParams.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).doCodeActions(document, codeActionParams.range, codeActionParams.context, stylesheet); + } + return []; + }, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`, token); + }); + + connection.onDocumentColor((params, token) => { + return runSafeAsync(async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).findDocumentColors(document, stylesheet); + } + return []; + }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); + }); + + connection.onColorPresentation((params, token) => { + return runSafeAsync(async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).getColorPresentations(document, stylesheet, params.color, params.range); + } + return []; + }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); + }); + + connection.onRenameRequest((renameParameters, token) => { + return runSafeAsync(async () => { + const document = documents.get(renameParameters.textDocument.uri); + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).doRename(document, renameParameters.position, renameParameters.newName, stylesheet); + } + return null; + }, null, `Error while computing renames for ${renameParameters.textDocument.uri}`, token); + }); + + connection.onFoldingRanges((params, token) => { + return runSafeAsync(async () => { + const document = documents.get(params.textDocument.uri); + if (document) { + await dataProvidersReady; + return getLanguageService(document).getFoldingRanges(document, { rangeLimit: foldingRangeLimit }); + } + return null; + }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); + }); + + connection.onSelectionRanges((params, token) => { + return runSafeAsync(async () => { + const document = documents.get(params.textDocument.uri); + const positions: Position[] = params.positions; + + if (document) { + await dataProvidersReady; + const stylesheet = stylesheets.get(document); + return getLanguageService(document).getSelectionRanges(document, positions, stylesheet); + } + return []; + }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); + }); + + connection.onNotification(CustomDataChangedNotification.type, updateDataProviders); + + // Listen on the connection + connection.listen(); + +} + + diff --git a/extensions/css-language-features/server/src/cssServerMain.ts b/extensions/css-language-features/server/src/cssServerMain.ts deleted file mode 100644 index 6a9db0b77e7..00000000000 --- a/extensions/css-language-features/server/src/cssServerMain.ts +++ /dev/null @@ -1,390 +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 { - createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, ServerCapabilities, ConfigurationRequest, WorkspaceFolder, TextDocumentSyncKind -} from 'vscode-languageserver'; -import { URI } from 'vscode-uri'; -import { stat as fsStat } from 'fs'; -import { getCSSLanguageService, getSCSSLanguageService, getLESSLanguageService, LanguageSettings, LanguageService, Stylesheet, FileSystemProvider, FileType, TextDocument, CompletionList, Position } from 'vscode-css-languageservice'; -import { getLanguageModelCache } from './languageModelCache'; -import { getPathCompletionParticipant } from './pathCompletion'; -import { formatError, runSafe, runSafeAsync } from './utils/runner'; -import { getDocumentContext } from './utils/documentContext'; -import { getDataProviders } from './customData'; - -export interface Settings { - css: LanguageSettings; - less: LanguageSettings; - scss: LanguageSettings; -} - -// Create a connection for the server. -const connection: IConnection = createConnection(); - -console.log = connection.console.log.bind(connection.console); -console.error = connection.console.error.bind(connection.console); - -process.on('unhandledRejection', (e: any) => { - connection.console.error(formatError(`Unhandled exception`, e)); -}); - -// Create a text document manager. -const documents = new TextDocuments(TextDocument); -// Make the text document manager listen on the connection -// for open, change and close text document events -documents.listen(connection); - -const stylesheets = getLanguageModelCache(10, 60, document => getLanguageService(document).parseStylesheet(document)); -documents.onDidClose(e => { - stylesheets.onDocumentRemoved(e.document); -}); -connection.onShutdown(() => { - stylesheets.dispose(); -}); - -let scopedSettingsSupport = false; -let foldingRangeLimit = Number.MAX_VALUE; -let workspaceFolders: WorkspaceFolder[]; - -const languageServices: { [id: string]: LanguageService } = {}; - -const fileSystemProvider: FileSystemProvider = { - stat(documentUri: string) { - const filePath = URI.parse(documentUri).fsPath; - - return new Promise((c, e) => { - fsStat(filePath, (err, stats) => { - if (err) { - if (err.code === 'ENOENT') { - return c({ - type: FileType.Unknown, - ctime: -1, - mtime: -1, - size: -1 - }); - } else { - return e(err); - } - } - - let type = FileType.Unknown; - if (stats.isFile()) { - type = FileType.File; - } else if (stats.isDirectory()) { - type = FileType.Directory; - } else if (stats.isSymbolicLink()) { - type = FileType.SymbolicLink; - } - - c({ - type, - ctime: stats.ctime.getTime(), - mtime: stats.mtime.getTime(), - size: stats.size - }); - }); - }); - } -}; - -// After the server has started the client sends an initialize request. The server receives -// in the passed params the rootPath of the workspace plus the client capabilities. -connection.onInitialize((params: InitializeParams): InitializeResult => { - workspaceFolders = (params).workspaceFolders; - if (!Array.isArray(workspaceFolders)) { - workspaceFolders = []; - if (params.rootPath) { - workspaceFolders.push({ name: '', uri: URI.file(params.rootPath).toString() }); - } - } - - const dataPaths: string[] = params.initializationOptions.dataPaths || []; - const customDataProviders = getDataProviders(dataPaths); - - function getClientCapability(name: string, def: T) { - const keys = name.split('.'); - let c: any = params.capabilities; - for (let i = 0; c && i < keys.length; i++) { - if (!c.hasOwnProperty(keys[i])) { - return def; - } - c = c[keys[i]]; - } - return c; - } - const snippetSupport = !!getClientCapability('textDocument.completion.completionItem.snippetSupport', false); - scopedSettingsSupport = !!getClientCapability('workspace.configuration', false); - foldingRangeLimit = getClientCapability('textDocument.foldingRange.rangeLimit', Number.MAX_VALUE); - - languageServices.css = getCSSLanguageService({ customDataProviders, fileSystemProvider, clientCapabilities: params.capabilities }); - languageServices.scss = getSCSSLanguageService({ customDataProviders, fileSystemProvider, clientCapabilities: params.capabilities }); - languageServices.less = getLESSLanguageService({ customDataProviders, fileSystemProvider, clientCapabilities: params.capabilities }); - - const capabilities: ServerCapabilities = { - textDocumentSync: TextDocumentSyncKind.Incremental, - completionProvider: snippetSupport ? { resolveProvider: false, triggerCharacters: ['/', '-'] } : undefined, - hoverProvider: true, - documentSymbolProvider: true, - referencesProvider: true, - definitionProvider: true, - documentHighlightProvider: true, - documentLinkProvider: { - resolveProvider: false - }, - codeActionProvider: true, - renameProvider: true, - colorProvider: {}, - foldingRangeProvider: true, - selectionRangeProvider: true - }; - return { capabilities }; -}); - -function getLanguageService(document: TextDocument) { - let service = languageServices[document.languageId]; - if (!service) { - connection.console.log('Document type is ' + document.languageId + ', using css instead.'); - service = languageServices['css']; - } - return service; -} - -let documentSettings: { [key: string]: Thenable } = {}; -// remove document settings on close -documents.onDidClose(e => { - delete documentSettings[e.document.uri]; -}); -function getDocumentSettings(textDocument: TextDocument): Thenable { - if (scopedSettingsSupport) { - let promise = documentSettings[textDocument.uri]; - if (!promise) { - const configRequestParam = { items: [{ scopeUri: textDocument.uri, section: textDocument.languageId }] }; - promise = connection.sendRequest(ConfigurationRequest.type, configRequestParam).then(s => s[0]); - documentSettings[textDocument.uri] = promise; - } - return promise; - } - return Promise.resolve(undefined); -} - -// The settings have changed. Is send on server activation as well. -connection.onDidChangeConfiguration(change => { - updateConfiguration(change.settings); -}); - -function updateConfiguration(settings: Settings) { - for (const languageId in languageServices) { - languageServices[languageId].configure((settings as any)[languageId]); - } - // reset all document settings - documentSettings = {}; - // Revalidate any open text documents - documents.all().forEach(triggerValidation); -} - -const pendingValidationRequests: { [uri: string]: NodeJS.Timer } = {}; -const validationDelayMs = 500; - -// The content of a text document has changed. This event is emitted -// when the text document first opened or when its content has changed. -documents.onDidChangeContent(change => { - triggerValidation(change.document); -}); - -// a document has closed: clear all diagnostics -documents.onDidClose(event => { - cleanPendingValidation(event.document); - connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] }); -}); - -function cleanPendingValidation(textDocument: TextDocument): void { - const request = pendingValidationRequests[textDocument.uri]; - if (request) { - clearTimeout(request); - delete pendingValidationRequests[textDocument.uri]; - } -} - -function triggerValidation(textDocument: TextDocument): void { - cleanPendingValidation(textDocument); - pendingValidationRequests[textDocument.uri] = setTimeout(() => { - delete pendingValidationRequests[textDocument.uri]; - validateTextDocument(textDocument); - }, validationDelayMs); -} - -function validateTextDocument(textDocument: TextDocument): void { - const settingsPromise = getDocumentSettings(textDocument); - settingsPromise.then(settings => { - const stylesheet = stylesheets.get(textDocument); - const diagnostics = getLanguageService(textDocument).doValidation(textDocument, stylesheet, settings); - // Send the computed diagnostics to VSCode. - connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }); - }, e => { - connection.console.error(formatError(`Error while validating ${textDocument.uri}`, e)); - }); -} - -connection.onCompletion((textDocumentPosition, token) => { - return runSafe(() => { - const document = documents.get(textDocumentPosition.textDocument.uri); - if (!document) { - return null; - } - const cssLS = getLanguageService(document); - const pathCompletionList: CompletionList = { - isIncomplete: false, - items: [] - }; - cssLS.setCompletionParticipants([getPathCompletionParticipant(document, workspaceFolders, pathCompletionList)]); - const result = cssLS.doComplete(document, textDocumentPosition.position, stylesheets.get(document)); - return { - isIncomplete: pathCompletionList.isIncomplete, - items: [...pathCompletionList.items, ...result.items] - }; - }, null, `Error while computing completions for ${textDocumentPosition.textDocument.uri}`, token); -}); - -connection.onHover((textDocumentPosition, token) => { - return runSafe(() => { - const document = documents.get(textDocumentPosition.textDocument.uri); - if (document) { - const styleSheet = stylesheets.get(document); - return getLanguageService(document).doHover(document, textDocumentPosition.position, styleSheet); - } - return null; - }, null, `Error while computing hover for ${textDocumentPosition.textDocument.uri}`, token); -}); - -connection.onDocumentSymbol((documentSymbolParams, token) => { - return runSafe(() => { - const document = documents.get(documentSymbolParams.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).findDocumentSymbols(document, stylesheet); - } - return []; - }, [], `Error while computing document symbols for ${documentSymbolParams.textDocument.uri}`, token); -}); - -connection.onDefinition((documentDefinitionParams, token) => { - return runSafe(() => { - const document = documents.get(documentDefinitionParams.textDocument.uri); - if (document) { - - const stylesheet = stylesheets.get(document); - return getLanguageService(document).findDefinition(document, documentDefinitionParams.position, stylesheet); - } - return null; - }, null, `Error while computing definitions for ${documentDefinitionParams.textDocument.uri}`, token); -}); - -connection.onDocumentHighlight((documentHighlightParams, token) => { - return runSafe(() => { - const document = documents.get(documentHighlightParams.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).findDocumentHighlights(document, documentHighlightParams.position, stylesheet); - } - return []; - }, [], `Error while computing document highlights for ${documentHighlightParams.textDocument.uri}`, token); -}); - - -connection.onDocumentLinks(async (documentLinkParams, token) => { - return runSafeAsync(async () => { - const document = documents.get(documentLinkParams.textDocument.uri); - if (document) { - const documentContext = getDocumentContext(document.uri, workspaceFolders); - const stylesheet = stylesheets.get(document); - return await getLanguageService(document).findDocumentLinks2(document, stylesheet, documentContext); - } - return []; - }, [], `Error while computing document links for ${documentLinkParams.textDocument.uri}`, token); -}); - - -connection.onReferences((referenceParams, token) => { - return runSafe(() => { - const document = documents.get(referenceParams.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).findReferences(document, referenceParams.position, stylesheet); - } - return []; - }, [], `Error while computing references for ${referenceParams.textDocument.uri}`, token); -}); - -connection.onCodeAction((codeActionParams, token) => { - return runSafe(() => { - const document = documents.get(codeActionParams.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).doCodeActions(document, codeActionParams.range, codeActionParams.context, stylesheet); - } - return []; - }, [], `Error while computing code actions for ${codeActionParams.textDocument.uri}`, token); -}); - -connection.onDocumentColor((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).findDocumentColors(document, stylesheet); - } - return []; - }, [], `Error while computing document colors for ${params.textDocument.uri}`, token); -}); - -connection.onColorPresentation((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).getColorPresentations(document, stylesheet, params.color, params.range); - } - return []; - }, [], `Error while computing color presentations for ${params.textDocument.uri}`, token); -}); - -connection.onRenameRequest((renameParameters, token) => { - return runSafe(() => { - const document = documents.get(renameParameters.textDocument.uri); - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).doRename(document, renameParameters.position, renameParameters.newName, stylesheet); - } - return null; - }, null, `Error while computing renames for ${renameParameters.textDocument.uri}`, token); -}); - -connection.onFoldingRanges((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - if (document) { - return getLanguageService(document).getFoldingRanges(document, { rangeLimit: foldingRangeLimit }); - } - return null; - }, null, `Error while computing folding ranges for ${params.textDocument.uri}`, token); -}); - -connection.onSelectionRanges((params, token) => { - return runSafe(() => { - const document = documents.get(params.textDocument.uri); - const positions: Position[] = params.positions; - - if (document) { - const stylesheet = stylesheets.get(document); - return getLanguageService(document).getSelectionRanges(document, positions, stylesheet); - } - return []; - }, [], `Error while computing selection ranges for ${params.textDocument.uri}`, token); -}); - - -// Listen on the connection -connection.listen(); diff --git a/extensions/css-language-features/server/src/customData.ts b/extensions/css-language-features/server/src/customData.ts index f173d884a2b..724dbf9de3f 100644 --- a/extensions/css-language-features/server/src/customData.ts +++ b/extensions/css-language-features/server/src/customData.ts @@ -3,48 +3,36 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CSSDataV1, ICSSDataProvider } from 'vscode-css-languageservice'; -import * as fs from 'fs'; +import { ICSSDataProvider, newCSSDataProvider } from 'vscode-css-languageservice'; +import { RequestService } from './requests'; -export function getDataProviders(dataPaths: string[]): ICSSDataProvider[] { - const providers = dataPaths.map(p => { - if (fs.existsSync(p)) { - const data = parseCSSData(fs.readFileSync(p, 'utf-8')); - return { - provideProperties: () => data.properties || [], - provideAtDirectives: () => data.atDirectives || [], - providePseudoClasses: () => data.pseudoClasses || [], - providePseudoElements: () => data.pseudoElements || [] - }; - } else { - return { - provideProperties: () => [], - provideAtDirectives: () => [], - providePseudoClasses: () => [], - providePseudoElements: () => [] - }; +export function fetchDataProviders(dataPaths: string[], requestService: RequestService): Promise { + const providers = dataPaths.map(async p => { + try { + const content = await requestService.getContent(p); + return parseCSSData(content); + } catch (e) { + return newCSSDataProvider({ version: 1 }); } }); - return providers; + return Promise.all(providers); } -function parseCSSData(source: string): CSSDataV1 { +function parseCSSData(source: string): ICSSDataProvider { let rawData: any; try { rawData = JSON.parse(source); } catch (err) { - return { - version: 1 - }; + return newCSSDataProvider({ version: 1 }); } - return { + return newCSSDataProvider({ version: 1, properties: rawData.properties || [], atDirectives: rawData.atDirectives || [], pseudoClasses: rawData.pseudoClasses || [], pseudoElements: rawData.pseudoElements || [] - }; + }); } diff --git a/extensions/css-language-features/server/src/node/cssServerMain.ts b/extensions/css-language-features/server/src/node/cssServerMain.ts new file mode 100644 index 00000000000..9e145398ff1 --- /dev/null +++ b/extensions/css-language-features/server/src/node/cssServerMain.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 { createConnection, Connection } from 'vscode-languageserver/node'; +import { formatError } from '../utils/runner'; +import { startServer } from '../cssServer'; +import { getNodeFSRequestService } from './nodeFs'; + +// Create a connection for the server. +const connection: Connection = createConnection(); + +console.log = connection.console.log.bind(connection.console); +console.error = connection.console.error.bind(connection.console); + +process.on('unhandledRejection', (e: any) => { + connection.console.error(formatError(`Unhandled exception`, e)); +}); + +startServer(connection, { file: getNodeFSRequestService() }); diff --git a/extensions/css-language-features/server/src/node/nodeFs.ts b/extensions/css-language-features/server/src/node/nodeFs.ts new file mode 100644 index 00000000000..c7b1301296d --- /dev/null +++ b/extensions/css-language-features/server/src/node/nodeFs.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RequestService, getScheme } from '../requests'; +import { URI as Uri } from 'vscode-uri'; + +import * as fs from 'fs'; +import { FileType } from 'vscode-css-languageservice'; + +export function getNodeFSRequestService(): RequestService { + function ensureFileUri(location: string) { + if (getScheme(location) !== 'file') { + throw new Error('fileRequestService can only handle file URLs'); + } + } + return { + getContent(location: string, encoding?: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const uri = Uri.parse(location); + fs.readFile(uri.fsPath, encoding, (err, buf) => { + if (err) { + return e(err); + } + c(buf.toString()); + + }); + }); + }, + stat(location: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const uri = Uri.parse(location); + fs.stat(uri.fsPath, (err, stats) => { + if (err) { + if (err.code === 'ENOENT') { + return c({ type: FileType.Unknown, ctime: -1, mtime: -1, size: -1 }); + } else { + return e(err); + } + } + + let type = FileType.Unknown; + if (stats.isFile()) { + type = FileType.File; + } else if (stats.isDirectory()) { + type = FileType.Directory; + } else if (stats.isSymbolicLink()) { + type = FileType.SymbolicLink; + } + + c({ + type, + ctime: stats.ctime.getTime(), + mtime: stats.mtime.getTime(), + size: stats.size + }); + }); + }); + }, + readDirectory(location: string) { + ensureFileUri(location); + return new Promise((c, e) => { + const path = Uri.parse(location).fsPath; + + fs.readdir(path, { withFileTypes: true }, (err, children) => { + if (err) { + return e(err); + } + c(children.map(stat => { + if (stat.isSymbolicLink()) { + return [stat.name, FileType.SymbolicLink]; + } else if (stat.isDirectory()) { + return [stat.name, FileType.Directory]; + } else if (stat.isFile()) { + return [stat.name, FileType.File]; + } else { + return [stat.name, FileType.Unknown]; + } + })); + }); + }); + } + }; +} diff --git a/extensions/css-language-features/server/src/pathCompletion.ts b/extensions/css-language-features/server/src/pathCompletion.ts deleted file mode 100644 index 6862f28e034..00000000000 --- a/extensions/css-language-features/server/src/pathCompletion.ts +++ /dev/null @@ -1,213 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as path from 'path'; -import * as fs from 'fs'; -import { URI } from 'vscode-uri'; - -import { TextDocument, CompletionList, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-languageserver-types'; -import { WorkspaceFolder } from 'vscode-languageserver'; -import { ICompletionParticipant } from 'vscode-css-languageservice'; - -import { startsWith, endsWith } from './utils/strings'; - -export function getPathCompletionParticipant( - document: TextDocument, - workspaceFolders: WorkspaceFolder[], - result: CompletionList -): ICompletionParticipant { - return { - onCssURILiteralValue: ({ position, range, uriValue }) => { - const fullValue = stripQuotes(uriValue); - if (!shouldDoPathCompletion(uriValue, workspaceFolders)) { - if (fullValue === '.' || fullValue === '..') { - result.isIncomplete = true; - } - return; - } - - let suggestions = providePathSuggestions(uriValue, position, range, document, workspaceFolders); - result.items = [...suggestions, ...result.items]; - }, - onCssImportPath: ({ position, range, pathValue }) => { - const fullValue = stripQuotes(pathValue); - if (!shouldDoPathCompletion(pathValue, workspaceFolders)) { - if (fullValue === '.' || fullValue === '..') { - result.isIncomplete = true; - } - return; - } - - let suggestions = providePathSuggestions(pathValue, position, range, document, workspaceFolders); - - if (document.languageId === 'scss') { - suggestions.forEach(s => { - if (startsWith(s.label, '_') && endsWith(s.label, '.scss')) { - if (s.textEdit) { - s.textEdit.newText = s.label.slice(1, -5); - } else { - s.label = s.label.slice(1, -5); - } - } - }); - } - - result.items = [...suggestions, ...result.items]; - } - }; -} - -function providePathSuggestions(pathValue: string, position: Position, range: Range, document: TextDocument, workspaceFolders: WorkspaceFolder[]) { - const fullValue = stripQuotes(pathValue); - const isValueQuoted = startsWith(pathValue, `'`) || startsWith(pathValue, `"`); - const valueBeforeCursor = isValueQuoted - ? fullValue.slice(0, position.character - (range.start.character + 1)) - : fullValue.slice(0, position.character - range.start.character); - const workspaceRoot = resolveWorkspaceRoot(document, workspaceFolders); - const currentDocFsPath = URI.parse(document.uri).fsPath; - - const paths = providePaths(valueBeforeCursor, currentDocFsPath, workspaceRoot) - .filter(p => { - // Exclude current doc's path - return path.resolve(currentDocFsPath, '../', p) !== currentDocFsPath; - }) - .filter(p => { - // Exclude paths that start with `.` - return p[0] !== '.'; - }); - - const fullValueRange = isValueQuoted ? shiftRange(range, 1, -1) : range; - const replaceRange = pathToReplaceRange(valueBeforeCursor, fullValue, fullValueRange); - - const suggestions = paths.map(p => pathToSuggestion(p, replaceRange)); - return suggestions; -} - -function shouldDoPathCompletion(pathValue: string, workspaceFolders: WorkspaceFolder[]): boolean { - const fullValue = stripQuotes(pathValue); - if (fullValue === '.' || fullValue === '..') { - return false; - } - - if (!workspaceFolders || workspaceFolders.length === 0) { - return false; - } - - return true; -} - -function stripQuotes(fullValue: string) { - if (startsWith(fullValue, `'`) || startsWith(fullValue, `"`)) { - return fullValue.slice(1, -1); - } else { - return fullValue; - } -} - -/** - * Get a list of path suggestions. Folder suggestions are suffixed with a slash. - */ -function providePaths(valueBeforeCursor: string, activeDocFsPath: string, root?: string): string[] { - const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); - const valueBeforeLastSlash = valueBeforeCursor.slice(0, lastIndexOfSlash + 1); - - const startsWithSlash = startsWith(valueBeforeCursor, '/'); - let parentDir: string; - if (startsWithSlash) { - if (!root) { - return []; - } - parentDir = path.resolve(root, '.' + valueBeforeLastSlash); - } else { - parentDir = path.resolve(activeDocFsPath, '..', valueBeforeLastSlash); - } - - try { - return fs.readdirSync(parentDir).map(f => { - return isDir(path.resolve(parentDir, f)) - ? f + '/' - : f; - }); - } catch (e) { - return []; - } -} - -const isDir = (p: string) => { - try { - return fs.statSync(p).isDirectory(); - } catch (e) { - return false; - } -}; - -function pathToReplaceRange(valueBeforeCursor: string, fullValue: string, fullValueRange: Range) { - let replaceRange: Range; - const lastIndexOfSlash = valueBeforeCursor.lastIndexOf('/'); - if (lastIndexOfSlash === -1) { - replaceRange = fullValueRange; - } else { - // For cases where cursor is in the middle of attribute value, like `); - async function asWebviewUri(path: string) { - const root = await webview.webview.asWebviewUri(vscode.Uri.file(vscode.workspace.rootPath!)); + function asWebviewUri(path: string) { + const root = webview.webview.asWebviewUri(vscode.Uri.file(vscode.workspace.rootPath!)); return root.toString() + path; } { - const imagePath = await asWebviewUri('/image.png'); + const imagePath = asWebviewUri('/image.png'); const response = sendRecieveMessage(webview, { src: imagePath }); assert.strictEqual((await response).value, true); } { - const imagePath = await asWebviewUri('/no-such-image.png'); + const imagePath = asWebviewUri('/no-such-image.png'); const response = sendRecieveMessage(webview, { src: imagePath }); assert.strictEqual((await response).value, false); } { - const imagePath = vscode.Uri.file(join(vscode.workspace.rootPath!, '..', '..', '..', 'resources', 'linux', 'code.png')).with({ scheme: 'vscode-resource' }); + const imagePath = webview.webview.asWebviewUri(vscode.Uri.file(join(vscode.workspace.rootPath!, '..', '..', '..', 'resources', 'linux', 'code.png'))); const response = sendRecieveMessage(webview, { src: imagePath.toString() }); assert.strictEqual((await response).value, false); } @@ -292,18 +292,38 @@ suite('vscode API - webview', () => { }); `); - const workspaceRootUri = vscode.Uri.file(vscode.workspace.rootPath!).with({ scheme: 'vscode-resource' }); - { - const response = sendRecieveMessage(webview, { src: workspaceRootUri.toString() + '/sub/image.png' }); + const response = sendRecieveMessage(webview, { src: webview.webview.asWebviewUri(vscode.Uri.file(join(vscode.workspace.rootPath!, 'sub', 'image.png'))).toString() }); assert.strictEqual((await response).value, true); } { - const response = sendRecieveMessage(webview, { src: workspaceRootUri.toString() + '/image.png' }); + const response = sendRecieveMessage(webview, { src: webview.webview.asWebviewUri(vscode.Uri.file(join(vscode.workspace.rootPath!, 'image.png'))).toString() }); assert.strictEqual((await response).value, false); } }); + conditionalTest('webviews using hard-coded old style vscode-resource uri should work', async () => { + const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.One }, { + enableScripts: true, + localResourceRoots: [vscode.Uri.file(join(vscode.workspace.rootPath!, 'sub'))] + })); + + const imagePath = vscode.Uri.file(join(vscode.workspace.rootPath!, 'sub', 'image.png')).with({ scheme: 'vscode-resource' }).toString(); + + webview.webview.html = createHtmlDocumentWithBody(/*html*/` + + `); + + const firstResponse = getMesssage(webview); + + assert.strictEqual((await firstResponse).value, true); + }); + test('webviews should have real view column after they are created, #56097', async () => { const webview = _register(vscode.window.createWebviewPanel(webviewId, 'title', { viewColumn: vscode.ViewColumn.Active }, { enableScripts: true })); diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts index 2a58c4630e0..ff6a2418f4d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/workspace.test.ts @@ -8,6 +8,7 @@ import * as vscode from 'vscode'; import { createRandomFile, deleteFile, closeAllEditors, pathEquals, rndName, disposeAll, testFs, delay, withLogDisabled } from '../utils'; import { join, posix, basename } from 'path'; import * as fs from 'fs'; +import { TestFS } from '../memfs'; suite('vscode API - workspace', () => { @@ -163,6 +164,40 @@ suite('vscode API - workspace', () => { }); }); + test('openTextDocument, actual casing first', async function () { + + const fs = new TestFS('this-fs', false); + const reg = vscode.workspace.registerFileSystemProvider(fs.scheme, fs, { isCaseSensitive: fs.isCaseSensitive }); + + let uriOne = vscode.Uri.parse('this-fs:/one'); + let uriTwo = vscode.Uri.parse('this-fs:/two'); + let uriONE = vscode.Uri.parse('this-fs:/ONE'); // same resource, different uri + let uriTWO = vscode.Uri.parse('this-fs:/TWO'); + + fs.writeFile(uriOne, Buffer.from('one'), { create: true, overwrite: true }); + fs.writeFile(uriTwo, Buffer.from('two'), { create: true, overwrite: true }); + + // lower case (actual case) comes first + let docOne = await vscode.workspace.openTextDocument(uriOne); + assert.equal(docOne.uri.toString(), uriOne.toString()); + + let docONE = await vscode.workspace.openTextDocument(uriONE); + assert.equal(docONE === docOne, true); + assert.equal(docONE.uri.toString(), uriOne.toString()); + assert.equal(docONE.uri.toString() !== uriONE.toString(), true); // yep + + // upper case (NOT the actual case) comes first + let docTWO = await vscode.workspace.openTextDocument(uriTWO); + assert.equal(docTWO.uri.toString(), uriTWO.toString()); + + let docTwo = await vscode.workspace.openTextDocument(uriTwo); + assert.equal(docTWO === docTwo, true); + assert.equal(docTwo.uri.toString(), uriTWO.toString()); + assert.equal(docTwo.uri.toString() !== uriTwo.toString(), true); // yep + + reg.dispose(); + }); + test('eol, read', () => { const a = createRandomFile('foo\nbar\nbar').then(file => { return vscode.workspace.openTextDocument(file).then(doc => { diff --git a/extensions/vscode-api-tests/src/utils.ts b/extensions/vscode-api-tests/src/utils.ts index e270cd73adf..3c34028feb4 100644 --- a/extensions/vscode-api-tests/src/utils.ts +++ b/extensions/vscode-api-tests/src/utils.ts @@ -4,15 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { MemFS } from './memfs'; +import { TestFS } from './memfs'; import * as assert from 'assert'; export function rndName() { return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10); } -export const testFs = new MemFS(); -vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs); +export const testFs = new TestFS('fake-fs', true); +vscode.workspace.registerFileSystemProvider(testFs.scheme, testFs, { isCaseSensitive: testFs.isCaseSensitive }); export async function createRandomFile(contents = '', dir: vscode.Uri | undefined = undefined, ext = ''): Promise { let fakeFile: vscode.Uri; diff --git a/extensions/vscode-notebook-tests/package.json b/extensions/vscode-notebook-tests/package.json index 8d7e92bd255..83f73669e0a 100644 --- a/extensions/vscode-notebook-tests/package.json +++ b/extensions/vscode-notebook-tests/package.json @@ -27,6 +27,12 @@ "mocha": "^2.3.3" }, "contributes": { + "commands": [ + { + "command": "vscode-notebook-tests.createNewNotebook", + "title": "Create New Notebook" + } + ], "notebookProvider": [ { "viewType": "notebookCoreTest", @@ -37,6 +43,25 @@ "excludeFileNamePattern": "" } ] + }, + { + "viewType": "notebookSmokeTest", + "displayName": "Notebook Smoke Test", + "selector": [ + { + "filenamePattern": "*.smoke-nb", + "excludeFileNamePattern": "" + } + ] + } + ], + "notebookOutputRenderer": [ + { + "viewType": "notebookCoreTestRenderer", + "displayName": "Notebook Core Test Renderer", + "mimeTypes": [ + "text/custom" + ] } ] } diff --git a/extensions/vscode-notebook-tests/src/customRenderer.js b/extensions/vscode-notebook-tests/src/customRenderer.js new file mode 100644 index 00000000000..75e2ec1eb7a --- /dev/null +++ b/extensions/vscode-notebook-tests/src/customRenderer.js @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const vscode = acquireVsCodeApi(); + +vscode.postMessage({ + type: 'custom_renderer_initialize', + payload: { + firstMessage: true + } +}); diff --git a/extensions/vscode-notebook-tests/src/notebook.test.ts b/extensions/vscode-notebook-tests/src/notebook.test.ts index d512d5e0ff0..944b179a216 100644 --- a/extensions/vscode-notebook-tests/src/notebook.test.ts +++ b/extensions/vscode-notebook-tests/src/notebook.test.ts @@ -8,25 +8,282 @@ import * as assert from 'assert'; import * as vscode from 'vscode'; import { join } from 'path'; -function waitFor(ms: number): Promise { - let resolveFunc: () => void; - - const promise = new Promise(resolve => { - resolveFunc = resolve; +export function timeoutAsync(n: number): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, n); }); - setTimeout(() => { - resolveFunc!(); - }, ms); - - return promise; } +export function once(event: vscode.Event): vscode.Event { + return (listener: any, thisArgs = null, disposables?: any) => { + // we need this, in case the event fires during the listener call + let didFire = false; + let result: vscode.Disposable; + result = event(e => { + if (didFire) { + return; + } else if (result) { + result.dispose(); + } else { + didFire = true; + } + + return listener.call(thisArgs, e); + }, null, disposables); + + if (didFire) { + result.dispose(); + } + + return result; + }; +} + +async function getEventOncePromise(event: vscode.Event): Promise { + return new Promise((resolve, _reject) => { + once(event)((result: T) => resolve(result)); + }); +} + +// Since `workbench.action.splitEditor` command does await properly +// Notebook editor/document events are not guaranteed to be sent to the ext host when promise resolves +// The workaround here is waiting for the first visible notebook editor change event. +async function splitEditor() { + const once = getEventOncePromise(vscode.notebook.onDidChangeVisibleNotebookEditors); + await vscode.commands.executeCommand('workbench.action.splitEditor'); + await once; +} + +suite('API tests', () => { + test('document open/close event', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const firstDocumentOpen = getEventOncePromise(vscode.notebook.onDidOpenNotebookDocument); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await firstDocumentOpen; + + const firstDocumentClose = getEventOncePromise(vscode.notebook.onDidCloseNotebookDocument); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await firstDocumentClose; + }); + + test('shared document in notebook editors', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + let counter = 0; + const disposables: vscode.Disposable[] = []; + disposables.push(vscode.notebook.onDidOpenNotebookDocument(() => { + counter++; + })); + disposables.push(vscode.notebook.onDidCloseNotebookDocument(() => { + counter--; + })); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(counter, 1); + + await splitEditor(); + assert.equal(counter, 1); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + assert.equal(counter, 0); + + disposables.forEach(d => d.dispose()); + }); + + test('editor open/close event', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const firstEditorOpen = getEventOncePromise(vscode.notebook.onDidChangeVisibleNotebookEditors); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await firstEditorOpen; + + const firstEditorClose = getEventOncePromise(vscode.notebook.onDidChangeVisibleNotebookEditors); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + await firstEditorClose; + }); + + test('editor open/close event', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + let count = 0; + const disposables: vscode.Disposable[] = []; + disposables.push(vscode.notebook.onDidChangeVisibleNotebookEditors(() => { + count = vscode.notebook.visibleNotebookEditors.length; + })); + + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(count, 1); + + await splitEditor(); + assert.equal(count, 2); + + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + assert.equal(count, 0); + }); + + test('editor editing event 2', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + const cellChangeEventRet = await cellsChangeEvent; + assert.equal(cellChangeEventRet.document, vscode.notebook.activeNotebookEditor?.document); + assert.equal(cellChangeEventRet.changes.length, 1); + assert.deepEqual(cellChangeEventRet.changes[0], { + start: 1, + deletedCount: 0, + items: [ + vscode.notebook.activeNotebookEditor!.document.cells[1] + ] + }); + + const moveCellEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + await vscode.commands.executeCommand('notebook.cell.moveUp'); + const moveCellEventRet = await moveCellEvent; + assert.deepEqual(moveCellEventRet, { + document: vscode.notebook.activeNotebookEditor!.document, + changes: [ + { + start: 1, + deletedCount: 1, + items: [] + }, + { + start: 0, + deletedCount: 0, + items: [vscode.notebook.activeNotebookEditor?.document.cells[0]] + } + ] + }); + + const cellOutputChange = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.commands.executeCommand('notebook.cell.execute'); + const cellOutputsAddedRet = await cellOutputChange; + assert.deepEqual(cellOutputsAddedRet, { + document: vscode.notebook.activeNotebookEditor!.document, + cells: [vscode.notebook.activeNotebookEditor!.document.cells[0]] + }); + assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 1); + + const cellOutputClear = getEventOncePromise(vscode.notebook.onDidChangeCellOutputs); + await vscode.commands.executeCommand('notebook.cell.clearOutputs'); + const cellOutputsCleardRet = await cellOutputClear; + assert.deepEqual(cellOutputsCleardRet, { + document: vscode.notebook.activeNotebookEditor!.document, + cells: [vscode.notebook.activeNotebookEditor!.document.cells[0]] + }); + assert.equal(cellOutputsAddedRet.cells[0].outputs.length, 0); + + // const cellChangeLanguage = getEventOncePromise(vscode.notebook.onDidChangeCellLanguage); + // await vscode.commands.executeCommand('notebook.cell.changeToMarkdown'); + // const cellChangeLanguageRet = await cellChangeLanguage; + // assert.deepEqual(cellChangeLanguageRet, { + // document: vscode.notebook.activeNotebookEditor!.document, + // cells: vscode.notebook.activeNotebookEditor!.document.cells[0], + // language: 'markdown' + // }); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('editor move cell event', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('notebook.focusTop'); + + const activeCell = vscode.notebook.activeNotebookEditor!.selection; + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 0); + const moveChange = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + await vscode.commands.executeCommand('notebook.cell.moveDown'); + const ret = await moveChange; + assert.deepEqual(ret, { + document: vscode.notebook.activeNotebookEditor?.document, + changes: [ + { + start: 0, + deletedCount: 1, + items: [] + }, + { + start: 1, + deletedCount: 0, + items: [activeCell] + } + ] + }); + }); + + test('notebook editor active/visible', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const firstEditor = vscode.notebook.activeNotebookEditor; + assert.equal(firstEditor?.active, true); + assert.equal(firstEditor?.visible, true); + + await splitEditor(); + const secondEditor = vscode.notebook.activeNotebookEditor; + assert.equal(secondEditor?.active, true); + assert.equal(secondEditor?.visible, true); + assert.equal(firstEditor?.active, false); + + assert.equal(vscode.notebook.visibleNotebookEditors.length, 2); + + await vscode.commands.executeCommand('workbench.action.files.newUntitledFile'); + assert.equal(firstEditor?.visible, true); + assert.equal(firstEditor?.active, false); + assert.equal(secondEditor?.visible, false); + assert.equal(secondEditor?.active, false); + assert.equal(vscode.notebook.visibleNotebookEditors.length, 1); + + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + assert.equal(secondEditor?.active, true); + assert.equal(secondEditor?.visible, true); + assert.equal(vscode.notebook.visibleNotebookEditors.length, 2); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('notebook active editor change', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const firstEditorOpen = getEventOncePromise(vscode.notebook.onDidChangeActiveNotebookEditor); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await firstEditorOpen; + + const firstEditorDeactivate = getEventOncePromise(vscode.notebook.onDidChangeActiveNotebookEditor); + await vscode.commands.executeCommand('workbench.action.splitEditor'); + await firstEditorDeactivate; + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('edit API', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const cellsChangeEvent = getEventOncePromise(vscode.notebook.onDidChangeNotebookCells); + await vscode.notebook.activeNotebookEditor!.edit(editBuilder => { + editBuilder.insert(1, 'test 2', 'javascript', vscode.CellKind.Code, [], undefined); + }); + + const cellChangeEventRet = await cellsChangeEvent; + assert.equal(cellChangeEventRet.document, vscode.notebook.activeNotebookEditor?.document); + assert.equal(cellChangeEventRet.changes.length, 1); + assert.deepEqual(cellChangeEventRet.changes[0].start, 1); + assert.deepEqual(cellChangeEventRet.changes[0].deletedCount, 0); + assert.equal(cellChangeEventRet.changes[0].items[0], vscode.notebook.activeNotebookEditor!.document.cells[1]); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); +}); + suite('notebook workflow', () => { test('notebook open', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'test'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); @@ -46,10 +303,8 @@ suite('notebook workflow', () => { }); test('notebook cell actions', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'test'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); @@ -101,14 +356,18 @@ suite('notebook workflow', () => { // ---- move up and down ---- // await vscode.commands.executeCommand('notebook.cell.moveDown'); - await vscode.commands.executeCommand('notebook.cell.moveDown'); - activeCell = vscode.notebook.activeNotebookEditor!.selection; + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(vscode.notebook.activeNotebookEditor!.selection!), 1, + `first move down, active cell ${vscode.notebook.activeNotebookEditor!.selection!.uri.toString()}, ${vscode.notebook.activeNotebookEditor!.selection!.source}`); - assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 2); - assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[0].source, 'test'); - assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[1].source, ''); - assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[2].source, 'test'); - assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[3].source, ''); + // await vscode.commands.executeCommand('notebook.cell.moveDown'); + // activeCell = vscode.notebook.activeNotebookEditor!.selection; + + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 2, + // `second move down, active cell ${vscode.notebook.activeNotebookEditor!.selection!.uri.toString()}, ${vscode.notebook.activeNotebookEditor!.selection!.source}`); + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[0].source, 'test'); + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[1].source, ''); + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[2].source, 'test'); + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells[3].source, ''); // ---- ---- // @@ -116,11 +375,30 @@ suite('notebook workflow', () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); - // test.only('document metadata is respected', async function () { - // const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); - // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + test('move cells will not recreate cells in ExtHost', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('notebook.focusTop'); - // await waitFor(500); + const activeCell = vscode.notebook.activeNotebookEditor!.selection; + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 0); + await vscode.commands.executeCommand('notebook.cell.moveDown'); + await vscode.commands.executeCommand('notebook.cell.moveDown'); + + const newActiveCell = vscode.notebook.activeNotebookEditor!.selection; + assert.deepEqual(activeCell, newActiveCell); + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + + // TODO@rebornix, there are still some events order issue. + // assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(newActiveCell!), 2); + }); + + // test.only('document metadata is respected', async function () { + // const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + // await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); // assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); // const editor = vscode.notebook.activeNotebookEditor!; @@ -143,11 +421,8 @@ suite('notebook workflow', () => { // }); test('cell runnable metadata is respected', async () => { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); - assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.notebook.activeNotebookEditor!; @@ -167,11 +442,8 @@ suite('notebook workflow', () => { }); test('document runnable metadata is respected', async () => { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); - assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); const editor = vscode.notebook.activeNotebookEditor!; @@ -192,10 +464,8 @@ suite('notebook workflow', () => { suite('notebook dirty state', () => { test('notebook open', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'test'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); @@ -216,8 +486,6 @@ suite('notebook dirty state', () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[1], vscode.notebook.activeNotebookEditor?.selection); @@ -230,10 +498,8 @@ suite('notebook dirty state', () => { suite('notebook undo redo', () => { test('notebook open', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'test'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); @@ -275,10 +541,8 @@ suite('notebook undo redo', () => { suite('notebook working copy', () => { test('notebook revert on close', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); @@ -288,8 +552,6 @@ suite('notebook working copy', () => { // close active editor from command will revert the file await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[0], vscode.notebook.activeNotebookEditor?.selection); @@ -300,10 +562,8 @@ suite('notebook working copy', () => { }); test('notebook revert', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); @@ -319,29 +579,199 @@ suite('notebook working copy', () => { await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); }); + + test('multiple tabs: dirty + clean', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + + const secondResource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './second.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + + // make sure that the previous dirty editor is still restored in the extension host and no data loss + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[1], vscode.notebook.activeNotebookEditor?.selection); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 3); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'var abc = 0;'); + + await vscode.commands.executeCommand('workbench.action.files.save'); + await vscode.commands.executeCommand('workbench.action.closeActiveEditor'); + }); + + test('multiple tabs: two dirty tabs and switching', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + await vscode.commands.executeCommand('notebook.cell.insertCodeCellAbove'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + + const secondResource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './second.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); + + // switch to the first editor + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[1], vscode.notebook.activeNotebookEditor?.selection); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 3); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, 'var abc = 0;'); + + // switch to the second editor + await vscode.commands.executeCommand('vscode.openWith', secondResource, 'notebookCoreTest'); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true); + assert.equal(vscode.notebook.activeNotebookEditor?.selection !== undefined, true); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells[1], vscode.notebook.activeNotebookEditor?.selection); + assert.deepEqual(vscode.notebook.activeNotebookEditor?.document.cells.length, 2); + assert.equal(vscode.notebook.activeNotebookEditor?.selection?.source, ''); + + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('multiple tabs: different editors with same document', async function () { + + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + const firstNotebookEditor = vscode.notebook.activeNotebookEditor; + assert.equal(firstNotebookEditor !== undefined, true, 'notebook first'); + assert.equal(firstNotebookEditor!.selection?.source, 'test'); + assert.equal(firstNotebookEditor!.selection?.language, 'typescript'); + + await splitEditor(); + const secondNotebookEditor = vscode.notebook.activeNotebookEditor; + assert.equal(secondNotebookEditor !== undefined, true, 'notebook first'); + assert.equal(secondNotebookEditor!.selection?.source, 'test'); + assert.equal(secondNotebookEditor!.selection?.language, 'typescript'); + + assert.notEqual(firstNotebookEditor, secondNotebookEditor); + assert.equal(firstNotebookEditor?.document, secondNotebookEditor?.document, 'split notebook editors share the same document'); + assert.notEqual(firstNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png')), secondNotebookEditor?.asWebviewUri(vscode.Uri.file('./hello.png'))); + + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); }); suite('metadata', () => { test('custom metadata should be supported', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.metadata.custom!['testCellMetadata'] as number, 123); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); }); + + // TODO copy cell should not copy metadata + + test('custom metadata should be supported', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); + assert.equal(vscode.notebook.activeNotebookEditor!.document.metadata.custom!['testMetadata'] as boolean, false); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.metadata.custom!['testCellMetadata'] as number, 123); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + + await vscode.commands.executeCommand('notebook.cell.copyDown'); + const activeCell = vscode.notebook.activeNotebookEditor!.selection; + assert.equal(vscode.notebook.activeNotebookEditor!.document.cells.indexOf(activeCell!), 1); + assert.equal(activeCell?.metadata.custom, undefined); + }); }); suite('regression', () => { test('microsoft/vscode-github-issue-notebooks#26. Insert template cell in the new empty document', async function () { - const resource = vscode.Uri.parse(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); - - await waitFor(500); assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, ''); assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + test('#97830, #97764. Support switch to other editor types', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.source, 'var abc = 0;'); + assert.equal(vscode.notebook.activeNotebookEditor!.selection?.language, 'typescript'); + + await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); + assert.equal(vscode.window.activeTextEditor?.document.uri.path, resource.path); + + await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + // open text editor, pin, and then open a notebook + test('#96105 - dirty editors', async function () { + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './empty.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'default'); + await vscode.commands.executeCommand('notebook.cell.insertCodeCellBelow'); + await vscode.commands.executeCommand('default:type', { text: 'var abc = 0;' }); + + // now it's dirty, open the resource with notebook editor should open a new one + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.notEqual(vscode.notebook.activeNotebookEditor, undefined, 'notebook first'); + assert.notEqual(vscode.window.activeTextEditor, undefined); + + // await vscode.commands.executeCommand('workbench.action.files.saveAll'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + +}); + +suite('webview', () => { + // for web, `asWebUri` gets `https`? + test('asWebviewUri', async function () { + if (vscode.env.uiKind === vscode.UIKind.Web) { + return; + } + + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './first.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + assert.equal(vscode.notebook.activeNotebookEditor !== undefined, true, 'notebook first'); + const uri = vscode.notebook.activeNotebookEditor!.asWebviewUri(vscode.Uri.file('./hello.png')); + assert.equal(uri.scheme, 'vscode-webview-resource'); + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); + }); + + + // 404 on web + test('custom renderer message', async function () { + if (vscode.env.uiKind === vscode.UIKind.Web) { + return; + } + + const resource = vscode.Uri.file(join(vscode.workspace.rootPath || '', './customRenderer.vsctestnb')); + await vscode.commands.executeCommand('vscode.openWith', resource, 'notebookCoreTest'); + + const editor = vscode.notebook.activeNotebookEditor; + const promise = new Promise(resolve => { + const messageEmitter = editor?.onDidReceiveMessage(e => { + if (e.type === 'custom_renderer_initialize') { + resolve(); + messageEmitter?.dispose(); + } + }); + }); + + await vscode.commands.executeCommand('notebook.cell.execute'); + await promise; + await vscode.commands.executeCommand('workbench.action.closeAllEditors'); }); }); diff --git a/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts new file mode 100644 index 00000000000..0a8c4a3d105 --- /dev/null +++ b/extensions/vscode-notebook-tests/src/notebookSmokeTestMain.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as child_process from 'child_process'; +import * as path from 'path'; + +function wait(ms: number): Promise { + return new Promise(r => setTimeout(r, ms)); +} + +export function smokeTestActivate(context: vscode.ExtensionContext): any { + context.subscriptions.push(vscode.commands.registerCommand('vscode-notebook-tests.createNewNotebook', async () => { + const workspacePath = vscode.workspace.workspaceFolders![0].uri.fsPath; + const notebookPath = path.join(workspacePath, 'test.smoke-nb'); + child_process.execSync('echo \'\' > ' + notebookPath); + await wait(500); + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(notebookPath)); + })); + + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookSmokeTest', { + onDidChangeNotebook: new vscode.EventEmitter().event, + openNotebook: async (_resource: vscode.Uri) => { + const dto: vscode.NotebookData = { + languages: ['typescript'], + metadata: {}, + cells: [ + { + source: 'code()', + language: 'typescript', + cellKind: vscode.CellKind.Code, + outputs: [], + metadata: { + custom: { testCellMetadata: 123 } + } + }, + { + source: 'Markdown Cell', + language: 'markdown', + cellKind: vscode.CellKind.Markdown, + outputs: [], + metadata: { + custom: { testCellMetadata: 123 } + } + } + ] + }; + + return dto; + }, + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + } + })); + + context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookSmokeTest', ['*.vsctestnb'], { + label: 'notebookSmokeTest', + executeAllCells: async (_document: vscode.NotebookDocument) => { + for (let i = 0; i < _document.cells.length; i++) { + _document.cells[i].outputs = [{ + outputKind: vscode.CellOutputKind.Rich, + data: { + 'text/html': ['test output'] + } + }]; + } + }, + executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { + if (!_cell) { + _cell = _document.cells[0]; + } + + _cell.outputs = [{ + outputKind: vscode.CellOutputKind.Rich, + data: { + 'text/html': ['test output'] + } + }]; + return; + }, + })); +} diff --git a/extensions/vscode-notebook-tests/src/notebookTestMain.ts b/extensions/vscode-notebook-tests/src/notebookTestMain.ts index 6cdd3619de7..059f998acae 100644 --- a/extensions/vscode-notebook-tests/src/notebookTestMain.ts +++ b/extensions/vscode-notebook-tests/src/notebookTestMain.ts @@ -4,8 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as path from 'path'; +import { smokeTestActivate } from './notebookSmokeTestMain'; export function activate(context: vscode.ExtensionContext): any { + smokeTestActivate(context); + context.subscriptions.push(vscode.notebook.registerNotebookContentProvider('notebookCoreTest', { onDidChangeNotebook: new vscode.EventEmitter().event, openNotebook: async (_resource: vscode.Uri) => { @@ -37,24 +41,64 @@ export function activate(context: vscode.ExtensionContext): any { return dto; }, + saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + }, + saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; + } + })); + + context.subscriptions.push(vscode.notebook.registerNotebookKernel('notebookKernelTest', ['*.vsctestnb'], { + label: 'Notebook Test Kernel', + executeAllCells: async (_document: vscode.NotebookDocument, _token: vscode.CancellationToken) => { + let cell = _document.cells[0]; + + cell.outputs = [{ + outputKind: vscode.CellOutputKind.Rich, + data: { + 'text/plain': ['my output'] + } + }]; + return; + }, executeCell: async (_document: vscode.NotebookDocument, _cell: vscode.NotebookCell | undefined, _token: vscode.CancellationToken) => { if (!_cell) { _cell = _document.cells[0]; } + if (_document.uri.path.endsWith('customRenderer.vsctestnb')) { + _cell.outputs = [{ + outputKind: vscode.CellOutputKind.Rich, + data: { + 'text/custom': 'test' + } + }]; + + return; + } + _cell.outputs = [{ outputKind: vscode.CellOutputKind.Rich, data: { 'text/plain': ['my output'] } }]; - return; - }, - saveNotebook: async (_document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { - return; - }, - saveNotebookAs: async (_targetResource: vscode.Uri, _document: vscode.NotebookDocument, _cancellation: vscode.CancellationToken) => { + return; } })); + + const preloadUri = vscode.Uri.file(path.resolve(__dirname, '../src/customRenderer.js')); + context.subscriptions.push(vscode.notebook.registerNotebookOutputRenderer('notebookCoreTestRenderer', { + type: 'display_data', + subTypes: [ + 'text/custom' + ] + }, { + preloads: [preloadUri], + render(_document: vscode.NotebookDocument, _output: vscode.CellDisplayOutput, _mimeType: string): string { + return '
test
'; + } + })); } diff --git a/extensions/vscode-notebook-tests/test/customRenderer.vsctestnb b/extensions/vscode-notebook-tests/test/customRenderer.vsctestnb new file mode 100644 index 00000000000..a4a092d8349 --- /dev/null +++ b/extensions/vscode-notebook-tests/test/customRenderer.vsctestnb @@ -0,0 +1,4 @@ +/*--------------------------------------------------------------------------------------------- + * 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-notebook-tests/test/second.vsctestnb b/extensions/vscode-notebook-tests/test/second.vsctestnb new file mode 100644 index 00000000000..a4a092d8349 --- /dev/null +++ b/extensions/vscode-notebook-tests/test/second.vsctestnb @@ -0,0 +1,4 @@ +/*--------------------------------------------------------------------------------------------- + * 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/package.json b/extensions/vscode-web-playground/package.json index 46b6102c10e..05fa8bdeb78 100644 --- a/extensions/vscode-web-playground/package.json +++ b/extensions/vscode-web-playground/package.json @@ -10,7 +10,7 @@ "onFileSystem:memfs", "onDebug" ], - "main": "./out/extension", + "browser": "./out/extension", "engines": { "vscode": "^1.25.0" }, diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 4e0ca82acd1..31924f9e866 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@^3.9.2-insiders.20200509: - version "3.9.2-insiders.20200509" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.2-insiders.20200509.tgz#8c90ed86a91f9692f10f5ac9c1fd6cb241419e6c" - integrity sha512-AAbhs55BZMbyHGfJd0pNfO3+B6jjPpa38zgaIb9MRExkRGLkIUpbUetoh+HgmM5LAtg128sHGiwhLc49pOcgFw== +typescript@3.9.4: + version "3.9.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.4.tgz#5aa0a54904b51b96dfd67870ce2db70251802f10" + integrity sha512-9OL+r0KVHqsYVH7K18IBR9hhC82YwLNlpSZfQDupGcfg8goB9p/s/9Okcy+ztnTeHR2U68xq21/igW9xpoGTgA== diff --git a/package.json b/package.json index d4bc7dbf2d8..92aa5c37eee 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.46.0", - "distro": "19675eb92c1d28a53751978b99c47674b997ea3f", + "distro": "da9a049e7e7a7beb80287dce52a3966d8cfcf554", "author": { "name": "Microsoft Corporation" }, @@ -33,8 +33,7 @@ "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", "web": "node scripts/code-web.js", - "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions", - "generate-github-config": "node extensions/github-authentication/build/generateconfig.js" + "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions" }, "dependencies": { "applicationinsights": "1.0.8", @@ -60,11 +59,10 @@ "vscode-ripgrep": "^1.5.8", "vscode-sqlite3": "4.0.10", "vscode-textmate": "5.1.1", - "xterm": "4.6.0-beta.44", - "xterm-addon-search": "0.7.0-beta.2", - "xterm-addon-unicode11": "0.2.0-beta.5", - "xterm-addon-web-links": "0.4.0-beta.6", - "xterm-addon-webgl": "0.7.0-beta.10", + "xterm": "4.7.0-beta.3", + "xterm-addon-search": "0.7.0", + "xterm-addon-unicode11": "0.2.0", + "xterm-addon-webgl": "0.7.0", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, @@ -101,7 +99,7 @@ "css-loader": "^3.2.0", "debounce": "^1.0.0", "deemon": "^1.4.0", - "electron": "7.2.4", + "electron": "7.3.0", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", @@ -156,7 +154,7 @@ "source-map": "^0.4.4", "style-loader": "^1.0.0", "ts-loader": "^4.4.2", - "typescript": "^3.9.1-rc", + "typescript": "^3.9.3", "typescript-formatter": "7.1.0", "underscore": "^1.8.2", "vinyl": "^2.0.0", diff --git a/product.json b/product.json index 77409b04faa..740f44e9ee6 100644 --- a/product.json +++ b/product.json @@ -11,8 +11,10 @@ "win32RegValueName": "CodeOSS", "win32AppId": "{{E34003BB-9E10-4501-8C11-BE3FAA83F23F}", "win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}", + "win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}", "win32UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}", "win32x64UserAppId": "{{C6065F05-9603-4FC4-8101-B9781A25D88E}", + "win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}", "win32AppUserModelId": "Microsoft.CodeOSS", "win32ShellNameShort": "C&ode - OSS", "darwinBundleIdentifier": "com.visualstudio.code.oss", @@ -21,6 +23,7 @@ "reportIssueUrl": "https://github.com/Microsoft/vscode/issues/new", "urlProtocol": "code-oss", "extensionAllowedProposedApi": [ + "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", "ms-vscode.references-view" ], @@ -42,7 +45,7 @@ }, { "name": "ms-vscode.node-debug2", - "version": "1.42.2", + "version": "1.42.4", "repo": "https://github.com/Microsoft/vscode-node-debug2", "metadata": { "id": "36d19e17-7569-4841-a001-947eb18602b2", @@ -57,7 +60,7 @@ }, { "name": "ms-vscode.references-view", - "version": "0.0.53", + "version": "0.0.57", "repo": "https://github.com/Microsoft/vscode-reference-view", "metadata": { "id": "dc489f46-520d-4556-ae85-1f9eab3c412d", @@ -72,7 +75,7 @@ }, { "name": "ms-vscode.js-debug-companion", - "version": "1.0.0", + "version": "1.0.2", "repo": "https://github.com/microsoft/vscode-js-debug-companion", "metadata": { "id": "99cb0b7f-7354-4278-b8da-6cc79972169d", @@ -87,7 +90,7 @@ }, { "name": "ms-vscode.js-debug-nightly", - "version": "2020.5.417", + "version": "2020.5.2717", "repo": "https://github.com/Microsoft/vscode-js-debug", "metadata": { "id": "7acbb4ce-c85a-49d4-8d95-a8054406ae97", @@ -102,7 +105,7 @@ }, { "name": "ms-vscode.vscode-js-profile-table", - "version": "0.0.2", + "version": "0.0.4", "repo": "https://github.com/Microsoft/vscode-js-debug", "metadata": { "id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb", diff --git a/remote/package.json b/remote/package.json index b8927215f16..cba584d935d 100644 --- a/remote/package.json +++ b/remote/package.json @@ -20,11 +20,10 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.8", "vscode-textmate": "5.1.1", - "xterm": "4.6.0-beta.44", - "xterm-addon-search": "0.7.0-beta.2", - "xterm-addon-unicode11": "0.2.0-beta.5", - "xterm-addon-web-links": "0.4.0-beta.6", - "xterm-addon-webgl": "0.7.0-beta.10", + "xterm": "4.7.0-beta.3", + "xterm-addon-search": "0.7.0", + "xterm-addon-unicode11": "0.2.0", + "xterm-addon-webgl": "0.7.0", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 98baff7fd26..5a4ef271910 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,10 +5,9 @@ "semver-umd": "^5.5.6", "vscode-oniguruma": "1.3.1", "vscode-textmate": "5.1.1", - "xterm": "4.6.0-beta.44", - "xterm-addon-search": "0.7.0-beta.2", - "xterm-addon-unicode11": "0.2.0-beta.5", - "xterm-addon-web-links": "0.4.0-beta.6", - "xterm-addon-webgl": "0.7.0-beta.10" + "xterm": "4.7.0-beta.3", + "xterm-addon-search": "0.7.0", + "xterm-addon-unicode11": "0.2.0", + "xterm-addon-webgl": "0.7.0" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 4b7efc15550..fa3681e1a98 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -17,27 +17,22 @@ vscode-textmate@5.1.1: resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-5.1.1.tgz#d88dbf271bee7cede455a21bd4894ba5724a4a7e" integrity sha512-5VHjF+Fglf9d2JI5OyQ7FHutK6/29G0qYyD920K0SWO7uY8JTWbqyKAHEtfB/ZDk2fOe/E23n3wz9fHXKi63yg== -xterm-addon-search@0.7.0-beta.2: - version "0.7.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.7.0-beta.2.tgz#384bda136c707f97a77eefc76cc7d9e572ce0719" - integrity sha512-A9fyiBBvG6ZNIwSJ03+sRCv9y20/uzd1wjCoaYUqp9fu3YGiHaGwyo9rAfm2M/fQM5vBmyJk4Qw/lwVq7TtlAw== +xterm-addon-search@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.7.0.tgz#c929d3e5cbb335e82bff72f158ea82936d9cd4ef" + integrity sha512-6060evmJJ+tZcjnx33FXaeEHLpuXEa7l9UzUsYfMlCKbu88AbE+5LJocTKCHYd71cwCwb9pjmv/G1o9Rf9Zbcg== -xterm-addon-unicode11@0.2.0-beta.5: - version "0.2.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0-beta.5.tgz#5961850162df20b5e966166423cd6957ac2db298" - integrity sha512-IjnbBcyfS5JgJDXPO0W2nk/VBtGwx6GWE2snMC676z4DmAABUqPXfTzJKfUoWqoT6UcbxB0oIjDzykCfoRJp6Q== +xterm-addon-unicode11@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0.tgz#9ed0c482b353908bba27778893ca80823382737c" + integrity sha512-rjFDItPc/IDoSiEnoDFwKroNwLD/7t9vYKENjrcKVZg5tgJuuUj8D4rZtP6iVCjSB1LTLYmUs4L/EmCqIyLR/Q== -xterm-addon-web-links@0.4.0-beta.6: - version "0.4.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.6.tgz#d159d4542eb9a02d57977fe7eb5f42f8ef2f27fa" - integrity sha512-dsQVD/EyVq8PtAYGh2PGQTCt009UipIfX6Q2SBDlz+W9x7IkXjhRxRaryMmLsBCca20qeVKwmbQ+ANhLi+nTaQ== +xterm-addon-webgl@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0.tgz#a13732ac937170e53ce02ec91963da042c80614b" + integrity sha512-PMWLgccAF31GulCYkQxIA8qwMI4q4UbRi5O/zwMnSJWBozB0yy84lX31ZhJeJhcrlEn1Vpcd+OUGPE8Z1hBjnw== -xterm-addon-webgl@0.7.0-beta.10: - version "0.7.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0-beta.10.tgz#39fdb96351e97a1bf15f4c4c8944ba3d05cacee4" - integrity sha512-nQl/ASk+ck11aSrBZXb2a0tu+SNDnm89owBk/sAZeZzi5MHNo6bB8y2VTKNNC6D3i3aFouTz4VorYB25LUgNFg== - -xterm@4.6.0-beta.44: - version "4.6.0-beta.44" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.44.tgz#76b2a6b8e147595ab44aa752c0e721d935464615" - integrity sha512-vYtfz4spFcSKLEUpC6anH7TwDams71+k2wAtUzCJ47dNL2IrwYafcFsvGPm46QLTtq4M2Bp9rQo3R3V746yxNg== +xterm@4.7.0-beta.3: + version "4.7.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.7.0-beta.3.tgz#d8997f190430d750201717adf3857f6c8052f149" + integrity sha512-mL9VCB7Ql7KSql2PJmRQYba77mMXlliK9lVKd3XCDqtOYYWjg+CKKeNtFljIrPoiI25nvoqlkrv5dFuuIAR5hA== diff --git a/remote/yarn.lock b/remote/yarn.lock index 20fbc3637a4..7ba3b88854f 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -404,30 +404,25 @@ vscode-windows-registry@1.0.2: resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a" integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA== -xterm-addon-search@0.7.0-beta.2: - version "0.7.0-beta.2" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.7.0-beta.2.tgz#384bda136c707f97a77eefc76cc7d9e572ce0719" - integrity sha512-A9fyiBBvG6ZNIwSJ03+sRCv9y20/uzd1wjCoaYUqp9fu3YGiHaGwyo9rAfm2M/fQM5vBmyJk4Qw/lwVq7TtlAw== +xterm-addon-search@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.7.0.tgz#c929d3e5cbb335e82bff72f158ea82936d9cd4ef" + integrity sha512-6060evmJJ+tZcjnx33FXaeEHLpuXEa7l9UzUsYfMlCKbu88AbE+5LJocTKCHYd71cwCwb9pjmv/G1o9Rf9Zbcg== -xterm-addon-unicode11@0.2.0-beta.5: - version "0.2.0-beta.5" - resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0-beta.5.tgz#5961850162df20b5e966166423cd6957ac2db298" - integrity sha512-IjnbBcyfS5JgJDXPO0W2nk/VBtGwx6GWE2snMC676z4DmAABUqPXfTzJKfUoWqoT6UcbxB0oIjDzykCfoRJp6Q== +xterm-addon-unicode11@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.2.0.tgz#9ed0c482b353908bba27778893ca80823382737c" + integrity sha512-rjFDItPc/IDoSiEnoDFwKroNwLD/7t9vYKENjrcKVZg5tgJuuUj8D4rZtP6iVCjSB1LTLYmUs4L/EmCqIyLR/Q== -xterm-addon-web-links@0.4.0-beta.6: - version "0.4.0-beta.6" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.4.0-beta.6.tgz#d159d4542eb9a02d57977fe7eb5f42f8ef2f27fa" - integrity sha512-dsQVD/EyVq8PtAYGh2PGQTCt009UipIfX6Q2SBDlz+W9x7IkXjhRxRaryMmLsBCca20qeVKwmbQ+ANhLi+nTaQ== +xterm-addon-webgl@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0.tgz#a13732ac937170e53ce02ec91963da042c80614b" + integrity sha512-PMWLgccAF31GulCYkQxIA8qwMI4q4UbRi5O/zwMnSJWBozB0yy84lX31ZhJeJhcrlEn1Vpcd+OUGPE8Z1hBjnw== -xterm-addon-webgl@0.7.0-beta.10: - version "0.7.0-beta.10" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.7.0-beta.10.tgz#39fdb96351e97a1bf15f4c4c8944ba3d05cacee4" - integrity sha512-nQl/ASk+ck11aSrBZXb2a0tu+SNDnm89owBk/sAZeZzi5MHNo6bB8y2VTKNNC6D3i3aFouTz4VorYB25LUgNFg== - -xterm@4.6.0-beta.44: - version "4.6.0-beta.44" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.6.0-beta.44.tgz#76b2a6b8e147595ab44aa752c0e721d935464615" - integrity sha512-vYtfz4spFcSKLEUpC6anH7TwDams71+k2wAtUzCJ47dNL2IrwYafcFsvGPm46QLTtq4M2Bp9rQo3R3V746yxNg== +xterm@4.7.0-beta.3: + version "4.7.0-beta.3" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.7.0-beta.3.tgz#d8997f190430d750201717adf3857f6c8052f149" + integrity sha512-mL9VCB7Ql7KSql2PJmRQYba77mMXlliK9lVKd3XCDqtOYYWjg+CKKeNtFljIrPoiI25nvoqlkrv5dFuuIAR5hA== yauzl@^2.9.2: version "2.10.0" diff --git a/scripts/code-web.js b/scripts/code-web.js index b7441ea0158..e78d39835b5 100755 --- a/scripts/code-web.js +++ b/scripts/code-web.js @@ -14,6 +14,7 @@ const path = require('path'); const util = require('util'); const opn = require('opn'); const minimist = require('minimist'); +const webpack = require('webpack'); const APP_ROOT = path.dirname(__dirname); const EXTENSIONS_ROOT = path.join(APP_ROOT, 'extensions'); @@ -21,6 +22,7 @@ const WEB_MAIN = path.join(APP_ROOT, 'src', 'vs', 'code', 'browser', 'workbench' const args = minimist(process.argv, { boolean: [ + 'watch', 'no-launch', 'help' ], @@ -35,6 +37,7 @@ const args = minimist(process.argv, { if (args.help) { console.log( 'yarn web [options]\n' + + ' --watch Watch extensions that require browser specific builds\n' + ' --no-launch Do not open VSCode web in the browser\n' + ' --scheme Protocol (https or http)\n' + ' --host Remote host\n' + @@ -53,6 +56,104 @@ const SCHEME = args.scheme || process.env.VSCODE_SCHEME || 'http'; const HOST = args.host || 'localhost'; const AUTHORITY = process.env.VSCODE_AUTHORITY || `${HOST}:${PORT}`; +const exists = (path) => util.promisify(fs.exists)(path); +const readFile = (path) => util.promisify(fs.readFile)(path); +const CharCode_PC = '%'.charCodeAt(0); + +async function initialize() { + const extensionFolders = await util.promisify(fs.readdir)(EXTENSIONS_ROOT); + + const staticExtensions = []; + + const webpackConfigs = []; + + await Promise.all(extensionFolders.map(async extensionFolder => { + const packageJSONPath = path.join(EXTENSIONS_ROOT, extensionFolder, 'package.json'); + if (await exists(packageJSONPath)) { + try { + const packageJSON = JSON.parse((await readFile(packageJSONPath)).toString()); + if (packageJSON.main && !packageJSON.browser) { + return; // unsupported + } + + if (packageJSON.browser) { + packageJSON.main = packageJSON.browser; + const webpackConfigPath = path.join(EXTENSIONS_ROOT, extensionFolder, 'extension-browser.webpack.config.js'); + if ((await exists(webpackConfigPath))) { + const configOrFnOrArray = require(webpackConfigPath); + function addConfig(configOrFn) { + if (typeof configOrFn === 'function') { + webpackConfigs.push(configOrFn({}, {})); + } else { + webpackConfigs.push(configOrFn); + } + } + if (Array.isArray(configOrFnOrArray)) { + configOrFnOrArray.forEach(addConfig); + } else { + addConfig(configOrFnOrArray); + } + } + } + + const packageNlsPath = path.join(EXTENSIONS_ROOT, extensionFolder, 'package.nls.json'); + if (await exists(packageNlsPath)) { + const packageNls = JSON.parse((await readFile(packageNlsPath)).toString()); + const translate = (obj) => { + for (let key in obj) { + const val = obj[key]; + if (Array.isArray(val)) { + val.forEach(translate); + } else if (val && typeof val === 'object') { + translate(val); + } else if (typeof val === 'string' && val.charCodeAt(0) === CharCode_PC && val.charCodeAt(val.length - 1) === CharCode_PC) { + const translated = packageNls[val.substr(1, val.length - 2)]; + if (translated) { + obj[key] = translated; + } + } + } + }; + translate(packageJSON); + } + packageJSON.extensionKind = ['web']; // enable for Web + staticExtensions.push({ + packageJSON, + extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/static-extension/${extensionFolder}` } + }); + } catch (e) { + console.log(e); + } + } + })); + + return new Promise((resolve, reject) => { + if (args.watch) { + webpack(webpackConfigs).watch({}, (err, stats) => { + if (err) { + console.log(err); + reject(); + } else { + console.log(stats.toString()); + resolve(staticExtensions); + } + }); + } else { + webpack(webpackConfigs).run((err, stats) => { + if (err) { + console.log(err); + reject(); + } else { + console.log(stats.toString()); + resolve(staticExtensions); + } + }); + } + }); +} + +const staticExtensionsPromise = initialize(); + const server = http.createServer((req, res) => { const parsedUrl = url.parse(req.url, true); const pathname = parsedUrl.pathname; @@ -139,43 +240,25 @@ function handleStaticExtension(req, res, parsedUrl) { * @param {import('http').ServerResponse} res */ async function handleRoot(req, res) { - const extensionFolders = await util.promisify(fs.readdir)(EXTENSIONS_ROOT); - const mapExtensionFolderToExtensionPackageJSON = new Map(); - - await Promise.all(extensionFolders.map(async extensionFolder => { - try { - const packageJSON = JSON.parse((await util.promisify(fs.readFile)(path.join(EXTENSIONS_ROOT, extensionFolder, 'package.json'))).toString()); - if (packageJSON.main && packageJSON.name !== 'vscode-web-playground') { - return; // unsupported - } - - if (packageJSON.name === 'scss') { - return; // seems to fail to JSON.parse()?! - } - - packageJSON.extensionKind = ['web']; // enable for Web - - mapExtensionFolderToExtensionPackageJSON.set(extensionFolder, packageJSON); - } catch (error) { - return null; + const match = req.url && req.url.match(/\?([^#]+)/); + let ghPath; + if (match) { + const qs = new URLSearchParams(match[1]); + ghPath = qs.get('gh'); + if (ghPath && !ghPath.startsWith('/')) { + ghPath = '/' + ghPath; } + } + + const staticExtensions = await staticExtensionsPromise; + const webConfiguration = escapeAttribute(JSON.stringify({ + staticExtensions, folderUri: ghPath + ? { scheme: 'github', authority: 'github.com', path: ghPath } + : { scheme: 'memfs', path: `/sample-folder` } })); - const staticExtensions = []; - - // Built in extensions - mapExtensionFolderToExtensionPackageJSON.forEach((packageJSON, extensionFolder) => { - staticExtensions.push({ - packageJSON, - extensionLocation: { scheme: SCHEME, authority: AUTHORITY, path: `/static-extension/${extensionFolder}` } - }); - }); - const data = (await util.promisify(fs.readFile)(WEB_MAIN)).toString() - .replace('{{WORKBENCH_WEB_CONFIGURATION}}', escapeAttribute(JSON.stringify({ - staticExtensions, - folderUri: { scheme: 'memfs', path: `/sample-folder` } - }))) + .replace('{{WORKBENCH_WEB_CONFIGURATION}}', () => webConfiguration) // use a replace function to avoid that regexp replace patterns ($&, $0, ...) are applied .replace('{{WEBVIEW_ENDPOINT}}', '') .replace('{{REMOTE_USER_DATA_URI}}', ''); diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index e5e6c7d8892..f250eb40f80 100644 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -42,6 +42,9 @@ if %errorlevel% neq 0 exit /b %errorlevel% :: Tests in the extension host +call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-notebook-tests\test --enable-proposed-api=vscode.vscode-notebook-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-notebook-tests --extensionTestsPath=%~dp0\..\extensions\vscode-notebook-tests\out --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +if %errorlevel% neq 0 exit /b %errorlevel% + call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\vscode-api-tests\testWorkspace --enable-proposed-api=vscode.vscode-api-tests --extensionDevelopmentPath=%~dp0\..\extensions\vscode-api-tests --extensionTestsPath=%~dp0\..\extensions\vscode-api-tests\out\singlefolder-tests --disable-telemetry --crash-reporter-directory=%VSCODECRASHDIR% --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 0342b23c838..25a7cc7a278 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -60,4 +60,4 @@ fi 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 -rm -r $VSCODEUSERDATADIR +rm -rf $VSCODEUSERDATADIR diff --git a/src/main.js b/src/main.js index 23915126b8f..1eef87ad080 100644 --- a/src/main.js +++ b/src/main.js @@ -79,7 +79,15 @@ setCurrentWorkingDirectory(); // Register custom schemes with privileges protocol.registerSchemesAsPrivileged([ - { scheme: 'vscode-resource', privileges: { secure: true, supportFetchAPI: true, corsEnabled: true } } + { + scheme: 'vscode-webview-resource', + privileges: { + secure: true, + standard: true, + supportFetchAPI: true, + corsEnabled: true, + } + }, ]); // Global app listeners diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index caf1d299038..6a5d3f79d2b 100644 --- a/src/vs/base/browser/contextmenu.ts +++ b/src/vs/base/browser/contextmenu.ts @@ -34,4 +34,5 @@ export interface IContextMenuDelegate { actionRunner?: IActionRunner; autoSelectFirstItem?: boolean; anchorAlignment?: AnchorAlignment; + anchorAsContainer?: boolean; } diff --git a/src/vs/base/browser/markdownRenderer.ts b/src/vs/base/browser/markdownRenderer.ts index da658dd0d57..4726ad8b8de 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -111,7 +111,10 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende } href = _href(href, false); if (options.baseUrl) { - href = resolvePath(options.baseUrl, href).toString(); + const hasScheme = /^\w[\w\d+.-]*:/.test(href); + if (!hasScheme) { + href = resolvePath(options.baseUrl, href).toString(); + } } title = removeMarkdownEscapes(title); href = removeMarkdownEscapes(href); @@ -185,6 +188,13 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende })); } + // Use our own sanitizer so that we can let through only spans. + // Otherwise, we'd be letting all html be rendered. + // If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize. + markedOptions.sanitizer = (html: string): string => { + const match = markdown.isTrusted ? html.match(/^()|(<\/\s*span>)$/) : undefined; + return match ? html : ''; + }; markedOptions.sanitize = true; markedOptions.renderer = renderer; @@ -200,18 +210,32 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende markedOptions ); + function filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean { + if (token.tag === 'span' && markdown.isTrusted) { + if (token.attrs['style'] && Object.keys(token.attrs).length === 1) { + return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/); + } + return false; + } + return true; + } + element.innerHTML = insane(renderedMarkdown, { allowedSchemes, + // allowedTags should included everything that markdown renders to. + // Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure. + // HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/ + allowedTags: ['ul', 'li', 'p', 'code', 'blockquote', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'em', 'pre', 'table', 'tr', 'td', 'div', 'del', 'a', 'strong', 'br', 'img', 'span'], allowedAttributes: { 'a': ['href', 'name', 'target', 'data-href'], - 'iframe': ['allowfullscreen', 'frameborder', 'src'], 'img': ['src', 'title', 'alt', 'width', 'height'], 'div': ['class', 'data-code'], - 'span': ['class'], + 'span': ['class', 'style'], // https://github.com/microsoft/vscode/issues/95937 'th': ['align'], 'td': ['align'] - } + }, + filter }); signalInnerHTML!(); diff --git a/src/vs/base/browser/ui/actionbar/actionbar.ts b/src/vs/base/browser/ui/actionbar/actionbar.ts index ba7c5ce4344..1f68379260d 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -405,6 +405,7 @@ export interface IActionBarOptions { ariaLabel?: string; animated?: boolean; triggerKeys?: ActionTrigger; + allowContextMenu?: boolean; } const defaultOptions: IActionBarOptions = { @@ -634,9 +635,11 @@ export class ActionBar extends Disposable implements IActionRunner { actionViewItemElement.setAttribute('role', 'presentation'); // Prevent native context menu on actions - this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => { - DOM.EventHelper.stop(e, true); - })); + if (!this.options.allowContextMenu) { + this._register(DOM.addDisposableListener(actionViewItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => { + DOM.EventHelper.stop(e, true); + })); + } let item: IActionViewItem | undefined; diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf index abcf8283afa..ba25be2a8e9 100644 Binary files a/src/vs/base/browser/ui/codicons/codicon/codicon.ttf and b/src/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/src/vs/base/browser/ui/contextview/contextview.css b/src/vs/base/browser/ui/contextview/contextview.css index b1b67bbe3c3..bb7ebbcfdb2 100644 --- a/src/vs/base/browser/ui/contextview/contextview.css +++ b/src/vs/base/browser/ui/contextview/contextview.css @@ -7,3 +7,12 @@ position: absolute; z-index: 2500; } + +.context-view.fixed { + all: initial; + font-family: inherit; + font-size: 13px; + position: fixed; + z-index: 2500; + color: inherit; +} diff --git a/src/vs/base/browser/ui/contextview/contextview.ts b/src/vs/base/browser/ui/contextview/contextview.ts index eacf37f358a..7be14b621ab 100644 --- a/src/vs/base/browser/ui/contextview/contextview.ts +++ b/src/vs/base/browser/ui/contextview/contextview.ts @@ -38,7 +38,7 @@ export interface IDelegate { } export interface IContextViewProvider { - showContextView(delegate: IDelegate): void; + showContextView(delegate: IDelegate, container?: HTMLElement): void; hideContextView(): void; layout(): void; } @@ -104,23 +104,25 @@ export class ContextView extends Disposable { private container: HTMLElement | null = null; private view: HTMLElement; + private useFixedPosition: boolean; private delegate: IDelegate | null = null; private toDisposeOnClean: IDisposable = Disposable.None; private toDisposeOnSetContainer: IDisposable = Disposable.None; - constructor(container: HTMLElement) { + constructor(container: HTMLElement, useFixedPosition: boolean) { super(); this.view = DOM.$('.context-view'); + this.useFixedPosition = false; DOM.hide(this.view); - this.setContainer(container); + this.setContainer(container, useFixedPosition); - this._register(toDisposable(() => this.setContainer(null))); + this._register(toDisposable(() => this.setContainer(null, false))); } - setContainer(container: HTMLElement | null): void { + setContainer(container: HTMLElement | null, useFixedPosition: boolean): void { if (this.container) { this.toDisposeOnSetContainer.dispose(); this.container.removeChild(this.view); @@ -146,6 +148,8 @@ export class ContextView extends Disposable { this.toDisposeOnSetContainer = toDisposeOnSetContainer; } + + this.useFixedPosition = useFixedPosition; } show(delegate: IDelegate): void { @@ -254,10 +258,11 @@ export class ContextView extends Disposable { DOM.removeClasses(this.view, 'top', 'bottom', 'left', 'right'); DOM.addClass(this.view, anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top'); DOM.addClass(this.view, anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right'); + DOM.toggleClass(this.view, 'fixed', this.useFixedPosition); const containerPosition = DOM.getDomNodePagePosition(this.container!); - this.view.style.top = `${top - containerPosition.top}px`; - this.view.style.left = `${left - containerPosition.left}px`; + this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top)}px`; + this.view.style.left = `${left - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).left : containerPosition.left)}px`; this.view.style.width = 'initial'; } diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 089af090e28..623aa9c00d0 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -73,6 +73,7 @@ export class Dialog extends Disposable { this.modal = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`)); this.shadowElement = this.modal.appendChild($('.dialog-shadow')); this.element = this.shadowElement.appendChild($('.monaco-dialog-box')); + this.element.setAttribute('role', 'dialog'); hide(this.element); // If no button is provided, default to OK @@ -109,6 +110,28 @@ export class Dialog extends Disposable { this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); } + private getAriaLabel(): string { + let typeLabel = nls.localize('dialogInfoMessage', 'Info'); + switch (this.options.type) { + case 'error': + nls.localize('dialogErrorMessage', 'Error'); + break; + case 'warning': + nls.localize('dialogWarningMessage', 'Warning'); + break; + case 'pending': + nls.localize('dialogPendingMessage', 'In Progress'); + break; + case 'none': + case 'info': + case 'question': + default: + break; + } + + return `${typeLabel}: ${this.message} ${this.options.detail || ''}`; + } + updateMessage(message: string): void { if (this.messageDetailElement) { this.messageDetailElement.innerText = message; @@ -242,7 +265,7 @@ export class Dialog extends Disposable { this.applyStyles(); - this.element.setAttribute('aria-label', this.message); + this.element.setAttribute('aria-label', this.getAriaLabel()); show(this.element); // Focus first element diff --git a/src/vs/base/browser/ui/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 99c11cdd6e2..22693e1971c 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -52,7 +52,7 @@ export class BaseDropdown extends ActionRunner { } for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap]) { - this._register(addDisposableListener(this._label, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger + this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger } for (const event of [EventType.MOUSE_DOWN, GestureEventType.Tap]) { @@ -266,7 +266,8 @@ export class DropdownMenu extends BaseDropdown { getMenuClassName: () => this.menuClassName, onHide: () => this.onHide(), actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined, - anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT + anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : AnchorAlignment.LEFT, + anchorAsContainer: true }); } diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index a61fd2c5bab..b08b72a9479 100644 --- a/src/vs/base/browser/ui/grid/gridview.ts +++ b/src/vs/base/browser/ui/grid/gridview.ts @@ -380,7 +380,7 @@ class BranchNode implements ISplitView, IDisposable { throw new Error('Invalid index'); } - this.splitview.addView(node, size, index); + this.splitview.addView(node, size, index, skipLayout); this._addChild(node, index); this.onDidChildrenChange(); } @@ -791,7 +791,7 @@ function flipNode(node: T, size: number, orthogonalSize: number) newSize += size - totalSize; } - result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0); + result.addChild(flipNode(child, orthogonalSize, newSize), newSize, 0, true); } return result as T; diff --git a/src/vs/editor/contrib/hover/hover.css b/src/vs/base/browser/ui/hover/hover.css similarity index 57% rename from src/vs/editor/contrib/hover/hover.css rename to src/vs/base/browser/ui/hover/hover.css index 9c7aae9f330..33957a8e43b 100644 --- a/src/vs/editor/contrib/hover/hover.css +++ b/src/vs/base/browser/ui/hover/hover.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-editor-hover { +.monaco-hover { cursor: default; position: absolute; overflow: hidden; @@ -16,34 +16,34 @@ line-height: 1.5em; } -.monaco-editor-hover.hidden { +.monaco-hover.hidden { display: none; } -.monaco-editor-hover .hover-contents { +.monaco-hover .hover-contents { padding: 4px 8px; } -.monaco-editor-hover .markdown-hover > .hover-contents:not(.code-hover-contents) { +.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) { max-width: 500px; word-wrap: break-word; } -.monaco-editor-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr { +.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr { /* This is a strange rule but it avoids https://github.com/microsoft/vscode/issues/96795, just 100vw on its own caused the actual hover width to increase */ min-width: calc(100% + 100vw); } -.monaco-editor-hover p, -.monaco-editor-hover ul { +.monaco-hover p, +.monaco-hover ul { margin: 8px 0; } -.monaco-editor-hover code { +.monaco-hover code { font-family: var(--monaco-monospace-font); } -.monaco-editor-hover hr { +.monaco-hover hr { margin-top: 4px; margin-bottom: -6px; margin-left: -10px; @@ -51,78 +51,78 @@ height: 1px; } -.monaco-editor-hover p:first-child, -.monaco-editor-hover ul:first-child { +.monaco-hover p:first-child, +.monaco-hover ul:first-child { margin-top: 0; } -.monaco-editor-hover p:last-child, -.monaco-editor-hover ul:last-child { +.monaco-hover p:last-child, +.monaco-hover ul:last-child { margin-bottom: 0; } /* MarkupContent Layout */ -.monaco-editor-hover ul { +.monaco-hover ul { padding-left: 20px; } -.monaco-editor-hover ol { +.monaco-hover ol { padding-left: 20px; } -.monaco-editor-hover li > p { +.monaco-hover li > p { margin-bottom: 0; } -.monaco-editor-hover li > ul { +.monaco-hover li > ul { margin-top: 0; } -.monaco-editor-hover code { +.monaco-hover code { border-radius: 3px; padding: 0 0.4em; } -.monaco-editor-hover .monaco-tokenized-source { +.monaco-hover .monaco-tokenized-source { white-space: pre-wrap; word-break: break-all; } -.monaco-editor-hover .hover-row.status-bar { +.monaco-hover .hover-row.status-bar { font-size: 12px; line-height: 22px; } -.monaco-editor-hover .hover-row.status-bar .actions { +.monaco-hover .hover-row.status-bar .actions { display: flex; padding: 0px 8px; } -.monaco-editor-hover .hover-row.status-bar .actions .action-container { +.monaco-hover .hover-row.status-bar .actions .action-container { margin-right: 16px; cursor: pointer; } -.monaco-editor-hover .hover-row.status-bar .actions .action-container .action .icon { +.monaco-hover .hover-row.status-bar .actions .action-container .action .icon { padding-right: 4px; } -.monaco-editor-hover .markdown-hover .hover-contents .codicon { +.monaco-hover .markdown-hover .hover-contents .codicon { color: inherit; font-size: inherit; vertical-align: middle; } -.monaco-editor-hover .hover-contents a.code-link:before { +.monaco-hover .hover-contents a.code-link:before { content: '('; } -.monaco-editor-hover .hover-contents a.code-link:after { +.monaco-hover .hover-contents a.code-link:after { content: ')'; } -.monaco-editor-hover .hover-contents a.code-link { +.monaco-hover .hover-contents a.code-link { color: inherit; } -.monaco-editor-hover .hover-contents a.code-link > span { +.monaco-hover .hover-contents a.code-link > span { text-decoration: underline; /** Hack to force underline to show **/ border-bottom: 1px solid transparent; diff --git a/src/vs/base/browser/ui/hover/hoverWidget.ts b/src/vs/base/browser/ui/hover/hoverWidget.ts new file mode 100644 index 00000000000..6162569486e --- /dev/null +++ b/src/vs/base/browser/ui/hover/hoverWidget.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 'vs/css!./hover'; +import * as dom from 'vs/base/browser/dom'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; + +const $ = dom.$; + +export class HoverWidget extends Disposable { + + public readonly containerDomNode: HTMLElement; + public readonly contentsDomNode: HTMLElement; + private readonly _scrollbar: DomScrollableElement; + + constructor() { + super(); + + this.containerDomNode = document.createElement('div'); + this.containerDomNode.className = 'monaco-hover'; + this.containerDomNode.tabIndex = 0; + this.containerDomNode.setAttribute('role', 'tooltip'); + + this.contentsDomNode = document.createElement('div'); + this.contentsDomNode.className = 'monaco-hover-content'; + + this._scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {})); + this.containerDomNode.appendChild(this._scrollbar.getDomNode()); + } + + public onContentsChanged(): void { + this._scrollbar.scanDomNode(); + } +} + +export function renderHoverAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }, keybindingLabel: string | null): IDisposable { + const actionContainer = dom.append(parent, $('div.action-container')); + const action = dom.append(actionContainer, $('a.action')); + action.setAttribute('href', '#'); + action.setAttribute('role', 'button'); + if (actionOptions.iconClass) { + dom.append(action, $(`span.icon.${actionOptions.iconClass}`)); + } + const label = dom.append(action, $('span')); + label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label; + return dom.addDisposableListener(actionContainer, dom.EventType.CLICK, e => { + e.stopPropagation(); + e.preventDefault(); + actionOptions.run(actionContainer); + }); +} diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index b76361c2695..84978eab45d 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -9,8 +9,8 @@ import { Gesture, EventType as TouchEventType, GestureEvent } from 'vs/base/brow import * as DOM from 'vs/base/browser/dom'; import { Event, Emitter } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; -import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; -import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions } from 'vs/base/common/scrollable'; +import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions, Scrollable } from 'vs/base/common/scrollable'; import { RangeMap, shift } from './rangeMap'; import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListDragEvent, IListDragAndDrop, ListDragOverEffect } from './list'; import { RowCache, IRow } from './rowCache'; @@ -44,11 +44,16 @@ export interface IListViewDragAndDrop extends IListDragAndDrop { export interface IListViewAccessibilityProvider { getSetSize?(element: T, index: number, listLength: number): number; getPosInSet?(element: T, index: number): number; - getRole?(element: T): string; + getRole?(element: T): string | undefined; isChecked?(element: T): boolean | undefined; } -export interface IListViewOptions { +export interface IListViewOptionsUpdate { + readonly additionalScrollHeight?: number; + readonly smoothScrolling?: boolean; +} + +export interface IListViewOptions extends IListViewOptionsUpdate { readonly dnd?: IListViewDragAndDrop; readonly useShadows?: boolean; readonly verticalScrollMode?: ScrollbarVisibility; @@ -58,7 +63,6 @@ export interface IListViewOptions { readonly mouseSupport?: boolean; readonly horizontalScrolling?: boolean; readonly accessibilityProvider?: IListViewAccessibilityProvider; - readonly additionalScrollHeight?: number; readonly transformOptimization?: boolean; } @@ -158,7 +162,7 @@ class ListViewAccessibilityProvider implements Required number; readonly getPosInSet: (element: any, index: number) => number; - readonly getRole: (element: T) => string; + readonly getRole: (element: T) => string | undefined; readonly isChecked: (element: T) => boolean | undefined; constructor(accessibilityProvider?: IListViewAccessibilityProvider) { @@ -204,7 +208,8 @@ export class ListView implements ISpliceable, IDisposable { private lastRenderHeight: number; private renderWidth = 0; private rowsContainer: HTMLElement; - private scrollableElement: ScrollableElement; + private scrollable: Scrollable; + private scrollableElement: SmoothScrollableElement; private _scrollHeight: number = 0; private scrollableElementUpdateDisposable: IDisposable | null = null; private scrollableElementWidthDelayer = new Delayer(50); @@ -285,12 +290,13 @@ export class ListView implements ISpliceable, IDisposable { this.disposables.add(Gesture.addTarget(this.rowsContainer)); - this.scrollableElement = this.disposables.add(new ScrollableElement(this.rowsContainer, { + this.scrollable = new Scrollable(getOrDefault(options, o => o.smoothScrolling, false) ? 125 : 0, cb => DOM.scheduleAtNextAnimationFrame(cb)); + this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, { alwaysConsumeMouseWheel: true, horizontal: this.horizontalScrolling ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden, vertical: getOrDefault(options, o => o.verticalScrollMode, DefaultOptions.verticalScrollMode), - useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows) - })); + useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows), + }, this.scrollable)); this.domNode.appendChild(this.scrollableElement.getDomNode()); container.appendChild(this.domNode); @@ -320,6 +326,10 @@ export class ListView implements ISpliceable, IDisposable { if (options.additionalScrollHeight !== undefined) { this.additionalScrollHeight = options.additionalScrollHeight; } + + if (options.smoothScrolling !== undefined) { + this.scrollable.setSmoothScrollDuration(options.smoothScrolling ? 125 : 0); + } } triggerScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent) { @@ -652,7 +662,7 @@ export class ListView implements ISpliceable, IDisposable { if (!item.row) { item.row = this.cache.alloc(item.templateId); - const role = this.accessibilityProvider.getRole(item.element); + const role = this.accessibilityProvider.getRole(item.element) || 'listitem'; item.row!.domNode!.setAttribute('role', role); const checked = this.accessibilityProvider.isChecked(item.element); if (typeof checked !== 'undefined') { diff --git a/src/vs/base/browser/ui/list/listWidget.ts b/src/vs/base/browser/ui/list/listWidget.ts index 856b97f1a6a..ca09bbf018a 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -16,7 +16,7 @@ import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardE import { Event, Emitter, EventBufferer } from 'vs/base/common/event'; import { domEvent } from 'vs/base/browser/event'; import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListError, IKeyboardNavigationDelegate } from './list'; -import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider } from './listView'; +import { ListView, IListViewOptions, IListViewDragAndDrop, IListViewAccessibilityProvider, IListViewOptionsUpdate } from './listView'; import { Color } from 'vs/base/common/color'; import { mixin } from 'vs/base/common/objects'; import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable'; @@ -1107,10 +1107,9 @@ class ListViewDragAndDrop implements IListViewDragAndDrop { } } -export interface IListOptionsUpdate { +export interface IListOptionsUpdate extends IListViewOptionsUpdate { readonly enableKeyboardNavigation?: boolean; readonly automaticKeyboardNavigation?: boolean; - readonly additionalScrollHeight?: number; } export class List implements ISpliceable, IDisposable { @@ -1288,9 +1287,7 @@ export class List implements ISpliceable, IDisposable { this.typeLabelController.updateOptions(this._options); } - if (optionsUpdate.additionalScrollHeight !== undefined) { - this.view.updateOptions(optionsUpdate); - } + this.view.updateOptions(optionsUpdate); } get options(): IListOptions { diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css index 140168678a2..d86ee055d51 100644 --- a/src/vs/base/browser/ui/menu/menu.css +++ b/src/vs/base/browser/ui/menu/menu.css @@ -88,6 +88,9 @@ padding: 0.5em 0 0 0; margin-bottom: 0.5em; width: 100%; + height: 0px !important; + margin-left: .8em !important; + margin-right: .8em !important; } .monaco-menu .monaco-action-bar.vertical .action-label.separator.text { diff --git a/src/vs/base/browser/ui/menu/menu.ts b/src/vs/base/browser/ui/menu/menu.ts index 8612b5b1b84..ed48038df11 100644 --- a/src/vs/base/browser/ui/menu/menu.ts +++ b/src/vs/base/browser/ui/menu/menu.ts @@ -41,6 +41,7 @@ export interface IMenuOptions { enableMnemonics?: boolean; anchorAlignment?: AnchorAlignment; expandDirection?: Direction; + useEventAsContext?: boolean; } export interface IMenuStyles { @@ -316,7 +317,7 @@ export class Menu extends ActionBar { return menuActionViewItem; } else { - const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics }; + const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics, useEventAsContext: options.useEventAsContext }; if (options.getKeyBinding) { const keybinding = options.getKeyBinding(action); if (keybinding) { diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index 463073dc0a7..f4506d877b5 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -945,7 +945,8 @@ export class MenuBar extends Disposable { actionRunner: this.actionRunner, enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics), ariaLabel: withNullAsUndefined(customMenu.buttonElement.getAttribute('aria-label')), - expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right + expandDirection: this.options.compactMode !== undefined ? this.options.compactMode : Direction.Right, + useEventAsContext: true }; let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions)); diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index da2f2b6cee8..933af1512ed 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -533,6 +533,14 @@ export class SmoothScrollableElement extends AbstractScrollableElement { super(element, options, scrollable); } + public setScrollPosition(update: INewScrollPosition): void { + this._scrollable.setScrollPositionNow(update); + } + + public getScrollPosition(): IScrollPosition { + return this._scrollable.getCurrentScrollPosition(); + } + } export class DomScrollableElement extends ScrollableElement { diff --git a/src/vs/base/browser/ui/selectBox/selectBox.css b/src/vs/base/browser/ui/selectBox/selectBox.css index f684dd1085c..199ca5f98b8 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.css +++ b/src/vs/base/browser/ui/selectBox/selectBox.css @@ -6,3 +6,9 @@ .monaco-select-box { width: 100%; } + +.monaco-select-box-dropdown-container { + font-size: 13px; + font-weight: normal; + text-transform: none; +} diff --git a/src/vs/base/browser/ui/selectBox/selectBox.ts b/src/vs/base/browser/ui/selectBox/selectBox.ts index cff14f50206..79f0da0e6cd 100644 --- a/src/vs/base/browser/ui/selectBox/selectBox.ts +++ b/src/vs/base/browser/ui/selectBox/selectBox.ts @@ -40,6 +40,7 @@ export interface ISelectBoxOptions { useCustomDrawn?: boolean; ariaLabel?: string; minBottomMargin?: number; + optionsAsChildren?: boolean; } // Utilize optionItem interface to capture all option parameters diff --git a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts index 4924646f11c..efb7421773a 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -89,6 +89,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private _isVisible: boolean; private selectBoxOptions: ISelectBoxOptions; private selectElement: HTMLSelectElement; + private container?: HTMLElement; private options: ISelectOptionItem[] = []; private selected: number; private readonly _onDidSelect: Emitter; @@ -307,6 +308,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi } public render(container: HTMLElement): void { + this.container = container; dom.addClass(container, 'select-container'); container.appendChild(this.selectElement); this.applyStyles(); @@ -442,7 +444,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi dom.toggleClass(this.selectElement, 'synthetic-focus', false); }, anchorPosition: this._dropDownPosition - }); + }, this.selectBoxOptions.optionsAsChildren ? this.container : undefined); // Hide so we can relay out this._isVisible = true; @@ -457,7 +459,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi dom.toggleClass(this.selectElement, 'synthetic-focus', false); }, anchorPosition: this._dropDownPosition - }); + }, this.selectBoxOptions.optionsAsChildren ? this.container : undefined); // Track initial selection the case user escape, blur this._currentSelection = this.selected; @@ -727,7 +729,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi mouseSupport: false, accessibilityProvider: { getAriaLabel: (element) => element.text, - getWidgetAriaLabel: () => localize('selectBox', "Select Box"), + getWidgetAriaLabel: () => localize({ key: 'selectBox', comment: ['Behave like native select dropdown element.'] }, "Select Box"), getRole: () => 'option', getWidgetRole: () => 'listbox' } diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 1a7c66cc8a9..292764c8807 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -126,7 +126,7 @@ export abstract class Pane extends Disposable implements IView { this._expanded = typeof options.expanded === 'undefined' ? true : !!options.expanded; this._orientation = typeof options.orientation === 'undefined' ? Orientation.VERTICAL : options.orientation; this.ariaHeaderLabel = localize('viewSection', "{0} Section", options.title); - this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : 120; + this._minimumBodySize = typeof options.minimumBodySize === 'number' ? options.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 200 : 120; this._maximumBodySize = typeof options.maximumBodySize === 'number' ? options.maximumBodySize : Number.POSITIVE_INFINITY; this.element = $('.pane'); @@ -263,8 +263,6 @@ export abstract class Pane extends Disposable implements IView { style(styles: IPaneStyles): void { this.styles = styles; - this.element.style.borderLeft = this.styles.leftBorder && this.orientation === Orientation.HORIZONTAL ? `1px solid ${this.styles.leftBorder}` : ''; - if (!this.header) { return; } @@ -284,6 +282,7 @@ export abstract class Pane extends Disposable implements IView { this.header.style.backgroundColor = this.styles.headerBackground ? this.styles.headerBackground.toString() : ''; this.header.style.borderTop = this.styles.headerBorder && this.orientation === Orientation.VERTICAL ? `1px solid ${this.styles.headerBorder}` : ''; this._dropBackground = this.styles.dropBackground; + this.element.style.borderLeft = this.styles.leftBorder && this.orientation === Orientation.HORIZONTAL ? `1px solid ${this.styles.leftBorder}` : ''; } protected abstract renderHeader(container: HTMLElement): void; diff --git a/src/vs/base/browser/ui/splitview/splitview.ts b/src/vs/base/browser/ui/splitview/splitview.ts index 863623a8c0d..482a3a3d51e 100644 --- a/src/vs/base/browser/ui/splitview/splitview.ts +++ b/src/vs/base/browser/ui/splitview/splitview.ts @@ -331,8 +331,8 @@ export class SplitView extends Disposable { } } - addView(view: IView, size: number | Sizing, index = this.viewItems.length): void { - this.doAddView(view, size, index, false); + addView(view: IView, size: number | Sizing, index = this.viewItems.length, skipLayout?: boolean): void { + this.doAddView(view, size, index, skipLayout); } removeView(index: number, sizing?: Sizing): IView { diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index a71e4d6ae48..bfac12835d1 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -707,7 +707,7 @@ class TypeFilterController implements IDisposable { .map(e => new StandardKeyboardEvent(e)) .filter(this.keyboardNavigationEventFilter || (() => true)) .filter(() => this.automaticKeyboardNavigation || this.triggered) - .filter(e => this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey))) + .filter(e => (this.keyboardNavigationDelegate.mightProducePrintableCharacter(e) && !(e.keyCode === KeyCode.DownArrow || e.keyCode === KeyCode.UpArrow || e.keyCode === KeyCode.LeftArrow || e.keyCode === KeyCode.RightArrow)) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? (e.altKey && !e.metaKey) : e.ctrlKey) && !e.shiftKey))) .forEach(e => { e.stopPropagation(); e.preventDefault(); }) .event; @@ -962,6 +962,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions { readonly simpleKeyboardNavigation?: boolean; readonly filterOnType?: boolean; readonly openOnSingleClick?: boolean; + readonly smoothScrolling?: boolean; } export interface IAbstractTreeOptions extends IAbstractTreeOptionsUpdate, IListOptions { @@ -1360,7 +1361,8 @@ export abstract class AbstractTree implements IDisposable this.view.updateOptions({ enableKeyboardNavigation: this._options.simpleKeyboardNavigation, - automaticKeyboardNavigation: this._options.automaticKeyboardNavigation + automaticKeyboardNavigation: this._options.automaticKeyboardNavigation, + smoothScrolling: this._options.smoothScrolling }); if (this.typeFilterController) { diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 3002b4bcb59..3c861744021 100644 --- a/src/vs/base/common/codicons.ts +++ b/src/vs/base/common/codicons.ts @@ -244,7 +244,7 @@ export namespace Codicon { export const collapseAll = new Codicon('collapse-all', { character: '\\eac5' }); export const colorMode = new Codicon('color-mode', { character: '\\eac6' }); export const commentDiscussion = new Codicon('comment-discussion', { character: '\\eac7' }); - export const compareChanges = new Codicon('compare-changes', { character: '\\eac8' }); + export const compareChanges = new Codicon('compare-changes', { character: '\\eafd' }); export const creditCard = new Codicon('credit-card', { character: '\\eac9' }); export const dash = new Codicon('dash', { character: '\\eacc' }); export const dashboard = new Codicon('dashboard', { character: '\\eacd' }); @@ -448,7 +448,6 @@ export namespace Codicon { export const debugReverseContinue = new Codicon('debug-reverse-continue', { character: '\\eb8e' }); export const debugStepBack = new Codicon('debug-step-back', { character: '\\eb8f' }); export const debugRestartFrame = new Codicon('debug-restart-frame', { character: '\\eb90' }); - export const debugAlternate = new Codicon('debug-alternate', { character: '\\eb91' }); export const callIncoming = new Codicon('call-incoming', { character: '\\eb92' }); export const callOutgoing = new Codicon('call-outgoing', { character: '\\eb93' }); export const menu = new Codicon('menu', { character: '\\eb94' }); @@ -465,10 +464,13 @@ export namespace Codicon { export const syncIgnored = new Codicon('sync-ignored', { character: '\\eb9f' }); export const pinned = new Codicon('pinned', { character: '\\eba0' }); export const githubInverted = new Codicon('github-inverted', { character: '\\eba1' }); - export const debugAlt2 = new Codicon('debug-alt-2', { character: '\\f101' }); - export const debugAlt = new Codicon('debug-alt', { character: '\\f102' }); + export const debugAlt = new Codicon('debug-alt', { character: '\\eb91' }); export const serverProcess = new Codicon('server-process', { character: '\\eba2' }); export const serverEnvironment = new Codicon('server-environment', { character: '\\eba3' }); + export const pass = new Codicon('pass', { character: '\\eba4' }); + export const stopCircle = new Codicon('stop-circle', { character: '\\eba5' }); + export const playCircle = new Codicon('play-circle', { character: '\\eba6' }); + export const record = new Codicon('record', { character: '\\eba7' }); } diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 032f447081e..b0bd0ab2799 100644 --- a/src/vs/base/common/event.ts +++ b/src/vs/base/common/event.ts @@ -328,8 +328,8 @@ export namespace Event { } export interface NodeEventEmitter { - on(event: string | symbol, listener: Function): this; - removeListener(event: string | symbol, listener: Function): this; + on(event: string | symbol, listener: Function): unknown; + removeListener(event: string | symbol, listener: Function): unknown; } export function fromNodeEventEmitter(emitter: NodeEventEmitter, eventName: string, map: (...args: any[]) => T = id => id): Event { diff --git a/src/vs/base/common/extpath.ts b/src/vs/base/common/extpath.ts index e064829c5a0..8fe9f9e1c2a 100644 --- a/src/vs/base/common/extpath.ts +++ b/src/vs/base/common/extpath.ts @@ -7,6 +7,7 @@ import { isWindows } from 'vs/base/common/platform'; import { startsWithIgnoreCase, equalsIgnoreCase, rtrim } from 'vs/base/common/strings'; import { CharCode } from 'vs/base/common/charCode'; import { sep, posix, isAbsolute, join, normalize } from 'vs/base/common/path'; +import { isNumber } from 'vs/base/common/types'; export function isPathSeparator(code: number) { return code === CharCode.Slash || code === CharCode.Backslash; @@ -300,3 +301,38 @@ export function indexOfPath(path: string, candidate: string, ignoreCase: boolean return path.indexOf(candidate); } + +export interface IPathWithLineAndColumn { + path: string; + line?: number; + column?: number; +} + +export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn { + const segments = rawPath.split(':'); // C:\file.txt:: + + let path: string | undefined = undefined; + let line: number | undefined = undefined; + let column: number | undefined = undefined; + + segments.forEach(segment => { + const segmentAsNumber = Number(segment); + if (!isNumber(segmentAsNumber)) { + path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...) + } else if (line === undefined) { + line = segmentAsNumber; + } else if (column === undefined) { + column = segmentAsNumber; + } + }); + + if (!path) { + throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`'); + } + + return { + path, + line: line !== undefined ? line : undefined, + column: column !== undefined ? column : line !== undefined ? 1 : undefined // if we have a line, make sure column is also set + }; +} diff --git a/src/vs/base/common/fuzzyScorer.ts b/src/vs/base/common/fuzzyScorer.ts index 8599e947205..278dc001699 100644 --- a/src/vs/base/common/fuzzyScorer.ts +++ b/src/vs/base/common/fuzzyScorer.ts @@ -833,7 +833,7 @@ export interface IPreparedQuery extends IPreparedQueryPiece { values: IPreparedQueryPiece[] | undefined; /** - * Wether the query contains path separator(s) or not. + * Whether the query contains path separator(s) or not. */ containsPathSeparator: boolean; } diff --git a/src/vs/base/common/insane/insane.d.ts b/src/vs/base/common/insane/insane.d.ts index 9b5a77c3b8e..13fa1f2662b 100644 --- a/src/vs/base/common/insane/insane.d.ts +++ b/src/vs/base/common/insane/insane.d.ts @@ -9,6 +9,7 @@ export function insane( readonly allowedSchemes?: readonly string[], readonly allowedTags?: readonly string[], readonly allowedAttributes?: { readonly [key: string]: string[] }, + readonly filter?: (token: { tag: string, attrs: { readonly [key: string]: string } }) => boolean, }, strict?: boolean, ): string; diff --git a/src/vs/base/common/keybindingLabels.ts b/src/vs/base/common/keybindingLabels.ts index 671816830bd..1cdbc8d8e9e 100644 --- a/src/vs/base/common/keybindingLabels.ts +++ b/src/vs/base/common/keybindingLabels.ts @@ -182,7 +182,9 @@ function _simpleAsString(modifiers: Modifiers, key: string, labels: ModifierLabe } // the actual key - result.push(key); + if (key !== '') { + result.push(key); + } return result.join(labels.separator); } diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index e43ab8e5f1d..ed8c14248af 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -486,7 +486,6 @@ export class ResourceMap implements Map { readonly [Symbol.toStringTag] = 'ResourceMap'; protected readonly map: Map; - protected readonly ignoreCase?: boolean = false; // in the future this should be an uri-comparator constructor(other?: ResourceMap) { this.map = other ? new Map(other.map) : new Map(); @@ -549,20 +548,7 @@ export class ResourceMap implements Map { } private toKey(resource: URI): string { - let key = resource.toString(); - if (this.ignoreCase) { - key = key.toLowerCase(); - } - - return key; - } - - clone(): ResourceMap { - const resourceMap = new ResourceMap(); - - this.map.forEach((value, key) => resourceMap.map.set(key, value)); - - return resourceMap; + return resource.toString(); } } diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index 0bf9ce1cc6b..26c88f7e3b0 100644 --- a/src/vs/base/common/mime.ts +++ b/src/vs/base/common/mime.ts @@ -335,3 +335,13 @@ export function getMediaMime(path: string): string | undefined { const ext = extname(path); return mapExtToMediaMimes[ext.toLowerCase()]; } + +export function getExtensionForMimeType(mimeType: string): string | undefined { + for (const extension in mapExtToMediaMimes) { + if (mapExtToMediaMimes[extension] === mimeType) { + return extension; + } + } + + return undefined; +} diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index e4546b2cf60..65a18ee4cbb 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -59,6 +59,8 @@ export namespace Schemas { export const vscodeSettings = 'vscode-settings'; export const webviewPanel = 'webview-panel'; + + export const vscodeWebviewResource = 'vscode-webview-resource'; } class RemoteAuthoritiesImpl { diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 616e2b03b38..9ffce85d3e8 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -17,279 +17,368 @@ export function originalFSPath(uri: URI): string { return uriToFsPath(uri, true); } -// DO NOT EXPORT, DO NOT USE -function _hasToIgnoreCase(resource: URI | undefined): boolean { - // A file scheme resource is in the same platform as code, so ignore case for non linux platforms - // Resource can be from another platform. Lowering the case as an hack. Should come from File system provider - return resource && resource.scheme === Schemas.file ? !isLinux : true; +//#region IExtUri + +export interface IExtUri { + + // --- identity + + /** + * Compares two uris. + * + * @param uri1 Uri + * @param uri2 Uri + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + compare(uri1: URI, uri2: URI, ignoreFragment?: boolean): number; + + /** + * Tests whether two uris are equal + * + * @param uri1 Uri + * @param uri2 Uri + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment?: boolean): boolean; + + /** + * Tests whether a `candidate` URI is a parent or equal of a given `base` URI. + * + * @param base A uri which is "longer" + * @param parentCandidate A uri which is "shorter" then `base` + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment?: boolean): boolean; + + /** + * Creates a key from a resource URI to be used to resource comparison and for resource maps. + * @see ResourceMap + * @param uri Uri + * @param ignoreFragment Ignore the fragment (defaults to `false`) + */ + getComparisonKey(uri: URI, ignoreFragment?: boolean): string; + + // --- path math + + basenameOrAuthority(resource: URI): string; + + /** + * Returns the basename of the path component of an uri. + * @param resource + */ + basename(resource: URI): string; + + /** + * Returns the extension of the path component of an uri. + * @param resource + */ + extname(resource: URI): string; + /** + * Return a URI representing the directory of a URI path. + * + * @param resource The input URI. + * @returns The URI representing the directory of the input URI. + */ + dirname(resource: URI): URI; + /** + * Join a URI path with path fragments and normalizes the resulting path. + * + * @param resource The input URI. + * @param pathFragment The path fragment to add to the URI path. + * @returns The resulting URI. + */ + joinPath(resource: URI, ...pathFragment: string[]): URI + /** + * Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names. + * + * @param resource The URI to normalize the path. + * @returns The URI with the normalized path. + */ + normalizePath(resource: URI): URI; + /** + * + * @param from + * @param to + */ + relativePath(from: URI, to: URI): string | undefined; + /** + * Resolves an absolute or relative path against a base URI. + * The path can be relative or absolute posix or a Windows path + */ + resolvePath(base: URI, path: string): URI; + + // --- misc + + /** + * Returns true if the URI path is absolute. + */ + isAbsolutePath(resource: URI): boolean; + /** + * Tests whether the two authorities are the same + */ + isEqualAuthority(a1: string, a2: string): boolean; + /** + * Returns true if the URI path has a trailing path separator + */ + hasTrailingPathSeparator(resource: URI, sep?: string): boolean; + /** + * Removes a trailing path separator, if there's one. + * Important: Doesn't remove the first slash, it would make the URI invalid + */ + removeTrailingPathSeparator(resource: URI, sep?: string): URI; + /** + * Adds a trailing path separator to the URI if there isn't one already. + * For example, c:\ would be unchanged, but c:\users would become c:\users\ + */ + addTrailingPathSeparator(resource: URI, sep?: string): URI; } -/** - * Creates a key from a resource URI to be used to resource comparison and for resource maps. - * - * @param resource Uri - * @param caseInsensitivePath Ignore casing when comparing path component (defaults mostly to `true`) - * @param ignoreFragment Ignore the fragment (defaults to `false`) - */ -export function getComparisonKey(resource: URI, caseInsensitivePath: boolean = _hasToIgnoreCase(resource), ignoreFragment: boolean = false): string { - return resource.with({ - path: caseInsensitivePath ? resource.path.toLowerCase() : undefined, - fragment: ignoreFragment ? null : undefined - }).toString(); -} +export class ExtUri implements IExtUri { -/** - * Tests whether two uris are equal - * - * @param first Uri - * @param second Uri - * @param caseInsensitivePath Ignore casing when comparing path component (defaults mostly to `true`) - * @param ignoreFragment Ignore the fragment (defaults to `false`) - */ -export function isEqual(first: URI | undefined, second: URI | undefined, caseInsensitivePath: boolean = _hasToIgnoreCase(first), ignoreFragment: boolean = false): boolean { - if (first === second) { - return true; - } - if (!first || !second) { - return false; - } - if (first.scheme !== second.scheme || !isEqualAuthority(first.authority, second.authority)) { - return false; - } - const p1 = first.path, p2 = second.path; - return (p1 === p2 || caseInsensitivePath && equalsIgnoreCase(p1, p2)) && first.query === second.query && (ignoreFragment || first.fragment === second.fragment); -} + constructor(private _ignorePathCasing: (uri: URI) => boolean) { } -export function compare(uri1: URI, uri2: URI, caseInsensitivePath: boolean = _hasToIgnoreCase(uri1), ignoreFragment: boolean = false): number { - // scheme - let ret = strCompare(uri1.scheme, uri2.scheme); - if (ret === 0) { - // authority - ret = compareIgnoreCase(uri1.authority, uri2.authority); + compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number { + // scheme + let ret = strCompare(uri1.scheme, uri2.scheme); if (ret === 0) { - // path - ret = caseInsensitivePath ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path); - // query + // authority + ret = compareIgnoreCase(uri1.authority, uri2.authority); if (ret === 0) { - ret = strCompare(uri1.query, uri2.query); - // fragment - if (ret === 0 && !ignoreFragment) { - ret = strCompare(uri1.fragment, uri2.fragment); + // path + ret = this._ignorePathCasing(uri1) ? compareIgnoreCase(uri1.path, uri2.path) : strCompare(uri1.path, uri2.path); + // query + if (ret === 0) { + ret = strCompare(uri1.query, uri2.query); + // fragment + if (ret === 0 && !ignoreFragment) { + ret = strCompare(uri1.fragment, uri2.fragment); + } } } } + return ret; } - return ret; -} -/** - * Tests whether a `candidate` URI is a parent or equal of a given `base` URI. - * - * @param base A uri which is "longer" - * @param parentCandidate A uri which is "shorter" then `base` - * @param caseInsensitivePath Ignore casing when comparing path component (defaults mostly to `true`) - * @param ignoreFragment Ignore the fragment (defaults to `false`) - */ -export function isEqualOrParent(base: URI, parentCandidate: URI, caseInsensitivePath: boolean = _hasToIgnoreCase(base), ignoreFragment: boolean = false): boolean { - if (base.scheme === parentCandidate.scheme) { - if (base.scheme === Schemas.file) { - return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), caseInsensitivePath) && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment); + getComparisonKey(uri: URI, ignoreFragment: boolean = false): string { + return uri.with({ + path: this._ignorePathCasing(uri) ? uri.path.toLowerCase() : undefined, + fragment: ignoreFragment ? null : undefined + }).toString(); + } + + isEqual(uri1: URI | undefined, uri2: URI | undefined, ignoreFragment: boolean = false): boolean { + if (uri1 === uri2) { + return true; } - if (isEqualAuthority(base.authority, parentCandidate.authority)) { - return extpath.isEqualOrParent(base.path, parentCandidate.path, caseInsensitivePath, '/') && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment); + if (!uri1 || !uri2) { + return false; } - } - return false; -} - - -export function basenameOrAuthority(resource: URI): string { - return basename(resource) || resource.authority; -} - -/** - * Tests whether the two authorities are the same - */ -export function isEqualAuthority(a1: string, a2: string) { - return a1 === a2 || equalsIgnoreCase(a1, a2); -} - -export function basename(resource: URI): string { - return paths.posix.basename(resource.path); -} - -export function extname(resource: URI): string { - return paths.posix.extname(resource.path); -} - -/** - * Return a URI representing the directory of a URI path. - * - * @param resource The input URI. - * @returns The URI representing the directory of the input URI. - */ -export function dirname(resource: URI): URI { - if (resource.path.length === 0) { - return resource; - } - let dirname; - if (resource.scheme === Schemas.file) { - dirname = URI.file(paths.dirname(originalFSPath(resource))).path; - } else { - dirname = paths.posix.dirname(resource.path); - if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { - console.error(`dirname("${resource.toString})) resulted in a relative path`); - dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character + if (uri1.scheme !== uri2.scheme || !isEqualAuthority(uri1.authority, uri2.authority)) { + return false; } + if (uri1.toString() === uri2.toString()) { + // TODO@jrieken see https://github.com/microsoft/vscode/issues/98934 + return true; + } + const p1 = uri1.path, p2 = uri2.path; + return (p1 === p2 || this._ignorePathCasing(uri1) && equalsIgnoreCase(p1, p2)) && uri1.query === uri2.query && (ignoreFragment || uri1.fragment === uri2.fragment); } - return resource.with({ - path: dirname - }); -} -/** - * Join a URI path with path fragments and normalizes the resulting path. - * - * @param resource The input URI. - * @param pathFragment The path fragment to add to the URI path. - * @returns The resulting URI. - */ -export function joinPath(resource: URI, ...pathFragment: string[]): URI { - let joinedPath: string; - if (resource.scheme === 'file') { - joinedPath = URI.file(paths.join(originalFSPath(resource), ...pathFragment)).path; - } else { - joinedPath = paths.posix.join(resource.path || '/', ...pathFragment); - } - return resource.with({ - path: joinedPath - }); -} - -/** - * Normalizes the path part of a URI: Resolves `.` and `..` elements with directory names. - * - * @param resource The URI to normalize the path. - * @returns The URI with the normalized path. - */ -export function normalizePath(resource: URI): URI { - if (!resource.path.length) { - return resource; - } - let normalizedPath: string; - if (resource.scheme === Schemas.file) { - normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path; - } else { - normalizedPath = paths.posix.normalize(resource.path); - } - return resource.with({ - path: normalizedPath - }); -} - -/** - * Returns true if the URI path is absolute. - */ -export function isAbsolutePath(resource: URI): boolean { - return !!resource.path && resource.path[0] === '/'; -} - -/** - * Returns true if the URI path has a trailing path separator - */ -export function hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean { - if (resource.scheme === Schemas.file) { - const fsp = originalFSPath(resource); - return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep; - } else { - const p = resource.path; - return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0 - } -} - -/** - * Removes a trailing path separator, if there's one. - * Important: Doesn't remove the first slash, it would make the URI invalid - */ -export function removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI { - // Make sure that the path isn't a drive letter. A trailing separator there is not removable. - if (hasTrailingPathSeparator(resource, sep)) { - return resource.with({ path: resource.path.substr(0, resource.path.length - 1) }); - } - return resource; -} - -/** - * Adds a trailing path separator to the URI if there isn't one already. - * For example, c:\ would be unchanged, but c:\users would become c:\users\ - */ -export function addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI { - let isRootSep: boolean = false; - if (resource.scheme === Schemas.file) { - const fsp = originalFSPath(resource); - isRootSep = ((fsp !== undefined) && (fsp.length === extpath.getRoot(fsp).length) && (fsp[fsp.length - 1] === sep)); - } else { - sep = '/'; - const p = resource.path; - isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash; - } - if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) { - return resource.with({ path: resource.path + '/' }); - } - return resource; -} - -/** - * Returns a relative path between two URIs. If the URIs don't have the same schema or authority, `undefined` is returned. - * The returned relative path always uses forward slashes. - */ -export function relativePath(from: URI, to: URI, caseInsensitivePath = _hasToIgnoreCase(from)): string | undefined { - if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) { - return undefined; - } - if (from.scheme === Schemas.file) { - const relativePath = paths.relative(originalFSPath(from), originalFSPath(to)); - return isWindows ? extpath.toSlashes(relativePath) : relativePath; - } - let fromPath = from.path || '/', toPath = to.path || '/'; - if (caseInsensitivePath) { - // make casing of fromPath match toPath - let i = 0; - for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) { - if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) { - if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) { - break; - } + isEqualOrParent(base: URI, parentCandidate: URI, ignoreFragment: boolean = false): boolean { + if (base.scheme === parentCandidate.scheme) { + if (base.scheme === Schemas.file) { + return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), this._ignorePathCasing(base)) && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment); + } + if (isEqualAuthority(base.authority, parentCandidate.authority)) { + return extpath.isEqualOrParent(base.path, parentCandidate.path, this._ignorePathCasing(base), '/') && base.query === parentCandidate.query && (ignoreFragment || base.fragment === parentCandidate.fragment); } } - fromPath = toPath.substr(0, i) + fromPath.substr(i); + return false; } - return paths.posix.relative(fromPath, toPath); -} -/** - * Resolves an absolute or relative path against a base URI. - * The path can be relative or absolute posix or a Windows path - */ -export function resolvePath(base: URI, path: string): URI { - if (base.scheme === Schemas.file) { - const newURI = URI.file(paths.resolve(originalFSPath(base), path)); - return base.with({ - authority: newURI.authority, - path: newURI.path + // --- path math + + joinPath(resource: URI, ...pathFragment: string[]): URI { + return URI.joinPath(resource, ...pathFragment); + } + + basenameOrAuthority(resource: URI): string { + return basename(resource) || resource.authority; + } + + basename(resource: URI): string { + return paths.posix.basename(resource.path); + } + + extname(resource: URI): string { + return paths.posix.extname(resource.path); + } + + dirname(resource: URI): URI { + if (resource.path.length === 0) { + return resource; + } + let dirname; + if (resource.scheme === Schemas.file) { + dirname = URI.file(paths.dirname(originalFSPath(resource))).path; + } else { + dirname = paths.posix.dirname(resource.path); + if (resource.authority && dirname.length && dirname.charCodeAt(0) !== CharCode.Slash) { + console.error(`dirname("${resource.toString})) resulted in a relative path`); + dirname = '/'; // If a URI contains an authority component, then the path component must either be empty or begin with a CharCode.Slash ("/") character + } + } + return resource.with({ + path: dirname }); } - if (path.indexOf('/') === -1) { // no slashes? it's likely a Windows path - path = extpath.toSlashes(path); - if (/^[a-zA-Z]:(\/|$)/.test(path)) { // starts with a drive letter - path = '/' + path; + + normalizePath(resource: URI): URI { + if (!resource.path.length) { + return resource; + } + let normalizedPath: string; + if (resource.scheme === Schemas.file) { + normalizedPath = URI.file(paths.normalize(originalFSPath(resource))).path; + } else { + normalizedPath = paths.posix.normalize(resource.path); + } + return resource.with({ + path: normalizedPath + }); + } + + relativePath(from: URI, to: URI): string | undefined { + if (from.scheme !== to.scheme || !isEqualAuthority(from.authority, to.authority)) { + return undefined; + } + if (from.scheme === Schemas.file) { + const relativePath = paths.relative(originalFSPath(from), originalFSPath(to)); + return isWindows ? extpath.toSlashes(relativePath) : relativePath; + } + let fromPath = from.path || '/', toPath = to.path || '/'; + if (this._ignorePathCasing(from)) { + // make casing of fromPath match toPath + let i = 0; + for (const len = Math.min(fromPath.length, toPath.length); i < len; i++) { + if (fromPath.charCodeAt(i) !== toPath.charCodeAt(i)) { + if (fromPath.charAt(i).toLowerCase() !== toPath.charAt(i).toLowerCase()) { + break; + } + } + } + fromPath = toPath.substr(0, i) + fromPath.substr(i); + } + return paths.posix.relative(fromPath, toPath); + } + + resolvePath(base: URI, path: string): URI { + if (base.scheme === Schemas.file) { + const newURI = URI.file(paths.resolve(originalFSPath(base), path)); + return base.with({ + authority: newURI.authority, + path: newURI.path + }); + } + if (path.indexOf('/') === -1) { // no slashes? it's likely a Windows path + path = extpath.toSlashes(path); + if (/^[a-zA-Z]:(\/|$)/.test(path)) { // starts with a drive letter + path = '/' + path; + } + } + return base.with({ + path: paths.posix.resolve(base.path, path) + }); + } + + // --- misc + + isAbsolutePath(resource: URI): boolean { + return !!resource.path && resource.path[0] === '/'; + } + + isEqualAuthority(a1: string, a2: string) { + return a1 === a2 || equalsIgnoreCase(a1, a2); + } + + hasTrailingPathSeparator(resource: URI, sep: string = paths.sep): boolean { + if (resource.scheme === Schemas.file) { + const fsp = originalFSPath(resource); + return fsp.length > extpath.getRoot(fsp).length && fsp[fsp.length - 1] === sep; + } else { + const p = resource.path; + return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0 } } - return base.with({ - path: paths.posix.resolve(base.path, path) - }); + + removeTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI { + // Make sure that the path isn't a drive letter. A trailing separator there is not removable. + if (hasTrailingPathSeparator(resource, sep)) { + return resource.with({ path: resource.path.substr(0, resource.path.length - 1) }); + } + return resource; + } + + addTrailingPathSeparator(resource: URI, sep: string = paths.sep): URI { + let isRootSep: boolean = false; + if (resource.scheme === Schemas.file) { + const fsp = originalFSPath(resource); + isRootSep = ((fsp !== undefined) && (fsp.length === extpath.getRoot(fsp).length) && (fsp[fsp.length - 1] === sep)); + } else { + sep = '/'; + const p = resource.path; + isRootSep = p.length === 1 && p.charCodeAt(p.length - 1) === CharCode.Slash; + } + if (!isRootSep && !hasTrailingPathSeparator(resource, sep)) { + return resource.with({ path: resource.path + '/' }); + } + return resource; + } } +/** + * Unbiased utility that takes uris "as they are". This means it can be interchanged with + * uri#toString() usages. The following is true + * ``` + * assertEqual(aUri.toString() === bUri.toString(), exturi.isEqual(aUri, bUri)) + * ``` + */ +export const extUri = new ExtUri(() => false); + +/** + * BIASED utility that always ignores the casing of uris path. ONLY use these util if you + * understand what you are doing. + * + * Note that `IUriIdentityService#extUri` is a better replacement for this because that utility + * knows when path casing matters and when not. + */ +export const extUriIgnorePathCase = new ExtUri(_ => true); + +const exturiBiasedIgnorePathCase = new ExtUri(uri => { + // A file scheme resource is in the same platform as code, so ignore case for non linux platforms + // Resource can be from another platform. Lowering the case as an hack. Should come from File system provider + return uri && uri.scheme === Schemas.file ? !isLinux : true; +}); + +export const isEqual = exturiBiasedIgnorePathCase.isEqual.bind(exturiBiasedIgnorePathCase); +export const isEqualOrParent = exturiBiasedIgnorePathCase.isEqualOrParent.bind(exturiBiasedIgnorePathCase); +export const getComparisonKey = exturiBiasedIgnorePathCase.getComparisonKey.bind(exturiBiasedIgnorePathCase); +export const basenameOrAuthority = exturiBiasedIgnorePathCase.basenameOrAuthority.bind(exturiBiasedIgnorePathCase); +export const basename = exturiBiasedIgnorePathCase.basename.bind(exturiBiasedIgnorePathCase); +export const extname = exturiBiasedIgnorePathCase.extname.bind(exturiBiasedIgnorePathCase); +export const dirname = exturiBiasedIgnorePathCase.dirname.bind(exturiBiasedIgnorePathCase); +export const joinPath = extUri.joinPath.bind(extUri); +export const normalizePath = exturiBiasedIgnorePathCase.normalizePath.bind(exturiBiasedIgnorePathCase); +export const relativePath = exturiBiasedIgnorePathCase.relativePath.bind(exturiBiasedIgnorePathCase); +export const resolvePath = exturiBiasedIgnorePathCase.resolvePath.bind(exturiBiasedIgnorePathCase); +export const isAbsolutePath = exturiBiasedIgnorePathCase.isAbsolutePath.bind(exturiBiasedIgnorePathCase); +export const isEqualAuthority = exturiBiasedIgnorePathCase.isEqualAuthority.bind(exturiBiasedIgnorePathCase); +export const hasTrailingPathSeparator = exturiBiasedIgnorePathCase.hasTrailingPathSeparator.bind(exturiBiasedIgnorePathCase); +export const removeTrailingPathSeparator = exturiBiasedIgnorePathCase.removeTrailingPathSeparator.bind(exturiBiasedIgnorePathCase); +export const addTrailingPathSeparator = exturiBiasedIgnorePathCase.addTrailingPathSeparator.bind(exturiBiasedIgnorePathCase); + +//#endregion + export function distinctParents(items: T[], resourceAccessor: (item: T) => URI): T[] { const distinctParents: T[] = []; for (let i = 0; i < items.length; i++) { diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index c507366e853..a45e0763ab1 100644 --- a/src/vs/base/common/scrollable.ts +++ b/src/vs/base/common/scrollable.ts @@ -13,10 +13,18 @@ export const enum ScrollbarVisibility { } export interface ScrollEvent { + oldWidth: number; + oldScrollWidth: number; + oldScrollLeft: number; + width: number; scrollWidth: number; scrollLeft: number; + oldHeight: number; + oldScrollHeight: number; + oldScrollTop: number; + height: number; scrollHeight: number; scrollTop: number; @@ -134,10 +142,18 @@ export class ScrollState implements IScrollDimensions, IScrollPosition { const scrollTopChanged = (this.scrollTop !== previous.scrollTop); return { + oldWidth: previous.width, + oldScrollWidth: previous.scrollWidth, + oldScrollLeft: previous.scrollLeft, + width: this.width, scrollWidth: this.scrollWidth, scrollLeft: this.scrollLeft, + oldHeight: previous.height, + oldScrollHeight: previous.scrollHeight, + oldScrollTop: previous.scrollTop, + height: this.height, scrollHeight: this.scrollHeight, scrollTop: this.scrollTop, diff --git a/src/vs/base/common/skipList.ts b/src/vs/base/common/skipList.ts new file mode 100644 index 00000000000..1243ebdddf7 --- /dev/null +++ b/src/vs/base/common/skipList.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +class Node { + readonly forward: Node[]; + constructor(readonly level: number, readonly key: K, public value: V) { + this.forward = []; + } +} + +const NIL: undefined = undefined; + +interface Comparator { + (a: K, b: K): number; +} + +export class SkipList implements Map { + + readonly [Symbol.toStringTag] = 'SkipList'; + + private _maxLevel: number; + private _level: number = 1; + private _header: Node; + private _size: number = 0; + + /** + * + * @param capacity Capacity at which the list performs best + */ + constructor( + readonly comparator: (a: K, b: K) => number, + capacity: number = 2 ** 16 + ) { + this._maxLevel = Math.max(1, Math.log2(capacity) | 0); + this._header = new Node(this._maxLevel, NIL, NIL); + } + + get size(): number { + return this._size; + } + + clear(): void { + this._header = new Node(this._maxLevel, NIL, NIL); + } + + has(key: K): boolean { + return Boolean(SkipList._search(this, key, this.comparator)); + } + + get(key: K): V | undefined { + return SkipList._search(this, key, this.comparator)?.value; + } + + set(key: K, value: V): this { + if (SkipList._insert(this, key, value, this.comparator)) { + this._size += 1; + } + return this; + } + + delete(key: K): boolean { + const didDelete = SkipList._delete(this, key, this.comparator); + if (didDelete) { + this._size -= 1; + } + return didDelete; + } + + // --- iteration + + forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: any): void { + let node = this._header.forward[0]; + while (node) { + callbackfn.call(thisArg, node.value, node.key, this); + node = node.forward[0]; + } + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } + + *entries(): IterableIterator<[K, V]> { + let node = this._header.forward[0]; + while (node) { + yield [node.key, node.value]; + node = node.forward[0]; + } + } + + *keys(): IterableIterator { + let node = this._header.forward[0]; + while (node) { + yield node.key; + node = node.forward[0]; + } + } + + *values(): IterableIterator { + let node = this._header.forward[0]; + while (node) { + yield node.value; + node = node.forward[0]; + } + } + + toString(): string { + // debug string... + let result = '[SkipList]:'; + let node = this._header.forward[0]; + while (node) { + result += `node(${node.key}, ${node.value}, lvl:${node.level})`; + node = node.forward[0]; + } + return result; + } + + // from https://www.epaperpress.com/sortsearch/download/skiplist.pdf + + private static _search(list: SkipList, searchKey: K, comparator: Comparator) { + let x = list._header; + for (let i = list._level; i >= 0; i--) { + while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) { + x = x.forward[i]; + } + } + x = x.forward[0]; + if (x && comparator(x.key, searchKey) === 0) { + return x; + } + return undefined; + } + + private static _insert(list: SkipList, searchKey: K, value: V, comparator: Comparator) { + let update: Node[] = []; + let x = list._header; + for (let i = list._level; i >= 0; i--) { + while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) { + x = x.forward[i]; + } + update[i] = x; + } + x = x.forward[0]; + if (x && comparator(x.key, searchKey) === 0) { + // update + x.value = value; + return false; + } else { + // insert + let lvl = SkipList._randomLevel(list); + if (lvl > list._level) { + for (let i = list._level + 1; i <= lvl; i++) { + update[i] = list._header; + } + list._level = lvl; + } + x = new Node(lvl, searchKey, value); + for (let i = 0; i <= lvl; i++) { + x.forward[i] = update[i].forward[i]; + update[i].forward[i] = x; + } + return true; + } + } + + private static _randomLevel(list: SkipList, p: number = 0.5): number { + let lvl = 1; + while (Math.random() < p && lvl < list._maxLevel) { + lvl += 1; + } + return lvl; + } + + private static _delete(list: SkipList, searchKey: K, comparator: Comparator) { + let update: Node[] = []; + let x = list._header; + for (let i = list._level; i >= 0; i--) { + while (x.forward[i] && comparator(x.forward[i].key, searchKey) < 0) { + x = x.forward[i]; + } + update[i] = x; + } + x = x.forward[0]; + if (!x || comparator(x.key, searchKey) !== 0) { + // not found + return false; + } + for (let i = 0; i < list._level; i++) { + if (update[i].forward[i] !== x) { + break; + } + update[i].forward[i] = x.forward[i]; + } + while (list._level >= 1 && list._header.forward[list._level] === NIL) { + list._level -= 1; + } + return true; + } + +} diff --git a/src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts b/src/vs/base/parts/contextmenu/electron-sandbox/contextmenu.ts similarity index 87% rename from src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts rename to src/vs/base/parts/contextmenu/electron-sandbox/contextmenu.ts index da008e59930..4b5c3fbdf75 100644 --- a/src/vs/base/parts/contextmenu/electron-browser/contextmenu.ts +++ b/src/vs/base/parts/contextmenu/electron-sandbox/contextmenu.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ipcRenderer, Event } from 'electron'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { IContextMenuItem, ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions, IContextMenuEvent } from 'vs/base/parts/contextmenu/common/contextmenu'; let contextMenuIdPool = 0; @@ -13,7 +13,7 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions): void const contextMenuId = contextMenuIdPool++; const onClickChannel = `vscode:onContextMenu${contextMenuId}`; - const onClickChannelHandler = (_event: Event, itemId: number, context: IContextMenuEvent) => { + const onClickChannelHandler = (event: unknown, itemId: number, context: IContextMenuEvent) => { const item = processedItems[itemId]; if (item.click) { item.click(context); @@ -21,7 +21,7 @@ export function popup(items: IContextMenuItem[], options?: IPopupOptions): void }; ipcRenderer.once(onClickChannel, onClickChannelHandler); - ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (_event: Event, closedContextMenuId: number) => { + ipcRenderer.once(CONTEXT_MENU_CLOSE_CHANNEL, (event: unknown, closedContextMenuId: number) => { if (closedContextMenuId !== contextMenuId) { return; } diff --git a/src/vs/base/parts/ipc/node/ipc.electron.ts b/src/vs/base/parts/ipc/common/ipc.electron.ts similarity index 83% rename from src/vs/base/parts/ipc/node/ipc.electron.ts rename to src/vs/base/parts/ipc/common/ipc.electron.ts index 09c97ba47b1..516351e1509 100644 --- a/src/vs/base/parts/ipc/node/ipc.electron.ts +++ b/src/vs/base/parts/ipc/common/ipc.electron.ts @@ -8,7 +8,7 @@ import { Event } from 'vs/base/common/event'; import { VSBuffer } from 'vs/base/common/buffer'; export interface Sender { - send(channel: string, msg: Buffer | null): void; + send(channel: string, msg: unknown): void; } export class Protocol implements IMessagePassingProtocol { @@ -17,13 +17,13 @@ export class Protocol implements IMessagePassingProtocol { send(message: VSBuffer): void { try { - this.sender.send('ipc:message', (message.buffer)); + this.sender.send('vscode:message', message.buffer); } catch (e) { // systems are going down } } dispose(): void { - this.sender.send('ipc:disconnect', null); + this.sender.send('vscode:disconnect', null); } -} \ No newline at end of file +} diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 8acfc6bce4e..411bfe46cca 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -10,7 +10,9 @@ import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cance import * as errors from 'vs/base/common/errors'; import { VSBuffer } from 'vs/base/common/buffer'; import { getRandomElement } from 'vs/base/common/arrays'; -import { isFunction } from 'vs/base/common/types'; +import { isFunction, isUndefinedOrNull } from 'vs/base/common/types'; +import { revive } from 'vs/base/common/marshalling'; +import { isUpperAsciiLetter } from 'vs/base/common/strings'; /** * An `IChannel` is an abstraction over a collection of commands. @@ -919,3 +921,142 @@ export class StaticRouter implements IClientRouter return await this.route(hub); } } + + +//#region createChannelReceiver / createChannelSender + +/** + * Use both `createChannelReceiver` and `createChannelSender` + * for automated process <=> process communication over methods + * and events. You do not need to spell out each method on both + * sides, a proxy will take care of this. + * + * Rules: + * - if marshalling is enabled, only `URI` and `RegExp` is converted + * automatically for you + * - events must follow the naming convention `onUppercase` + * - `CancellationToken` is currently not supported + * - if a context is provided, you can use `AddFirstParameterToFunctions` + * utility to signal this in the receiving side type + */ + +export interface IBaseChannelOptions { + + /** + * Disables automatic marshalling of `URI`. + * If marshalling is disabled, `UriComponents` + * must be used instead. + */ + disableMarshalling?: boolean; +} + +export interface IChannelReceiverOptions extends IBaseChannelOptions { } + +export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel { + const handler = service as { [key: string]: unknown }; + const disableMarshalling = options && options.disableMarshalling; + + // Buffer any event that should be supported by + // iterating over all property keys and finding them + const mapEventNameToEvent = new Map>(); + for (const key in handler) { + if (propertyIsEvent(key)) { + mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event, true)); + } + } + + return new class implements IServerChannel { + + listen(_: unknown, event: string): Event { + const eventImpl = mapEventNameToEvent.get(event); + if (eventImpl) { + return eventImpl as Event; + } + + throw new Error(`Event not found: ${event}`); + } + + call(_: unknown, command: string, args?: any[]): Promise { + const target = handler[command]; + if (typeof target === 'function') { + + // Revive unless marshalling disabled + if (!disableMarshalling && Array.isArray(args)) { + for (let i = 0; i < args.length; i++) { + args[i] = revive(args[i]); + } + } + + return target.apply(handler, args); + } + + throw new Error(`Method not found: ${command}`); + } + }; +} + +export interface IChannelSenderOptions extends IBaseChannelOptions { + + /** + * If provided, will add the value of `context` + * to each method call to the target. + */ + context?: unknown; + + /** + * If provided, will not proxy any of the properties + * that are part of the Map but rather return that value. + */ + properties?: Map; +} + +export function createChannelSender(channel: IChannel, options?: IChannelSenderOptions): T { + const disableMarshalling = options && options.disableMarshalling; + + return new Proxy({}, { + get(_target: T, propKey: PropertyKey) { + if (typeof propKey === 'string') { + + // Check for predefined values + if (options?.properties?.has(propKey)) { + return options.properties.get(propKey); + } + + // Event + if (propertyIsEvent(propKey)) { + return channel.listen(propKey); + } + + // Function + return async function (...args: any[]) { + + // Add context if any + let methodArgs: any[]; + if (options && !isUndefinedOrNull(options.context)) { + methodArgs = [options.context, ...args]; + } else { + methodArgs = args; + } + + const result = await channel.call(propKey, methodArgs); + + // Revive unless marshalling disabled + if (!disableMarshalling) { + return revive(result); + } + + return result; + }; + } + + throw new Error(`Property not found: ${String(propKey)}`); + } + }) as T; +} + +function propertyIsEvent(name: string): boolean { + // Assume a property is an event if it has a form of "onSomething" + return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2)); +} + +//#endregion diff --git a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts b/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts index bac2223ede6..b84a1d653dc 100644 --- a/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts +++ b/src/vs/base/parts/ipc/electron-main/ipc.electron-main.ts @@ -5,7 +5,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IPCServer, ClientConnectionEvent } from 'vs/base/parts/ipc/common/ipc'; -import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron'; +import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron'; import { ipcMain, WebContents } from 'electron'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -26,7 +26,7 @@ export class Server extends IPCServer { private static readonly Clients = new Map(); private static getOnDidClientConnect(): Event { - const onHello = Event.fromNodeEventEmitter(ipcMain, 'ipc:hello', ({ sender }) => sender); + const onHello = Event.fromNodeEventEmitter(ipcMain, 'vscode:hello', ({ sender }) => sender); return Event.map(onHello, webContents => { const id = webContents.id; @@ -39,8 +39,8 @@ export class Server extends IPCServer { const onDidClientReconnect = new Emitter(); Server.Clients.set(id, toDisposable(() => onDidClientReconnect.fire())); - const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event; - const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')), onDidClientReconnect.event); + const onMessage = createScopedOnMessageEvent(id, 'vscode:message') as Event; + const onDidClientDisconnect = Event.any(Event.signal(createScopedOnMessageEvent(id, 'vscode:disconnect')), onDidClientReconnect.event); const protocol = new Protocol(webContents, onMessage); return { protocol, onDidClientDisconnect }; diff --git a/src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts b/src/vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox.ts similarity index 80% rename from src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts rename to src/vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox.ts index 2690300b83a..19ca487c890 100644 --- a/src/vs/base/parts/ipc/electron-browser/ipc.electron-browser.ts +++ b/src/vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox.ts @@ -5,18 +5,18 @@ import { Event } from 'vs/base/common/event'; import { IPCClient } from 'vs/base/parts/ipc/common/ipc'; -import { Protocol } from 'vs/base/parts/ipc/node/ipc.electron'; -import { ipcRenderer } from 'electron'; +import { Protocol } from 'vs/base/parts/ipc/common/ipc.electron'; import { IDisposable } from 'vs/base/common/lifecycle'; import { VSBuffer } from 'vs/base/common/buffer'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; export class Client extends IPCClient implements IDisposable { private protocol: Protocol; private static createProtocol(): Protocol { - const onMessage = Event.fromNodeEventEmitter(ipcRenderer, 'ipc:message', (_, message: Buffer) => VSBuffer.wrap(message)); - ipcRenderer.send('ipc:hello'); + const onMessage = Event.fromNodeEventEmitter(ipcRenderer, 'vscode:message', (_, message) => VSBuffer.wrap(message)); + ipcRenderer.send('vscode:hello'); return new Protocol(ipcRenderer, onMessage); } @@ -29,4 +29,4 @@ export class Client extends IPCClient implements IDisposable { dispose(): void { this.protocol.dispose(); } -} \ No newline at end of file +} diff --git a/src/vs/base/parts/ipc/node/ipc.ts b/src/vs/base/parts/ipc/node/ipc.ts deleted file mode 100644 index 631e5139133..00000000000 --- a/src/vs/base/parts/ipc/node/ipc.ts +++ /dev/null @@ -1,133 +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 { Event } from 'vs/base/common/event'; -import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc'; -import { revive } from 'vs/base/common/marshalling'; -import { isUndefinedOrNull } from 'vs/base/common/types'; -import { isUpperAsciiLetter } from 'vs/base/common/strings'; - -/** - * Use both `createChannelReceiver` and `createChannelSender` - * for automated process <=> process communication over methods - * and events. You do not need to spell out each method on both - * sides, a proxy will take care of this. - * - * Rules: - * - if marshalling is enabled, only `URI` and `RegExp` is converted - * automatically for you - * - events must follow the naming convention `onUppercase` - * - `CancellationToken` is currently not supported - * - if a context is provided, you can use `AddFirstParameterToFunctions` - * utility to signal this in the receiving side type - */ - -export interface IBaseChannelOptions { - - /** - * Disables automatic marshalling of `URI`. - * If marshalling is disabled, `UriComponents` - * must be used instead. - */ - disableMarshalling?: boolean; -} - -export interface IChannelReceiverOptions extends IBaseChannelOptions { } - -export function createChannelReceiver(service: unknown, options?: IChannelReceiverOptions): IServerChannel { - const handler = service as { [key: string]: unknown }; - const disableMarshalling = options && options.disableMarshalling; - - // Buffer any event that should be supported by - // iterating over all property keys and finding them - const mapEventNameToEvent = new Map>(); - for (const key in handler) { - if (propertyIsEvent(key)) { - mapEventNameToEvent.set(key, Event.buffer(handler[key] as Event, true)); - } - } - - return new class implements IServerChannel { - - listen(_: unknown, event: string): Event { - const eventImpl = mapEventNameToEvent.get(event); - if (eventImpl) { - return eventImpl as Event; - } - - throw new Error(`Event not found: ${event}`); - } - - call(_: unknown, command: string, args?: any[]): Promise { - const target = handler[command]; - if (typeof target === 'function') { - - // Revive unless marshalling disabled - if (!disableMarshalling && Array.isArray(args)) { - for (let i = 0; i < args.length; i++) { - args[i] = revive(args[i]); - } - } - - return target.apply(handler, args); - } - - throw new Error(`Method not found: ${command}`); - } - }; -} - -export interface IChannelSenderOptions extends IBaseChannelOptions { - - /** - * If provided, will add the value of `context` - * to each method call to the target. - */ - context?: unknown; -} - -export function createChannelSender(channel: IChannel, options?: IChannelSenderOptions): T { - const disableMarshalling = options && options.disableMarshalling; - - return new Proxy({}, { - get(_target: T, propKey: PropertyKey) { - if (typeof propKey === 'string') { - - // Event - if (propertyIsEvent(propKey)) { - return channel.listen(propKey); - } - - // Function - return async function (...args: any[]) { - - // Add context if any - let methodArgs: any[]; - if (options && !isUndefinedOrNull(options.context)) { - methodArgs = [options.context, ...args]; - } else { - methodArgs = args; - } - - const result = await channel.call(propKey, methodArgs); - - // Revive unless marshalling disabled - if (!disableMarshalling) { - return revive(result); - } - - return result; - }; - } - - throw new Error(`Property not found: ${String(propKey)}`); - } - }) as T; -} - -function propertyIsEvent(name: string): boolean { - // Assume a property is an event if it has a form of "onSomething" - return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2)); -} diff --git a/src/vs/base/parts/ipc/test/node/ipc.test.ts b/src/vs/base/parts/ipc/test/common/ipc.test.ts similarity index 95% rename from src/vs/base/parts/ipc/test/node/ipc.test.ts rename to src/vs/base/parts/ipc/test/common/ipc.test.ts index 3539f5dea8c..8ed0f3797f1 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/common/ipc.test.ts @@ -4,8 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient } from 'vs/base/parts/ipc/common/ipc'; -import { createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/node/ipc'; +import { IChannel, IServerChannel, IMessagePassingProtocol, IPCServer, ClientConnectionEvent, IPCClient, createChannelReceiver, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { Emitter, Event } from 'vs/base/common/event'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; @@ -103,7 +102,7 @@ interface ITestService { error(message: string): Promise; neverComplete(): Promise; neverCompleteCT(cancellationToken: CancellationToken): Promise; - buffersLength(buffers: Buffer[]): Promise; + buffersLength(buffers: VSBuffer[]): Promise; marshall(uri: URI): Promise; context(): Promise; @@ -135,8 +134,8 @@ class TestService implements ITestService { return new Promise((_, e) => cancellationToken.onCancellationRequested(() => e(canceled()))); } - buffersLength(buffers: Buffer[]): Promise { - return Promise.resolve(buffers.reduce((r, b) => r + b.length, 0)); + buffersLength(buffers: VSBuffer[]): Promise { + return Promise.resolve(buffers.reduce((r, b) => r + b.buffer.length, 0)); } ping(msg: string): void { @@ -199,7 +198,7 @@ class TestChannelClient implements ITestService { return this.channel.call('neverCompleteCT', undefined, cancellationToken); } - buffersLength(buffers: Buffer[]): Promise { + buffersLength(buffers: VSBuffer[]): Promise { return this.channel.call('buffersLength', buffers); } @@ -317,7 +316,7 @@ suite('Base IPC', function () { }); test('buffers in arrays', async function () { - const r = await ipcService.buffersLength([Buffer.allocUnsafe(2), Buffer.allocUnsafe(3)]); + const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]); return assert.equal(r, 5); }); }); @@ -383,7 +382,7 @@ suite('Base IPC', function () { }); test('buffers in arrays', async function () { - const r = await ipcService.buffersLength([Buffer.allocUnsafe(2), Buffer.allocUnsafe(3)]); + const r = await ipcService.buffersLength([VSBuffer.alloc(2), VSBuffer.alloc(3)]); return assert.equal(r, 5); }); }); diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts index f09e508bed7..ebad819fffa 100644 --- a/src/vs/base/parts/quickinput/common/quickInput.ts +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -307,7 +307,7 @@ export interface IQuickInputButton { iconClass?: string; tooltip?: string; /** - * Wether to always show the button. By default buttons + * Whether to always show the button. By default buttons * are only visible when hovering over them with the mouse */ alwaysVisible?: boolean; diff --git a/src/vs/base/parts/sandbox/common/electronTypes.ts b/src/vs/base/parts/sandbox/common/electronTypes.ts new file mode 100644 index 00000000000..651a9d575ef --- /dev/null +++ b/src/vs/base/parts/sandbox/common/electronTypes.ts @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +// ####################################################################### +// ### ### +// ### electron.d.ts types we need in a common layer for reuse ### +// ### (copied from Electron 7.x) ### +// ### ### +// ####################################################################### + + +export interface MessageBoxOptions { + /** + * Can be `"none"`, `"info"`, `"error"`, `"question"` or `"warning"`. On Windows, + * `"question"` displays the same icon as `"info"`, unless you set an icon using + * the `"icon"` option. On macOS, both `"warning"` and `"error"` display the same + * warning icon. + */ + type?: string; + /** + * Array of texts for buttons. On Windows, an empty array will result in one button + * labeled "OK". + */ + buttons?: string[]; + /** + * Index of the button in the buttons array which will be selected by default when + * the message box opens. + */ + defaultId?: number; + /** + * Title of the message box, some platforms will not show it. + */ + title?: string; + /** + * Content of the message box. + */ + message: string; + /** + * Extra information of the message. + */ + detail?: string; + /** + * If provided, the message box will include a checkbox with the given label. + */ + checkboxLabel?: string; + /** + * Initial checked state of the checkbox. `false` by default. + */ + checkboxChecked?: boolean; + // icon?: NativeImage; + /** + * The index of the button to be used to cancel the dialog, via the `Esc` key. By + * default this is assigned to the first button with "cancel" or "no" as the label. + * If no such labeled buttons exist and this option is not set, `0` will be used as + * the return value. + */ + cancelId?: number; + /** + * On Windows Electron will try to figure out which one of the `buttons` are common + * buttons (like "Cancel" or "Yes"), and show the others as command links in the + * dialog. This can make the dialog appear in the style of modern Windows apps. If + * you don't like this behavior, you can set `noLink` to `true`. + */ + noLink?: boolean; + /** + * Normalize the keyboard access keys across platforms. Default is `false`. + * Enabling this assumes `&` is used in the button labels for the placement of the + * keyboard shortcut access key and labels will be converted so they work correctly + * on each platform, `&` characters are removed on macOS, converted to `_` on + * Linux, and left untouched on Windows. For example, a button label of `Vie&w` + * will be converted to `Vie_w` on Linux and `View` on macOS and can be selected + * via `Alt-W` on Windows and Linux. + */ + normalizeAccessKeys?: boolean; +} + +export interface MessageBoxReturnValue { + /** + * The index of the clicked button. + */ + response: number; + /** + * The checked state of the checkbox if `checkboxLabel` was set. Otherwise `false`. + */ + checkboxChecked: boolean; +} + +export interface OpenDevToolsOptions { + /** + * Opens the devtools with specified dock state, can be `right`, `bottom`, + * `undocked`, `detach`. Defaults to last used dock state. In `undocked` mode it's + * possible to dock back. In `detach` mode it's not. + */ + mode: ('right' | 'bottom' | 'undocked' | 'detach'); + /** + * Whether to bring the opened devtools window to the foreground. The default is + * `true`. + */ + activate?: boolean; +} + +export interface SaveDialogOptions { + title?: string; + /** + * Absolute directory path, absolute file path, or file name to use by default. + */ + defaultPath?: string; + /** + * Custom label for the confirmation button, when left empty the default label will + * be used. + */ + buttonLabel?: string; + filters?: FileFilter[]; + /** + * Message to display above text fields. + * + * @platform darwin + */ + message?: string; + /** + * Custom label for the text displayed in front of the filename text field. + * + * @platform darwin + */ + nameFieldLabel?: string; + /** + * Show the tags input box, defaults to `true`. + * + * @platform darwin + */ + showsTagField?: boolean; + /** + * Create a security scoped bookmark when packaged for the Mac App Store. If this + * option is enabled and the file doesn't already exist a blank file will be + * created at the chosen path. + * + * @platform darwin,mas + */ + securityScopedBookmarks?: boolean; +} + +export interface OpenDialogOptions { + title?: string; + defaultPath?: string; + /** + * Custom label for the confirmation button, when left empty the default label will + * be used. + */ + buttonLabel?: string; + filters?: FileFilter[]; + /** + * Contains which features the dialog should use. The following values are + * supported: + */ + properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles' | 'createDirectory' | 'promptToCreate' | 'noResolveAliases' | 'treatPackageAsDirectory'>; + /** + * Message to display above input boxes. + * + * @platform darwin + */ + message?: string; + /** + * Create security scoped bookmarks when packaged for the Mac App Store. + * + * @platform darwin,mas + */ + securityScopedBookmarks?: boolean; +} + +export interface OpenDialogReturnValue { + /** + * whether or not the dialog was canceled. + */ + canceled: boolean; + /** + * An array of file paths chosen by the user. If the dialog is cancelled this will + * be an empty array. + */ + filePaths: string[]; + /** + * An array matching the `filePaths` array of base64 encoded strings which contains + * security scoped bookmark data. `securityScopedBookmarks` must be enabled for + * this to be populated. (For return values, see table here.) + * + * @platform darwin,mas + */ + bookmarks?: string[]; +} + +export interface SaveDialogReturnValue { + /** + * whether or not the dialog was canceled. + */ + canceled: boolean; + /** + * If the dialog is canceled, this will be `undefined`. + */ + filePath?: string; + /** + * Base64 encoded string which contains the security scoped bookmark data for the + * saved file. `securityScopedBookmarks` must be enabled for this to be present. + * (For return values, see table here.) + * + * @platform darwin,mas + */ + bookmark?: string; +} + +export interface CrashReporterStartOptions { + companyName: string; + /** + * URL that crash reports will be sent to as POST. + */ + submitURL: string; + /** + * Defaults to `app.name`. + */ + productName?: string; + /** + * Whether crash reports should be sent to the server. Default is `true`. + */ + uploadToServer?: boolean; + /** + * Default is `false`. + */ + ignoreSystemCrashHandler?: boolean; + /** + * An object you can define that will be sent along with the report. Only string + * properties are sent correctly. Nested objects are not supported. When using + * Windows, the property names and values must be fewer than 64 characters. + */ + extra?: Record; + /** + * Directory to store the crash reports temporarily (only used when the crash + * reporter is started via `process.crashReporter.start`). + */ + crashesDirectory?: string; +} + +export interface FileFilter { + + // Docs: http://electronjs.org/docs/api/structures/file-filter + + extensions: string[]; + name: string; +} diff --git a/src/vs/base/parts/sandbox/electron-browser/preload.js b/src/vs/base/parts/sandbox/electron-browser/preload.js new file mode 100644 index 00000000000..1cb22cfcdd8 --- /dev/null +++ b/src/vs/base/parts/sandbox/electron-browser/preload.js @@ -0,0 +1,115 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-check +(function () { + 'use strict'; + + const { ipcRenderer, webFrame, crashReporter } = require('electron'); + + // @ts-ignore + window.vscode = { + + /** + * A minimal set of methods exposed from ipcRenderer + * to support communication to electron-main + * + * @type {typeof import('../electron-sandbox/globals').ipcRenderer} + */ + ipcRenderer: { + + /** + * @param {string} channel + * @param {any[]} args + */ + send(channel, ...args) { + validateIPC(channel); + + ipcRenderer.send(channel, ...args); + }, + + /** + * @param {string} channel + * @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener + */ + on(channel, listener) { + validateIPC(channel); + + ipcRenderer.on(channel, listener); + }, + + /** + * @param {string} channel + * @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener + */ + once(channel, listener) { + validateIPC(channel); + + ipcRenderer.once(channel, listener); + }, + + /** + * @param {string} channel + * @param {(event: import('electron').IpcRendererEvent, ...args: any[]) => void} listener + */ + removeListener(channel, listener) { + validateIPC(channel); + + ipcRenderer.removeListener(channel, listener); + } + }, + + /** + * Support for methods of webFrame type. + * + * @type {typeof import('../electron-sandbox/globals').webFrame} + */ + webFrame: { + + getZoomFactor() { + return webFrame.getZoomFactor(); + }, + + getZoomLevel() { + return webFrame.getZoomLevel(); + }, + + /** + * @param {number} level + */ + setZoomLevel(level) { + webFrame.setZoomLevel(level); + } + }, + + /** + * Support for methods of crashReporter type. + * + * @type {typeof import('../electron-sandbox/globals').crashReporter} + */ + crashReporter: { + + /** + * @param {Electron.CrashReporterStartOptions} options + */ + start(options) { + crashReporter.start(options); + } + } + }; + + //#region Utilities + + /** + * @param {string} channel + */ + function validateIPC(channel) { + if (!channel || !channel.startsWith('vscode:')) { + throw new Error(`Unsupported event IPC channel '${channel}'`); + } + } + + //#endregion +}()); diff --git a/src/vs/base/parts/sandbox/electron-sandbox/globals.ts b/src/vs/base/parts/sandbox/electron-sandbox/globals.ts new file mode 100644 index 00000000000..0cd04e036d4 --- /dev/null +++ b/src/vs/base/parts/sandbox/electron-sandbox/globals.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 { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes'; + +export const ipcRenderer = (window as any).vscode.ipcRenderer as { + + /** + * Listens to `channel`, when a new message arrives `listener` would be called with + * `listener(event, args...)`. + */ + on(channel: string, listener: (event: unknown, ...args: any[]) => void): void; + + /** + * Adds a one time `listener` function for the event. This `listener` is invoked + * only the next time a message is sent to `channel`, after which it is removed. + */ + once(channel: string, listener: (event: unknown, ...args: any[]) => void): void; + + /** + * Removes the specified `listener` from the listener array for the specified + * `channel`. + */ + removeListener(channel: string, listener: (event: unknown, ...args: any[]) => void): void; + + /** + * Send an asynchronous message to the main process via `channel`, along with + * arguments. Arguments will be serialized with the Structured Clone Algorithm, + * just like `postMessage`, so prototype chains will not be included. Sending + * Functions, Promises, Symbols, WeakMaps, or WeakSets will throw an exception. + * + * > **NOTE**: Sending non-standard JavaScript types such as DOM objects or special + * Electron objects is deprecated, and will begin throwing an exception starting + * with Electron 9. + * + * The main process handles it by listening for `channel` with the `ipcMain` + * module. + */ + send(channel: string, ...args: any[]): void; +}; + +export const webFrame = (window as any).vscode.webFrame as { + + /** + * The current zoom factor. + */ + getZoomFactor(): number; + + /** + * The current zoom level. + */ + getZoomLevel(): number; + + /** + * Changes the zoom level to the specified level. The original size is 0 and each + * increment above or below represents zooming 20% larger or smaller to default + * limits of 300% and 50% of original size, respectively. + */ + setZoomLevel(level: number): void; +}; + +export const crashReporter = (window as any).vscode.crashReporter as { + + /** + * You are required to call this method before using any other `crashReporter` APIs + * and in each process (main/renderer) from which you want to collect crash + * reports. You can pass different options to `crashReporter.start` when calling + * from different processes. + * + * **Note** Child processes created via the `child_process` module will not have + * access to the Electron modules. Therefore, to collect crash reports from them, + * use `process.crashReporter.start` instead. Pass the same options as above along + * with an additional one called `crashesDirectory` that should point to a + * directory to store the crash reports temporarily. You can test this out by + * calling `process.crash()` to crash the child process. + * + * **Note:** If you need send additional/updated `extra` parameters after your + * first call `start` you can call `addExtraParameter` on macOS or call `start` + * again with the new/updated `extra` parameters on Linux and Windows. + * + * **Note:** On macOS and windows, Electron uses a new `crashpad` client for crash + * collection and reporting. If you want to enable crash reporting, initializing + * `crashpad` from the main process using `crashReporter.start` is required + * regardless of which process you want to collect crashes from. Once initialized + * this way, the crashpad handler collects crashes from all processes. You still + * have to call `crashReporter.start` from the renderer or child process, otherwise + * crashes from them will get reported without `companyName`, `productName` or any + * of the `extra` information. + */ + start(options: CrashReporterStartOptions): void; +}; diff --git a/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts new file mode 100644 index 00000000000..ac3e6511710 --- /dev/null +++ b/src/vs/base/parts/sandbox/test/electron-sandbox/globals.test.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ipcRenderer, crashReporter, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; + +suite('Sandbox', () => { + test('globals', () => { + assert.ok(ipcRenderer); + assert.ok(crashReporter); + assert.ok(webFrame); + }); +}); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index 02aa3a96377..0760e2c8b91 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -130,4 +130,41 @@ suite('Paths', () => { assert.equal(extpath.indexOfPath('/some/long/path', '/some/long', false), 0); assert.equal(extpath.indexOfPath('/some/long/path', '/PATH', true), 10); }); + + test('parseLineAndColumnAware', () => { + let res = extpath.parseLineAndColumnAware('/foo/bar'); + assert.equal(res.path, '/foo/bar'); + assert.equal(res.line, undefined); + assert.equal(res.column, undefined); + + res = extpath.parseLineAndColumnAware('/foo/bar:33'); + assert.equal(res.path, '/foo/bar'); + assert.equal(res.line, 33); + assert.equal(res.column, 1); + + res = extpath.parseLineAndColumnAware('/foo/bar:33:34'); + assert.equal(res.path, '/foo/bar'); + assert.equal(res.line, 33); + assert.equal(res.column, 34); + + res = extpath.parseLineAndColumnAware('C:\\foo\\bar'); + assert.equal(res.path, 'C:\\foo\\bar'); + assert.equal(res.line, undefined); + assert.equal(res.column, undefined); + + res = extpath.parseLineAndColumnAware('C:\\foo\\bar:33'); + assert.equal(res.path, 'C:\\foo\\bar'); + assert.equal(res.line, 33); + assert.equal(res.column, 1); + + res = extpath.parseLineAndColumnAware('C:\\foo\\bar:33:34'); + assert.equal(res.path, 'C:\\foo\\bar'); + assert.equal(res.line, 33); + assert.equal(res.column, 34); + + res = extpath.parseLineAndColumnAware('/foo/bar:abb'); + assert.equal(res.path, '/foo/bar:abb'); + assert.equal(res.line, undefined); + assert.equal(res.column, undefined); + }); }); diff --git a/src/vs/workbench/test/browser/api/mock.ts b/src/vs/base/test/common/mock.ts similarity index 100% rename from src/vs/workbench/test/browser/api/mock.ts rename to src/vs/base/test/common/mock.ts diff --git a/src/vs/base/test/common/resources.test.ts b/src/vs/base/test/common/resources.test.ts index 0d26e8f04db..1e0d4496e26 100644 --- a/src/vs/base/test/common/resources.test.ts +++ b/src/vs/base/test/common/resources.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 { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey, compare } from 'vs/base/common/resources'; +import { dirname, basename, distinctParents, joinPath, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { isWindows } from 'vs/base/common/platform'; import { toSlashes } from 'vs/base/common/extpath'; @@ -235,16 +235,19 @@ suite('Resources', () => { } }); - function assertEqualURI(actual: URI, expected: URI, message?: string) { - if (!isEqual(expected, actual, undefined, false)) { + function assertEqualURI(actual: URI, expected: URI, message?: string, ignoreCase?: boolean) { + let util = ignoreCase ? extUriIgnorePathCase : extUri; + if (!util.isEqual(expected, actual)) { assert.equal(actual.toString(), expected.toString(), message); } } function assertRelativePath(u1: URI, u2: URI, expectedPath: string | undefined, ignoreJoin?: boolean, ignoreCase?: boolean) { - assert.equal(relativePath(u1, u2, ignoreCase), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); + let util = ignoreCase ? extUriIgnorePathCase : extUri; + + assert.equal(util.relativePath(u1, u2), expectedPath, `from ${u1.toString()} to ${u2.toString()}`); if (expectedPath !== undefined && !ignoreJoin) { - assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal'); + assertEqualURI(removeTrailingPathSeparator(joinPath(u1, expectedPath)), removeTrailingPathSeparator(u2), 'joinPath on relativePath should be equal', ignoreCase); } } @@ -347,10 +350,16 @@ suite('Resources', () => { }); function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean | undefined, expected: boolean) { - assert.equal(isEqual(u1, u2, ignoreCase), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); - assert.equal(compare(u1, u2, ignoreCase) === 0, expected); - assert.equal(getComparisonKey(u1, ignoreCase) === getComparisonKey(u2, ignoreCase), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); - assert.equal(isEqualOrParent(u1, u2, ignoreCase), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); + + let util = ignoreCase ? extUriIgnorePathCase : extUri; + + assert.equal(util.isEqual(u1, u2), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); + assert.equal(util.compare(u1, u2) === 0, expected); + assert.equal(util.getComparisonKey(u1) === util.getComparisonKey(u2), expected, `comparison keys ${u1.toString()}, ${u2.toString()}`); + assert.equal(util.isEqualOrParent(u1, u2), expected, `isEqualOrParent ${u1.toString()}, ${u2.toString()}`); + if (!ignoreCase) { + assert.equal(u1.toString() === u2.toString(), expected); + } } @@ -390,34 +399,35 @@ suite('Resources', () => { }); test('isEqualOrParent', () => { + let fileURI = isWindows ? URI.file('c:\\foo\\bar') : URI.file('/foo/bar'); let fileURI2 = isWindows ? URI.file('c:\\foo') : URI.file('/foo'); let fileURI2b = isWindows ? URI.file('C:\\Foo\\') : URI.file('/Foo/'); - assert.equal(isEqualOrParent(fileURI, fileURI, true), true, '1'); - assert.equal(isEqualOrParent(fileURI, fileURI, false), true, '2'); - assert.equal(isEqualOrParent(fileURI, fileURI2, true), true, '3'); - assert.equal(isEqualOrParent(fileURI, fileURI2, false), true, '4'); - assert.equal(isEqualOrParent(fileURI, fileURI2b, true), true, '5'); - assert.equal(isEqualOrParent(fileURI, fileURI2b, false), false, '6'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI), true, '1'); + assert.equal(extUri.isEqualOrParent(fileURI, fileURI), true, '2'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2), true, '3'); + assert.equal(extUri.isEqualOrParent(fileURI, fileURI2), true, '4'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI, fileURI2b), true, '5'); + assert.equal(extUri.isEqualOrParent(fileURI, fileURI2b), false, '6'); - assert.equal(isEqualOrParent(fileURI2, fileURI, false), false, '7'); - assert.equal(isEqualOrParent(fileURI2b, fileURI2, true), true, '8'); + assert.equal(extUri.isEqualOrParent(fileURI2, fileURI), false, '7'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI2b, fileURI2), true, '8'); let fileURI3 = URI.parse('foo://server:453/foo/bar/goo'); let fileURI4 = URI.parse('foo://server:453/foo/'); let fileURI5 = URI.parse('foo://server:453/foo'); - assert.equal(isEqualOrParent(fileURI3, fileURI3, true), true, '11'); - assert.equal(isEqualOrParent(fileURI3, fileURI3, false), true, '12'); - assert.equal(isEqualOrParent(fileURI3, fileURI4, true), true, '13'); - assert.equal(isEqualOrParent(fileURI3, fileURI4, false), true, '14'); - assert.equal(isEqualOrParent(fileURI3, fileURI, true), false, '15'); - assert.equal(isEqualOrParent(fileURI5, fileURI5, true), true, '16'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI3, true), true, '11'); + assert.equal(extUri.isEqualOrParent(fileURI3, fileURI3), true, '12'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI4, true), true, '13'); + assert.equal(extUri.isEqualOrParent(fileURI3, fileURI4), true, '14'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI3, fileURI, true), false, '15'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI5, fileURI5, true), true, '16'); let fileURI6 = URI.parse('foo://server:453/foo?q=1'); let fileURI7 = URI.parse('foo://server:453/foo/bar?q=1'); - assert.equal(isEqualOrParent(fileURI6, fileURI5, true), false, '17'); - assert.equal(isEqualOrParent(fileURI6, fileURI6, true), true, '18'); - assert.equal(isEqualOrParent(fileURI7, fileURI6, true), true, '19'); - assert.equal(isEqualOrParent(fileURI7, fileURI5, true), false, '20'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI5), false, '17'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI6, fileURI6), true, '18'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI6), true, '19'); + assert.equal(extUriIgnorePathCase.isEqualOrParent(fileURI7, fileURI5), false, '20'); }); }); diff --git a/src/vs/base/test/common/skipList.test.ts b/src/vs/base/test/common/skipList.test.ts new file mode 100644 index 00000000000..f6a083e3cb2 --- /dev/null +++ b/src/vs/base/test/common/skipList.test.ts @@ -0,0 +1,218 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SkipList } from 'vs/base/common/skipList'; +import { StopWatch } from 'vs/base/common/stopwatch'; +import { binarySearch } from 'vs/base/common/arrays'; + + +suite('SkipList', function () { + + function assertValues(list: SkipList, expected: V[]) { + assert.equal(list.size, expected.length); + assert.deepEqual([...list.values()], expected); + + let valuesFromEntries = [...list.entries()].map(entry => entry[1]); + assert.deepEqual(valuesFromEntries, expected); + + let valuesFromIter = [...list].map(entry => entry[1]); + assert.deepEqual(valuesFromIter, expected); + + let i = 0; + list.forEach((value, _key, map) => { + assert.ok(map === list); + assert.deepEqual(value, expected[i++]); + }); + } + + function assertKeys(list: SkipList, expected: K[]) { + assert.equal(list.size, expected.length); + assert.deepEqual([...list.keys()], expected); + + let keysFromEntries = [...list.entries()].map(entry => entry[0]); + assert.deepEqual(keysFromEntries, expected); + + let keysFromIter = [...list].map(entry => entry[0]); + assert.deepEqual(keysFromIter, expected); + + let i = 0; + list.forEach((_value, key, map) => { + assert.ok(map === list); + assert.deepEqual(key, expected[i++]); + }); + } + + test('set/get/delete', function () { + let list = new SkipList((a, b) => a - b); + + assert.equal(list.get(3), undefined); + list.set(3, 1); + assert.equal(list.get(3), 1); + assertValues(list, [1]); + + list.set(3, 3); + assertValues(list, [3]); + + list.set(1, 1); + list.set(4, 4); + assert.equal(list.get(3), 3); + assert.equal(list.get(1), 1); + assert.equal(list.get(4), 4); + assertValues(list, [1, 3, 4]); + + assert.equal(list.delete(17), false); + + assert.equal(list.delete(1), true); + assert.equal(list.get(1), undefined); + assert.equal(list.get(3), 3); + assert.equal(list.get(4), 4); + + assertValues(list, [3, 4]); + }); + + test('Figure 3', function () { + let list = new SkipList((a, b) => a - b); + list.set(3, true); + list.set(6, true); + list.set(7, true); + list.set(9, true); + list.set(12, true); + list.set(19, true); + list.set(21, true); + list.set(25, true); + + assertKeys(list, [3, 6, 7, 9, 12, 19, 21, 25]); + + list.set(17, true); + assert.deepEqual(list.size, 9); + assertKeys(list, [3, 6, 7, 9, 12, 17, 19, 21, 25]); + }); + + test('capacity max', function () { + let list = new SkipList((a, b) => a - b, 10); + list.set(1, true); + list.set(2, true); + list.set(3, true); + list.set(4, true); + list.set(5, true); + list.set(6, true); + list.set(7, true); + list.set(8, true); + list.set(9, true); + list.set(10, true); + list.set(11, true); + list.set(12, true); + + assertKeys(list, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + const cmp = (a: number, b: number): number => { + if (a < b) { + return -1; + } else if (a > b) { + return 1; + } else { + return 0; + } + }; + + function insertArraySorted(array: number[], element: number) { + let idx = binarySearch(array, element, cmp); + if (idx >= 0) { + array[idx] = element; + } else { + idx = ~idx; + // array = array.slice(0, idx).concat(element, array.slice(idx)); + array.splice(idx, 0, element); + } + return array; + } + + function delArraySorted(array: number[], element: number) { + let idx = binarySearch(array, element, cmp); + if (idx >= 0) { + // array = array.slice(0, idx).concat(array.slice(idx)); + array.splice(idx, 1); + } + return array; + } + + + test('perf', function () { + this.skip(); + + // data + const max = 2 ** 16; + const values = new Set(); + for (let i = 0; i < max; i++) { + let value = Math.floor(Math.random() * max); + values.add(value); + } + console.log(values.size); + + // init + let list = new SkipList(cmp, max); + let sw = new StopWatch(true); + values.forEach(value => list.set(value, true)); + sw.stop(); + console.log(`[LIST] ${list.size} elements after ${sw.elapsed()}ms`); + let array: number[] = []; + sw = new StopWatch(true); + values.forEach(value => array = insertArraySorted(array, value)); + sw.stop(); + console.log(`[ARRAY] ${array.length} elements after ${sw.elapsed()}ms`); + + // get + sw = new StopWatch(true); + let someValues = [...values].slice(0, values.size / 4); + someValues.forEach(key => { + let value = list.get(key); // find + console.assert(value, '[LIST] must have ' + key); + list.get(-key); // miss + }); + sw.stop(); + console.log(`[LIST] retrieve ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); + sw = new StopWatch(true); + someValues.forEach(key => { + let idx = binarySearch(array, key, cmp); // find + console.assert(idx >= 0, '[ARRAY] must have ' + key); + binarySearch(array, -key, cmp); // miss + }); + sw.stop(); + console.log(`[ARRAY] retrieve ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); + + + // insert + sw = new StopWatch(true); + someValues.forEach(key => { + list.set(-key, false); + }); + sw.stop(); + console.log(`[LIST] insert ${sw.elapsed()}ms (${(sw.elapsed() / someValues.length).toPrecision(4)}ms/op)`); + sw = new StopWatch(true); + someValues.forEach(key => { + array = insertArraySorted(array, -key); + }); + sw.stop(); + console.log(`[ARRAY] insert ${sw.elapsed()}ms (${(sw.elapsed() / someValues.length).toPrecision(4)}ms/op)`); + + // delete + sw = new StopWatch(true); + someValues.forEach(key => { + list.delete(key); // find + list.delete(-key); // miss + }); + sw.stop(); + console.log(`[LIST] delete ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); + sw = new StopWatch(true); + someValues.forEach(key => { + array = delArraySorted(array, key); // find + array = delArraySorted(array, -key); // miss + }); + sw.stop(); + console.log(`[ARRAY] delete ${sw.elapsed()}ms (${(sw.elapsed() / (someValues.length * 2)).toPrecision(4)}ms/op)`); + }); +}); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index d7547c626a0..74ee36bca78 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -32,7 +32,6 @@ 'xterm': `${window.location.origin}/static/remote/web/node_modules/xterm/lib/xterm.js`, 'xterm-addon-search': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, 'xterm-addon-unicode11': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, - 'xterm-addon-web-links': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, 'xterm-addon-webgl': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, 'semver-umd': `${window.location.origin}/static/remote/web/node_modules/semver-umd/lib/semver-umd.js`, } diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 07445237528..1825bcffec6 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -36,7 +36,6 @@ 'xterm': `${window.location.origin}/static/node_modules/xterm/lib/xterm.js`, 'xterm-addon-search': `${window.location.origin}/static/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, 'xterm-addon-unicode11': `${window.location.origin}/static/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, - 'xterm-addon-web-links': `${window.location.origin}/static/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, 'xterm-addon-webgl': `${window.location.origin}/static/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, 'semver-umd': `${window.location.origin}/static/node_modules/semver-umd/lib/semver-umd.js`, } diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index 24c2b5372ac..06797ea956c 100644 --- a/src/vs/code/electron-browser/issue/issueReporterMain.ts +++ b/src/vs/code/electron-browser/issue/issueReporterMain.ts @@ -3,10 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { clipboard, ipcRenderer, shell, webFrame } from 'electron'; +import 'vs/css!./media/issueReporter'; +import { ElectronService, IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; +import { ipcRenderer, webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import * as os from 'os'; import * as browser from 'vs/base/browser/browser'; -import { $ } from 'vs/base/browser/dom'; +import { $, windowOpenNoOpener } from 'vs/base/browser/dom'; import { Button } from 'vs/base/browser/ui/button/button'; import 'vs/base/browser/ui/codicons/codiconStyles'; // make sure codicon css is loaded import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; @@ -15,21 +17,19 @@ import { debounce } from 'vs/base/common/decorators'; import { Disposable } from 'vs/base/common/lifecycle'; import * as platform from 'vs/base/common/platform'; import { escape } from 'vs/base/common/strings'; -import { getDelayedChannel } from 'vs/base/parts/ipc/common/ipc'; -import { createChannelSender } from 'vs/base/parts/ipc/node/ipc'; +import { getDelayedChannel, createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { connect as connectNet } from 'vs/base/parts/ipc/node/ipc.net'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; import { IssueReporterData as IssueReporterModelData, IssueReporterModel } from 'vs/code/electron-browser/issue/issueReporterModel'; import BaseHtml from 'vs/code/electron-browser/issue/issueReporterPage'; -import 'vs/css!./media/issueReporter'; import { localize } from 'vs/nls'; import { isRemoteDiagnosticError, SystemInfo } from 'vs/platform/diagnostics/common/diagnostics'; import { EnvironmentService, INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; +import { IMainProcessService, MainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService'; -import { ISettingsSearchIssueReporterData, IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/node/issue'; +import { ISettingsSearchIssueReporterData, IssueReporterData, IssueReporterExtensionData, IssueReporterFeatures, IssueReporterStyles, IssueType } from 'vs/platform/issue/common/issue'; import { getLogLevel, ILogService } from 'vs/platform/log/common/log'; import { FollowerLogService, LoggerChannelClient } from 'vs/platform/log/common/logIpc'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; @@ -64,6 +64,7 @@ export function startup(configuration: IssueReporterConfiguration) { export class IssueReporter extends Disposable { private environmentService!: INativeEnvironmentService; + private electronService!: IElectronService; private telemetryService!: ITelemetryService; private logService!: ILogService; private readonly issueReporterModel: IssueReporterModel; @@ -324,6 +325,9 @@ export class IssueReporter extends Disposable { const mainProcessService = new MainProcessService(configuration.windowId); serviceCollection.set(IMainProcessService, mainProcessService); + this.electronService = new ElectronService(configuration.windowId, mainProcessService) as IElectronService; + serviceCollection.set(IElectronService, this.electronService); + this.environmentService = new EnvironmentService(configuration, configuration.execPath); const logService = new SpdLogService(`issuereporter${configuration.windowId}`, this.environmentService.logsPath, getLogLevel(this.environmentService)); @@ -462,7 +466,7 @@ export class IssueReporter extends Disposable { this.addEventListener('extensionBugsLink', 'click', (e: Event) => { const url = (e.target).innerText; - shell.openExternal(url); + windowOpenNoOpener(url); }); this.addEventListener('disableExtensions', 'keydown', (e: Event) => { @@ -941,9 +945,9 @@ export class IssueReporter extends Disposable { private async writeToClipboard(baseUrl: string, issueBody: string): Promise { return new Promise((resolve, reject) => { - ipcRenderer.once('vscode:issueReporterClipboardResponse', (_: unknown, shouldWrite: boolean) => { + ipcRenderer.once('vscode:issueReporterClipboardResponse', async (event: unknown, shouldWrite: boolean) => { if (shouldWrite) { - clipboard.writeText(issueBody); + await this.electronService.writeClipboardText(issueBody); resolve(baseUrl + `&body=${encodeURIComponent(localize('pasteData', "We have written the needed data into your clipboard because it was too large to send. Please paste."))}`); } else { reject(); @@ -1194,7 +1198,7 @@ export class IssueReporter extends Disposable { event.stopPropagation(); // Exclude right click if (event.which < 3) { - shell.openExternal((event.target).href); + windowOpenNoOpener((event.target).href); this.telemetryService.publicLog2('issueReporterViewSimilarIssue'); } } diff --git a/src/vs/code/electron-browser/issue/issueReporterModel.ts b/src/vs/code/electron-browser/issue/issueReporterModel.ts index 96bae9110ee..47059817368 100644 --- a/src/vs/code/electron-browser/issue/issueReporterModel.ts +++ b/src/vs/code/electron-browser/issue/issueReporterModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { assign } from 'vs/base/common/objects'; -import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/node/issue'; +import { IssueType, ISettingSearchResult, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; import { SystemInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; export interface IssueReporterData { diff --git a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts b/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts index 2dda07e6fed..e7ce5b7e463 100644 --- a/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts +++ b/src/vs/code/electron-browser/issue/test/testReporterModel.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IssueReporterModel } from 'vs/code/electron-browser/issue/issueReporterModel'; import { normalizeGitHubUrl } from 'vs/platform/issue/common/issueReporterUtil'; -import { IssueType } from 'vs/platform/issue/node/issue'; +import { IssueType } from 'vs/platform/issue/common/issue'; suite('IssueReporter', () => { diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 9684cbd4c20..250f09adab6 100644 --- a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts +++ b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts @@ -4,16 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/processExplorer'; -import { webFrame, ipcRenderer, clipboard } from 'electron'; +import { clipboard } from 'electron'; +import { webFrame, ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { repeat } from 'vs/base/common/strings'; import { totalmem } from 'os'; import product from 'vs/platform/product/common/product'; import { localize } from 'vs/nls'; -import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/node/issue'; +import { ProcessExplorerStyles, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import * as browser from 'vs/base/browser/browser'; import * as platform from 'vs/base/common/platform'; import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu'; -import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu'; +import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu'; import { ProcessItem } from 'vs/base/common/processes'; import { addDisposableListener } from 'vs/base/browser/dom'; import { DisposableStore } from 'vs/base/common/lifecycle'; @@ -369,7 +370,7 @@ function requestProcessList(totalWaitTime: number): void { // Wait at least a second between requests. if (waited > 1000) { - ipcRenderer.send('windowsInfoRequest'); + ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); } else { requestProcessList(waited); @@ -393,18 +394,18 @@ export function startup(data: ProcessExplorerData): void { createCloseListener(); // Map window process pids to titles, annotate process names with this when rendering to distinguish between them - ipcRenderer.on('vscode:windowsInfoResponse', (_event: unknown, windows: any[]) => { + ipcRenderer.on('vscode:windowsInfoResponse', (event: unknown, windows: any[]) => { mapPidToWindowTitle = new Map(); windows.forEach(window => mapPidToWindowTitle.set(window.pid, window.title)); }); - ipcRenderer.on('vscode:listProcessesResponse', (_event: Event, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { + ipcRenderer.on('vscode:listProcessesResponse', (event: unknown, processRoots: [{ name: string, rootProcess: ProcessItem | IRemoteDiagnosticError }]) => { updateProcessInfo(processRoots); requestProcessList(0); }); lastRequestTime = Date.now(); - ipcRenderer.send('windowsInfoRequest'); + ipcRenderer.send('vscode:windowsInfoRequest'); ipcRenderer.send('vscode:listProcesses'); document.onkeydown = (e: KeyboardEvent) => { diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 927e10e7577..850ae498d0c 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -27,7 +27,7 @@ import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProper import { TelemetryAppenderChannel } from 'vs/platform/telemetry/node/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { AppInsightsAppender } from 'vs/platform/telemetry/node/appInsightsAppender'; -import { ipcRenderer } from 'electron'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ILogService, LogLevel, ILoggerService } from 'vs/platform/log/common/log'; import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; import { LocalizationsService } from 'vs/platform/localizations/node/localizations'; @@ -35,26 +35,25 @@ import { ILocalizationsService } from 'vs/platform/localizations/common/localiza import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; import { DownloadService } from 'vs/platform/download/common/downloadService'; import { IDownloadService } from 'vs/platform/download/common/download'; -import { IChannel, IServerChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; -import { createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/node/ipc'; +import { IChannel, IServerChannel, StaticRouter, createChannelSender, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import { NodeCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/nodeCachedDataCleaner'; import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner'; import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner'; import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner'; -import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { SpdLogService } from 'vs/platform/log/node/spdlogService'; import { DiagnosticsService, IDiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsService'; import { DiagnosticsChannel } from 'vs/platform/diagnostics/node/diagnosticsIpc'; import { FileService } from 'vs/platform/files/common/fileService'; import { IFileService } from 'vs/platform/files/common/files'; -import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { Schemas } from 'vs/base/common/network'; import { IProductService } from 'vs/platform/product/common/productService'; import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; -import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient } from 'vs/platform/userDataSync/common/userDataSyncIpc'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; @@ -66,10 +65,11 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { GlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionEnablementService'; import { UserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSyncEnablementService'; import { IAuthenticationTokenService, AuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; -import { AuthenticationTokenServiceChannel } from 'vs/platform/authentication/common/authenticationIpc'; +import { AuthenticationTokenServiceChannel } from 'vs/platform/authentication/electron-browser/authenticationIpc'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { ExtensionTipsService } from 'vs/platform/extensionManagement/node/extensionTipsService'; +import { UserDataSyncMachinesService, IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; export interface ISharedProcessConfiguration { readonly machineId: string; @@ -89,7 +89,11 @@ interface ISharedProcessInitData { const eventPrefix = 'monacoworkbench'; class MainProcessService implements IMainProcessService { - constructor(private server: Server, private mainRouter: StaticRouter) { } + + constructor( + private server: Server, + private mainRouter: StaticRouter + ) { } _serviceBrand: undefined; @@ -109,7 +113,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const onExit = () => disposables.dispose(); process.once('exit', onExit); - ipcRenderer.once('electron-main->shared-process: exit', onExit); + ipcRenderer.once('vscode:electron-main->shared-process=exit', onExit); disposables.add(server); @@ -200,6 +204,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService)); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); + services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService)); services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService)); services.set(IUserDataSyncEnablementService, new SyncDescriptor(UserDataSyncEnablementService)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); @@ -225,12 +230,16 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const extensionTipsChannel = new ExtensionTipsChannel(extensionTipsService); server.registerChannel('extensionTipsService', extensionTipsChannel); + const userDataSyncMachinesService = accessor.get(IUserDataSyncMachinesService); + const userDataSyncMachineChannel = new UserDataSyncMachinesServiceChannel(userDataSyncMachinesService); + server.registerChannel('userDataSyncMachines', userDataSyncMachineChannel); + const authTokenService = accessor.get(IAuthenticationTokenService); const authTokenChannel = new AuthenticationTokenServiceChannel(authTokenService); server.registerChannel('authToken', authTokenChannel); const userDataSyncService = accessor.get(IUserDataSyncService); - const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); + const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService, logService); server.registerChannel('userDataSync', userDataSyncChannel); const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService); @@ -292,17 +301,17 @@ async function handshake(configuration: ISharedProcessConfiguration): Promise(c => { - ipcRenderer.once('electron-main->shared-process: payload', (_: any, r: ISharedProcessInitData) => c(r)); + ipcRenderer.once('vscode:electron-main->shared-process=payload', (event: unknown, r: ISharedProcessInitData) => c(r)); // tell electron-main we are ready to receive payload - ipcRenderer.send('shared-process->electron-main: ready-for-payload'); + ipcRenderer.send('vscode:shared-process->electron-main=ready-for-payload'); }); // await IPC connection and signal this back to electron-main const server = await setupIPC(data.sharedIPCHandle); - ipcRenderer.send('shared-process->electron-main: ipc-ready'); + ipcRenderer.send('vscode:shared-process->electron-main=ipc-ready'); // await initialization and signal this back to electron-main await main(server, data, configuration); - ipcRenderer.send('shared-process->electron-main: init-done'); + ipcRenderer.send('vscode:shared-process->electron-main=init-done'); } diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index fd287c25045..de09ab8f939 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -8,7 +8,6 @@ import { IProcessEnvironment, isWindows, isMacintosh } from 'vs/base/common/plat import { WindowsMainService } from 'vs/platform/windows/electron-main/windowsMainService'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; import { OpenContext } from 'vs/platform/windows/node/window'; -import { ActiveWindowManager } from 'vs/code/node/activeWindowTracker'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { getShellEnvironment } from 'vs/code/node/shellEnv'; import { IUpdateService } from 'vs/platform/update/common/update'; @@ -32,12 +31,12 @@ import { NullTelemetryService, combinedAppender, LogAppender } from 'vs/platform import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService'; import { resolveCommonProperties } from 'vs/platform/telemetry/node/commonProperties'; -import { getDelayedChannel, StaticRouter } from 'vs/base/parts/ipc/common/ipc'; -import { createChannelReceiver } from 'vs/base/parts/ipc/node/ipc'; +import { getDelayedChannel, StaticRouter, createChannelReceiver } from 'vs/base/parts/ipc/common/ipc'; import product from 'vs/platform/product/common/product'; import { ProxyAuthHandler } from 'vs/code/electron-main/auth'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; +import { ActiveWindowManager } from 'vs/platform/windows/electron-main/windowTracker'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { WorkspacesService } from 'vs/platform/workspaces/electron-main/workspacesService'; @@ -45,14 +44,12 @@ import { getMachineId } from 'vs/base/node/id'; import { Win32UpdateService } from 'vs/platform/update/electron-main/updateService.win32'; import { LinuxUpdateService } from 'vs/platform/update/electron-main/updateService.linux'; import { DarwinUpdateService } from 'vs/platform/update/electron-main/updateService.darwin'; -import { IIssueService } from 'vs/platform/issue/node/issue'; -import { IssueMainService } from 'vs/platform/issue/electron-main/issueMainService'; +import { IssueMainService, IIssueMainService } from 'vs/platform/issue/electron-main/issueMainService'; import { LoggerChannel } from 'vs/platform/log/common/logIpc'; import { setUnexpectedErrorHandler, onUnexpectedError } from 'vs/base/common/errors'; import { ElectronURLListener } from 'vs/platform/url/electron-main/electronUrlListener'; import { serve as serveDriver } from 'vs/platform/driver/electron-main/driver'; -import { IMenubarService } from 'vs/platform/menubar/node/menubar'; -import { MenubarMainService } from 'vs/platform/menubar/electron-main/menubarMainService'; +import { IMenubarMainService, MenubarMainService } from 'vs/platform/menubar/electron-main/menubarMainService'; import { RunOnceScheduler } from 'vs/base/common/async'; import { registerContextMenuListener } from 'vs/base/parts/contextmenu/electron-main/contextmenu'; import { homedir } from 'os'; @@ -65,7 +62,7 @@ import { GlobalStorageDatabaseChannel } from 'vs/platform/storage/node/storageIp import { BackupMainService } from 'vs/platform/backup/electron-main/backupMainService'; import { IBackupMainService } from 'vs/platform/backup/electron-main/backup'; import { WorkspacesHistoryMainService, IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { URLService } from 'vs/platform/url/node/urlService'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { WorkspacesMainService, IWorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; import { statSync } from 'fs'; import { DiagnosticsService } from 'vs/platform/diagnostics/node/diagnosticsIpc'; @@ -81,6 +78,8 @@ import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common import { StorageKeysSyncRegistryChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc'; import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; import { mnemonicButtonLabel, getPathLabel } from 'vs/base/common/labels'; +import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMainService'; +import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService'; export class CodeApplication extends Disposable { private windowsMainService: IWindowsMainService | undefined; @@ -467,10 +466,11 @@ export class CodeApplication extends Disposable { const diagnosticsChannel = getDelayedChannel(sharedProcessReady.then(client => client.getChannel('diagnostics'))); services.set(IDiagnosticsService, new SyncDescriptor(DiagnosticsService, [diagnosticsChannel])); - services.set(IIssueService, new SyncDescriptor(IssueMainService, [machineId, this.userEnv])); + services.set(IIssueMainService, new SyncDescriptor(IssueMainService, [machineId, this.userEnv])); services.set(IElectronMainService, new SyncDescriptor(ElectronMainService)); + services.set(IWebviewManagerService, new SyncDescriptor(WebviewMainService)); services.set(IWorkspacesService, new SyncDescriptor(WorkspacesService)); - services.set(IMenubarService, new SyncDescriptor(MenubarMainService)); + services.set(IMenubarMainService, new SyncDescriptor(MenubarMainService)); const storageMainService = new StorageMainService(this.logService, this.environmentService); services.set(IStorageMainService, storageMainService); @@ -480,7 +480,7 @@ export class CodeApplication extends Disposable { services.set(IBackupMainService, backupMainService); services.set(IWorkspacesHistoryMainService, new SyncDescriptor(WorkspacesHistoryMainService)); - services.set(IURLService, new SyncDescriptor(URLService)); + services.set(IURLService, new SyncDescriptor(NativeURLService)); services.set(IWorkspacesMainService, new SyncDescriptor(WorkspacesMainService)); // Telemetry @@ -551,8 +551,8 @@ export class CodeApplication extends Disposable { const updateChannel = new UpdateChannel(updateService); electronIpcServer.registerChannel('update', updateChannel); - const issueService = accessor.get(IIssueService); - const issueChannel = createChannelReceiver(issueService); + const issueMainService = accessor.get(IIssueMainService); + const issueChannel = createChannelReceiver(issueMainService); electronIpcServer.registerChannel('issue', issueChannel); const electronMainService = accessor.get(IElectronMainService); @@ -568,14 +568,18 @@ export class CodeApplication extends Disposable { const workspacesChannel = createChannelReceiver(workspacesService); electronIpcServer.registerChannel('workspaces', workspacesChannel); - const menubarService = accessor.get(IMenubarService); - const menubarChannel = createChannelReceiver(menubarService); + const menubarMainService = accessor.get(IMenubarMainService); + const menubarChannel = createChannelReceiver(menubarMainService); electronIpcServer.registerChannel('menubar', menubarChannel); const urlService = accessor.get(IURLService); const urlChannel = createChannelReceiver(urlService); electronIpcServer.registerChannel('url', urlChannel); + const webviewManagerService = accessor.get(IWebviewManagerService); + const webviewChannel = createChannelReceiver(webviewManagerService); + electronIpcServer.registerChannel('webview', webviewChannel); + const storageMainService = accessor.get(IStorageMainService); const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService)); electronIpcServer.registerChannel('storage', storageChannel); diff --git a/src/vs/code/electron-main/auth.ts b/src/vs/code/electron-main/auth.ts index b74e1c4e9c1..5e09e170c84 100644 --- a/src/vs/code/electron-main/auth.ts +++ b/src/vs/code/electron-main/auth.ts @@ -59,7 +59,8 @@ export class ProxyAuthHandler extends Disposable { title: 'VS Code', webPreferences: { nodeIntegration: true, - webviewTag: true + webviewTag: true, + enableWebSQL: false } }; diff --git a/src/vs/code/electron-main/main.ts b/src/vs/code/electron-main/main.ts index 650d665d2ca..744bfea9530 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -13,7 +13,7 @@ import { mkdirp } from 'vs/base/node/pfs'; import { validatePaths } from 'vs/code/node/paths'; import { LifecycleMainService, ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { Server, serve, connect } from 'vs/base/parts/ipc/node/ipc.net'; -import { createChannelSender } from 'vs/base/parts/ipc/node/ipc'; +import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index 782cb228ea4..7ace2583d75 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { URI } from 'vs/base/common/uri'; import { memoize } from 'vs/base/common/decorators'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { BrowserWindow, ipcMain, WebContents, Event as ElectronEvent } from 'electron'; @@ -32,7 +33,7 @@ export class SharedProcess implements ISharedProcess { @IThemeMainService private readonly themeMainService: IThemeMainService ) { // overall ready promise when shared process signals initialization is done - this._whenReady = new Promise(c => ipcMain.once('shared-process->electron-main: init-done', () => c(undefined))); + this._whenReady = new Promise(c => ipcMain.once('vscode:shared-process->electron-main=init-done', () => c(undefined))); } @memoize @@ -41,9 +42,11 @@ export class SharedProcess implements ISharedProcess { show: false, backgroundColor: this.themeMainService.getBackgroundColor(), webPreferences: { + preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, images: false, nodeIntegration: true, webgl: false, + enableWebSQL: false, disableBlinkFeatures: 'Auxclick' // do NOT change, allows us to identify this window as shared-process in the process explorer } }); @@ -104,18 +107,18 @@ export class SharedProcess implements ISharedProcess { return new Promise(c => { // send payload once shared process is ready to receive it - disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'shared-process->electron-main: ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => { - sender.send('electron-main->shared-process: payload', { + disposables.add(Event.once(Event.fromNodeEventEmitter(ipcMain, 'vscode:shared-process->electron-main=ready-for-payload', ({ sender }: { sender: WebContents }) => sender))(sender => { + sender.send('vscode:electron-main->shared-process=payload', { sharedIPCHandle: this.environmentService.sharedIPCHandle, args: this.environmentService.args, logLevel: this.logService.getLevel() }); // signal exit to shared process when we get disposed - disposables.add(toDisposable(() => sender.send('electron-main->shared-process: exit'))); + disposables.add(toDisposable(() => sender.send('vscode:electron-main->shared-process=exit'))); // complete IPC-ready promise when shared process signals this to us - ipcMain.once('shared-process->electron-main: ipc-ready', () => c(undefined)); + ipcMain.once('vscode:shared-process->electron-main=ipc-ready', () => c(undefined)); })); }); } diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index 2cdb6b90d6d..78095162798 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -163,9 +163,11 @@ export class CodeWindow extends Disposable implements ICodeWindow { show: !isFullscreenOrMaximized, title: product.nameLong, webPreferences: { + preload: URI.parse(this.doGetPreloadUrl()).fsPath, nodeIntegration: true, nodeIntegrationInWorker: RUN_TEXTMATE_IN_WORKER, - webviewTag: true + webviewTag: true, + enableWebSQL: false } }; @@ -599,9 +601,12 @@ export class CodeWindow extends Disposable implements ICodeWindow { } // Do not set to empty configuration at startup if setting is empty to not override configuration through CLI options: const env = process.env; - const newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() + let newHttpProxy = (this.configurationService.getValue('http.proxy') || '').trim() || (env.https_proxy || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.HTTP_PROXY || '').trim() // Not standardized. || undefined; + if (newHttpProxy?.endsWith('/')) { + newHttpProxy = newHttpProxy.substr(0, newHttpProxy.length - 1); + } const newNoProxy = (env.no_proxy || env.NO_PROXY || '').trim() || undefined; // Not standardized. if ((newHttpProxy || '').indexOf('@') === -1 && (newHttpProxy !== this.currentHttpProxy || newNoProxy !== this.currentNoProxy)) { this.currentHttpProxy = newHttpProxy; @@ -776,6 +781,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`; } + private doGetPreloadUrl(): string { + return require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js'); + } + serializeWindowState(): IWindowState { if (!this._win) { return defaultWindowState(); diff --git a/src/vs/code/node/cliProcessMain.ts b/src/vs/code/node/cliProcessMain.ts index ea0f62156a2..e68a950da08 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -86,7 +86,7 @@ export class Main { } else if (argv['list-extensions']) { await this.listExtensions(!!argv['show-versions'], argv['category']); } else if (argv['install-extension']) { - await this.installExtensions(argv['install-extension'], !!argv['force']); + await this.installExtensions(argv['install-extension'], !!argv['force'], !!argv['donot-sync']); } else if (argv['uninstall-extension']) { await this.uninstallExtension(argv['uninstall-extension']); } else if (argv['locate-extension']) { @@ -126,7 +126,7 @@ export class Main { extensions.forEach(e => console.log(getId(e.manifest, showVersions))); } - private async installExtensions(extensions: string[], force: boolean): Promise { + private async installExtensions(extensions: string[], force: boolean, donotSync: boolean): Promise { const failed: string[] = []; const installedExtensionsManifests: IExtensionManifest[] = []; if (extensions.length) { @@ -135,7 +135,7 @@ export class Main { for (const extension of extensions) { try { - const manifest = await this.installExtension(extension, force); + const manifest = await this.installExtension(extension, force, donotSync); if (manifest) { installedExtensionsManifests.push(manifest); } @@ -150,7 +150,7 @@ export class Main { return failed.length ? Promise.reject(localize('installation failed', "Failed Installing Extensions: {0}", failed.join(', '))) : Promise.resolve(); } - private async installExtension(extension: string, force: boolean): Promise { + private async installExtension(extension: string, force: boolean, donotSync: boolean): Promise { if (/\.vsix$/i.test(extension)) { extension = path.isAbsolute(extension) ? extension : path.join(process.cwd(), extension); @@ -158,7 +158,7 @@ export class Main { const valid = await this.validate(manifest, force); if (valid) { - return this.extensionManagementService.install(URI.file(extension)).then(id => { + return this.extensionManagementService.install(URI.file(extension), donotSync).then(id => { console.log(localize('successVsixInstall', "Extension '{0}' was successfully installed.", getBaseLabel(extension))); return manifest; }, error => { @@ -205,7 +205,7 @@ export class Main { } console.log(localize('updateMessage', "Updating the extension '{0}' to the version {1}", id, extension.version)); } - await this.installFromGallery(id, extension); + await this.installFromGallery(id, extension, donotSync); return manifest; })); } @@ -227,11 +227,11 @@ export class Main { return true; } - private async installFromGallery(id: string, extension: IGalleryExtension): Promise { + private async installFromGallery(id: string, extension: IGalleryExtension, donotSync: boolean): Promise { console.log(localize('installing', "Installing extension '{0}' v{1}...", id, extension.version)); try { - await this.extensionManagementService.installFromGallery(extension); + await this.extensionManagementService.installFromGallery(extension, donotSync); console.log(localize('successInstall', "Extension '{0}' v{1} was successfully installed.", id, extension.version)); } catch (error) { if (isPromiseCanceledError(error)) { diff --git a/src/vs/code/node/paths.ts b/src/vs/code/node/paths.ts index 24c2529c587..b2912383c3c 100644 --- a/src/vs/code/node/paths.ts +++ b/src/vs/code/node/paths.ts @@ -33,9 +33,9 @@ function doValidatePaths(args: string[], gotoLineMode?: boolean): string[] { const result = args.map(arg => { let pathCandidate = String(arg); - let parsedPath: IPathWithLineAndColumn | undefined = undefined; + let parsedPath: extpath.IPathWithLineAndColumn | undefined = undefined; if (gotoLineMode) { - parsedPath = parseLineAndColumnAware(pathCandidate); + parsedPath = extpath.parseLineAndColumnAware(pathCandidate); pathCandidate = parsedPath.path; } @@ -87,42 +87,7 @@ function preparePath(cwd: string, p: string): string { return p; } -export interface IPathWithLineAndColumn { - path: string; - line?: number; - column?: number; -} - -export function parseLineAndColumnAware(rawPath: string): IPathWithLineAndColumn { - const segments = rawPath.split(':'); // C:\file.txt:: - - let path: string | null = null; - let line: number | null = null; - let column: number | null = null; - - segments.forEach(segment => { - const segmentAsNumber = Number(segment); - if (!types.isNumber(segmentAsNumber)) { - path = !!path ? [path, segment].join(':') : segment; // a colon can well be part of a path (e.g. C:\...) - } else if (line === null) { - line = segmentAsNumber; - } else if (column === null) { - column = segmentAsNumber; - } - }); - - if (!path) { - throw new Error('Format for `--goto` should be: `FILE:LINE(:COLUMN)`'); - } - - return { - path: path, - line: line !== null ? line : undefined, - column: column !== null ? column : line !== null ? 1 : undefined // if we have a line, make sure column is also set - }; -} - -function toPath(p: IPathWithLineAndColumn): string { +function toPath(p: extpath.IPathWithLineAndColumn): string { const segments = [p.path]; if (types.isNumber(p.line)) { diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 6b28b324b2f..3de78f2018c 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -10,7 +10,7 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Command, EditorCommand, ICommandOptions, registerEditorCommand } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ColumnSelection, IColumnSelectResult } from 'vs/editor/common/controller/cursorColumnSelection'; -import { CursorContext, CursorState, EditOperationType, IColumnSelectData, ICursors, PartialCursorState, RevealTarget } from 'vs/editor/common/controller/cursorCommon'; +import { CursorState, EditOperationType, IColumnSelectData, PartialCursorState } from 'vs/editor/common/controller/cursorCommon'; import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { CursorMove as CursorMove_, CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands'; @@ -32,29 +32,15 @@ const CORE_WEIGHT = KeybindingWeight.EditorCore; export abstract class CoreEditorCommand extends EditorCommand { public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void { - const cursors = editor._getCursors(); - if (!cursors) { - // the editor has no view => has no cursors - return; - } - this.runCoreEditorCommand(cursors, args || {}); - } - - public abstract runCoreEditorCommand(cursors: ICursors, args: any): void; -} - -export abstract class CoreEditorCommand2 extends EditorCommand { - public runEditorCommand(accessor: ServicesAccessor | null, editor: ICodeEditor, args: any): void { - const cursors = editor._getCursors(); const viewModel = editor._getViewModel(); - if (!cursors || !viewModel) { + if (!viewModel) { // the editor has no view => has no cursors return; } - this.runCoreEditorCommand(editor, viewModel, cursors, args || {}); + this.runCoreEditorCommand(viewModel, args || {}); } - public abstract runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void; + public abstract runCoreEditorCommand(viewModel: IViewModel, args: any): void; } export namespace EditorScroll_ { @@ -301,16 +287,16 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ - CursorMoveCommands.moveTo(cursors.context, cursors.getPrimaryCursor(), this._inSelectionMode, args.position, args.viewPosition) + CursorMoveCommands.moveTo(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position, args.viewPosition) ] ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } } @@ -327,21 +313,25 @@ export namespace CoreNavigationCommands { })); abstract class ColumnSelectCommand extends CoreEditorCommand { - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - const result = this._getColumnSelectResult(cursors.context, cursors.getPrimaryCursor(), cursors.getColumnSelectData(), args); - cursors.setStates(args.source, CursorChangeReason.Explicit, result.viewStates.map((viewState) => CursorState.fromViewState(viewState))); - cursors.setColumnSelectData({ + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + const result = this._getColumnSelectResult(viewModel, viewModel.getPrimaryCursorState(), viewModel.getCursorColumnSelectData(), args); + viewModel.setCursorStates(args.source, CursorChangeReason.Explicit, result.viewStates.map((viewState) => CursorState.fromViewState(viewState))); + viewModel.setCursorColumnSelectData({ isReal: true, fromViewLineNumber: result.fromLineNumber, fromViewVisualColumn: result.fromVisualColumn, toViewLineNumber: result.toLineNumber, toViewVisualColumn: result.toVisualColumn }); - cursors.reveal(args.source, true, (result.reversed ? RevealTarget.TopMost : RevealTarget.BottomMost), ScrollType.Smooth); + if (result.reversed) { + viewModel.revealTopMostCursor(args.source); + } else { + viewModel.revealBottomMostCursor(args.source); + } } - protected abstract _getColumnSelectResult(context: CursorContext, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult; + protected abstract _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult; } @@ -353,15 +343,15 @@ export namespace CoreNavigationCommands { }); } - protected _getColumnSelectResult(context: CursorContext, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { // validate `args` - const validatedPosition = context.model.validatePosition(args.position); - const validatedViewPosition = context.coordinatesConverter.validateViewPosition(new Position(args.viewPosition.lineNumber, args.viewPosition.column), validatedPosition); + const validatedPosition = viewModel.model.validatePosition(args.position); + const validatedViewPosition = viewModel.coordinatesConverter.validateViewPosition(new Position(args.viewPosition.lineNumber, args.viewPosition.column), validatedPosition); let fromViewLineNumber = args.doColumnSelect ? prevColumnSelectData.fromViewLineNumber : validatedViewPosition.lineNumber; let fromViewVisualColumn = args.doColumnSelect ? prevColumnSelectData.fromViewVisualColumn : args.mouseColumn - 1; - return ColumnSelection.columnSelect(context.config, context.viewModel, fromViewLineNumber, fromViewVisualColumn, validatedViewPosition.lineNumber, args.mouseColumn - 1); + return ColumnSelection.columnSelect(viewModel.cursorConfig, viewModel, fromViewLineNumber, fromViewVisualColumn, validatedViewPosition.lineNumber, args.mouseColumn - 1); } }); @@ -379,8 +369,8 @@ export namespace CoreNavigationCommands { }); } - protected _getColumnSelectResult(context: CursorContext, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { - return ColumnSelection.columnSelectLeft(context.config, context.viewModel, prevColumnSelectData); + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + return ColumnSelection.columnSelectLeft(viewModel.cursorConfig, viewModel, prevColumnSelectData); } }); @@ -398,8 +388,8 @@ export namespace CoreNavigationCommands { }); } - protected _getColumnSelectResult(context: CursorContext, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { - return ColumnSelection.columnSelectRight(context.config, context.viewModel, prevColumnSelectData); + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + return ColumnSelection.columnSelectRight(viewModel.cursorConfig, viewModel, prevColumnSelectData); } }); @@ -412,8 +402,8 @@ export namespace CoreNavigationCommands { this._isPaged = opts.isPaged; } - protected _getColumnSelectResult(context: CursorContext, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { - return ColumnSelection.columnSelectUp(context.config, context.viewModel, prevColumnSelectData, this._isPaged); + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + return ColumnSelection.columnSelectUp(viewModel.cursorConfig, viewModel, prevColumnSelectData, this._isPaged); } } @@ -450,8 +440,8 @@ export namespace CoreNavigationCommands { this._isPaged = opts.isPaged; } - protected _getColumnSelectResult(context: CursorContext, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { - return ColumnSelection.columnSelectDown(context.config, context.viewModel, prevColumnSelectData, this._isPaged); + protected _getColumnSelectResult(viewModel: IViewModel, primary: CursorState, prevColumnSelectData: IColumnSelectData, args: any): IColumnSelectResult { + return ColumnSelection.columnSelectDown(viewModel.cursorConfig, viewModel, prevColumnSelectData, this._isPaged); } } @@ -479,7 +469,7 @@ export namespace CoreNavigationCommands { } })); - export class CursorMoveImpl extends CoreEditorCommand2 { + export class CursorMoveImpl extends CoreEditorCommand { constructor() { super({ id: 'cursorMove', @@ -488,26 +478,26 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { const parsed = CursorMove_.parse(args); if (!parsed) { // illegal arguments return; } - this._runCursorMove(editor, viewModel, cursors, args.source, parsed); + this._runCursorMove(viewModel, args.source, parsed); } - private _runCursorMove(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, source: string | null | undefined, args: CursorMove_.ParsedArguments): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + private _runCursorMove(viewModel: IViewModel, source: string | null | undefined, args: CursorMove_.ParsedArguments): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( source, CursorChangeReason.Explicit, - CursorMoveImpl._move(editor, viewModel, cursors.context, cursors.getAll(), args) + CursorMoveImpl._move(viewModel, viewModel.getCursorStates(), args) ); - cursors.reveal(source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(source, true); } - private static _move(editor: ICodeEditor, viewModel: IViewModel, context: CursorContext, cursors: CursorState[], args: CursorMove_.ParsedArguments): PartialCursorState[] | null { + private static _move(viewModel: IViewModel, cursors: CursorState[], args: CursorMove_.ParsedArguments): PartialCursorState[] | null { const inSelectionMode = args.select; const value = args.value; @@ -521,13 +511,13 @@ export namespace CoreNavigationCommands { case CursorMove_.Direction.WrappedLineColumnCenter: case CursorMove_.Direction.WrappedLineEnd: case CursorMove_.Direction.WrappedLineLastNonWhitespaceCharacter: - return CursorMoveCommands.simpleMove(context, cursors, args.direction, inSelectionMode, value, args.unit); + return CursorMoveCommands.simpleMove(viewModel, cursors, args.direction, inSelectionMode, value, args.unit); case CursorMove_.Direction.ViewPortTop: case CursorMove_.Direction.ViewPortBottom: case CursorMove_.Direction.ViewPortCenter: case CursorMove_.Direction.ViewPortIfOutside: - return CursorMoveCommands.viewportMove(viewModel, context, cursors, args.direction, inSelectionMode, value); + return CursorMoveCommands.viewportMove(viewModel, cursors, args.direction, inSelectionMode, value); } return null; @@ -549,7 +539,7 @@ export namespace CoreNavigationCommands { this._staticArgs = opts.args; } - public runCoreEditorCommand(cursors: ICursors, dynamicArgs: any): void { + public runCoreEditorCommand(viewModel: IViewModel, dynamicArgs: any): void { let args = this._staticArgs; if (this._staticArgs.value === Constants.PAGE_SIZE_MARKER) { // -1 is a marker for page size @@ -557,17 +547,17 @@ export namespace CoreNavigationCommands { direction: this._staticArgs.direction, unit: this._staticArgs.unit, select: this._staticArgs.select, - value: cursors.context.config.pageSize + value: viewModel.cursorConfig.pageSize }; } - cursors.context.model.pushStackElement(); - cursors.setStates( + viewModel.model.pushStackElement(); + viewModel.setCursorStates( dynamicArgs.source, CursorChangeReason.Explicit, - CursorMoveCommands.simpleMove(cursors.context, cursors.getAll(), args.direction, args.select, args.value, args.unit) + CursorMoveCommands.simpleMove(viewModel, viewModel.getCursorStates(), args.direction, args.select, args.value, args.unit) ); - cursors.reveal(dynamicArgs.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(dynamicArgs.source, true); } } @@ -781,17 +771,15 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - const context = cursors.context; - + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { let newState: PartialCursorState; if (args.wholeLine) { - newState = CursorMoveCommands.line(context, cursors.getPrimaryCursor(), false, args.position, args.viewPosition); + newState = CursorMoveCommands.line(viewModel, viewModel.getPrimaryCursorState(), false, args.position, args.viewPosition); } else { - newState = CursorMoveCommands.moveTo(context, cursors.getPrimaryCursor(), false, args.position, args.viewPosition); + newState = CursorMoveCommands.moveTo(viewModel, viewModel.getPrimaryCursorState(), false, args.position, args.viewPosition); } - const states: PartialCursorState[] = cursors.getAll(); + const states: PartialCursorState[] = viewModel.getCursorStates(); // Check if we should remove a cursor (sort of like a toggle) if (states.length > 1) { @@ -812,8 +800,8 @@ export namespace CoreNavigationCommands { // => Remove the cursor states.splice(i, 1); - cursors.context.model.pushStackElement(); - cursors.setStates( + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, states @@ -825,8 +813,8 @@ export namespace CoreNavigationCommands { // => Add the new cursor states.push(newState); - cursors.context.model.pushStackElement(); - cursors.setStates( + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, states @@ -842,17 +830,15 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - const context = cursors.context; + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + const lastAddedCursorIndex = viewModel.getLastAddedCursorIndex(); - const lastAddedCursorIndex = cursors.getLastAddedCursorIndex(); - - const states = cursors.getAll(); + const states = viewModel.getCursorStates(); const newStates: PartialCursorState[] = states.slice(0); - newStates[lastAddedCursorIndex] = CursorMoveCommands.moveTo(context, states[lastAddedCursorIndex], true, args.position, args.viewPosition); + newStates[lastAddedCursorIndex] = CursorMoveCommands.moveTo(viewModel, states[lastAddedCursorIndex], true, args.position, args.viewPosition); - cursors.context.model.pushStackElement(); - cursors.setStates( + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, newStates @@ -869,14 +855,14 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.moveToBeginningOfLine(cursors.context, cursors.getAll(), this._inSelectionMode) + CursorMoveCommands.moveToBeginningOfLine(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } } @@ -913,17 +899,17 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - this._exec(cursors.context, cursors.getAll()) + this._exec(viewModel.getCursorStates()) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } - private _exec(context: CursorContext, cursors: CursorState[]): PartialCursorState[] { + private _exec(cursors: CursorState[]): PartialCursorState[] { const result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; @@ -967,14 +953,14 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.moveToEndOfLine(cursors.context, cursors.getAll(), this._inSelectionMode) + CursorMoveCommands.moveToEndOfLine(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } } @@ -1011,22 +997,22 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - this._exec(cursors.context, cursors.getAll()) + this._exec(viewModel, viewModel.getCursorStates()) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } - private _exec(context: CursorContext, cursors: CursorState[]): PartialCursorState[] { + private _exec(viewModel: IViewModel, cursors: CursorState[]): PartialCursorState[] { const result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const lineNumber = cursor.modelState.position.lineNumber; - const maxColumn = context.model.getLineMaxColumn(lineNumber); + const maxColumn = viewModel.model.getLineMaxColumn(lineNumber); result[i] = CursorState.fromModelState(cursor.modelState.move(this._inSelectionMode, lineNumber, maxColumn, 0)); } return result; @@ -1066,14 +1052,14 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.moveToBeginningOfBuffer(cursors.context, cursors.getAll(), this._inSelectionMode) + CursorMoveCommands.moveToBeginningOfBuffer(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } } @@ -1110,14 +1096,14 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.moveToEndOfBuffer(cursors.context, cursors.getAll(), this._inSelectionMode) + CursorMoveCommands.moveToEndOfBuffer(viewModel, viewModel.getCursorStates(), this._inSelectionMode) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } } @@ -1145,7 +1131,7 @@ export namespace CoreNavigationCommands { } })); - export class EditorScrollImpl extends CoreEditorCommand2 { + export class EditorScrollImpl extends CoreEditorCommand { constructor() { super({ id: 'editorScroll', @@ -1154,40 +1140,40 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { const parsed = EditorScroll_.parse(args); if (!parsed) { // illegal arguments return; } - this._runEditorScroll(editor, viewModel, cursors, args.source, parsed); + this._runEditorScroll(viewModel, args.source, parsed); } - _runEditorScroll(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, source: string | null | undefined, args: EditorScroll_.ParsedArguments): void { + _runEditorScroll(viewModel: IViewModel, source: string | null | undefined, args: EditorScroll_.ParsedArguments): void { - const desiredScrollTop = this._computeDesiredScrollTop(editor, viewModel, cursors.context, args); + const desiredScrollTop = this._computeDesiredScrollTop(viewModel, args); if (args.revealCursor) { // must ensure cursor is in new visible range const desiredVisibleViewRange = viewModel.getCompletelyVisibleViewRangeAtScrollTop(desiredScrollTop); - cursors.setStates( + viewModel.setCursorStates( source, CursorChangeReason.Explicit, [ - CursorMoveCommands.findPositionInViewportIfOutside(cursors.context, cursors.getPrimaryCursor(), desiredVisibleViewRange, args.select) + CursorMoveCommands.findPositionInViewportIfOutside(viewModel, viewModel.getPrimaryCursorState(), desiredVisibleViewRange, args.select) ] ); } - editor.setScrollTop(desiredScrollTop, ScrollType.Smooth); + viewModel.setScrollTop(desiredScrollTop, ScrollType.Smooth); } - private _computeDesiredScrollTop(editor: ICodeEditor, viewModel: IViewModel, context: CursorContext, args: EditorScroll_.ParsedArguments): number { + private _computeDesiredScrollTop(viewModel: IViewModel, args: EditorScroll_.ParsedArguments): number { if (args.unit === EditorScroll_.Unit.Line) { // scrolling by model lines const visibleViewRange = viewModel.getCompletelyVisibleViewRange(); - const visibleModelRange = context.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); + const visibleModelRange = viewModel.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); let desiredTopModelLineNumber: number; if (args.direction === EditorScroll_.Direction.Up) { @@ -1195,28 +1181,29 @@ export namespace CoreNavigationCommands { desiredTopModelLineNumber = Math.max(1, visibleModelRange.startLineNumber - args.value); } else { // must go x model lines down - desiredTopModelLineNumber = Math.min(context.model.getLineCount(), visibleModelRange.startLineNumber + args.value); + desiredTopModelLineNumber = Math.min(viewModel.model.getLineCount(), visibleModelRange.startLineNumber + args.value); } - return editor.getTopForLineNumber(desiredTopModelLineNumber); + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(new Position(desiredTopModelLineNumber, 1)); + return viewModel.getVerticalOffsetForLineNumber(viewPosition.lineNumber); } let noOfLines: number; if (args.unit === EditorScroll_.Unit.Page) { - noOfLines = context.config.pageSize * args.value; + noOfLines = viewModel.cursorConfig.pageSize * args.value; } else if (args.unit === EditorScroll_.Unit.HalfPage) { - noOfLines = Math.round(context.config.pageSize / 2) * args.value; + noOfLines = Math.round(viewModel.cursorConfig.pageSize / 2) * args.value; } else { noOfLines = args.value; } const deltaLines = (args.direction === EditorScroll_.Direction.Up ? -1 : 1) * noOfLines; - return editor.getScrollTop() + deltaLines * context.config.lineHeight; + return viewModel.getScrollTop() + deltaLines * viewModel.cursorConfig.lineHeight; } } export const EditorScroll: EditorScrollImpl = registerEditorCommand(new EditorScrollImpl()); - export const ScrollLineUp: CoreEditorCommand2 = registerEditorCommand(new class extends CoreEditorCommand2 { + export const ScrollLineUp: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollLineUp', @@ -1230,8 +1217,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(editor, viewModel, cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.WrappedLine, value: 1, @@ -1241,7 +1228,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollPageUp: CoreEditorCommand2 = registerEditorCommand(new class extends CoreEditorCommand2 { + export const ScrollPageUp: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollPageUp', @@ -1256,8 +1243,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(editor, viewModel, cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.Page, value: 1, @@ -1267,7 +1254,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollLineDown: CoreEditorCommand2 = registerEditorCommand(new class extends CoreEditorCommand2 { + export const ScrollLineDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollLineDown', @@ -1281,8 +1268,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(editor, viewModel, cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.WrappedLine, value: 1, @@ -1292,7 +1279,7 @@ export namespace CoreNavigationCommands { } }); - export const ScrollPageDown: CoreEditorCommand2 = registerEditorCommand(new class extends CoreEditorCommand2 { + export const ScrollPageDown: CoreEditorCommand = registerEditorCommand(new class extends CoreEditorCommand { constructor() { super({ id: 'scrollPageDown', @@ -1307,8 +1294,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(editor: ICodeEditor, viewModel: IViewModel, cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(editor, viewModel, cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.Page, value: 1, @@ -1327,16 +1314,16 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ - CursorMoveCommands.word(cursors.context, cursors.getPrimaryCursor(), this._inSelectionMode, args.position) + CursorMoveCommands.word(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position) ] ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } } @@ -1360,18 +1347,16 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - const context = cursors.context; + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + const lastAddedCursorIndex = viewModel.getLastAddedCursorIndex(); - const lastAddedCursorIndex = cursors.getLastAddedCursorIndex(); - - const states = cursors.getAll(); + const states = viewModel.getCursorStates(); const newStates: PartialCursorState[] = states.slice(0); const lastAddedState = states[lastAddedCursorIndex]; - newStates[lastAddedCursorIndex] = CursorMoveCommands.word(context, lastAddedState, lastAddedState.modelState.hasSelection(), args.position); + newStates[lastAddedCursorIndex] = CursorMoveCommands.word(viewModel, lastAddedState, lastAddedState.modelState.hasSelection(), args.position); - context.model.pushStackElement(); - cursors.setStates( + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, newStates @@ -1387,16 +1372,16 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ - CursorMoveCommands.line(cursors.context, cursors.getPrimaryCursor(), this._inSelectionMode, args.position, args.viewPosition) + CursorMoveCommands.line(viewModel, viewModel.getPrimaryCursorState(), this._inSelectionMode, args.position, args.viewPosition) ] ); - cursors.reveal(args.source, false, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, false); } } @@ -1420,15 +1405,15 @@ export namespace CoreNavigationCommands { this._inSelectionMode = opts.inSelectionMode; } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - const lastAddedCursorIndex = cursors.getLastAddedCursorIndex(); + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + const lastAddedCursorIndex = viewModel.getLastAddedCursorIndex(); - const states = cursors.getAll(); + const states = viewModel.getCursorStates(); const newStates: PartialCursorState[] = states.slice(0); - newStates[lastAddedCursorIndex] = CursorMoveCommands.line(cursors.context, states[lastAddedCursorIndex], this._inSelectionMode, args.position, args.viewPosition); + newStates[lastAddedCursorIndex] = CursorMoveCommands.line(viewModel, states[lastAddedCursorIndex], this._inSelectionMode, args.position, args.viewPosition); - cursors.context.model.pushStackElement(); - cursors.setStates( + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, newStates @@ -1461,14 +1446,14 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.expandLineSelection(cursors.context, cursors.getAll()) + CursorMoveCommands.expandLineSelection(viewModel, viewModel.getCursorStates()) ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } }); @@ -1487,16 +1472,16 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ - CursorMoveCommands.cancelSelection(cursors.context, cursors.getPrimaryCursor()) + CursorMoveCommands.cancelSelection(viewModel, viewModel.getPrimaryCursorState()) ] ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } }); @@ -1514,16 +1499,16 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ - cursors.getPrimaryCursor() + viewModel.getPrimaryCursorState() ] ); - cursors.reveal(args.source, true, RevealTarget.Primary, ScrollType.Smooth); + viewModel.revealPrimaryCursor(args.source, true); } }); @@ -1536,20 +1521,20 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { const revealLineArg = args; let lineNumber = (revealLineArg.lineNumber || 0) + 1; if (lineNumber < 1) { lineNumber = 1; } - const lineCount = cursors.context.model.getLineCount(); + const lineCount = viewModel.model.getLineCount(); if (lineNumber > lineCount) { lineNumber = lineCount; } const range = new Range( lineNumber, 1, - lineNumber, cursors.context.model.getLineMaxColumn(lineNumber) + lineNumber, viewModel.model.getLineMaxColumn(lineNumber) ); let revealAt = VerticalRevealType.Simple; @@ -1569,9 +1554,9 @@ export namespace CoreNavigationCommands { } } - const viewRange = cursors.context.coordinatesConverter.convertModelRangeToViewRange(range); + const viewRange = viewModel.coordinatesConverter.convertModelRangeToViewRange(range); - cursors.revealRange(args.source, false, viewRange, revealAt, ScrollType.Smooth); + viewModel.revealRange(args.source, false, viewRange, revealAt, ScrollType.Smooth); } }); @@ -1583,13 +1568,13 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ - CursorMoveCommands.selectAll(cursors.context, cursors.getPrimaryCursor()) + CursorMoveCommands.selectAll(viewModel, viewModel.getPrimaryCursorState()) ] ); } @@ -1603,9 +1588,9 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { - cursors.context.model.pushStackElement(); - cursors.setStates( + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { + viewModel.model.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, [ @@ -1736,15 +1721,15 @@ export namespace CoreEditingCommands { export abstract class CoreEditingCommand extends EditorCommand { public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { - const cursors = editor._getCursors(); - if (!cursors) { + const viewModel = editor._getViewModel(); + if (!viewModel) { // the editor has no view => has no cursors return; } - this.runCoreEditingCommand(editor, cursors, args || {}); + this.runCoreEditingCommand(editor, viewModel, args || {}); } - public abstract runCoreEditingCommand(editor: ICodeEditor, cursors: ICursors, args: any): void; + public abstract runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void; } export const LineBreakInsert: EditorCommand = registerEditorCommand(new class extends CoreEditingCommand { @@ -1761,9 +1746,9 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, cursors: ICursors, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineBreakInsert(cursors.context.config, cursors.context.model, cursors.getAll().map(s => s.modelState.selection))); + editor.executeCommands(this.id, TypeOperations.lineBreakInsert(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); } }); @@ -1783,9 +1768,9 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, cursors: ICursors, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.outdent(cursors.context.config, cursors.context.model, cursors.getAll().map(s => s.modelState.selection))); + editor.executeCommands(this.id, TypeOperations.outdent(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); editor.pushUndoStop(); } }); @@ -1806,9 +1791,9 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, cursors: ICursors, args: any): void { + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.tab(cursors.context.config, cursors.context.model, cursors.getAll().map(s => s.modelState.selection))); + editor.executeCommands(this.id, TypeOperations.tab(viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection))); editor.pushUndoStop(); } }); @@ -1828,13 +1813,13 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, cursors: ICursors, args: any): void { - const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteLeft(cursors.getPrevEditOperationType(), cursors.context.config, cursors.context.model, cursors.getAll().map(s => s.modelState.selection)); + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteLeft(viewModel.getPrevEditOperationType(), viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection)); if (shouldPushStackElementBefore) { editor.pushUndoStop(); } editor.executeCommands(this.id, commands); - cursors.setPrevEditOperationType(EditOperationType.DeletingLeft); + viewModel.setPrevEditOperationType(EditOperationType.DeletingLeft); } }); @@ -1852,13 +1837,13 @@ export namespace CoreEditingCommands { }); } - public runCoreEditingCommand(editor: ICodeEditor, cursors: ICursors, args: any): void { - const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteRight(cursors.getPrevEditOperationType(), cursors.context.config, cursors.context.model, cursors.getAll().map(s => s.modelState.selection)); + public runCoreEditingCommand(editor: ICodeEditor, viewModel: IViewModel, args: any): void { + const [shouldPushStackElementBefore, commands] = DeleteOperations.deleteRight(viewModel.getPrevEditOperationType(), viewModel.cursorConfig, viewModel.model, viewModel.getCursorStates().map(s => s.modelState.selection)); if (shouldPushStackElementBefore) { editor.pushUndoStop(); } editor.executeCommands(this.id, commands); - cursors.setPrevEditOperationType(EditOperationType.DeletingRight); + viewModel.setPrevEditOperationType(EditOperationType.DeletingRight); } }); diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index f0ad0cf79dc..6123c7971ae 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -420,7 +420,7 @@ class HitTestRequest extends BareHitTestRequest { let mouseColumn = this.mouseColumn; if (position && position.column < this._ctx.model.getLineMaxColumn(position.lineNumber)) { // Most likely, the line contains foreign decorations... - mouseColumn = CursorColumns.visibleColumnFromColumn(this._ctx.model.getLineContent(position.lineNumber), position.column, this._ctx.model.getOptions().tabSize) + 1; + mouseColumn = CursorColumns.visibleColumnFromColumn(this._ctx.model.getLineContent(position.lineNumber), position.column, this._ctx.model.getTextModelOptions().tabSize) + 1; } return new MouseTarget(this.target, type, mouseColumn, position, range, detail); } diff --git a/src/vs/editor/browser/controller/pointerHandler.ts b/src/vs/editor/browser/controller/pointerHandler.ts index ffe55acb07c..2b930bb8461 100644 --- a/src/vs/editor/browser/controller/pointerHandler.ts +++ b/src/vs/editor/browser/controller/pointerHandler.ts @@ -100,7 +100,7 @@ class StandardPointerHandler extends MouseHandler implements IDisposable { } private _onGestureChange(e: IThrottledGestureEvent): void { - this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); + this._context.model.deltaScrollNow(-e.translationX, -e.translationY); } public dispose(): void { @@ -177,7 +177,7 @@ export class PointerEventHandler extends MouseHandler { private onChange(e: GestureEvent): void { if (this._lastPointerType === 'touch') { - this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); + this._context.model.deltaScrollNow(-e.translationX, -e.translationY); } } @@ -215,7 +215,7 @@ class TouchHandler extends MouseHandler { } private onChange(e: GestureEvent): void { - this._context.viewLayout.deltaScrollNow(-e.translationX, -e.translationY); + this._context.model.deltaScrollNow(-e.translationX, -e.translationY); } } diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index b07d751625d..9eee0dfe476 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -258,14 +258,13 @@ export class TextAreaHandler extends ViewPart { const lineNumber = this._selections[0].startLineNumber; const column = this._selections[0].startColumn - (e.moveOneCharacterLeft ? 1 : 0); - this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( + this._context.model.revealRange( 'keyboard', - new Range(lineNumber, column, lineNumber, column), - null, - viewEvents.VerticalRevealType.Simple, true, + new Range(lineNumber, column, lineNumber, column), + viewEvents.VerticalRevealType.Simple, ScrollType.Immediate - )); + ); // Find range pixel position const visibleRange = this._viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column); @@ -307,11 +306,11 @@ export class TextAreaHandler extends ViewPart { })); this._register(this._textAreaInput.onFocus(() => { - this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(true)); + this._context.model.setHasFocus(true); })); this._register(this._textAreaInput.onBlur(() => { - this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(false)); + this._context.model.setHasFocus(false); })); } diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 980e6f30af2..d9c89ccef50 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -7,7 +7,6 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IMouseEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; import { IDisposable } from 'vs/base/common/lifecycle'; import { OverviewRulerPosition, ConfigurationChangedEvent, EditorLayoutInfo, IComputedEditorOptions, EditorOption, FindComputedEditorOptionValueById, IEditorOptions, IDiffEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { ICursors } from 'vs/editor/common/controller/cursorCommon'; import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -663,11 +662,6 @@ export interface ICodeEditor extends editorCommon.IEditor { */ executeCommands(source: string | null | undefined, commands: (editorCommon.ICommand | null)[]): void; - /** - * @internal - */ - _getCursors(): ICursors | null; - /** * @internal */ @@ -861,11 +855,6 @@ export interface IActiveCodeEditor extends ICodeEditor { */ getModel(): ITextModel; - /** - * @internal - */ - _getCursors(): ICursors; - /** * @internal */ diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 3ec0408abfd..6fc163d9c36 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -6,7 +6,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { CoreEditorCommand, CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { IEditorMouseEvent, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; -import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; +import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { IConfiguration } from 'vs/editor/common/editorCommon'; @@ -40,8 +40,8 @@ export interface ICommandDelegate { paste(text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void; type(text: string): void; replacePreviousChar(text: string, replaceCharCnt: number): void; - compositionStart(): void; - compositionEnd(): void; + startComposition(): void; + endComposition(): void; cut(): void; } @@ -49,18 +49,18 @@ export class ViewController { private readonly configuration: IConfiguration; private readonly viewModel: IViewModel; - private readonly outgoingEvents: ViewOutgoingEvents; + private readonly userInputEvents: ViewUserInputEvents; private readonly commandDelegate: ICommandDelegate; constructor( configuration: IConfiguration, viewModel: IViewModel, - outgoingEvents: ViewOutgoingEvents, + userInputEvents: ViewUserInputEvents, commandDelegate: ICommandDelegate ) { this.configuration = configuration; this.viewModel = viewModel; - this.outgoingEvents = outgoingEvents; + this.userInputEvents = userInputEvents; this.commandDelegate = commandDelegate; } @@ -82,11 +82,11 @@ export class ViewController { } public compositionStart(): void { - this.commandDelegate.compositionStart(); + this.commandDelegate.startComposition(); } public compositionEnd(): void { - this.commandDelegate.compositionEnd(); + this.commandDelegate.endComposition(); } public cut(): void { @@ -289,42 +289,42 @@ export class ViewController { } public emitKeyDown(e: IKeyboardEvent): void { - this.outgoingEvents.emitKeyDown(e); + this.userInputEvents.emitKeyDown(e); } public emitKeyUp(e: IKeyboardEvent): void { - this.outgoingEvents.emitKeyUp(e); + this.userInputEvents.emitKeyUp(e); } public emitContextMenu(e: IEditorMouseEvent): void { - this.outgoingEvents.emitContextMenu(e); + this.userInputEvents.emitContextMenu(e); } public emitMouseMove(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseMove(e); + this.userInputEvents.emitMouseMove(e); } public emitMouseLeave(e: IPartialEditorMouseEvent): void { - this.outgoingEvents.emitMouseLeave(e); + this.userInputEvents.emitMouseLeave(e); } public emitMouseUp(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseUp(e); + this.userInputEvents.emitMouseUp(e); } public emitMouseDown(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseDown(e); + this.userInputEvents.emitMouseDown(e); } public emitMouseDrag(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseDrag(e); + this.userInputEvents.emitMouseDrag(e); } public emitMouseDrop(e: IPartialEditorMouseEvent): void { - this.outgoingEvents.emitMouseDrop(e); + this.userInputEvents.emitMouseDrop(e); } public emitMouseWheel(e: IMouseWheelEvent): void { - this.outgoingEvents.emitMouseWheel(e); + this.userInputEvents.emitMouseWheel(e); } } diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index ca608d35b5c..1e156722710 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -14,7 +14,7 @@ import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { ITextAreaHandlerHelper, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; -import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; +import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewContentWidgets } from 'vs/editor/browser/viewParts/contentWidgets/contentWidgets'; @@ -42,7 +42,6 @@ import { Range } from 'vs/editor/common/core/range'; import { IConfiguration, ScrollType } from 'vs/editor/common/editorCommon'; import { RenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; -import { ViewEventDispatcher } from 'vs/editor/common/view/viewEventDispatcher'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; @@ -64,31 +63,27 @@ export interface IOverlayWidgetData { export class View extends ViewEventHandler { - private readonly eventDispatcher: ViewEventDispatcher; - - private _scrollbar: EditorScrollbar; + private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; private _selections: Selection[]; // The view lines - private viewLines: ViewLines; + private readonly _viewLines: ViewLines; // These are parts, but we must do some API related calls on them, so we keep a reference - private viewZones: ViewZones; - private contentWidgets: ViewContentWidgets; - private overlayWidgets: ViewOverlayWidgets; - private viewCursors: ViewCursors; - private viewParts: ViewPart[]; + private readonly _viewZones: ViewZones; + private readonly _contentWidgets: ViewContentWidgets; + private readonly _overlayWidgets: ViewOverlayWidgets; + private readonly _viewCursors: ViewCursors; + private readonly _viewParts: ViewPart[]; private readonly _textAreaHandler: TextAreaHandler; - private readonly pointerHandler: PointerHandler; - - private readonly outgoingEvents: ViewOutgoingEvents; + private readonly _pointerHandler: PointerHandler; // Dom nodes - private linesContent: FastDomNode; - public domNode: FastDomNode; - private overflowGuardContainer: FastDomNode; + private readonly _linesContent: FastDomNode; + public readonly domNode: FastDomNode; + private readonly _overflowGuardContainer: FastDomNode; // Actual mutable state private _renderAnimationFrame: IDisposable | null; @@ -98,77 +93,73 @@ export class View extends ViewEventHandler { configuration: IConfiguration, themeService: IThemeService, model: IViewModel, - outgoingEvents: ViewOutgoingEvents + userInputEvents: ViewUserInputEvents ) { super(); this._selections = [new Selection(1, 1, 1, 1)]; this._renderAnimationFrame = null; - this.outgoingEvents = outgoingEvents; - const viewController = new ViewController(configuration, model, this.outgoingEvents, commandDelegate); - - // The event dispatcher will always go through _renderOnce before dispatching any events - this.eventDispatcher = new ViewEventDispatcher((callback: () => void) => this._renderOnce(callback)); - - // Ensure the view is the first event handler in order to update the layout - this.eventDispatcher.addEventHandler(this); + const viewController = new ViewController(configuration, model, userInputEvents, commandDelegate); // The view context is passed on to most classes (basically to reduce param. counts in ctors) - this._context = new ViewContext(configuration, themeService.getColorTheme(), model, this.eventDispatcher); + this._context = new ViewContext(configuration, themeService.getColorTheme(), model); + + // Ensure the view is the first event handler in order to update the layout + this._context.addEventHandler(this); this._register(themeService.onDidColorThemeChange(theme => { this._context.theme.update(theme); - this.eventDispatcher.emit(new viewEvents.ViewThemeChangedEvent()); + this._context.model.onDidColorThemeChange(); this.render(true, false); })); - this.viewParts = []; + this._viewParts = []; // Keyboard handler - this._textAreaHandler = new TextAreaHandler(this._context, viewController, this.createTextAreaHandlerHelper()); - this.viewParts.push(this._textAreaHandler); + this._textAreaHandler = new TextAreaHandler(this._context, viewController, this._createTextAreaHandlerHelper()); + this._viewParts.push(this._textAreaHandler); // These two dom nodes must be constructed up front, since references are needed in the layout provider (scrolling & co.) - this.linesContent = createFastDomNode(document.createElement('div')); - this.linesContent.setClassName('lines-content' + ' monaco-editor-background'); - this.linesContent.setPosition('absolute'); + this._linesContent = createFastDomNode(document.createElement('div')); + this._linesContent.setClassName('lines-content' + ' monaco-editor-background'); + this._linesContent.setPosition('absolute'); this.domNode = createFastDomNode(document.createElement('div')); - this.domNode.setClassName(this.getEditorClassName()); + this.domNode.setClassName(this._getEditorClassName()); // Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438 this.domNode.setAttribute('role', 'code'); - this.overflowGuardContainer = createFastDomNode(document.createElement('div')); - PartFingerprints.write(this.overflowGuardContainer, PartFingerprint.OverflowGuard); - this.overflowGuardContainer.setClassName('overflow-guard'); + this._overflowGuardContainer = createFastDomNode(document.createElement('div')); + PartFingerprints.write(this._overflowGuardContainer, PartFingerprint.OverflowGuard); + this._overflowGuardContainer.setClassName('overflow-guard'); - this._scrollbar = new EditorScrollbar(this._context, this.linesContent, this.domNode, this.overflowGuardContainer); - this.viewParts.push(this._scrollbar); + this._scrollbar = new EditorScrollbar(this._context, this._linesContent, this.domNode, this._overflowGuardContainer); + this._viewParts.push(this._scrollbar); // View Lines - this.viewLines = new ViewLines(this._context, this.linesContent); + this._viewLines = new ViewLines(this._context, this._linesContent); // View Zones - this.viewZones = new ViewZones(this._context); - this.viewParts.push(this.viewZones); + this._viewZones = new ViewZones(this._context); + this._viewParts.push(this._viewZones); // Decorations overview ruler const decorationsOverviewRuler = new DecorationsOverviewRuler(this._context); - this.viewParts.push(decorationsOverviewRuler); + this._viewParts.push(decorationsOverviewRuler); const scrollDecoration = new ScrollDecorationViewPart(this._context); - this.viewParts.push(scrollDecoration); + this._viewParts.push(scrollDecoration); const contentViewOverlays = new ContentViewOverlays(this._context); - this.viewParts.push(contentViewOverlays); + this._viewParts.push(contentViewOverlays); contentViewOverlays.addDynamicOverlay(new CurrentLineHighlightOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new SelectionsOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new IndentGuidesOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new DecorationsOverlay(this._context)); const marginViewOverlays = new MarginViewOverlays(this._context); - this.viewParts.push(marginViewOverlays); + this._viewParts.push(marginViewOverlays); marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new GlyphMarginOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context)); @@ -176,26 +167,26 @@ export class View extends ViewEventHandler { marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context)); const margin = new Margin(this._context); - margin.getDomNode().appendChild(this.viewZones.marginDomNode); + margin.getDomNode().appendChild(this._viewZones.marginDomNode); margin.getDomNode().appendChild(marginViewOverlays.getDomNode()); - this.viewParts.push(margin); + this._viewParts.push(margin); // Content widgets - this.contentWidgets = new ViewContentWidgets(this._context, this.domNode); - this.viewParts.push(this.contentWidgets); + this._contentWidgets = new ViewContentWidgets(this._context, this.domNode); + this._viewParts.push(this._contentWidgets); - this.viewCursors = new ViewCursors(this._context); - this.viewParts.push(this.viewCursors); + this._viewCursors = new ViewCursors(this._context); + this._viewParts.push(this._viewCursors); // Overlay widgets - this.overlayWidgets = new ViewOverlayWidgets(this._context); - this.viewParts.push(this.overlayWidgets); + this._overlayWidgets = new ViewOverlayWidgets(this._context); + this._viewParts.push(this._overlayWidgets); const rulers = new Rulers(this._context); - this.viewParts.push(rulers); + this._viewParts.push(rulers); const minimap = new Minimap(this._context); - this.viewParts.push(minimap); + this._viewParts.push(minimap); // -------------- Wire dom nodes up @@ -204,78 +195,74 @@ export class View extends ViewEventHandler { overviewRulerData.parent.insertBefore(decorationsOverviewRuler.getDomNode(), overviewRulerData.insertBefore); } - this.linesContent.appendChild(contentViewOverlays.getDomNode()); - this.linesContent.appendChild(rulers.domNode); - this.linesContent.appendChild(this.viewZones.domNode); - this.linesContent.appendChild(this.viewLines.getDomNode()); - this.linesContent.appendChild(this.contentWidgets.domNode); - this.linesContent.appendChild(this.viewCursors.getDomNode()); - this.overflowGuardContainer.appendChild(margin.getDomNode()); - this.overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); - this.overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); - this.overflowGuardContainer.appendChild(this._textAreaHandler.textArea); - this.overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); - this.overflowGuardContainer.appendChild(this.overlayWidgets.getDomNode()); - this.overflowGuardContainer.appendChild(minimap.getDomNode()); - this.domNode.appendChild(this.overflowGuardContainer); - this.domNode.appendChild(this.contentWidgets.overflowingContentWidgetsDomNode); + this._linesContent.appendChild(contentViewOverlays.getDomNode()); + this._linesContent.appendChild(rulers.domNode); + this._linesContent.appendChild(this._viewZones.domNode); + this._linesContent.appendChild(this._viewLines.getDomNode()); + this._linesContent.appendChild(this._contentWidgets.domNode); + this._linesContent.appendChild(this._viewCursors.getDomNode()); + this._overflowGuardContainer.appendChild(margin.getDomNode()); + this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); + this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); + this._overflowGuardContainer.appendChild(this._textAreaHandler.textArea); + this._overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); + this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()); + this._overflowGuardContainer.appendChild(minimap.getDomNode()); + this.domNode.appendChild(this._overflowGuardContainer); + this.domNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode); this._applyLayout(); // Pointer handler - this.pointerHandler = this._register(new PointerHandler(this._context, viewController, this.createPointerHandlerHelper())); - - this._register(model.addViewEventListener((events: viewEvents.ViewEvent[]) => { - this.eventDispatcher.emitMany(events); - })); + this._pointerHandler = this._register(new PointerHandler(this._context, viewController, this._createPointerHandlerHelper())); } private _flushAccumulatedAndRenderNow(): void { this._renderNow(); } - private createPointerHandlerHelper(): IPointerHandlerHelper { + private _createPointerHandlerHelper(): IPointerHandlerHelper { return { viewDomNode: this.domNode.domNode, - linesContentDomNode: this.linesContent.domNode, + linesContentDomNode: this._linesContent.domNode, focusTextArea: () => { this.focus(); }, getLastRenderData: (): PointerHandlerLastRenderData => { - const lastViewCursorsRenderData = this.viewCursors.getLastRenderData() || []; + const lastViewCursorsRenderData = this._viewCursors.getLastRenderData() || []; const lastTextareaPosition = this._textAreaHandler.getLastRenderData(); return new PointerHandlerLastRenderData(lastViewCursorsRenderData, lastTextareaPosition); }, shouldSuppressMouseDownOnViewZone: (viewZoneId: string) => { - return this.viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); + return this._viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); }, shouldSuppressMouseDownOnWidget: (widgetId: string) => { - return this.contentWidgets.shouldSuppressMouseDownOnWidget(widgetId); + return this._contentWidgets.shouldSuppressMouseDownOnWidget(widgetId); }, getPositionFromDOMInfo: (spanNode: HTMLElement, offset: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.getPositionFromDOMInfo(spanNode, offset); + return this._viewLines.getPositionFromDOMInfo(spanNode, offset); }, visibleRangeForPosition: (lineNumber: number, column: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.visibleRangeForPosition(new Position(lineNumber, column)); + return this._viewLines.visibleRangeForPosition(new Position(lineNumber, column)); }, getLineWidth: (lineNumber: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.getLineWidth(lineNumber); + return this._viewLines.getLineWidth(lineNumber); } }; } - private createTextAreaHandlerHelper(): ITextAreaHandlerHelper { + private _createTextAreaHandlerHelper(): ITextAreaHandlerHelper { return { visibleRangeForPositionRelativeToEditor: (lineNumber: number, column: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.visibleRangeForPosition(new Position(lineNumber, column)); + return this._viewLines.visibleRangeForPosition(new Position(lineNumber, column)); } }; } @@ -287,27 +274,26 @@ export class View extends ViewEventHandler { this.domNode.setWidth(layoutInfo.width); this.domNode.setHeight(layoutInfo.height); - this.overflowGuardContainer.setWidth(layoutInfo.width); - this.overflowGuardContainer.setHeight(layoutInfo.height); + this._overflowGuardContainer.setWidth(layoutInfo.width); + this._overflowGuardContainer.setHeight(layoutInfo.height); - this.linesContent.setWidth(1000000); - this.linesContent.setHeight(1000000); + this._linesContent.setWidth(1000000); + this._linesContent.setHeight(1000000); } - private getEditorClassName() { + private _getEditorClassName() { const focused = this._textAreaHandler.isFocused() ? ' focused' : ''; return this._context.configuration.options.get(EditorOption.editorClassName) + ' ' + getThemeTypeSelector(this._context.theme.type) + focused; } // --- begin event handlers - - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); - this._applyLayout(); - return false; + public handleEvents(events: viewEvents.ViewEvent[]): void { + super.handleEvents(events); + this._scheduleRender(); } - public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { - this.outgoingEvents.emitContentSizeChange(e); + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this.domNode.setClassName(this._getEditorClassName()); + this._applyLayout(); return false; } public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { @@ -315,21 +301,11 @@ export class View extends ViewEventHandler { return false; } public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); - this._context.model.setHasFocus(e.isFocused); - if (e.isFocused) { - this.outgoingEvents.emitViewFocusGained(); - } else { - this.outgoingEvents.emitViewFocusLost(); - } - return false; - } - public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { - this.outgoingEvents.emitScrollChanged(e); + this.domNode.setClassName(this._getEditorClassName()); return false; } public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); + this.domNode.setClassName(this._getEditorClassName()); return false; } @@ -341,26 +317,18 @@ export class View extends ViewEventHandler { this._renderAnimationFrame = null; } - this.eventDispatcher.removeEventHandler(this); - this.outgoingEvents.dispose(); + this._context.removeEventHandler(this); - this.viewLines.dispose(); + this._viewLines.dispose(); // Destroy view parts - for (let i = 0, len = this.viewParts.length; i < len; i++) { - this.viewParts[i].dispose(); + for (let i = 0, len = this._viewParts.length; i < len; i++) { + this._viewParts[i].dispose(); } - this.viewParts = []; super.dispose(); } - private _renderOnce(callback: () => T): T { - const r = safeInvokeNoArg(callback); - this._scheduleRender(); - return r; - } - private _scheduleRender(): void { if (this._renderAnimationFrame === null) { this._renderAnimationFrame = dom.runAtThisOrScheduleAtNextAnimationFrame(this._onRenderScheduled.bind(this), 100); @@ -378,8 +346,8 @@ export class View extends ViewEventHandler { private _getViewPartsToRender(): ViewPart[] { let result: ViewPart[] = [], resultLen = 0; - for (let i = 0, len = this.viewParts.length; i < len; i++) { - const viewPart = this.viewParts[i]; + for (let i = 0, len = this._viewParts.length; i < len; i++) { + const viewPart = this._viewParts[i]; if (viewPart.shouldRender()) { result[resultLen++] = viewPart; } @@ -394,7 +362,7 @@ export class View extends ViewEventHandler { let viewPartsToRender = this._getViewPartsToRender(); - if (!this.viewLines.shouldRender() && viewPartsToRender.length === 0) { + if (!this._viewLines.shouldRender() && viewPartsToRender.length === 0) { // Nothing to render return; } @@ -409,20 +377,20 @@ export class View extends ViewEventHandler { this._context.model ); - if (this.contentWidgets.shouldRender()) { + if (this._contentWidgets.shouldRender()) { // Give the content widgets a chance to set their max width before a possible synchronous layout - this.contentWidgets.onBeforeRender(viewportData); + this._contentWidgets.onBeforeRender(viewportData); } - if (this.viewLines.shouldRender()) { - this.viewLines.renderText(viewportData); - this.viewLines.onDidRender(); + if (this._viewLines.shouldRender()) { + this._viewLines.renderText(viewportData); + this._viewLines.onDidRender(); // Rendering of viewLines might cause scroll events to occur, so collect view parts to render again viewPartsToRender = this._getViewPartsToRender(); } - const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this.viewLines); + const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this._viewLines); // Render the rest of the parts for (let i = 0, len = viewPartsToRender.length; i < len; i++) { @@ -444,11 +412,11 @@ export class View extends ViewEventHandler { } public restoreState(scrollPosition: { scrollLeft: number; scrollTop: number; }): void { - this._context.viewLayout.setScrollPosition({ scrollTop: scrollPosition.scrollTop }, ScrollType.Immediate); + this._context.model.setScrollPosition({ scrollTop: scrollPosition.scrollTop }, ScrollType.Immediate); this._context.model.tokenizeViewport(); this._renderNow(); - this.viewLines.updateLineWidths(); - this._context.viewLayout.setScrollPosition({ scrollLeft: scrollPosition.scrollLeft }, ScrollType.Immediate); + this._viewLines.updateLineWidths(); + this._context.model.setScrollPosition({ scrollLeft: scrollPosition.scrollLeft }, ScrollType.Immediate); } public getOffsetForColumn(modelLineNumber: number, modelColumn: number): number { @@ -458,7 +426,7 @@ export class View extends ViewEventHandler { }); const viewPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); this._flushAccumulatedAndRenderNow(); - const visibleRange = this.viewLines.visibleRangeForPosition(new Position(viewPosition.lineNumber, viewPosition.column)); + const visibleRange = this._viewLines.visibleRangeForPosition(new Position(viewPosition.lineNumber, viewPosition.column)); if (!visibleRange) { return -1; } @@ -466,34 +434,28 @@ export class View extends ViewEventHandler { } public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { - const mouseTarget = this.pointerHandler.getTargetAtClientPoint(clientX, clientY); + const mouseTarget = this._pointerHandler.getTargetAtClientPoint(clientX, clientY); if (!mouseTarget) { return null; } - return ViewOutgoingEvents.convertViewToModelMouseTarget(mouseTarget, this._context.model.coordinatesConverter); + return ViewUserInputEvents.convertViewToModelMouseTarget(mouseTarget, this._context.model.coordinatesConverter); } public createOverviewRuler(cssClassName: string): OverviewRuler { return new OverviewRuler(this._context, cssClassName); } - public change(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean { - return this._renderOnce(() => { - const zonesHaveChanged = this.viewZones.changeViewZones(callback); - if (zonesHaveChanged) { - this._context.viewLayout.onHeightMaybeChanged(); - this._context.privateViewEventBus.emit(new viewEvents.ViewZonesChangedEvent()); - } - return zonesHaveChanged; - }); + public change(callback: (changeAccessor: IViewZoneChangeAccessor) => any): void { + this._viewZones.changeViewZones(callback); + this._scheduleRender(); } public render(now: boolean, everything: boolean): void { if (everything) { // Force everything to render... - this.viewLines.forceShouldRender(); - for (let i = 0, len = this.viewParts.length; i < len; i++) { - const viewPart = this.viewParts[i]; + this._viewLines.forceShouldRender(); + for (let i = 0, len = this._viewParts.length; i < len; i++) { + const viewPart = this._viewParts[i]; viewPart.forceShouldRender(); } } @@ -521,7 +483,7 @@ export class View extends ViewEventHandler { } public addContentWidget(widgetData: IContentWidgetData): void { - this.contentWidgets.addWidget(widgetData.widget); + this._contentWidgets.addWidget(widgetData.widget); this.layoutContentWidget(widgetData); this._scheduleRender(); } @@ -535,31 +497,31 @@ export class View extends ViewEventHandler { } } const newPreference = widgetData.position ? widgetData.position.preference : null; - this.contentWidgets.setWidgetPosition(widgetData.widget, newRange, newPreference); + this._contentWidgets.setWidgetPosition(widgetData.widget, newRange, newPreference); this._scheduleRender(); } public removeContentWidget(widgetData: IContentWidgetData): void { - this.contentWidgets.removeWidget(widgetData.widget); + this._contentWidgets.removeWidget(widgetData.widget); this._scheduleRender(); } public addOverlayWidget(widgetData: IOverlayWidgetData): void { - this.overlayWidgets.addWidget(widgetData.widget); + this._overlayWidgets.addWidget(widgetData.widget); this.layoutOverlayWidget(widgetData); this._scheduleRender(); } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this.overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); if (shouldRender) { this._scheduleRender(); } } public removeOverlayWidget(widgetData: IOverlayWidgetData): void { - this.overlayWidgets.removeWidget(widgetData.widget); + this._overlayWidgets.removeWidget(widgetData.widget); this._scheduleRender(); } diff --git a/src/vs/editor/browser/view/viewOutgoingEvents.ts b/src/vs/editor/browser/view/viewUserInputEvents.ts similarity index 76% rename from src/vs/editor/browser/view/viewOutgoingEvents.ts rename to src/vs/editor/browser/view/viewUserInputEvents.ts index 6ba6ff3f123..22906bda60e 100644 --- a/src/vs/editor/browser/view/viewOutgoingEvents.ts +++ b/src/vs/editor/browser/view/viewUserInputEvents.ts @@ -4,66 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Disposable } from 'vs/base/common/lifecycle'; import { MouseTarget } from 'vs/editor/browser/controller/mouseTarget'; import { IEditorMouseEvent, IMouseTarget, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IScrollEvent, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; -import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { IViewModel, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; export interface EventCallback { (event: T): void; } -export class ViewOutgoingEvents extends Disposable { +export class ViewUserInputEvents { - public onDidContentSizeChange: EventCallback | null = null; - public onDidScroll: EventCallback | null = null; - public onDidGainFocus: EventCallback | null = null; - public onDidLoseFocus: EventCallback | null = null; public onKeyDown: EventCallback | null = null; public onKeyUp: EventCallback | null = null; public onContextMenu: EventCallback | null = null; public onMouseMove: EventCallback | null = null; public onMouseLeave: EventCallback | null = null; - public onMouseUp: EventCallback | null = null; public onMouseDown: EventCallback | null = null; + public onMouseUp: EventCallback | null = null; public onMouseDrag: EventCallback | null = null; public onMouseDrop: EventCallback | null = null; public onMouseWheel: EventCallback | null = null; - private readonly _viewModel: IViewModel; + private readonly _coordinatesConverter: ICoordinatesConverter; - constructor(viewModel: IViewModel) { - super(); - this._viewModel = viewModel; - } - - public emitContentSizeChange(e: viewEvents.ViewContentSizeChangedEvent): void { - if (this.onDidContentSizeChange) { - this.onDidContentSizeChange(e); - } - } - - public emitScrollChanged(e: viewEvents.ViewScrollChangedEvent): void { - if (this.onDidScroll) { - this.onDidScroll(e); - } - } - - public emitViewFocusGained(): void { - if (this.onDidGainFocus) { - this.onDidGainFocus(undefined); - } - } - - public emitViewFocusLost(): void { - if (this.onDidLoseFocus) { - this.onDidLoseFocus(undefined); - } + constructor(coordinatesConverter: ICoordinatesConverter) { + this._coordinatesConverter = coordinatesConverter; } public emitKeyDown(e: IKeyboardEvent): void { @@ -96,18 +64,18 @@ export class ViewOutgoingEvents extends Disposable { } } - public emitMouseUp(e: IEditorMouseEvent): void { - if (this.onMouseUp) { - this.onMouseUp(this._convertViewToModelMouseEvent(e)); - } - } - public emitMouseDown(e: IEditorMouseEvent): void { if (this.onMouseDown) { this.onMouseDown(this._convertViewToModelMouseEvent(e)); } } + public emitMouseUp(e: IEditorMouseEvent): void { + if (this.onMouseUp) { + this.onMouseUp(this._convertViewToModelMouseEvent(e)); + } + } + public emitMouseDrag(e: IEditorMouseEvent): void { if (this.onMouseDrag) { this.onMouseDrag(this._convertViewToModelMouseEvent(e)); @@ -139,7 +107,7 @@ export class ViewOutgoingEvents extends Disposable { } private _convertViewToModelMouseTarget(target: IMouseTarget): IMouseTarget { - return ViewOutgoingEvents.convertViewToModelMouseTarget(target, this._viewModel.coordinatesConverter); + return ViewUserInputEvents.convertViewToModelMouseTarget(target, this._coordinatesConverter); } public static convertViewToModelMouseTarget(target: IMouseTarget, coordinatesConverter: ICoordinatesConverter): IMouseTarget { diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 419287e19c1..21193bac130 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -88,7 +88,7 @@ export class EditorScrollbar extends ViewPart { } } - this._context.viewLayout.setScrollPosition(newScrollPosition, ScrollType.Immediate); + this._context.model.setScrollPosition(newScrollPosition, ScrollType.Immediate); }; // I've seen this happen both on the view dom node & on the lines content dom node. diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index 33979994f9d..f94c06247b4 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -112,7 +112,7 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { const visibleStartLineNumber = ctx.visibleRange.startLineNumber; const visibleEndLineNumber = ctx.visibleRange.endLineNumber; - const { indentSize } = this._context.model.getOptions(); + const { indentSize } = this._context.model.getTextModelOptions(); const indentWidth = indentSize * this._spaceWidth; const scrollWidth = ctx.scrollWidth; const lineHeight = this._lineHeight; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 7ee3267d063..6e92c558e11 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -282,7 +282,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, const scrollTopDelta = Math.abs(this._context.viewLayout.getCurrentScrollTop() - newScrollPosition.scrollTop); const scrollType = (scrollTopDelta <= this._lineHeight ? ScrollType.Immediate : e.scrollType); - this._context.viewLayout.setScrollPosition(newScrollPosition, scrollType); + this._context.model.setScrollPosition(newScrollPosition, scrollType); return true; } @@ -307,7 +307,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, return this._visibleLines.onTokensChanged(e); } public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { - this._context.viewLayout.onMaxLineWidthChanged(this._maxLineWidth); + this._context.model.setMaxLineWidth(this._maxLineWidth); return this._visibleLines.onZonesChanged(e); } public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { @@ -587,7 +587,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset); } // set `scrollLeft` - this._context.viewLayout.setScrollPosition({ + this._context.model.setScrollPosition({ scrollLeft: newScrollLeft.scrollLeft }, horizontalRevealRequest.scrollType); } @@ -626,7 +626,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, const iLineWidth = Math.ceil(lineWidth); if (this._maxLineWidth < iLineWidth) { this._maxLineWidth = iLineWidth; - this._context.viewLayout.onMaxLineWidthChanged(this._maxLineWidth); + this._context.model.setMaxLineWidth(this._maxLineWidth); } } diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 72961f79a18..1be53aad7a3 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1004,25 +1004,24 @@ export class Minimap extends ViewPart implements IMinimapModel { } public getOptions(): TextModelResolvedOptions { - return this._context.model.getOptions(); + return this._context.model.getTextModelOptions(); } public revealLineNumber(lineNumber: number): void { if (this._samplingState) { lineNumber = this._samplingState.minimapLines[lineNumber - 1]; } - this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( + this._context.model.revealRange( 'mouse', - new Range(lineNumber, 1, lineNumber, 1), - null, - viewEvents.VerticalRevealType.Center, false, + new Range(lineNumber, 1, lineNumber, 1), + viewEvents.VerticalRevealType.Center, ScrollType.Smooth - )); + ); } public setScrollTop(scrollTop: number): void { - this._context.viewLayout.setScrollPosition({ + this._context.model.setScrollPosition({ scrollTop: scrollTop }, ScrollType.Immediate); } diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index 68621081a8e..696e088de58 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -64,7 +64,7 @@ export class Rulers extends ViewPart { } if (currentCount < desiredCount) { - const { tabSize } = this._context.model.getOptions(); + const { tabSize } = this._context.model.getTextModelOptions(); const rulerWidth = tabSize; let addCount = desiredCount - currentCount; while (addCount > 0) { diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 5e28b7785ec..a73026a3f9b 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -79,9 +79,8 @@ export class ViewZones extends ViewPart { for (const whitespace of whitespaces) { oldWhitespaces.set(whitespace.id, whitespace); } - return this._context.viewLayout.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => { - let hadAChange = false; - + let hadAChange = false; + this._context.model.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => { const keys = Object.keys(this._zones); for (let i = 0, len = keys.length; i < len; i++) { const id = keys[i]; @@ -94,9 +93,8 @@ export class ViewZones extends ViewPart { hadAChange = true; } } - - return hadAChange; }); + return hadAChange; } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { @@ -115,11 +113,7 @@ export class ViewZones extends ViewPart { } public onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { - const hadAChange = this._recomputeWhitespacesProps(); - if (hadAChange) { - this._context.viewLayout.onHeightMaybeChanged(); - } - return hadAChange; + return this._recomputeWhitespacesProps(); } public onLinesDeleted(e: viewEvents.ViewLinesDeletedEvent): boolean { @@ -199,9 +193,9 @@ export class ViewZones extends ViewPart { } public changeViewZones(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean { + let zonesHaveChanged = false; - return this._context.viewLayout.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => { - let zonesHaveChanged = false; + this._context.model.changeWhitespace((whitespaceAccessor: IWhitespaceChangeAccessor) => { const changeAccessor: IViewZoneChangeAccessor = { addZone: (zone: IViewZone): string => { @@ -228,9 +222,9 @@ export class ViewZones extends ViewPart { changeAccessor.addZone = invalidFunc; changeAccessor.removeZone = invalidFunc; changeAccessor.layoutZone = invalidFunc; - - return zonesHaveChanged; }); + + return zonesHaveChanged; } private _addZone(whitespaceAccessor: IWhitespaceChangeAccessor, zone: IViewZone): string { diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index adf1ef5a746..f57ab7693d6 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -21,10 +21,10 @@ import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/edi import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; import { IContentWidgetData, IOverlayWidgetData, View } from 'vs/editor/browser/view/viewImpl'; -import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; +import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, IEditorConstructionOptions, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; -import { Cursor, CursorStateChangedEvent } from 'vs/editor/common/controller/cursor'; -import { CursorColumns, ICursors } from 'vs/editor/common/controller/cursorCommon'; +import { Cursor } from 'vs/editor/common/controller/cursor'; +import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; @@ -54,6 +54,7 @@ import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/m import { DOMLineBreaksComputerFactory } from 'vs/editor/browser/view/domLineBreaksComputer'; import { WordOperations } from 'vs/editor/common/controller/cursorWordOperations'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { OutgoingViewModelEventKind } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; let EDITOR_ID = 0; @@ -527,7 +528,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return null; } - return this._modelData.viewModel.cursor.getPosition(); + return this._modelData.viewModel.getPosition(); } public setPosition(position: IPosition): void { @@ -537,7 +538,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!Position.isIPosition(position)) { throw new Error('Invalid arguments'); } - this._modelData.viewModel.cursor.setSelections('api', [{ + this._modelData.viewModel.setSelections('api', [{ selectionStartLineNumber: position.lineNumber, selectionStartColumn: position.column, positionLineNumber: position.lineNumber, @@ -555,7 +556,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE const validatedModelRange = this._modelData.model.validateRange(modelRange); const viewRange = this._modelData.viewModel.coordinatesConverter.convertModelRangeToViewRange(validatedModelRange); - this._modelData.viewModel.cursor.emitCursorRevealRange('api', viewRange, null, verticalType, revealHorizontal, scrollType); + this._modelData.viewModel.revealRange('api', revealHorizontal, viewRange, verticalType, scrollType); } public revealLine(lineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { @@ -640,14 +641,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return null; } - return this._modelData.viewModel.cursor.getSelection(); + return this._modelData.viewModel.getSelection(); } public getSelections(): Selection[] | null { if (!this._modelData) { return null; } - return this._modelData.viewModel.cursor.getSelections(); + return this._modelData.viewModel.getSelections(); } public setSelection(range: IRange): void; @@ -681,7 +682,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return; } const selection = new Selection(sel.selectionStartLineNumber, sel.selectionStartColumn, sel.positionLineNumber, sel.positionColumn); - this._modelData.viewModel.cursor.setSelections('api', [selection]); + this._modelData.viewModel.setSelections('api', [selection]); } public revealLines(startLineNumber: number, endLineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { @@ -812,7 +813,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE throw new Error('Invalid arguments'); } } - this._modelData.viewModel.cursor.setSelections(source, ranges); + this._modelData.viewModel.setSelections(source, ranges); } public getContentWidth(): number { @@ -862,7 +863,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (typeof newScrollLeft !== 'number') { throw new Error('Invalid arguments'); } - this._modelData.viewModel.viewLayout.setScrollPosition({ + this._modelData.viewModel.setScrollPosition({ scrollLeft: newScrollLeft }, scrollType); } @@ -873,7 +874,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (typeof newScrollTop !== 'number') { throw new Error('Invalid arguments'); } - this._modelData.viewModel.viewLayout.setScrollPosition({ + this._modelData.viewModel.setScrollPosition({ scrollTop: newScrollTop }, scrollType); } @@ -881,7 +882,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return; } - this._modelData.viewModel.viewLayout.setScrollPosition(position, scrollType); + this._modelData.viewModel.setScrollPosition(position, scrollType); } public saveViewState(): editorCommon.ICodeEditorViewState | null { @@ -898,7 +899,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } } - const cursorState = this._modelData.viewModel.cursor.saveState(); + const cursorState = this._modelData.viewModel.saveCursorState(); const viewState = this._modelData.viewModel.saveState(); return { cursorState: cursorState, @@ -915,10 +916,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (codeEditorState && codeEditorState.cursorState && codeEditorState.viewState) { const cursorState = codeEditorState.cursorState; if (Array.isArray(cursorState)) { - this._modelData.viewModel.cursor.restoreState(cursorState); + this._modelData.viewModel.restoreCursorState(cursorState); } else { // Backwards compatibility - this._modelData.viewModel.cursor.restoreState([cursorState]); + this._modelData.viewModel.restoreCursorState([cursorState]); } const contributionsState = codeEditorState.contributionsState || {}; @@ -976,40 +977,31 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE public trigger(source: string | null | undefined, handlerId: string, payload: any): void { payload = payload || {}; - // Special case for typing - if (handlerId === editorCommon.Handler.Type) { - if (!this._modelData || typeof payload.text !== 'string' || payload.text.length === 0) { - // nothing to do + switch (handlerId) { + case editorCommon.Handler.CompositionStart: + this._startComposition(); + return; + case editorCommon.Handler.CompositionEnd: + this._endComposition(source); + return; + case editorCommon.Handler.Type: { + const args = >payload; + this._type(source, args.text || ''); return; } - if (source === 'keyboard') { - this._onWillType.fire(payload.text); - } - this._modelData.viewModel.cursor.trigger(source, handlerId, payload); - if (source === 'keyboard') { - this._onDidType.fire(payload.text); - } - return; - } - - // Special case for pasting - if (handlerId === editorCommon.Handler.Paste) { - if (!this._modelData || typeof payload.text !== 'string' || payload.text.length === 0) { - // nothing to do + case editorCommon.Handler.ReplacePreviousChar: { + const args = >payload; + this._replacePreviousChar(source, args.text || '', args.replaceCharCnt || 0); return; } - const startPosition = this._modelData.viewModel.cursor.getSelection().getStartPosition(); - this._modelData.viewModel.cursor.trigger(source, handlerId, payload); - const endPosition = this._modelData.viewModel.cursor.getSelection().getStartPosition(); - if (source === 'keyboard') { - this._onDidPaste.fire( - { - range: new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), - mode: payload.mode - } - ); + case editorCommon.Handler.Paste: { + const args = >payload; + this._paste(source, args.text || '', args.pasteOnNewLine || false, args.multicursorText || null, args.mode || null); + return; } - return; + case editorCommon.Handler.Cut: + this._cut(source); + return; } const action = this.getAction(handlerId); @@ -1025,15 +1017,64 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (this._triggerEditorCommand(source, handlerId, payload)) { return; } + } - this._modelData.viewModel.cursor.trigger(source, handlerId, payload); + private _startComposition(): void { + if (!this._modelData) { + return; + } + this._modelData.viewModel.startComposition(); + this._onDidCompositionStart.fire(); + } - if (handlerId === editorCommon.Handler.CompositionStart) { - this._onDidCompositionStart.fire(); + private _endComposition(source: string | null | undefined): void { + if (!this._modelData) { + return; } - if (handlerId === editorCommon.Handler.CompositionEnd) { - this._onDidCompositionEnd.fire(); + this._modelData.viewModel.endComposition(source); + this._onDidCompositionEnd.fire(); + } + + private _type(source: string | null | undefined, text: string): void { + if (!this._modelData || text.length === 0) { + return; } + if (source === 'keyboard') { + this._onWillType.fire(text); + } + this._modelData.viewModel.type(text, source); + if (source === 'keyboard') { + this._onDidType.fire(text); + } + } + + private _replacePreviousChar(source: string | null | undefined, text: string, replaceCharCnt: number): void { + if (!this._modelData) { + return; + } + this._modelData.viewModel.replacePreviousChar(text, replaceCharCnt, source); + } + + private _paste(source: string | null | undefined, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void { + if (!this._modelData || text.length === 0) { + return; + } + const startPosition = this._modelData.viewModel.getSelection().getStartPosition(); + this._modelData.viewModel.paste(text, pasteOnNewLine, multicursorText, source); + const endPosition = this._modelData.viewModel.getSelection().getStartPosition(); + if (source === 'keyboard') { + this._onDidPaste.fire({ + range: new Range(startPosition.lineNumber, startPosition.column, endPosition.lineNumber, endPosition.column), + mode: mode + }); + } + } + + private _cut(source: string | null | undefined): void { + if (!this._modelData) { + return; + } + this._modelData.viewModel.cut(source); } private _triggerEditorCommand(source: string | null | undefined, handlerId: string, payload: any): boolean { @@ -1050,13 +1091,6 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return false; } - public _getCursors(): ICursors | null { - if (!this._modelData) { - return null; - } - return this._modelData.viewModel.cursor; - } - public _getViewModel(): IViewModel | null { if (!this._modelData) { return null; @@ -1094,7 +1128,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE cursorStateComputer = endCursorState; } - this._modelData.viewModel.cursor.executeEdits(source, edits, cursorStateComputer); + this._modelData.viewModel.executeEdits(source, edits, cursorStateComputer); return true; } @@ -1102,14 +1136,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return; } - this._modelData.viewModel.cursor.trigger(source, editorCommon.Handler.ExecuteCommand, command); + this._modelData.viewModel.executeCommand(command, source); } public executeCommands(source: string | null | undefined, commands: editorCommon.ICommand[]): void { if (!this._modelData) { return; } - this._modelData.viewModel.cursor.trigger(source, editorCommon.Handler.ExecuteCommands, commands); + this._modelData.viewModel.executeCommands(commands, source); } public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { @@ -1351,10 +1385,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData || !this._modelData.hasRealView) { return; } - const hasChanges = this._modelData.view.change(callback); - if (hasChanges) { - this._onDidChangeViewZones.fire(); - } + this._modelData.view.change(callback); } public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget | null { @@ -1442,38 +1473,56 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE // Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model listenersToRemove.push(model.onWillDispose(() => this.setModel(null))); - listenersToRemove.push(viewModel.cursor.onDidReachMaxCursorCount(() => { - this._notificationService.warn(nls.localize('cursors.maximum', "The number of cursors has been limited to {0}.", Cursor.MAX_CURSOR_COUNT)); - })); + listenersToRemove.push(viewModel.onEvent((e) => { + switch (e.kind) { + case OutgoingViewModelEventKind.ContentSizeChanged: + this._onDidContentSizeChange.fire(e); + break; + case OutgoingViewModelEventKind.FocusChanged: + this._editorTextFocus.setValue(e.hasFocus); + break; + case OutgoingViewModelEventKind.ScrollChanged: + this._onDidScrollChange.fire(e); + break; + case OutgoingViewModelEventKind.ViewZonesChanged: + this._onDidChangeViewZones.fire(); + break; + case OutgoingViewModelEventKind.ReadOnlyEditAttempt: + this._onDidAttemptReadOnlyEdit.fire(); + break; + case OutgoingViewModelEventKind.CursorStateChanged: { + if (e.reachedMaxCursorCount) { + this._notificationService.warn(nls.localize('cursors.maximum', "The number of cursors has been limited to {0}.", Cursor.MAX_CURSOR_COUNT)); + } - listenersToRemove.push(viewModel.cursor.onDidAttemptReadOnlyEdit(() => { - this._onDidAttemptReadOnlyEdit.fire(undefined); - })); + const positions: Position[] = []; + for (let i = 0, len = e.selections.length; i < len; i++) { + positions[i] = e.selections[i].getPosition(); + } + + const e1: ICursorPositionChangedEvent = { + position: positions[0], + secondaryPositions: positions.slice(1), + reason: e.reason, + source: e.source + }; + this._onDidChangeCursorPosition.fire(e1); + + const e2: ICursorSelectionChangedEvent = { + selection: e.selections[0], + secondarySelections: e.selections.slice(1), + modelVersionId: e.modelVersionId, + oldSelections: e.oldSelections, + oldModelVersionId: e.oldModelVersionId, + source: e.source, + reason: e.reason + }; + this._onDidChangeCursorSelection.fire(e2); + + break; + } - listenersToRemove.push(viewModel.cursor.onDidChange((e: CursorStateChangedEvent) => { - const positions: Position[] = []; - for (let i = 0, len = e.selections.length; i < len; i++) { - positions[i] = e.selections[i].getPosition(); } - - const e1: ICursorPositionChangedEvent = { - position: positions[0], - secondaryPositions: positions.slice(1), - reason: e.reason, - source: e.source - }; - this._onDidChangeCursorPosition.fire(e1); - - const e2: ICursorSelectionChangedEvent = { - selection: e.selections[0], - secondarySelections: e.selections.slice(1), - modelVersionId: e.modelVersionId, - oldSelections: e.oldSelections, - oldModelVersionId: e.oldModelVersionId, - source: e.source, - reason: e.reason - }; - this._onDidChangeCursorSelection.fire(e2); })); const [view, hasRealView] = this._createView(viewModel); @@ -1504,55 +1553,48 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (this.isSimpleWidget) { commandDelegate = { executeEditorCommand: (editorCommand: CoreEditorCommand, args: any): void => { - editorCommand.runCoreEditorCommand(viewModel.cursor, args); + editorCommand.runCoreEditorCommand(viewModel, args); }, paste: (text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { - this.trigger('keyboard', editorCommon.Handler.Paste, { text, pasteOnNewLine, multicursorText, mode }); + this._paste('keyboard', text, pasteOnNewLine, multicursorText, mode); }, type: (text: string) => { - this.trigger('keyboard', editorCommon.Handler.Type, { text }); + this._type('keyboard', text); }, replacePreviousChar: (text: string, replaceCharCnt: number) => { - this.trigger('keyboard', editorCommon.Handler.ReplacePreviousChar, { text, replaceCharCnt }); + this._replacePreviousChar('keyboard', text, replaceCharCnt); }, - compositionStart: () => { - this.trigger('keyboard', editorCommon.Handler.CompositionStart, undefined); + startComposition: () => { + this._startComposition(); }, - compositionEnd: () => { - this.trigger('keyboard', editorCommon.Handler.CompositionEnd, undefined); + endComposition: () => { + this._endComposition('keyboard'); }, cut: () => { - this.trigger('keyboard', editorCommon.Handler.Cut, undefined); + this._cut('keyboard'); } }; } else { commandDelegate = { executeEditorCommand: (editorCommand: CoreEditorCommand, args: any): void => { - editorCommand.runCoreEditorCommand(viewModel.cursor, args); + editorCommand.runCoreEditorCommand(viewModel, args); }, paste: (text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { - this._commandService.executeCommand(editorCommon.Handler.Paste, { - text: text, - pasteOnNewLine: pasteOnNewLine, - multicursorText: multicursorText, - mode - }); + const payload: editorCommon.PastePayload = { text, pasteOnNewLine, multicursorText, mode }; + this._commandService.executeCommand(editorCommon.Handler.Paste, payload); }, type: (text: string) => { - this._commandService.executeCommand(editorCommon.Handler.Type, { - text: text - }); + const payload: editorCommon.TypePayload = { text }; + this._commandService.executeCommand(editorCommon.Handler.Type, payload); }, replacePreviousChar: (text: string, replaceCharCnt: number) => { - this._commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, { - text: text, - replaceCharCnt: replaceCharCnt - }); + const payload: editorCommon.ReplacePreviousCharPayload = { text, replaceCharCnt }; + this._commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, payload); }, - compositionStart: () => { + startComposition: () => { this._commandService.executeCommand(editorCommon.Handler.CompositionStart, {}); }, - compositionEnd: () => { + endComposition: () => { this._commandService.executeCommand(editorCommon.Handler.CompositionEnd, {}); }, cut: () => { @@ -1561,35 +1603,24 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }; } - const onDidChangeTextFocus = (textFocus: boolean) => { - if (this._modelData) { - this._modelData.viewModel.cursor.setHasFocus(textFocus); - } - this._editorTextFocus.setValue(textFocus); - }; - - const viewOutgoingEvents = new ViewOutgoingEvents(viewModel); - viewOutgoingEvents.onDidContentSizeChange = (e) => this._onDidContentSizeChange.fire(e); - viewOutgoingEvents.onDidScroll = (e) => this._onDidScrollChange.fire(e); - viewOutgoingEvents.onDidGainFocus = () => onDidChangeTextFocus(true); - viewOutgoingEvents.onDidLoseFocus = () => onDidChangeTextFocus(false); - viewOutgoingEvents.onContextMenu = (e) => this._onContextMenu.fire(e); - viewOutgoingEvents.onMouseDown = (e) => this._onMouseDown.fire(e); - viewOutgoingEvents.onMouseUp = (e) => this._onMouseUp.fire(e); - viewOutgoingEvents.onMouseDrag = (e) => this._onMouseDrag.fire(e); - viewOutgoingEvents.onMouseDrop = (e) => this._onMouseDrop.fire(e); - viewOutgoingEvents.onKeyUp = (e) => this._onKeyUp.fire(e); - viewOutgoingEvents.onMouseMove = (e) => this._onMouseMove.fire(e); - viewOutgoingEvents.onMouseLeave = (e) => this._onMouseLeave.fire(e); - viewOutgoingEvents.onMouseWheel = (e) => this._onMouseWheel.fire(e); - viewOutgoingEvents.onKeyDown = (e) => this._onKeyDown.fire(e); + const viewUserInputEvents = new ViewUserInputEvents(viewModel.coordinatesConverter); + viewUserInputEvents.onKeyDown = (e) => this._onKeyDown.fire(e); + viewUserInputEvents.onKeyUp = (e) => this._onKeyUp.fire(e); + viewUserInputEvents.onContextMenu = (e) => this._onContextMenu.fire(e); + viewUserInputEvents.onMouseMove = (e) => this._onMouseMove.fire(e); + viewUserInputEvents.onMouseLeave = (e) => this._onMouseLeave.fire(e); + viewUserInputEvents.onMouseDown = (e) => this._onMouseDown.fire(e); + viewUserInputEvents.onMouseUp = (e) => this._onMouseUp.fire(e); + viewUserInputEvents.onMouseDrag = (e) => this._onMouseDrag.fire(e); + viewUserInputEvents.onMouseDrop = (e) => this._onMouseDrop.fire(e); + viewUserInputEvents.onMouseWheel = (e) => this._onMouseWheel.fire(e); const view = new View( commandDelegate, this._configuration, this._themeService, viewModel, - viewOutgoingEvents + viewUserInputEvents ); return [view, true]; diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index c32c43fef19..db4e550e94f 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -269,6 +269,7 @@ export class DiffReview extends Disposable { private hide(): void { this._isVisible = false; + this._diffEditor.updateOptions({ readOnly: false }); this._diffEditor.focus(); this._diffEditor.doLayout(); this._render(); @@ -541,6 +542,7 @@ export class DiffReview extends Disposable { return; } + this._diffEditor.updateOptions({ readOnly: true }); const diffIndex = this._findDiffIndex(this._diffEditor.getPosition()!); if (this._diffs[diffIndex] === this._currentDiff) { diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 609f97f351d..954af2fa4f2 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -283,6 +283,9 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; + private _onDidChangeFast = this._register(new Emitter()); + public readonly onDidChangeFast: Event = this._onDidChangeFast.event; + public readonly isSimpleWidget: boolean; private _computeOptionsMemory: ComputeOptionsMemory; public options!: ComputedEditorOptions; @@ -334,6 +337,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC } this.options = newOptions; + this._onDidChangeFast.fire(changeEvent); this._onDidChange.fire(changeEvent); } } diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index a802a723b50..bb0e19e5d9e 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -95,9 +95,9 @@ export interface IEditorOptions { renderFinalNewline?: boolean; /** * Remove unusual line terminators like LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL). - * Defaults to true. + * Defaults to 'prompt'. */ - removeUnusualLineTerminators?: boolean; + unusualLineTerminators?: 'off' | 'prompt' | 'auto'; /** * Should the corresponding line be selected when clicking on the line number? * Defaults to true. @@ -3552,7 +3552,6 @@ export const enum EditorOption { quickSuggestions, quickSuggestionsDelay, readOnly, - removeUnusualLineTerminators, renameOnType, renderControlCharacters, renderIndentGuides, @@ -3582,6 +3581,7 @@ export const enum EditorOption { suggestOnTriggerCharacters, suggestSelection, tabCompletion, + unusualLineTerminators, useTabStops, wordSeparators, wordWrap, @@ -3971,10 +3971,6 @@ export const EditorOptions = { readOnly: register(new EditorBooleanOption( EditorOption.readOnly, 'readOnly', false, )), - removeUnusualLineTerminators: register(new EditorBooleanOption( - EditorOption.removeUnusualLineTerminators, 'removeUnusualLineTerminators', true, - { description: nls.localize('removeUnusualLineTerminators', "Remove unusual line terminators that might cause problems.") } - )), renameOnType: register(new EditorBooleanOption( EditorOption.renameOnType, 'renameOnType', false, { description: nls.localize('renameOnType', "Controls whether the editor auto renames on type.") } @@ -4144,6 +4140,19 @@ export const EditorOptions = { description: nls.localize('tabCompletion', "Enables tab completions.") } )), + unusualLineTerminators: register(new EditorStringEnumOption( + EditorOption.unusualLineTerminators, 'unusualLineTerminators', + 'prompt' as 'off' | 'prompt' | 'auto', + ['off', 'prompt', 'auto'] as const, + { + enumDescriptions: [ + nls.localize('unusualLineTerminators.off', "Unusual line terminators are ignored."), + nls.localize('unusualLineTerminators.prompt', "Unusual line terminators prompt to be removed."), + nls.localize('unusualLineTerminators.auto', "Unusual line terminators are automatically removed."), + ], + description: nls.localize('unusualLineTerminators', "Remove unusual line terminators that might cause problems.") + } + )), useTabStops: register(new EditorBooleanOption( EditorOption.useTabStops, 'useTabStops', true, { description: nls.localize('useTabStops', "Inserting and deleting whitespace follows tab stops.") } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 11e0462b5a0..7a966ed223e 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -4,10 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { onUnexpectedError } from 'vs/base/common/errors'; -import { Emitter, Event } from 'vs/base/common/event'; import * as strings from 'vs/base/common/strings'; import { CursorCollection } from 'vs/editor/common/controller/cursorCollection'; -import { CursorColumns, CursorConfiguration, CursorContext, CursorState, EditOperationResult, EditOperationType, IColumnSelectData, ICursors, PartialCursorState, RevealTarget, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon'; +import { CursorColumns, CursorConfiguration, CursorContext, CursorState, EditOperationResult, EditOperationType, IColumnSelectData, PartialCursorState, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon'; import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { TypeOperations, TypeWithAutoClosingCommand } from 'vs/editor/common/controller/cursorTypeOperations'; @@ -16,48 +15,11 @@ import { Range, IRange } from 'vs/editor/common/core/range'; import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model'; -import { RawContentChangedType, ModelRawContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { dispose } from 'vs/base/common/lifecycle'; -import { EditorOption, ConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions'; +import { RawContentChangedType, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; +import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from 'vs/editor/common/view/viewEvents'; +import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; - -export class CursorStateChangedEvent { - /** - * The new selections. - * The primary selection is always at index 0. - */ - readonly selections: Selection[]; - /** - * The new model version id that `selections` apply to. - */ - readonly modelVersionId: number; - /** - * The old selections. - */ - readonly oldSelections: Selection[] | null; - /** - * The model version id the that `oldSelections` apply to. - */ - readonly oldModelVersionId: number; - /** - * Source of the call that caused the event. - */ - readonly source: string; - /** - * Reason. - */ - readonly reason: CursorChangeReason; - - constructor(selections: Selection[], modelVersionId: number, oldSelections: Selection[] | null, oldModelVersionId: number, source: string, reason: CursorChangeReason) { - this.selections = selections; - this.modelVersionId = modelVersionId; - this.oldSelections = oldSelections; - this.oldModelVersionId = oldModelVersionId; - this.source = source; - this.reason = reason; - } -} +import { CursorStateChangedEvent, ViewModelEventsCollector } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; /** * A snapshot of the cursor and the model state @@ -69,7 +31,7 @@ export class CursorModelState { constructor(model: ITextModel, cursor: Cursor) { this.modelVersionId = model.getVersionId(); - this.cursorState = cursor.getAll(); + this.cursorState = cursor.getCursorStates(); } public equals(other: CursorModelState | null): boolean { @@ -157,20 +119,10 @@ class AutoClosedAction { } } -export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { +export class Cursor extends Disposable { public static readonly MAX_CURSOR_COUNT = 10000; - private readonly _onDidReachMaxCursorCount: Emitter = this._register(new Emitter()); - public readonly onDidReachMaxCursorCount: Event = this._onDidReachMaxCursorCount.event; - - private readonly _onDidAttemptReadOnlyEdit: Emitter = this._register(new Emitter()); - public readonly onDidAttemptReadOnlyEdit: Event = this._onDidAttemptReadOnlyEdit.event; - - private readonly _onDidChange: Emitter = this._register(new Emitter()); - public readonly onDidChange: Event = this._onDidChange.event; - - private readonly _configuration: editorCommon.IConfiguration; private readonly _model: ITextModel; private _knownModelVersionId: number; private readonly _viewModel: ICursorSimpleModel; @@ -186,14 +138,13 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { private _autoClosedActions: AutoClosedAction[]; private _prevEditOperationType: EditOperationType; - constructor(configuration: editorCommon.IConfiguration, model: ITextModel, viewModel: ICursorSimpleModel, coordinatesConverter: ICoordinatesConverter) { + constructor(model: ITextModel, viewModel: ICursorSimpleModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) { super(); - this._configuration = configuration; this._model = model; this._knownModelVersionId = this._model.getVersionId(); this._viewModel = viewModel; this._coordinatesConverter = coordinatesConverter; - this.context = new CursorContext(this._configuration, this._model, this._viewModel, this._coordinatesConverter); + this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig); this._cursors = new CursorCollection(this.context); this._hasFocus = false; @@ -211,12 +162,12 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { super.dispose(); } - private _updateCursorContext(): void { - this.context = new CursorContext(this._configuration, this._model, this._viewModel, this._coordinatesConverter); + public updateConfiguration(cursorConfig: CursorConfiguration): void { + this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig); this._cursors.updateContext(this.context); } - public onLineMappingChanged(): void { + public onLineMappingChanged(eventsCollector: ViewModelEventsCollector): void { if (this._knownModelVersionId !== this._model.getVersionId()) { // There are model change events that I didn't yet receive. // @@ -228,25 +179,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return; } // Ensure valid state - this.setStates('viewModel', CursorChangeReason.NotSet, this.getAll()); - } - - public onDidChangeModelLanguage(e: IModelLanguageChangedEvent): void { - this._updateCursorContext(); - } - - public onDidChangeModelLanguageConfiguration(): void { - this._updateCursorContext(); - } - - public onDidChangeModelOptions(): void { - this._updateCursorContext(); - } - - public onDidChangeConfiguration(e: ConfigurationChangedEvent): void { - if (CursorConfiguration.shouldRecreate(e)) { - this._updateCursorContext(); - } + this.setStates(eventsCollector, 'viewModel', CursorChangeReason.NotSet, this.getCursorStates()); } public setHasFocus(hasFocus: boolean): void { @@ -269,7 +202,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ------ some getters/setters - public getPrimaryCursor(): CursorState { + public getPrimaryCursorState(): CursorState { return this._cursors.getPrimaryCursor(); } @@ -277,14 +210,15 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return this._cursors.getLastAddedCursorIndex(); } - public getAll(): CursorState[] { + public getCursorStates(): CursorState[] { return this._cursors.getAll(); } - public setStates(source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean { + public setStates(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean { + let reachedMaxCursorCount = false; if (states !== null && states.length > Cursor.MAX_CURSOR_COUNT) { states = states.slice(0, Cursor.MAX_CURSOR_COUNT); - this._onDidReachMaxCursorCount.fire(undefined); + reachedMaxCursorCount = true; } const oldState = new CursorModelState(this._model, this); @@ -295,19 +229,38 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._validateAutoClosedActions(); - return this._emitStateChangedIfNecessary(source, reason, oldState); + return this._emitStateChangedIfNecessary(eventsCollector, source, reason, oldState, reachedMaxCursorCount); } - public setColumnSelectData(columnSelectData: IColumnSelectData): void { + public setCursorColumnSelectData(columnSelectData: IColumnSelectData): void { this._columnSelectData = columnSelectData; } - public reveal(source: string | null | undefined, horizontal: boolean, target: RevealTarget, scrollType: editorCommon.ScrollType): void { - this._revealRange(source, target, viewEvents.VerticalRevealType.Simple, horizontal, scrollType); + public revealPrimary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { + const viewPositions = this._cursors.getViewPositions(); + if (viewPositions.length > 1) { + this._emitCursorRevealRange(eventsCollector, source, null, this._cursors.getViewSelections(), VerticalRevealType.Simple, revealHorizontal, scrollType); + return; + } else { + const viewPosition = viewPositions[0]; + const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); + this._emitCursorRevealRange(eventsCollector, source, viewRange, null, VerticalRevealType.Simple, revealHorizontal, scrollType); + } } - public revealRange(source: string | null | undefined, revealHorizontal: boolean, viewRange: Range, verticalType: viewEvents.VerticalRevealType, scrollType: editorCommon.ScrollType) { - this.emitCursorRevealRange(source, viewRange, null, verticalType, revealHorizontal, scrollType); + private _revealPrimaryCursor(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { + const viewPositions = this._cursors.getViewPositions(); + if (viewPositions.length > 1) { + this._emitCursorRevealRange(eventsCollector, source, null, this._cursors.getViewSelections(), verticalType, revealHorizontal, scrollType); + } else { + const viewPosition = viewPositions[0]; + const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); + this._emitCursorRevealRange(eventsCollector, source, viewRange, null, verticalType, revealHorizontal, scrollType); + } + } + + private _emitCursorRevealRange(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, viewRange: Range | null, viewSelections: Selection[] | null, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) { + eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, viewRange, viewSelections, verticalType, revealHorizontal, scrollType)); } public saveState(): editorCommon.ICursorState[] { @@ -334,7 +287,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return result; } - public restoreState(states: editorCommon.ICursorState[]): void { + public restoreState(eventsCollector: ViewModelEventsCollector, states: editorCommon.ICursorState[]): void { let desiredSelections: ISelection[] = []; @@ -371,11 +324,11 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { }); } - this.setStates('restoreState', CursorChangeReason.NotSet, CursorState.fromModelSelections(desiredSelections)); - this.reveal('restoreState', true, RevealTarget.Primary, editorCommon.ScrollType.Immediate); + this.setStates(eventsCollector, 'restoreState', CursorChangeReason.NotSet, CursorState.fromModelSelections(desiredSelections)); + this.revealPrimary(eventsCollector, 'restoreState', true, editorCommon.ScrollType.Immediate); } - public onModelContentChanged(e: ModelRawContentChangedEvent): void { + public onModelContentChanged(eventsCollector: ViewModelEventsCollector, e: ModelRawContentChangedEvent): void { this._knownModelVersionId = e.versionId; if (this._isHandling) { @@ -390,16 +343,16 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._cursors.dispose(); this._cursors = new CursorCollection(this.context); this._validateAutoClosedActions(); - this._emitStateChangedIfNecessary('model', CursorChangeReason.ContentFlush, null); + this._emitStateChangedIfNecessary(eventsCollector, 'model', CursorChangeReason.ContentFlush, null, false); } else { if (this._hasFocus && e.resultingSelection && e.resultingSelection.length > 0) { const cursorState = CursorState.fromModelSelections(e.resultingSelection); - if (this.setStates('modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) { - this._revealRange('modelChange', RevealTarget.Primary, viewEvents.VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); + if (this.setStates(eventsCollector, 'modelChange', e.isUndoing ? CursorChangeReason.Undo : e.isRedoing ? CursorChangeReason.Redo : CursorChangeReason.RecoverFromMarkers, cursorState)) { + this._revealPrimaryCursor(eventsCollector, 'modelChange', VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); } } else { const selectionsFromMarkers = this._cursors.readSelectionFromMarkers(); - this.setStates('modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers)); + this.setStates(eventsCollector, 'modelChange', CursorChangeReason.RecoverFromMarkers, CursorState.fromModelSelections(selectionsFromMarkers)); } } } @@ -408,7 +361,15 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return this._cursors.getPrimaryCursor().modelState.selection; } - public getColumnSelectData(): IColumnSelectData { + public getTopMostViewPosition(): Position { + return this._cursors.getTopMostViewPosition(); + } + + public getBottomMostViewPosition(): Position { + return this._cursors.getBottomMostViewPosition(); + } + + public getCursorColumnSelectData(): IColumnSelectData { if (this._columnSelectData) { return this._columnSelectData; } @@ -418,9 +379,9 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return { isReal: false, fromViewLineNumber: viewSelectionStart.lineNumber, - fromViewVisualColumn: CursorColumns.visibleColumnFromColumn2(this.context.config, this.context.viewModel, viewSelectionStart), + fromViewVisualColumn: CursorColumns.visibleColumnFromColumn2(this.context.cursorConfig, this._viewModel, viewSelectionStart), toViewLineNumber: viewPosition.lineNumber, - toViewVisualColumn: CursorColumns.visibleColumnFromColumn2(this.context.config, this.context.viewModel, viewPosition), + toViewVisualColumn: CursorColumns.visibleColumnFromColumn2(this.context.cursorConfig, this._viewModel, viewPosition), }; } @@ -432,8 +393,8 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return this._cursors.getPrimaryCursor().modelState.position; } - public setSelections(source: string | null | undefined, selections: readonly ISelection[]): void { - this.setStates(source, CursorChangeReason.NotSet, CursorState.fromModelSelections(selections)); + public setSelections(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, selections: readonly ISelection[]): void { + this.setStates(eventsCollector, source, CursorChangeReason.NotSet, CursorState.fromModelSelections(selections)); } public getPrevEditOperationType(): EditOperationType { @@ -524,7 +485,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ----------------------------------------------------------------------------------------------------------- // ----- emitting events - private _emitStateChangedIfNecessary(source: string | null | undefined, reason: CursorChangeReason, oldState: CursorModelState | null): boolean { + private _emitStateChangedIfNecessary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, oldState: CursorModelState | null, reachedMaxCursorCount: boolean): boolean { const newState = new CursorModelState(this._model, this); if (newState.equals(oldState)) { return false; @@ -534,7 +495,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { const viewSelections = this._cursors.getViewSelections(); // Let the view get the event first. - this._emitSingleViewEvent(new viewEvents.ViewCursorStateChangedEvent(viewSelections, selections)); + eventsCollector.emitViewEvent(new ViewCursorStateChangedEvent(viewSelections, selections)); // Only after the view has been notified, let the rest of the world know... if (!oldState @@ -543,44 +504,12 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { ) { const oldSelections = oldState ? oldState.cursorState.map(s => s.modelState.selection) : null; const oldModelVersionId = oldState ? oldState.modelVersionId : 0; - this._onDidChange.fire(new CursorStateChangedEvent(selections, newState.modelVersionId, oldSelections, oldModelVersionId, source || 'keyboard', reason)); + eventsCollector.emitOutgoingEvent(new CursorStateChangedEvent(oldSelections, selections, oldModelVersionId, newState.modelVersionId, source || 'keyboard', reason, reachedMaxCursorCount)); } return true; } - private _revealRange(source: string | null | undefined, revealTarget: RevealTarget, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { - const viewPositions = this._cursors.getViewPositions(); - - let viewPosition = viewPositions[0]; - - if (revealTarget === RevealTarget.TopMost) { - for (let i = 1; i < viewPositions.length; i++) { - if (viewPositions[i].isBefore(viewPosition)) { - viewPosition = viewPositions[i]; - } - } - } else if (revealTarget === RevealTarget.BottomMost) { - for (let i = 1; i < viewPositions.length; i++) { - if (viewPosition.isBeforeOrEqual(viewPositions[i])) { - viewPosition = viewPositions[i]; - } - } - } else { - if (viewPositions.length > 1) { - this.emitCursorRevealRange(source, null, this._cursors.getViewSelections(), verticalType, revealHorizontal, scrollType); - return; - } - } - - const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); - this.emitCursorRevealRange(source, viewRange, null, verticalType, revealHorizontal, scrollType); - } - - public emitCursorRevealRange(source: string | null | undefined, viewRange: Range | null, viewSelections: Selection[] | null, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) { - this._emitSingleViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, viewSelections, verticalType, revealHorizontal, scrollType)); - } - // ----------------------------------------------------------------------------------------------------------- // ----- handlers beyond this point @@ -602,7 +531,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { } const closeChar = m[1]; - const autoClosingPairsCandidates = this.context.config.autoClosingPairsClose2.get(closeChar); + const autoClosingPairsCandidates = this.context.cursorConfig.autoClosingPairsClose2.get(closeChar); if (!autoClosingPairsCandidates || autoClosingPairsCandidates.length !== 1) { return null; } @@ -620,7 +549,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return indices; } - public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { + public executeEdits(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { let autoClosingIndices: [number, number][] | null = null; if (source === 'snippet') { autoClosingIndices = this._findAutoClosingPairs(edits); @@ -655,146 +584,117 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { }); if (selections) { this._isHandling = false; - this.setSelections(source, selections); + this.setSelections(eventsCollector, source, selections); } if (autoClosedCharactersRanges.length > 0) { this._pushAutoClosedAction(autoClosedCharactersRanges, autoClosedEnclosingRanges); } } - public trigger(source: string | null | undefined, handlerId: string, payload: any): void { - const H = editorCommon.Handler; - - if (handlerId === H.CompositionStart) { - this._isDoingComposition = true; - this._selectionsWhenCompositionStarted = this.getSelections().slice(0); - return; - } - - if (handlerId === H.CompositionEnd) { - this._isDoingComposition = false; - } - - if (this._configuration.options.get(EditorOption.readOnly)) { - // All the remaining handlers will try to edit the model, - // but we cannot edit when read only... - this._onDidAttemptReadOnlyEdit.fire(undefined); + private _executeEdit(callback: () => void, eventsCollector: ViewModelEventsCollector, source: string | null | undefined, cursorChangeReason: CursorChangeReason = CursorChangeReason.NotSet): void { + if (this.context.cursorConfig.readOnly) { + // we cannot edit when read only... return; } const oldState = new CursorModelState(this._model, this); - let cursorChangeReason = CursorChangeReason.NotSet; - this._cursors.stopTrackingSelections(); - - // ensure valid state on all cursors - this._cursors.ensureValidState(); - this._isHandling = true; try { - switch (handlerId) { - case H.Type: - this._type(source, payload.text); - break; - - case H.ReplacePreviousChar: - this._replacePreviousChar(payload.text, payload.replaceCharCnt); - break; - - case H.Paste: - cursorChangeReason = CursorChangeReason.Paste; - this._paste(payload.text, payload.pasteOnNewLine, payload.multicursorText || []); - break; - - case H.Cut: - this._cut(); - break; - - case H.ExecuteCommand: - this._externalExecuteCommand(payload); - break; - - case H.ExecuteCommands: - this._externalExecuteCommands(payload); - break; - - case H.CompositionEnd: - this._interpretCompositionEnd(source); - break; - } + this._cursors.ensureValidState(); + callback(); } catch (err) { onUnexpectedError(err); } this._isHandling = false; - this._cursors.startTrackingSelections(); - this._validateAutoClosedActions(); - - if (this._emitStateChangedIfNecessary(source, cursorChangeReason, oldState)) { - this._revealRange(source, RevealTarget.Primary, viewEvents.VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); + if (this._emitStateChangedIfNecessary(eventsCollector, source, cursorChangeReason, oldState, false)) { + this._revealPrimaryCursor(eventsCollector, source, VerticalRevealType.Simple, true, editorCommon.ScrollType.Smooth); } } - private _interpretCompositionEnd(source: string | null | undefined) { - if (!this._isDoingComposition && source === 'keyboard') { - // composition finishes, let's check if we need to auto complete if necessary. - const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); - this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this._selectionsWhenCompositionStarted, this.getSelections(), autoClosedCharacters)); - this._selectionsWhenCompositionStarted = null; - } + public setIsDoingComposition(isDoingComposition: boolean): void { + this._isDoingComposition = isDoingComposition; } - private _type(source: string | null | undefined, text: string): void { - if (source === 'keyboard') { - // If this event is coming straight from the keyboard, look for electric characters and enter + public startComposition(eventsCollector: ViewModelEventsCollector): void { + this._selectionsWhenCompositionStarted = this.getSelections().slice(0); + } - const len = text.length; - let offset = 0; - while (offset < len) { - const charLength = strings.nextCharLength(text, offset); - const chr = text.substr(offset, charLength); - - // Here we must interpret each typed character individually + public endComposition(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void { + this._executeEdit(() => { + if (source === 'keyboard') { + // composition finishes, let's check if we need to auto complete if necessary. const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); - this._executeEditOperation(TypeOperations.typeWithInterceptors(this._isDoingComposition, this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), autoClosedCharacters, chr)); - - offset += charLength; + this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this._selectionsWhenCompositionStarted, this.getSelections(), autoClosedCharacters)); + this._selectionsWhenCompositionStarted = null; } - - } else { - this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), text)); - } + }, eventsCollector, source); } - private _replacePreviousChar(text: string, replaceCharCnt: number): void { - this._executeEditOperation(TypeOperations.replacePreviousChar(this._prevEditOperationType, this.context.config, this.context.model, this.getSelections(), text, replaceCharCnt)); + public type(eventsCollector: ViewModelEventsCollector, text: string, source?: string | null | undefined): void { + this._executeEdit(() => { + if (source === 'keyboard') { + // If this event is coming straight from the keyboard, look for electric characters and enter + + const len = text.length; + let offset = 0; + while (offset < len) { + const charLength = strings.nextCharLength(text, offset); + const chr = text.substr(offset, charLength); + + // Here we must interpret each typed character individually + const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); + this._executeEditOperation(TypeOperations.typeWithInterceptors(this._isDoingComposition, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), autoClosedCharacters, chr)); + + offset += charLength; + } + + } else { + this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text)); + } + }, eventsCollector, source); } - private _paste(text: string, pasteOnNewLine: boolean, multicursorText: string[]): void { - this._executeEditOperation(TypeOperations.paste(this.context.config, this.context.model, this.getSelections(), text, pasteOnNewLine, multicursorText)); + public replacePreviousChar(eventsCollector: ViewModelEventsCollector, text: string, replaceCharCnt: number, source?: string | null | undefined): void { + this._executeEdit(() => { + this._executeEditOperation(TypeOperations.replacePreviousChar(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replaceCharCnt)); + }, eventsCollector, source); } - private _cut(): void { - this._executeEditOperation(DeleteOperations.cut(this.context.config, this.context.model, this.getSelections())); + public paste(eventsCollector: ViewModelEventsCollector, text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void { + this._executeEdit(() => { + this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || [])); + }, eventsCollector, source, CursorChangeReason.Paste); } - private _externalExecuteCommand(command: editorCommon.ICommand): void { - this._cursors.killSecondaryCursors(); - - this._executeEditOperation(new EditOperationResult(EditOperationType.Other, [command], { - shouldPushStackElementBefore: false, - shouldPushStackElementAfter: false - })); + public cut(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void { + this._executeEdit(() => { + this._executeEditOperation(DeleteOperations.cut(this.context.cursorConfig, this._model, this.getSelections())); + }, eventsCollector, source); } - private _externalExecuteCommands(commands: editorCommon.ICommand[]): void { - this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, { - shouldPushStackElementBefore: false, - shouldPushStackElementAfter: false - })); + public executeCommand(eventsCollector: ViewModelEventsCollector, command: editorCommon.ICommand, source?: string | null | undefined): void { + this._executeEdit(() => { + this._cursors.killSecondaryCursors(); + + this._executeEditOperation(new EditOperationResult(EditOperationType.Other, [command], { + shouldPushStackElementBefore: false, + shouldPushStackElementAfter: false + })); + }, eventsCollector, source); + } + + public executeCommands(eventsCollector: ViewModelEventsCollector, commands: editorCommon.ICommand[], source?: string | null | undefined): void { + this._executeEdit(() => { + this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, { + shouldPushStackElementBefore: false, + shouldPushStackElementAfter: false + })); + }, eventsCollector, source); } } diff --git a/src/vs/editor/common/controller/cursorCollection.ts b/src/vs/editor/common/controller/cursorCollection.ts index 63cc38e4862..746039533ad 100644 --- a/src/vs/editor/common/controller/cursorCollection.ts +++ b/src/vs/editor/common/controller/cursorCollection.ts @@ -82,6 +82,28 @@ export class CursorCollection { return result; } + public getTopMostViewPosition(): Position { + let result = this.primaryCursor.viewState.position; + for (let i = 0, len = this.secondaryCursors.length; i < len; i++) { + const viewPosition = this.secondaryCursors[i].viewState.position; + if (viewPosition.isBefore(result)) { + result = viewPosition; + } + } + return result; + } + + public getBottomMostViewPosition(): Position { + let result = this.primaryCursor.viewState.position; + for (let i = 0, len = this.secondaryCursors.length; i < len; i++) { + const viewPosition = this.secondaryCursors[i].viewState.position; + if (result.isBeforeOrEqual(viewPosition)) { + result = viewPosition; + } + } + return result; + } + public getSelections(): Selection[] { let result: Selection[] = []; result[0] = this.primaryCursor.modelState.selection; @@ -204,7 +226,7 @@ export class CursorCollection { const currentSelection = current.selection; const nextSelection = next.selection; - if (!this.context.config.multiCursorMergeOverlapping) { + if (!this.context.cursorConfig.multiCursorMergeOverlapping) { continue; } diff --git a/src/vs/editor/common/controller/cursorCommon.ts b/src/vs/editor/common/controller/cursorCommon.ts index 4676e2d03e7..e49c7dea933 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -7,17 +7,15 @@ import { CharCode } from 'vs/base/common/charCode'; import { onUnexpectedError } from 'vs/base/common/errors'; import * as strings from 'vs/base/common/strings'; import { EditorAutoClosingStrategy, EditorAutoSurroundStrategy, ConfigurationChangedEvent, EditorAutoClosingOvertypeStrategy, EditorOption, EditorAutoIndentStrategy } from 'vs/editor/common/config/editorOptions'; -import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ISelection, Selection } from 'vs/editor/common/core/selection'; -import { ICommand, IConfiguration, ScrollType } from 'vs/editor/common/editorCommon'; +import { ICommand, IConfiguration } from 'vs/editor/common/editorCommon'; import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { LanguageIdentifier } from 'vs/editor/common/modes'; import { IAutoClosingPair, StandardAutoClosingPairConditional } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; -import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; import { Constants } from 'vs/base/common/uint'; @@ -46,23 +44,6 @@ export const enum EditOperationType { DeletingRight = 3 } -export interface ICursors { - readonly context: CursorContext; - getPrimaryCursor(): CursorState; - getLastAddedCursorIndex(): number; - getAll(): CursorState[]; - - getColumnSelectData(): IColumnSelectData; - setColumnSelectData(columnSelectData: IColumnSelectData): void; - - setStates(source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): void; - reveal(source: string | null | undefined, horizontal: boolean, target: RevealTarget, scrollType: ScrollType): void; - revealRange(source: string | null | undefined, revealHorizontal: boolean, viewRange: Range, verticalType: VerticalRevealType, scrollType: ScrollType): void; - - getPrevEditOperationType(): EditOperationType; - setPrevEditOperationType(type: EditOperationType): void; -} - export interface CharacterMap { [char: string]: string; } @@ -355,19 +336,13 @@ export class CursorContext { _cursorContextBrand: void; public readonly model: ITextModel; - public readonly viewModel: ICursorSimpleModel; public readonly coordinatesConverter: ICoordinatesConverter; - public readonly config: CursorConfiguration; + public readonly cursorConfig: CursorConfiguration; - constructor(configuration: IConfiguration, model: ITextModel, viewModel: ICursorSimpleModel, coordinatesConverter: ICoordinatesConverter) { + constructor(model: ITextModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) { this.model = model; - this.viewModel = viewModel; this.coordinatesConverter = coordinatesConverter; - this.config = new CursorConfiguration( - this.model.getLanguageIdentifier(), - this.model.getOptions(), - configuration - ); + this.cursorConfig = cursorConfig; } } diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index f1422f2df5d..0dd8834348f 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as types from 'vs/base/common/types'; -import { CursorContext, CursorState, ICursorSimpleModel, PartialCursorState, SingleCursorState } from 'vs/editor/common/controller/cursorCommon'; +import { CursorState, ICursorSimpleModel, PartialCursorState, SingleCursorState } from 'vs/editor/common/controller/cursorCommon'; import { MoveOperations } from 'vs/editor/common/controller/cursorMoveOperations'; import { WordOperations } from 'vs/editor/common/controller/cursorWordOperations'; import { IPosition, Position } from 'vs/editor/common/core/position'; @@ -14,122 +14,122 @@ import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; export class CursorMoveCommands { - public static addCursorDown(context: CursorContext, cursors: CursorState[], useLogicalLine: boolean): PartialCursorState[] { + public static addCursorDown(viewModel: IViewModel, cursors: CursorState[], useLogicalLine: boolean): PartialCursorState[] { let result: PartialCursorState[] = [], resultLen = 0; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; result[resultLen++] = new CursorState(cursor.modelState, cursor.viewState); if (useLogicalLine) { - result[resultLen++] = CursorState.fromModelState(MoveOperations.translateDown(context.config, context.model, cursor.modelState)); + result[resultLen++] = CursorState.fromModelState(MoveOperations.translateDown(viewModel.cursorConfig, viewModel.model, cursor.modelState)); } else { - result[resultLen++] = CursorState.fromViewState(MoveOperations.translateDown(context.config, context.viewModel, cursor.viewState)); + result[resultLen++] = CursorState.fromViewState(MoveOperations.translateDown(viewModel.cursorConfig, viewModel, cursor.viewState)); } } return result; } - public static addCursorUp(context: CursorContext, cursors: CursorState[], useLogicalLine: boolean): PartialCursorState[] { + public static addCursorUp(viewModel: IViewModel, cursors: CursorState[], useLogicalLine: boolean): PartialCursorState[] { let result: PartialCursorState[] = [], resultLen = 0; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; result[resultLen++] = new CursorState(cursor.modelState, cursor.viewState); if (useLogicalLine) { - result[resultLen++] = CursorState.fromModelState(MoveOperations.translateUp(context.config, context.model, cursor.modelState)); + result[resultLen++] = CursorState.fromModelState(MoveOperations.translateUp(viewModel.cursorConfig, viewModel.model, cursor.modelState)); } else { - result[resultLen++] = CursorState.fromViewState(MoveOperations.translateUp(context.config, context.viewModel, cursor.viewState)); + result[resultLen++] = CursorState.fromViewState(MoveOperations.translateUp(viewModel.cursorConfig, viewModel, cursor.viewState)); } } return result; } - public static moveToBeginningOfLine(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + public static moveToBeginningOfLine(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = this._moveToLineStart(context, cursor, inSelectionMode); + result[i] = this._moveToLineStart(viewModel, cursor, inSelectionMode); } return result; } - private static _moveToLineStart(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { + private static _moveToLineStart(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { const currentViewStateColumn = cursor.viewState.position.column; const currentModelStateColumn = cursor.modelState.position.column; const isFirstLineOfWrappedLine = currentViewStateColumn === currentModelStateColumn; const currentViewStatelineNumber = cursor.viewState.position.lineNumber; - const firstNonBlankColumn = context.viewModel.getLineFirstNonWhitespaceColumn(currentViewStatelineNumber); + const firstNonBlankColumn = viewModel.getLineFirstNonWhitespaceColumn(currentViewStatelineNumber); const isBeginningOfViewLine = currentViewStateColumn === firstNonBlankColumn; if (!isFirstLineOfWrappedLine && !isBeginningOfViewLine) { - return this._moveToLineStartByView(context, cursor, inSelectionMode); + return this._moveToLineStartByView(viewModel, cursor, inSelectionMode); } else { - return this._moveToLineStartByModel(context, cursor, inSelectionMode); + return this._moveToLineStartByModel(viewModel, cursor, inSelectionMode); } } - private static _moveToLineStartByView(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { + private static _moveToLineStartByView(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { return CursorState.fromViewState( - MoveOperations.moveToBeginningOfLine(context.config, context.viewModel, cursor.viewState, inSelectionMode) + MoveOperations.moveToBeginningOfLine(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode) ); } - private static _moveToLineStartByModel(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { + private static _moveToLineStartByModel(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { return CursorState.fromModelState( - MoveOperations.moveToBeginningOfLine(context.config, context.model, cursor.modelState, inSelectionMode) + MoveOperations.moveToBeginningOfLine(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode) ); } - public static moveToEndOfLine(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + public static moveToEndOfLine(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = this._moveToLineEnd(context, cursor, inSelectionMode); + result[i] = this._moveToLineEnd(viewModel, cursor, inSelectionMode); } return result; } - private static _moveToLineEnd(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { + private static _moveToLineEnd(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { const viewStatePosition = cursor.viewState.position; - const viewModelMaxColumn = context.viewModel.getLineMaxColumn(viewStatePosition.lineNumber); + const viewModelMaxColumn = viewModel.getLineMaxColumn(viewStatePosition.lineNumber); const isEndOfViewLine = viewStatePosition.column === viewModelMaxColumn; const modelStatePosition = cursor.modelState.position; - const modelMaxColumn = context.model.getLineMaxColumn(modelStatePosition.lineNumber); + const modelMaxColumn = viewModel.model.getLineMaxColumn(modelStatePosition.lineNumber); const isEndLineOfWrappedLine = viewModelMaxColumn - viewStatePosition.column === modelMaxColumn - modelStatePosition.column; if (isEndOfViewLine || isEndLineOfWrappedLine) { - return this._moveToLineEndByModel(context, cursor, inSelectionMode); + return this._moveToLineEndByModel(viewModel, cursor, inSelectionMode); } else { - return this._moveToLineEndByView(context, cursor, inSelectionMode); + return this._moveToLineEndByView(viewModel, cursor, inSelectionMode); } } - private static _moveToLineEndByView(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { + private static _moveToLineEndByView(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { return CursorState.fromViewState( - MoveOperations.moveToEndOfLine(context.config, context.viewModel, cursor.viewState, inSelectionMode) + MoveOperations.moveToEndOfLine(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode) ); } - private static _moveToLineEndByModel(context: CursorContext, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { + private static _moveToLineEndByModel(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean): PartialCursorState { return CursorState.fromModelState( - MoveOperations.moveToEndOfLine(context.config, context.model, cursor.modelState, inSelectionMode) + MoveOperations.moveToEndOfLine(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode) ); } - public static expandLineSelection(context: CursorContext, cursors: CursorState[]): PartialCursorState[] { + public static expandLineSelection(viewModel: IViewModel, cursors: CursorState[]): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const startLineNumber = cursor.modelState.selection.startLineNumber; - const lineCount = context.model.getLineCount(); + const lineCount = viewModel.model.getLineCount(); let endLineNumber = cursor.modelState.selection.endLineNumber; let endColumn: number; if (endLineNumber === lineCount) { - endColumn = context.model.getLineMaxColumn(lineCount); + endColumn = viewModel.model.getLineMaxColumn(lineCount); } else { endLineNumber++; endColumn = 1; @@ -143,27 +143,27 @@ export class CursorMoveCommands { return result; } - public static moveToBeginningOfBuffer(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + public static moveToBeginningOfBuffer(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = CursorState.fromModelState(MoveOperations.moveToBeginningOfBuffer(context.config, context.model, cursor.modelState, inSelectionMode)); + result[i] = CursorState.fromModelState(MoveOperations.moveToBeginningOfBuffer(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode)); } return result; } - public static moveToEndOfBuffer(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + public static moveToEndOfBuffer(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = CursorState.fromModelState(MoveOperations.moveToEndOfBuffer(context.config, context.model, cursor.modelState, inSelectionMode)); + result[i] = CursorState.fromModelState(MoveOperations.moveToEndOfBuffer(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode)); } return result; } - public static selectAll(context: CursorContext, cursor: CursorState): PartialCursorState { - const lineCount = context.model.getLineCount(); - const maxColumn = context.model.getLineMaxColumn(lineCount); + public static selectAll(viewModel: IViewModel, cursor: CursorState): PartialCursorState { + const lineCount = viewModel.model.getLineCount(); + const maxColumn = viewModel.model.getLineMaxColumn(lineCount); return CursorState.fromModelState(new SingleCursorState( new Range(1, 1, 1, 1), 0, @@ -171,23 +171,23 @@ export class CursorMoveCommands { )); } - public static line(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): PartialCursorState { - const position = context.model.validatePosition(_position); + public static line(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): PartialCursorState { + const position = viewModel.model.validatePosition(_position); const viewPosition = ( _viewPosition - ? context.coordinatesConverter.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position) - : context.coordinatesConverter.convertModelPositionToViewPosition(position) + ? viewModel.coordinatesConverter.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position) + : viewModel.coordinatesConverter.convertModelPositionToViewPosition(position) ); if (!inSelectionMode || !cursor.modelState.hasSelection()) { // Entering line selection for the first time - const lineCount = context.model.getLineCount(); + const lineCount = viewModel.model.getLineCount(); let selectToLineNumber = position.lineNumber + 1; let selectToColumn = 1; if (selectToLineNumber > lineCount) { selectToLineNumber = lineCount; - selectToColumn = context.model.getLineMaxColumn(selectToLineNumber); + selectToColumn = viewModel.model.getLineMaxColumn(selectToLineNumber); } return CursorState.fromModelState(new SingleCursorState( @@ -207,13 +207,13 @@ export class CursorMoveCommands { } else if (position.lineNumber > enteringLineNumber) { - const lineCount = context.viewModel.getLineCount(); + const lineCount = viewModel.getLineCount(); let selectToViewLineNumber = viewPosition.lineNumber + 1; let selectToViewColumn = 1; if (selectToViewLineNumber > lineCount) { selectToViewLineNumber = lineCount; - selectToViewColumn = context.viewModel.getLineMaxColumn(selectToViewLineNumber); + selectToViewColumn = viewModel.getLineMaxColumn(selectToViewLineNumber); } return CursorState.fromViewState(cursor.viewState.move( @@ -230,12 +230,12 @@ export class CursorMoveCommands { } } - public static word(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, _position: IPosition): PartialCursorState { - const position = context.model.validatePosition(_position); - return CursorState.fromModelState(WordOperations.word(context.config, context.model, cursor.modelState, inSelectionMode, position)); + public static word(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition): PartialCursorState { + const position = viewModel.model.validatePosition(_position); + return CursorState.fromModelState(WordOperations.word(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode, position)); } - public static cancelSelection(context: CursorContext, cursor: CursorState): PartialCursorState { + public static cancelSelection(viewModel: IViewModel, cursor: CursorState): PartialCursorState { if (!cursor.modelState.hasSelection()) { return new CursorState(cursor.modelState, cursor.viewState); } @@ -249,107 +249,107 @@ export class CursorMoveCommands { )); } - public static moveTo(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): PartialCursorState { - const position = context.model.validatePosition(_position); + public static moveTo(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, _position: IPosition, _viewPosition: IPosition): PartialCursorState { + const position = viewModel.model.validatePosition(_position); const viewPosition = ( _viewPosition - ? context.coordinatesConverter.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position) - : context.coordinatesConverter.convertModelPositionToViewPosition(position) + ? viewModel.coordinatesConverter.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position) + : viewModel.coordinatesConverter.convertModelPositionToViewPosition(position) ); return CursorState.fromViewState(cursor.viewState.move(inSelectionMode, viewPosition.lineNumber, viewPosition.column, 0)); } - public static simpleMove(context: CursorContext, cursors: CursorState[], direction: CursorMove.SimpleMoveDirection, inSelectionMode: boolean, value: number, unit: CursorMove.Unit): PartialCursorState[] | null { + public static simpleMove(viewModel: IViewModel, cursors: CursorState[], direction: CursorMove.SimpleMoveDirection, inSelectionMode: boolean, value: number, unit: CursorMove.Unit): PartialCursorState[] | null { switch (direction) { case CursorMove.Direction.Left: { if (unit === CursorMove.Unit.HalfLine) { // Move left by half the current line length - return this._moveHalfLineLeft(context, cursors, inSelectionMode); + return this._moveHalfLineLeft(viewModel, cursors, inSelectionMode); } else { // Move left by `moveParams.value` columns - return this._moveLeft(context, cursors, inSelectionMode, value); + return this._moveLeft(viewModel, cursors, inSelectionMode, value); } } case CursorMove.Direction.Right: { if (unit === CursorMove.Unit.HalfLine) { // Move right by half the current line length - return this._moveHalfLineRight(context, cursors, inSelectionMode); + return this._moveHalfLineRight(viewModel, cursors, inSelectionMode); } else { // Move right by `moveParams.value` columns - return this._moveRight(context, cursors, inSelectionMode, value); + return this._moveRight(viewModel, cursors, inSelectionMode, value); } } case CursorMove.Direction.Up: { if (unit === CursorMove.Unit.WrappedLine) { // Move up by view lines - return this._moveUpByViewLines(context, cursors, inSelectionMode, value); + return this._moveUpByViewLines(viewModel, cursors, inSelectionMode, value); } else { // Move up by model lines - return this._moveUpByModelLines(context, cursors, inSelectionMode, value); + return this._moveUpByModelLines(viewModel, cursors, inSelectionMode, value); } } case CursorMove.Direction.Down: { if (unit === CursorMove.Unit.WrappedLine) { // Move down by view lines - return this._moveDownByViewLines(context, cursors, inSelectionMode, value); + return this._moveDownByViewLines(viewModel, cursors, inSelectionMode, value); } else { // Move down by model lines - return this._moveDownByModelLines(context, cursors, inSelectionMode, value); + return this._moveDownByModelLines(viewModel, cursors, inSelectionMode, value); } } case CursorMove.Direction.WrappedLineStart: { // Move to the beginning of the current view line - return this._moveToViewMinColumn(context, cursors, inSelectionMode); + return this._moveToViewMinColumn(viewModel, cursors, inSelectionMode); } case CursorMove.Direction.WrappedLineFirstNonWhitespaceCharacter: { // Move to the first non-whitespace column of the current view line - return this._moveToViewFirstNonWhitespaceColumn(context, cursors, inSelectionMode); + return this._moveToViewFirstNonWhitespaceColumn(viewModel, cursors, inSelectionMode); } case CursorMove.Direction.WrappedLineColumnCenter: { // Move to the "center" of the current view line - return this._moveToViewCenterColumn(context, cursors, inSelectionMode); + return this._moveToViewCenterColumn(viewModel, cursors, inSelectionMode); } case CursorMove.Direction.WrappedLineEnd: { // Move to the end of the current view line - return this._moveToViewMaxColumn(context, cursors, inSelectionMode); + return this._moveToViewMaxColumn(viewModel, cursors, inSelectionMode); } case CursorMove.Direction.WrappedLineLastNonWhitespaceCharacter: { // Move to the last non-whitespace column of the current view line - return this._moveToViewLastNonWhitespaceColumn(context, cursors, inSelectionMode); + return this._moveToViewLastNonWhitespaceColumn(viewModel, cursors, inSelectionMode); } } return null; } - public static viewportMove(viewModel: IViewModel, context: CursorContext, cursors: CursorState[], direction: CursorMove.ViewportDirection, inSelectionMode: boolean, value: number): PartialCursorState[] | null { + public static viewportMove(viewModel: IViewModel, cursors: CursorState[], direction: CursorMove.ViewportDirection, inSelectionMode: boolean, value: number): PartialCursorState[] | null { const visibleViewRange = viewModel.getCompletelyVisibleViewRange(); - const visibleModelRange = context.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); + const visibleModelRange = viewModel.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); switch (direction) { case CursorMove.Direction.ViewPortTop: { // Move to the nth line start in the viewport (from the top) - const modelLineNumber = this._firstLineNumberInRange(context.model, visibleModelRange, value); - const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber); - return [this._moveToModelPosition(context, cursors[0], inSelectionMode, modelLineNumber, modelColumn)]; + const modelLineNumber = this._firstLineNumberInRange(viewModel.model, visibleModelRange, value); + const modelColumn = viewModel.model.getLineFirstNonWhitespaceColumn(modelLineNumber); + return [this._moveToModelPosition(viewModel, cursors[0], inSelectionMode, modelLineNumber, modelColumn)]; } case CursorMove.Direction.ViewPortBottom: { // Move to the nth line start in the viewport (from the bottom) - const modelLineNumber = this._lastLineNumberInRange(context.model, visibleModelRange, value); - const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber); - return [this._moveToModelPosition(context, cursors[0], inSelectionMode, modelLineNumber, modelColumn)]; + const modelLineNumber = this._lastLineNumberInRange(viewModel.model, visibleModelRange, value); + const modelColumn = viewModel.model.getLineFirstNonWhitespaceColumn(modelLineNumber); + return [this._moveToModelPosition(viewModel, cursors[0], inSelectionMode, modelLineNumber, modelColumn)]; } case CursorMove.Direction.ViewPortCenter: { // Move to the line start in the viewport center const modelLineNumber = Math.round((visibleModelRange.startLineNumber + visibleModelRange.endLineNumber) / 2); - const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber); - return [this._moveToModelPosition(context, cursors[0], inSelectionMode, modelLineNumber, modelColumn)]; + const modelColumn = viewModel.model.getLineFirstNonWhitespaceColumn(modelLineNumber); + return [this._moveToModelPosition(viewModel, cursors[0], inSelectionMode, modelLineNumber, modelColumn)]; } case CursorMove.Direction.ViewPortIfOutside: { // Move to a position inside the viewport let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = this.findPositionInViewportIfOutside(context, cursor, visibleViewRange, inSelectionMode); + result[i] = this.findPositionInViewportIfOutside(viewModel, cursor, visibleViewRange, inSelectionMode); } return result; } @@ -358,7 +358,7 @@ export class CursorMoveCommands { return null; } - public static findPositionInViewportIfOutside(context: CursorContext, cursor: CursorState, visibleViewRange: Range, inSelectionMode: boolean): PartialCursorState { + public static findPositionInViewportIfOutside(viewModel: IViewModel, cursor: CursorState, visibleViewRange: Range, inSelectionMode: boolean): PartialCursorState { let viewLineNumber = cursor.viewState.position.lineNumber; if (visibleViewRange.startLineNumber <= viewLineNumber && viewLineNumber <= visibleViewRange.endLineNumber - 1) { @@ -372,8 +372,8 @@ export class CursorMoveCommands { if (viewLineNumber < visibleViewRange.startLineNumber) { viewLineNumber = visibleViewRange.startLineNumber; } - const viewColumn = context.viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber); - return this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn); + const viewColumn = viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber); + return this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); } } @@ -403,19 +403,20 @@ export class CursorMoveCommands { return Math.max(startLineNumber, range.endLineNumber - count + 1); } - private static _moveLeft(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { + private static _moveLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { + const hasMultipleCursors = (cursors.length > 1); let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; + const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); + let newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - let newViewState = MoveOperations.moveLeft(context.config, context.viewModel, cursor.viewState, inSelectionMode, noOfColumns); - - if (!cursor.viewState.hasSelection() && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { + if (skipWrappingPointStop && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { // moved over to the previous view line - const newViewModelPosition = context.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); + const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) { // stayed on the same model line => pass wrapping point where 2 view positions map to a single model position - newViewState = MoveOperations.moveLeft(context.config, context.viewModel, newViewState, inSelectionMode, 1); + newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1); } } @@ -424,29 +425,31 @@ export class CursorMoveCommands { return result; } - private static _moveHalfLineLeft(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveHalfLineLeft(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const halfLine = Math.round(context.viewModel.getLineContent(viewLineNumber).length / 2); - result[i] = CursorState.fromViewState(MoveOperations.moveLeft(context.config, context.viewModel, cursor.viewState, inSelectionMode, halfLine)); + const halfLine = Math.round(viewModel.getLineContent(viewLineNumber).length / 2); + result[i] = CursorState.fromViewState(MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, halfLine)); } return result; } - private static _moveRight(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { + private static _moveRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, noOfColumns: number): PartialCursorState[] { + const hasMultipleCursors = (cursors.length > 1); let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - let newViewState = MoveOperations.moveRight(context.config, context.viewModel, cursor.viewState, inSelectionMode, noOfColumns); + const skipWrappingPointStop = hasMultipleCursors || !cursor.viewState.hasSelection(); + let newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); - if (!cursor.viewState.hasSelection() && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { + if (skipWrappingPointStop && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { // moved over to the next view line - const newViewModelPosition = context.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); + const newViewModelPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(newViewState.position); if (newViewModelPosition.lineNumber === cursor.modelState.position.lineNumber) { // stayed on the same model line => pass wrapping point where 2 view positions map to a single model position - newViewState = MoveOperations.moveRight(context.config, context.viewModel, newViewState, inSelectionMode, 1); + newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, newViewState, inSelectionMode, 1); } } @@ -455,112 +458,112 @@ export class CursorMoveCommands { return result; } - private static _moveHalfLineRight(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveHalfLineRight(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const halfLine = Math.round(context.viewModel.getLineContent(viewLineNumber).length / 2); - result[i] = CursorState.fromViewState(MoveOperations.moveRight(context.config, context.viewModel, cursor.viewState, inSelectionMode, halfLine)); + const halfLine = Math.round(viewModel.getLineContent(viewLineNumber).length / 2); + result[i] = CursorState.fromViewState(MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, halfLine)); } return result; } - private static _moveDownByViewLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { + private static _moveDownByViewLines(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = CursorState.fromViewState(MoveOperations.moveDown(context.config, context.viewModel, cursor.viewState, inSelectionMode, linesCount)); + result[i] = CursorState.fromViewState(MoveOperations.moveDown(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, linesCount)); } return result; } - private static _moveDownByModelLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { + private static _moveDownByModelLines(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = CursorState.fromModelState(MoveOperations.moveDown(context.config, context.model, cursor.modelState, inSelectionMode, linesCount)); + result[i] = CursorState.fromModelState(MoveOperations.moveDown(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode, linesCount)); } return result; } - private static _moveUpByViewLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { + private static _moveUpByViewLines(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = CursorState.fromViewState(MoveOperations.moveUp(context.config, context.viewModel, cursor.viewState, inSelectionMode, linesCount)); + result[i] = CursorState.fromViewState(MoveOperations.moveUp(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, linesCount)); } return result; } - private static _moveUpByModelLines(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { + private static _moveUpByModelLines(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean, linesCount: number): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - result[i] = CursorState.fromModelState(MoveOperations.moveUp(context.config, context.model, cursor.modelState, inSelectionMode, linesCount)); + result[i] = CursorState.fromModelState(MoveOperations.moveUp(viewModel.cursorConfig, viewModel.model, cursor.modelState, inSelectionMode, linesCount)); } return result; } - private static _moveToViewPosition(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, toViewLineNumber: number, toViewColumn: number): PartialCursorState { + private static _moveToViewPosition(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, toViewLineNumber: number, toViewColumn: number): PartialCursorState { return CursorState.fromViewState(cursor.viewState.move(inSelectionMode, toViewLineNumber, toViewColumn, 0)); } - private static _moveToModelPosition(context: CursorContext, cursor: CursorState, inSelectionMode: boolean, toModelLineNumber: number, toModelColumn: number): PartialCursorState { + private static _moveToModelPosition(viewModel: IViewModel, cursor: CursorState, inSelectionMode: boolean, toModelLineNumber: number, toModelColumn: number): PartialCursorState { return CursorState.fromModelState(cursor.modelState.move(inSelectionMode, toModelLineNumber, toModelColumn, 0)); } - private static _moveToViewMinColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveToViewMinColumn(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const viewColumn = context.viewModel.getLineMinColumn(viewLineNumber); - result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn); + const viewColumn = viewModel.getLineMinColumn(viewLineNumber); + result[i] = this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); } return result; } - private static _moveToViewFirstNonWhitespaceColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveToViewFirstNonWhitespaceColumn(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const viewColumn = context.viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber); - result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn); + const viewColumn = viewModel.getLineFirstNonWhitespaceColumn(viewLineNumber); + result[i] = this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); } return result; } - private static _moveToViewCenterColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveToViewCenterColumn(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const viewColumn = Math.round((context.viewModel.getLineMaxColumn(viewLineNumber) + context.viewModel.getLineMinColumn(viewLineNumber)) / 2); - result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn); + const viewColumn = Math.round((viewModel.getLineMaxColumn(viewLineNumber) + viewModel.getLineMinColumn(viewLineNumber)) / 2); + result[i] = this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); } return result; } - private static _moveToViewMaxColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveToViewMaxColumn(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const viewColumn = context.viewModel.getLineMaxColumn(viewLineNumber); - result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn); + const viewColumn = viewModel.getLineMaxColumn(viewLineNumber); + result[i] = this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); } return result; } - private static _moveToViewLastNonWhitespaceColumn(context: CursorContext, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { + private static _moveToViewLastNonWhitespaceColumn(viewModel: IViewModel, cursors: CursorState[], inSelectionMode: boolean): PartialCursorState[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; const viewLineNumber = cursor.viewState.position.lineNumber; - const viewColumn = context.viewModel.getLineLastNonWhitespaceColumn(viewLineNumber); - result[i] = this._moveToViewPosition(context, cursor, inSelectionMode, viewLineNumber, viewColumn); + const viewColumn = viewModel.getLineLastNonWhitespaceColumn(viewLineNumber); + result[i] = this._moveToViewPosition(viewModel, cursor, inSelectionMode, viewLineNumber, viewColumn); } return result; } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 6a24b832b9a..94c4a3c48df 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -150,6 +150,7 @@ export interface ILineChange extends IChange { * @internal */ export interface IConfiguration extends IDisposable { + onDidChangeFast(listener: (e: ConfigurationChangedEvent) => void): IDisposable; onDidChange(listener: (e: ConfigurationChangedEvent) => void): IDisposable; readonly options: IComputedEditorOptions; @@ -688,14 +689,36 @@ export const EditorType = { * Built-in commands. * @internal */ -export const Handler = { - ExecuteCommand: 'executeCommand', - ExecuteCommands: 'executeCommands', +export const enum Handler { + CompositionStart = 'compositionStart', + CompositionEnd = 'compositionEnd', + Type = 'type', + ReplacePreviousChar = 'replacePreviousChar', + Paste = 'paste', + Cut = 'cut', +} - Type: 'type', - ReplacePreviousChar: 'replacePreviousChar', - CompositionStart: 'compositionStart', - CompositionEnd: 'compositionEnd', - Paste: 'paste', - Cut: 'cut', -}; +/** + * @internal + */ +export interface TypePayload { + text: string; +} + +/** + * @internal + */ +export interface ReplacePreviousCharPayload { + text: string; + replaceCharCnt: number; +} + +/** + * @internal + */ +export interface PastePayload { + text: string; + pasteOnNewLine: boolean; + multicursorText: string[] | null; + mode: string | null; +} diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index 471e2472b43..487e6d4f0fd 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -36,6 +36,8 @@ export namespace EditorContextKeys { export const canUndo = new RawContextKey('canUndo', false); export const canRedo = new RawContextKey('canRedo', false); + export const hoverVisible = new RawContextKey('editorHoverVisible', false); + /** * A context key that is set when an editor is part of a larger editor, like notebooks or * (future) a diff editor diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 24c38f88141..17801ba86e1 100644 --- a/src/vs/editor/common/model/editStack.ts +++ b/src/vs/editor/common/model/editStack.ts @@ -12,6 +12,7 @@ import { IUndoRedoService, IResourceUndoRedoElement, UndoRedoElementType, IWorks import { URI } from 'vs/base/common/uri'; import { TextChange, compressConsecutiveTextChanges } from 'vs/editor/common/model/textChange'; import * as buffer from 'vs/base/common/buffer'; +import { IDisposable } from 'vs/base/common/lifecycle'; function uriGetComparisonKey(resource: URI): string { return resource.toString(); @@ -138,6 +139,10 @@ class SingleModelEditStackData { } } +export interface IUndoRedoDelegate { + prepareUndoRedo(element: MultiModelEditStackElement): Promise | IDisposable | void; +} + export class SingleModelEditStackElement implements IResourceUndoRedoElement { public model: ITextModel | URI; @@ -163,6 +168,11 @@ export class SingleModelEditStackElement implements IResourceUndoRedoElement { this._data = SingleModelEditStackData.create(model, beforeCursorState); } + public matchesResource(resource: URI): boolean { + const uri = (URI.isUri(this.model) ? this.model : this.model.uri); + return (uri.toString() === resource.toString()); + } + public setModel(model: ITextModel | URI): void { this.model = model; } @@ -224,6 +234,8 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { private readonly _editStackElementsArr: SingleModelEditStackElement[]; private readonly _editStackElementsMap: Map; + private _delegate: IUndoRedoDelegate | null; + public get resources(): readonly URI[] { return this._editStackElementsArr.map(editStackElement => editStackElement.resource); } @@ -240,6 +252,32 @@ export class MultiModelEditStackElement implements IWorkspaceUndoRedoElement { const key = uriGetComparisonKey(editStackElement.resource); this._editStackElementsMap.set(key, editStackElement); } + this._delegate = null; + } + + public setDelegate(delegate: IUndoRedoDelegate): void { + this._delegate = delegate; + } + + public prepareUndoRedo(): Promise | IDisposable | void { + if (this._delegate) { + return this._delegate.prepareUndoRedo(this); + } + } + + public getMissingModels(): URI[] { + const result: URI[] = []; + for (const editStackElement of this._editStackElementsArr) { + if (URI.isUri(editStackElement.model)) { + result.push(editStackElement.model); + } + } + return result; + } + + public matchesResource(resource: URI): boolean { + const key = uriGetComparisonKey(resource); + return (this._editStackElementsMap.has(key)); } public setModel(model: ITextModel | URI): void { @@ -310,7 +348,7 @@ function getModelEOL(model: ITextModel): EndOfLineSequence { } } -function isKnownStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement { +export function isEditStackElement(element: IResourceUndoRedoElement | IWorkspaceUndoRedoElement | null): element is EditStackElement { if (!element) { return false; } @@ -329,7 +367,7 @@ export class EditStack { public pushStackElement(): void { const lastElement = this._undoRedoService.getLastElement(this._model.uri); - if (isKnownStackElement(lastElement)) { + if (isEditStackElement(lastElement)) { lastElement.close(); } } @@ -340,7 +378,7 @@ export class EditStack { private _getOrCreateEditStackElement(beforeCursorState: Selection[] | null): EditStackElement { const lastElement = this._undoRedoService.getLastElement(this._model.uri); - if (isKnownStackElement(lastElement) && lastElement.canAppend(this._model)) { + if (isEditStackElement(lastElement) && lastElement.canAppend(this._model)) { return lastElement; } const newElement = new SingleModelEditStackElement(this._model, beforeCursorState); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 77e0df59de7..89a77eed92e 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1411,7 +1411,7 @@ export interface RenameProvider { */ export interface AuthenticationSession { id: string; - getAccessToken(): Thenable; + accessToken: string; account: { displayName: string; id: string; @@ -1603,7 +1603,7 @@ export interface IWebviewPortMapping { export interface IWebviewOptions { readonly enableScripts?: boolean; readonly enableCommandUris?: boolean; - readonly localResourceRoots?: ReadonlyArray; + readonly localResourceRoots?: ReadonlyArray; readonly portMapping?: ReadonlyArray; } diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index 9ab05132790..1c1969d3a33 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -26,7 +26,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ILogService } from 'vs/platform/log/common/log'; import { IUndoRedoService, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; import { StringSHA1 } from 'vs/base/common/hash'; -import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement } from 'vs/editor/common/model/editStack'; +import { SingleModelEditStackElement, MultiModelEditStackElement, EditStackElement, isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { SemanticTokensProviderStyling, toMultilineTokens2 } from 'vs/editor/common/services/semanticTokensProviderStyling'; @@ -113,12 +113,12 @@ interface IRawConfig { const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF; -interface EditStackPastFutureElements { +export interface EditStackPastFutureElements { past: EditStackElement[]; future: EditStackElement[]; } -function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements { +export function isEditStackPastFutureElements(undoElements: IPastFutureElements): undoElements is EditStackPastFutureElements { return (isEditStackElements(undoElements.past) && isEditStackElements(undoElements.future)); } @@ -139,6 +139,7 @@ class DisposedModelInfo { constructor( public readonly uri: URI, public readonly time: number, + public readonly sharesUndoRedoStack: boolean, public readonly heapSize: number, public readonly sha1: string, public readonly versionId: number, @@ -352,7 +353,11 @@ export class ModelServiceImpl extends Disposable implements IModelService { if (this._disposedModelsHeapSize > maxModelsHeapSize) { // we must remove some old undo stack elements to free up some memory const disposedModels: DisposedModelInfo[] = []; - this._disposedModels.forEach(entry => disposedModels.push(entry)); + this._disposedModels.forEach(entry => { + if (!entry.sharesUndoRedoStack) { + disposedModels.push(entry); + } + }); disposedModels.sort((a, b) => a.time - b.time); while (disposedModels.length > 0 && this._disposedModelsHeapSize > maxModelsHeapSize) { const disposedModel = disposedModels.shift()!; @@ -369,16 +374,23 @@ export class ModelServiceImpl extends Disposable implements IModelService { if (resource && this._disposedModels.has(MODEL_ID(resource))) { const disposedModelData = this._removeDisposedModel(resource)!; const elements = this._undoRedoService.getElements(resource); - if (computeModelSha1(model) === disposedModelData.sha1 && isEditStackPastFutureElements(elements)) { + const sha1IsEqual = (computeModelSha1(model) === disposedModelData.sha1); + if (sha1IsEqual || disposedModelData.sharesUndoRedoStack) { for (const element of elements.past) { - element.setModel(model); + if (isEditStackElement(element) && element.matchesResource(resource)) { + element.setModel(model); + } } for (const element of elements.future) { - element.setModel(model); + if (isEditStackElement(element) && element.matchesResource(resource)) { + element.setModel(model); + } + } + this._undoRedoService.setElementsValidFlag(resource, true, (element) => (isEditStackElement(element) && element.matchesResource(resource))); + if (sha1IsEqual) { + model._overwriteVersionId(disposedModelData.versionId); + model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId); } - this._undoRedoService.setElementsIsValid(resource, true); - model._overwriteVersionId(disposedModelData.versionId); - model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId); } else { this._undoRedoService.removeElements(resource); } @@ -504,31 +516,39 @@ export class ModelServiceImpl extends Disposable implements IModelService { return; } const model = modelData.model; + const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString()); let maintainUndoRedoStack = false; let heapSize = 0; - if (this._shouldRestoreUndoStack() && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData)) { + if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData))) { const elements = this._undoRedoService.getElements(resource); - if ((elements.past.length > 0 || elements.future.length > 0) && isEditStackPastFutureElements(elements)) { - maintainUndoRedoStack = true; + if (elements.past.length > 0 || elements.future.length > 0) { for (const element of elements.past) { - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance + if (isEditStackElement(element) && element.matchesResource(resource)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(resource); + element.setModel(resource); // remove reference from text buffer instance + } } for (const element of elements.future) { - heapSize += element.heapSize(resource); - element.setModel(resource); // remove reference from text buffer instance + if (isEditStackElement(element) && element.matchesResource(resource)) { + maintainUndoRedoStack = true; + heapSize += element.heapSize(resource); + element.setModel(resource); // remove reference from text buffer instance + } } } } if (!maintainUndoRedoStack) { - this._undoRedoService.removeElements(resource); + if (!sharesUndoRedoStack) { + this._undoRedoService.removeElements(resource); + } modelData.model.dispose(); return; } const maxMemory = ModelServiceImpl.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK; - if (heapSize > maxMemory) { + if (!sharesUndoRedoStack && heapSize > maxMemory) { // the undo stack for this file would never fit in the configured memory, so don't bother with it. this._undoRedoService.removeElements(resource); modelData.model.dispose(); @@ -538,8 +558,8 @@ export class ModelServiceImpl extends Disposable implements IModelService { this._ensureDisposedModelsHeapSize(maxMemory - heapSize); // We only invalidate the elements, but they remain in the undo-redo service. - this._undoRedoService.setElementsIsValid(resource, false); - this._insertDisposedModel(new DisposedModelInfo(resource, Date.now(), heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); + this._undoRedoService.setElementsValidFlag(resource, false, (element) => (isEditStackElement(element) && element.matchesResource(resource))); + this._insertDisposedModel(new DisposedModelInfo(resource, Date.now(), sharesUndoRedoStack, heapSize, computeModelSha1(model), model.getVersionId(), model.getAlternativeVersionId())); modelData.model.dispose(); } diff --git a/src/vs/editor/common/services/modelUndoRedoParticipant.ts b/src/vs/editor/common/services/modelUndoRedoParticipant.ts new file mode 100644 index 00000000000..0862ccf8d6e --- /dev/null +++ b/src/vs/editor/common/services/modelUndoRedoParticipant.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IUndoRedoService, UndoRedoElementType } from 'vs/platform/undoRedo/common/undoRedo'; +import { isEditStackPastFutureElements } from 'vs/editor/common/services/modelServiceImpl'; +import { IUndoRedoDelegate, MultiModelEditStackElement } from 'vs/editor/common/model/editStack'; + +export class ModelUndoRedoParticipant extends Disposable implements IUndoRedoDelegate { + constructor( + @IModelService private readonly _modelService: IModelService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + ) { + super(); + this._register(this._modelService.onModelRemoved((model) => { + // a model will get disposed, so let's check if the undo redo stack is maintained + const elements = this._undoRedoService.getElements(model.uri); + if (elements.past.length === 0 && elements.future.length === 0) { + return; + } + if (!isEditStackPastFutureElements(elements)) { + return; + } + for (const element of elements.past) { + if (element.type === UndoRedoElementType.Workspace) { + element.setDelegate(this); + } + } + for (const element of elements.future) { + if (element.type === UndoRedoElementType.Workspace) { + element.setDelegate(this); + } + } + })); + } + + public prepareUndoRedo(element: MultiModelEditStackElement): IDisposable | Promise { + // Load all the needed text models + const missingModels = element.getMissingModels(); + if (missingModels.length === 0) { + // All models are available! + return Disposable.None; + } + + const disposablesPromises = missingModels.map(async (uri) => { + try { + const reference = await this._textModelService.createModelReference(uri); + return reference; + } catch (err) { + // This model could not be loaded, maybe it was deleted in the meantime? + return Disposable.None; + } + }); + + return Promise.all(disposablesPromises).then(disposables => { + return { + dispose: () => dispose(disposables) + }; + }); + } +} diff --git a/src/vs/editor/common/services/resolverService.ts b/src/vs/editor/common/services/resolverService.ts index 46da05998ea..47e546b6025 100644 --- a/src/vs/editor/common/services/resolverService.ts +++ b/src/vs/editor/common/services/resolverService.ts @@ -26,9 +26,9 @@ export interface ITextModelService { registerTextModelContentProvider(scheme: string, provider: ITextModelContentProvider): IDisposable; /** - * Check if a provider for the given `scheme` exists + * Check if the given resource can be resolved to a text model. */ - hasTextModelContentProvider(scheme: string): boolean; + canHandleResource(resource: URI): boolean; } export interface ITextModelContentProvider { diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index f472ce72595..49e24e9eac5 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -240,36 +240,36 @@ export enum EditorOption { quickSuggestions = 70, quickSuggestionsDelay = 71, readOnly = 72, - removeUnusualLineTerminators = 73, - renameOnType = 74, - renderControlCharacters = 75, - renderIndentGuides = 76, - renderFinalNewline = 77, - renderLineHighlight = 78, - renderLineHighlightOnlyWhenFocus = 79, - renderValidationDecorations = 80, - renderWhitespace = 81, - revealHorizontalRightPadding = 82, - roundedSelection = 83, - rulers = 84, - scrollbar = 85, - scrollBeyondLastColumn = 86, - scrollBeyondLastLine = 87, - scrollPredominantAxis = 88, - selectionClipboard = 89, - selectionHighlight = 90, - selectOnLineNumbers = 91, - showFoldingControls = 92, - showUnused = 93, - snippetSuggestions = 94, - smoothScrolling = 95, - stopRenderingLineAfter = 96, - suggest = 97, - suggestFontSize = 98, - suggestLineHeight = 99, - suggestOnTriggerCharacters = 100, - suggestSelection = 101, - tabCompletion = 102, + renameOnType = 73, + renderControlCharacters = 74, + renderIndentGuides = 75, + renderFinalNewline = 76, + renderLineHighlight = 77, + renderLineHighlightOnlyWhenFocus = 78, + renderValidationDecorations = 79, + renderWhitespace = 80, + revealHorizontalRightPadding = 81, + roundedSelection = 82, + rulers = 83, + scrollbar = 84, + scrollBeyondLastColumn = 85, + scrollBeyondLastLine = 86, + scrollPredominantAxis = 87, + selectionClipboard = 88, + selectionHighlight = 89, + selectOnLineNumbers = 90, + showFoldingControls = 91, + showUnused = 92, + snippetSuggestions = 93, + smoothScrolling = 94, + stopRenderingLineAfter = 95, + suggest = 96, + suggestFontSize = 97, + suggestLineHeight = 98, + suggestOnTriggerCharacters = 99, + suggestSelection = 100, + tabCompletion = 101, + unusualLineTerminators = 102, useTabStops = 103, wordSeparators = 104, wordWrap = 105, diff --git a/src/vs/editor/common/view/viewContext.ts b/src/vs/editor/common/view/viewContext.ts index 573b0827f0a..74628ebb857 100644 --- a/src/vs/editor/common/view/viewContext.ts +++ b/src/vs/editor/common/view/viewContext.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IConfiguration } from 'vs/editor/common/editorCommon'; -import { ViewEventDispatcher } from 'vs/editor/common/view/viewEventDispatcher'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { IViewLayout, IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { IColorTheme, ThemeType } from 'vs/platform/theme/common/themeService'; @@ -37,27 +36,24 @@ export class ViewContext { public readonly configuration: IConfiguration; public readonly model: IViewModel; public readonly viewLayout: IViewLayout; - public readonly privateViewEventBus: ViewEventDispatcher; public readonly theme: EditorTheme; constructor( configuration: IConfiguration, theme: IColorTheme, - model: IViewModel, - privateViewEventBus: ViewEventDispatcher + model: IViewModel ) { this.configuration = configuration; this.theme = new EditorTheme(theme); this.model = model; this.viewLayout = model.viewLayout; - this.privateViewEventBus = privateViewEventBus; } public addEventHandler(eventHandler: ViewEventHandler): void { - this.privateViewEventBus.addEventHandler(eventHandler); + this.model.addViewEventHandler(eventHandler); } public removeEventHandler(eventHandler: ViewEventHandler): void { - this.privateViewEventBus.removeEventHandler(eventHandler); + this.model.removeViewEventHandler(eventHandler); } } diff --git a/src/vs/editor/common/view/viewEventDispatcher.ts b/src/vs/editor/common/view/viewEventDispatcher.ts deleted file mode 100644 index 54bd7a6e986..00000000000 --- a/src/vs/editor/common/view/viewEventDispatcher.ts +++ /dev/null @@ -1,92 +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 { ViewEvent } from 'vs/editor/common/view/viewEvents'; -import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; - -export class ViewEventDispatcher { - - private readonly _eventHandlerGateKeeper: (callback: () => void) => void; - private readonly _eventHandlers: ViewEventHandler[]; - private _eventQueue: ViewEvent[] | null; - private _isConsumingQueue: boolean; - - constructor(eventHandlerGateKeeper: (callback: () => void) => void) { - this._eventHandlerGateKeeper = eventHandlerGateKeeper; - this._eventHandlers = []; - this._eventQueue = null; - this._isConsumingQueue = false; - } - - public addEventHandler(eventHandler: ViewEventHandler): void { - for (let i = 0, len = this._eventHandlers.length; i < len; i++) { - if (this._eventHandlers[i] === eventHandler) { - console.warn('Detected duplicate listener in ViewEventDispatcher', eventHandler); - } - } - this._eventHandlers.push(eventHandler); - } - - public removeEventHandler(eventHandler: ViewEventHandler): void { - for (let i = 0; i < this._eventHandlers.length; i++) { - if (this._eventHandlers[i] === eventHandler) { - this._eventHandlers.splice(i, 1); - break; - } - } - } - - public emit(event: ViewEvent): void { - - if (this._eventQueue) { - this._eventQueue.push(event); - } else { - this._eventQueue = [event]; - } - - if (!this._isConsumingQueue) { - this.consumeQueue(); - } - } - - public emitMany(events: ViewEvent[]): void { - if (this._eventQueue) { - this._eventQueue = this._eventQueue.concat(events); - } else { - this._eventQueue = events; - } - - if (!this._isConsumingQueue) { - this.consumeQueue(); - } - } - - private consumeQueue(): void { - this._eventHandlerGateKeeper(() => { - try { - this._isConsumingQueue = true; - - this._doConsumeQueue(); - - } finally { - this._isConsumingQueue = false; - } - }); - } - - private _doConsumeQueue(): void { - while (this._eventQueue) { - // Empty event queue, as events might come in while sending these off - let events = this._eventQueue; - this._eventQueue = null; - - // Use a clone of the event handlers list, as they might remove themselves - let eventHandlers = this._eventHandlers.slice(0); - for (let i = 0, len = eventHandlers.length; i < len; i++) { - eventHandlers[i].handleEvents(events); - } - } - } -} diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 70d7b0577f4..804d1b977f5 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -3,33 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as errors from 'vs/base/common/errors'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ScrollType, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export const enum ViewEventType { - ViewConfigurationChanged = 1, - ViewContentSizeChanged = 2, - ViewCursorStateChanged = 3, - ViewDecorationsChanged = 4, - ViewFlushed = 5, - ViewFocusChanged = 6, - ViewLanguageConfigurationChanged = 7, - ViewLineMappingChanged = 8, - ViewLinesChanged = 9, - ViewLinesDeleted = 10, - ViewLinesInserted = 11, - ViewRevealRangeRequest = 12, - ViewScrollChanged = 13, - ViewThemeChanged = 14, - ViewTokensChanged = 15, - ViewTokensColorsChanged = 16, - ViewZonesChanged = 17, + ViewConfigurationChanged, + ViewCursorStateChanged, + ViewDecorationsChanged, + ViewFlushed, + ViewFocusChanged, + ViewLanguageConfigurationChanged, + ViewLineMappingChanged, + ViewLinesChanged, + ViewLinesDeleted, + ViewLinesInserted, + ViewRevealRangeRequest, + ViewScrollChanged, + ViewThemeChanged, + ViewTokensChanged, + ViewTokensColorsChanged, + ViewZonesChanged, } export class ViewConfigurationChangedEvent { @@ -47,25 +44,6 @@ export class ViewConfigurationChangedEvent { } } -export class ViewContentSizeChangedEvent implements IContentSizeChangedEvent { - - public readonly type = ViewEventType.ViewContentSizeChanged; - - public readonly contentWidth: number; - public readonly contentHeight: number; - - public readonly contentWidthChanged: boolean; - public readonly contentHeightChanged: boolean; - - constructor(source: IContentSizeChangedEvent) { - this.contentWidth = source.contentWidth; - this.contentHeight = source.contentHeight; - - this.contentWidthChanged = source.contentWidthChanged; - this.contentHeightChanged = source.contentHeightChanged; - } -} - export class ViewCursorStateChangedEvent { public readonly type = ViewEventType.ViewCursorStateChanged; @@ -308,7 +286,6 @@ export class ViewZonesChangedEvent { export type ViewEvent = ( ViewConfigurationChangedEvent - | ViewContentSizeChangedEvent | ViewCursorStateChangedEvent | ViewDecorationsChangedEvent | ViewFlushedEvent @@ -325,107 +302,3 @@ export type ViewEvent = ( | ViewTokensColorsChangedEvent | ViewZonesChangedEvent ); - -export interface IViewEventListener { - (events: ViewEvent[]): void; -} - -export interface IViewEventEmitter { - addViewEventListener(listener: IViewEventListener): IDisposable; -} - -export class ViewEventEmitter extends Disposable implements IViewEventEmitter { - private _listeners: IViewEventListener[]; - private _collector: ViewEventsCollector | null; - private _collectorCnt: number; - - constructor() { - super(); - this._listeners = []; - this._collector = null; - this._collectorCnt = 0; - } - - public dispose(): void { - this._listeners = []; - super.dispose(); - } - - protected _beginEmitViewEvents(): ViewEventsCollector { - this._collectorCnt++; - if (this._collectorCnt === 1) { - this._collector = new ViewEventsCollector(); - } - return this._collector!; - } - - protected _endEmitViewEvents(): void { - this._collectorCnt--; - if (this._collectorCnt === 0) { - const events = this._collector!.finalize(); - this._collector = null; - if (events.length > 0) { - this._emit(events); - } - } - } - - protected _emitSingleViewEvent(event: ViewEvent): void { - try { - const eventsCollector = this._beginEmitViewEvents(); - eventsCollector.emit(event); - } finally { - this._endEmitViewEvents(); - } - } - - private _emit(events: ViewEvent[]): void { - const listeners = this._listeners.slice(0); - for (let i = 0, len = listeners.length; i < len; i++) { - safeInvokeListener(listeners[i], events); - } - } - - public addViewEventListener(listener: IViewEventListener): IDisposable { - this._listeners.push(listener); - return toDisposable(() => { - let listeners = this._listeners; - for (let i = 0, len = listeners.length; i < len; i++) { - if (listeners[i] === listener) { - listeners.splice(i, 1); - break; - } - } - }); - } -} - -export class ViewEventsCollector { - - private _events: ViewEvent[]; - private _eventsLen = 0; - - constructor() { - this._events = []; - this._eventsLen = 0; - } - - public emit(event: ViewEvent) { - this._events[this._eventsLen++] = event; - } - - public finalize(): ViewEvent[] { - let result = this._events; - this._events = []; - return result; - } - -} - -function safeInvokeListener(listener: IViewEventListener, events: ViewEvent[]): void { - try { - listener(events); - } catch (e) { - errors.onUnexpectedError(e); - } -} diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index c549df8d22c..ceef064de9a 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -180,33 +180,36 @@ export class LinesLayout { this._lineCount = lineCount; } - public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => T): T { + public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean { + let hadAChange = false; try { - const accessor = { + const accessor: IWhitespaceChangeAccessor = { insertWhitespace: (afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string => { + hadAChange = true; afterLineNumber = afterLineNumber | 0; ordinal = ordinal | 0; heightInPx = heightInPx | 0; minWidth = minWidth | 0; - const id = this._instanceId + (++this._lastWhitespaceId); this._pendingChanges.insert(new EditorWhitespace(id, afterLineNumber, ordinal, heightInPx, minWidth)); return id; }, changeOneWhitespace: (id: string, newAfterLineNumber: number, newHeight: number): void => { + hadAChange = true; newAfterLineNumber = newAfterLineNumber | 0; newHeight = newHeight | 0; - this._pendingChanges.change({ id, newAfterLineNumber, newHeight }); }, removeWhitespace: (id: string): void => { + hadAChange = true; this._pendingChanges.remove({ id }); } }; - return callback(accessor); + callback(accessor); } finally { this._pendingChanges.commit(this); } + return hadAChange; } public _commitPendingChanges(inserts: EditorWhitespace[], changes: IPendingChange[], removes: IPendingRemove[]): void { diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index f3e949b8cda..3e72f30d0de 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -7,10 +7,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IConfiguration, IContentSizeChangedEvent, ScrollType } from 'vs/editor/common/editorCommon'; +import { IConfiguration, ScrollType } from 'vs/editor/common/editorCommon'; import { LinesLayout, IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IViewLayout, IViewWhitespaceViewportData, Viewport } from 'vs/editor/common/viewModel/viewModel'; +import { ContentSizeChangedEvent } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; const SMOOTH_SCROLLING_TIME = 125; @@ -75,8 +76,8 @@ class EditorScrollable extends Disposable { public readonly onDidScroll: Event; - private readonly _onDidContentSizeChange = this._register(new Emitter()); - public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + private readonly _onDidContentSizeChange = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; constructor(smoothScrollDuration: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); @@ -119,13 +120,10 @@ class EditorScrollable extends Disposable { const contentWidthChanged = (oldDimensions.contentWidth !== dimensions.contentWidth); const contentHeightChanged = (oldDimensions.contentHeight !== dimensions.contentHeight); if (contentWidthChanged || contentHeightChanged) { - this._onDidContentSizeChange.fire({ - contentWidth: dimensions.contentWidth, - contentHeight: dimensions.contentHeight, - - contentWidthChanged: contentWidthChanged, - contentHeightChanged: contentHeightChanged - }); + this._onDidContentSizeChange.fire(new ContentSizeChangedEvent( + oldDimensions.contentWidth, oldDimensions.contentHeight, + dimensions.contentWidth, dimensions.contentHeight + )); } } @@ -153,7 +151,7 @@ export class ViewLayout extends Disposable implements IViewLayout { private readonly _scrollable: EditorScrollable; public readonly onDidScroll: Event; - public readonly onDidContentSizeChange: Event; + public readonly onDidContentSizeChange: Event; constructor(configuration: IConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); @@ -324,7 +322,7 @@ export class ViewLayout extends Disposable implements IViewLayout { } } - public onMaxLineWidthChanged(maxLineWidth: number): void { + public setMaxLineWidth(maxLineWidth: number): void { const scrollDimensions = this._scrollable.getScrollDimensions(); // const newScrollWidth = ; this._scrollable.setScrollDimensions(new EditorScrollDimensions( @@ -353,8 +351,12 @@ export class ViewLayout extends Disposable implements IViewLayout { } // ---- IVerticalLayoutProvider - public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => T): T { - return this._linesLayout.changeWhitespace(callback); + public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean { + const hadAChange = this._linesLayout.changeWhitespace(callback); + if (hadAChange) { + this.onHeightMaybeChanged(); + } + return hadAChange; } public getVerticalOffsetForLineNumber(lineNumber: number): number { return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber); diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index b8d0bc823a6..aad7a0b4957 100644 --- a/src/vs/editor/common/viewModel/viewEventHandler.ts +++ b/src/vs/editor/common/viewModel/viewEventHandler.ts @@ -36,9 +36,7 @@ export class ViewEventHandler extends Disposable { public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return false; } - public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { - return false; - } + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return false; } @@ -102,12 +100,6 @@ export class ViewEventHandler extends Disposable { } break; - case viewEvents.ViewEventType.ViewContentSizeChanged: - if (this.onContentSizeChanged(e)) { - shouldRender = true; - } - break; - case viewEvents.ViewEventType.ViewCursorStateChanged: if (this.onCursorStateChanged(e)) { shouldRender = true; diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index b994a1277f7..91ab8513c4c 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -9,12 +9,14 @@ import { IViewLineTokens } from 'vs/editor/common/core/lineTokens'; import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { IViewEventEmitter } from 'vs/editor/common/view/viewEvents'; +import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel } from 'vs/editor/common/model'; +import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; -import { ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon'; +import { ICursorSimpleModel, PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; export interface IViewWhitespaceViewportData { readonly id: string; @@ -43,8 +45,6 @@ export interface IViewLayout { getScrollable(): Scrollable; - onMaxLineWidthChanged(width: number): void; - getScrollWidth(): number; getScrollHeight(): number; @@ -55,8 +55,6 @@ export interface IViewLayout { getFutureViewport(): Viewport; validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition; - setScrollPosition(position: INewScrollPosition, type: ScrollType): void; - deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void; getLinesViewportData(): IPartialViewLinesViewportData; getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData; @@ -67,18 +65,10 @@ export interface IViewLayout { getVerticalOffsetForLineNumber(lineNumber: number): number; getWhitespaceAtVerticalOffset(verticalOffset: number): IViewWhitespaceViewportData | null; - // --------------- Begin vertical whitespace management - changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => T): T; - /** * Get the layout information for whitespaces currently in the viewport */ getWhitespaceViewportData(): IViewWhitespaceViewportData[]; - - // TODO@Alex whitespace management should work via a change accessor sort of thing - onHeightMaybeChanged(): void; - - // --------------- End vertical whitespace management } export interface ICoordinatesConverter { @@ -94,18 +84,26 @@ export interface ICoordinatesConverter { modelPositionIsVisible(modelPosition: Position): boolean; } -export interface IViewModel extends IViewEventEmitter, ICursorSimpleModel { +export interface IViewModel extends ICursorSimpleModel { + + readonly model: ITextModel; readonly coordinatesConverter: ICoordinatesConverter; readonly viewLayout: IViewLayout; + readonly cursorConfig: CursorConfiguration; + + addViewEventHandler(eventHandler: ViewEventHandler): void; + removeViewEventHandler(eventHandler: ViewEventHandler): void; + /** * Gives a hint that a lot of requests are about to come in for these line numbers. */ setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void; tokenizeViewport(): void; setHasFocus(hasFocus: boolean): void; + onDidColorThemeChange(): void; getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[]; getViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData; @@ -114,7 +112,7 @@ export interface IViewModel extends IViewEventEmitter, ICursorSimpleModel { getCompletelyVisibleViewRange(): Range; getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range; - getOptions(): TextModelResolvedOptions; + getTextModelOptions(): TextModelResolvedOptions; getLineCount(): number; getLineContent(lineNumber: number): string; getLineLength(lineNumber: number): number; @@ -137,6 +135,38 @@ export interface IViewModel extends IViewEventEmitter, ICursorSimpleModel { getEOL(): string; getPlainTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean, forceCRLF: boolean): string | string[]; getRichTextToCopy(modelRanges: Range[], emptySelectionClipboard: boolean): { html: string, mode: string } | null; + + //#region model + + pushStackElement(): void; + + //#endregion + + + //#region cursor + getPrimaryCursorState(): CursorState; + getLastAddedCursorIndex(): number; + getCursorStates(): CursorState[]; + setCursorStates(source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): void; + getCursorColumnSelectData(): IColumnSelectData; + setCursorColumnSelectData(columnSelectData: IColumnSelectData): void; + getPrevEditOperationType(): EditOperationType; + setPrevEditOperationType(type: EditOperationType): void; + revealPrimaryCursor(source: string | null | undefined, revealHorizontal: boolean): void; + revealTopMostCursor(source: string | null | undefined): void; + revealBottomMostCursor(source: string | null | undefined): void; + revealRange(source: string | null | undefined, revealHorizontal: boolean, viewRange: Range, verticalType: VerticalRevealType, scrollType: ScrollType): void; + //#endregion + + //#region viewLayout + getVerticalOffsetForLineNumber(viewLineNumber: number): number; + getScrollTop(): number; + setScrollTop(newScrollTop: number, scrollType: ScrollType): void; + setScrollPosition(position: INewScrollPosition, type: ScrollType): void; + deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void; + changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): void; + setMaxLineWidth(maxLineWidth: number): void; + //#endregion } export class MinimapLinesRenderingData { diff --git a/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts new file mode 100644 index 00000000000..0691b542cd1 --- /dev/null +++ b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; +import { ViewEvent } from 'vs/editor/common/view/viewEvents'; +import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; +import { Emitter } from 'vs/base/common/event'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; + +export class ViewModelEventDispatcher extends Disposable { + + private readonly _onEvent = this._register(new Emitter()); + public readonly onEvent = this._onEvent.event; + + private readonly _eventHandlers: ViewEventHandler[]; + private _viewEventQueue: ViewEvent[] | null; + private _isConsumingViewEventQueue: boolean; + private _collector: ViewModelEventsCollector | null; + private _collectorCnt: number; + private _outgoingEvents: OutgoingViewModelEvent[]; + + constructor() { + super(); + this._eventHandlers = []; + this._viewEventQueue = null; + this._isConsumingViewEventQueue = false; + this._collector = null; + this._collectorCnt = 0; + this._outgoingEvents = []; + } + + public emitOutgoingEvent(e: OutgoingViewModelEvent): void { + this._addOutgoingEvent(e); + this._emitOugoingEvents(); + } + + private _addOutgoingEvent(e: OutgoingViewModelEvent): void { + for (let i = 0, len = this._outgoingEvents.length; i < len; i++) { + if (this._outgoingEvents[i].kind === e.kind) { + this._outgoingEvents[i] = this._outgoingEvents[i].merge(e); + return; + } + } + // not merged + this._outgoingEvents.push(e); + } + + private _emitOugoingEvents(): void { + while (this._outgoingEvents.length > 0) { + if (this._collector || this._isConsumingViewEventQueue) { + // right now collecting or emitting view events, so let's postpone emitting + return; + } + const event = this._outgoingEvents.shift()!; + if (event.isNoOp()) { + continue; + } + this._onEvent.fire(event); + } + } + + public addViewEventHandler(eventHandler: ViewEventHandler): void { + for (let i = 0, len = this._eventHandlers.length; i < len; i++) { + if (this._eventHandlers[i] === eventHandler) { + console.warn('Detected duplicate listener in ViewEventDispatcher', eventHandler); + } + } + this._eventHandlers.push(eventHandler); + } + + public removeViewEventHandler(eventHandler: ViewEventHandler): void { + for (let i = 0; i < this._eventHandlers.length; i++) { + if (this._eventHandlers[i] === eventHandler) { + this._eventHandlers.splice(i, 1); + break; + } + } + } + + public beginEmitViewEvents(): ViewModelEventsCollector { + this._collectorCnt++; + if (this._collectorCnt === 1) { + this._collector = new ViewModelEventsCollector(); + } + return this._collector!; + } + + public endEmitViewEvents(): void { + this._collectorCnt--; + if (this._collectorCnt === 0) { + const outgoingEvents = this._collector!.outgoingEvents; + const viewEvents = this._collector!.viewEvents; + this._collector = null; + + for (const outgoingEvent of outgoingEvents) { + this._addOutgoingEvent(outgoingEvent); + } + + if (viewEvents.length > 0) { + this._emitMany(viewEvents); + } + } + this._emitOugoingEvents(); + } + + public emitSingleViewEvent(event: ViewEvent): void { + try { + const eventsCollector = this.beginEmitViewEvents(); + eventsCollector.emitViewEvent(event); + } finally { + this.endEmitViewEvents(); + } + } + + private _emitMany(events: ViewEvent[]): void { + if (this._viewEventQueue) { + this._viewEventQueue = this._viewEventQueue.concat(events); + } else { + this._viewEventQueue = events; + } + + if (!this._isConsumingViewEventQueue) { + this._consumeViewEventQueue(); + } + } + + private _consumeViewEventQueue(): void { + try { + this._isConsumingViewEventQueue = true; + this._doConsumeQueue(); + } finally { + this._isConsumingViewEventQueue = false; + } + } + + private _doConsumeQueue(): void { + while (this._viewEventQueue) { + // Empty event queue, as events might come in while sending these off + const events = this._viewEventQueue; + this._viewEventQueue = null; + + // Use a clone of the event handlers list, as they might remove themselves + const eventHandlers = this._eventHandlers.slice(0); + for (const eventHandler of eventHandlers) { + eventHandler.handleEvents(events); + } + } + } +} + +export class ViewModelEventsCollector { + + public readonly viewEvents: ViewEvent[]; + public readonly outgoingEvents: OutgoingViewModelEvent[]; + + constructor() { + this.viewEvents = []; + this.outgoingEvents = []; + } + + public emitViewEvent(event: ViewEvent) { + this.viewEvents.push(event); + } + + public emitOutgoingEvent(e: OutgoingViewModelEvent): void { + this.outgoingEvents.push(e); + } +} + +export const enum OutgoingViewModelEventKind { + ContentSizeChanged, + FocusChanged, + ScrollChanged, + ViewZonesChanged, + ReadOnlyEditAttempt, + CursorStateChanged, +} + +export class ContentSizeChangedEvent implements IContentSizeChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.ContentSizeChanged; + + private readonly _oldContentWidth: number; + private readonly _oldContentHeight: number; + + readonly contentWidth: number; + readonly contentHeight: number; + readonly contentWidthChanged: boolean; + readonly contentHeightChanged: boolean; + + constructor(oldContentWidth: number, oldContentHeight: number, contentWidth: number, contentHeight: number) { + this._oldContentWidth = oldContentWidth; + this._oldContentHeight = oldContentHeight; + this.contentWidth = contentWidth; + this.contentHeight = contentHeight; + this.contentWidthChanged = (this._oldContentWidth !== this.contentWidth); + this.contentHeightChanged = (this._oldContentHeight !== this.contentHeight); + } + + public isNoOp(): boolean { + return (!this.contentWidthChanged && !this.contentHeightChanged); + } + + + public merge(other: OutgoingViewModelEvent): ContentSizeChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.ContentSizeChanged) { + return this; + } + return new ContentSizeChangedEvent(this._oldContentWidth, this._oldContentHeight, other.contentWidth, other.contentHeight); + } +} + +export class FocusChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.FocusChanged; + + readonly oldHasFocus: boolean; + readonly hasFocus: boolean; + + constructor(oldHasFocus: boolean, hasFocus: boolean) { + this.oldHasFocus = oldHasFocus; + this.hasFocus = hasFocus; + } + + public isNoOp(): boolean { + return (this.oldHasFocus === this.hasFocus); + } + + public merge(other: OutgoingViewModelEvent): FocusChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.FocusChanged) { + return this; + } + return new FocusChangedEvent(this.oldHasFocus, other.hasFocus); + } +} + +export class ScrollChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.ScrollChanged; + + private readonly _oldScrollWidth: number; + private readonly _oldScrollLeft: number; + private readonly _oldScrollHeight: number; + private readonly _oldScrollTop: number; + + public readonly scrollWidth: number; + public readonly scrollLeft: number; + public readonly scrollHeight: number; + public readonly scrollTop: number; + + public readonly scrollWidthChanged: boolean; + public readonly scrollLeftChanged: boolean; + public readonly scrollHeightChanged: boolean; + public readonly scrollTopChanged: boolean; + + constructor( + oldScrollWidth: number, oldScrollLeft: number, oldScrollHeight: number, oldScrollTop: number, + scrollWidth: number, scrollLeft: number, scrollHeight: number, scrollTop: number, + ) { + this._oldScrollWidth = oldScrollWidth; + this._oldScrollLeft = oldScrollLeft; + this._oldScrollHeight = oldScrollHeight; + this._oldScrollTop = oldScrollTop; + + this.scrollWidth = scrollWidth; + this.scrollLeft = scrollLeft; + this.scrollHeight = scrollHeight; + this.scrollTop = scrollTop; + + this.scrollWidthChanged = (this._oldScrollWidth !== this.scrollWidth); + this.scrollLeftChanged = (this._oldScrollLeft !== this.scrollLeft); + this.scrollHeightChanged = (this._oldScrollHeight !== this.scrollHeight); + this.scrollTopChanged = (this._oldScrollTop !== this.scrollTop); + } + + public isNoOp(): boolean { + return (!this.scrollWidthChanged && !this.scrollLeftChanged && !this.scrollHeightChanged && !this.scrollTopChanged); + } + + public merge(other: OutgoingViewModelEvent): ScrollChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.ScrollChanged) { + return this; + } + return new ScrollChangedEvent( + this._oldScrollWidth, this._oldScrollLeft, this._oldScrollHeight, this._oldScrollTop, + other.scrollWidth, other.scrollLeft, other.scrollHeight, other.scrollTop + ); + } +} + +export class ViewZonesChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.ViewZonesChanged; + + constructor() { + } + + public isNoOp(): boolean { + return false; + } + + public merge(other: OutgoingViewModelEvent): ViewZonesChangedEvent { + return this; + } +} + +export class CursorStateChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.CursorStateChanged; + + public readonly oldSelections: Selection[] | null; + public readonly selections: Selection[]; + public readonly oldModelVersionId: number; + public readonly modelVersionId: number; + public readonly source: string; + public readonly reason: CursorChangeReason; + public readonly reachedMaxCursorCount: boolean; + + constructor(oldSelections: Selection[] | null, selections: Selection[], oldModelVersionId: number, modelVersionId: number, source: string, reason: CursorChangeReason, reachedMaxCursorCount: boolean) { + this.oldSelections = oldSelections; + this.selections = selections; + this.oldModelVersionId = oldModelVersionId; + this.modelVersionId = modelVersionId; + this.source = source; + this.reason = reason; + this.reachedMaxCursorCount = reachedMaxCursorCount; + } + + private static _selectionsAreEqual(a: Selection[] | null, b: Selection[] | null): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + const aLen = a.length; + const bLen = b.length; + if (aLen !== bLen) { + return false; + } + for (let i = 0; i < aLen; i++) { + if (!a[i].equalsSelection(b[i])) { + return false; + } + } + return true; + } + + public isNoOp(): boolean { + return ( + CursorStateChangedEvent._selectionsAreEqual(this.oldSelections, this.selections) + && this.oldModelVersionId === this.modelVersionId + ); + } + + public merge(other: OutgoingViewModelEvent): CursorStateChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.CursorStateChanged) { + return this; + } + return new CursorStateChangedEvent( + this.oldSelections, other.selections, this.oldModelVersionId, other.modelVersionId, other.source, other.reason, this.reachedMaxCursorCount || other.reachedMaxCursorCount + ); + } +} + +export class ReadOnlyEditAttemptEvent { + + public readonly kind = OutgoingViewModelEventKind.ReadOnlyEditAttempt; + + constructor() { + } + + public isNoOp(): boolean { + return false; + } + + public merge(other: OutgoingViewModelEvent): ReadOnlyEditAttemptEvent { + return this; + } +} + +export type OutgoingViewModelEvent = ( + ContentSizeChangedEvent + | FocusChangedEvent + | ScrollChangedEvent + | ViewZonesChangedEvent + | ReadOnlyEditAttemptEvent + | CursorStateChangedEvent +); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 981b7c185f8..f7f130f0866 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -4,13 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import { Color } from 'vs/base/common/color'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { ConfigurationChangedEvent, EDITOR_FONT_DEFAULTS, EditorOption, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; +import { ISelection, Selection } from 'vs/editor/common/core/selection'; import { IRange, Range } from 'vs/editor/common/core/range'; -import { IConfiguration, IViewState, ScrollType } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions } from 'vs/editor/common/model'; +import { IConfiguration, IViewState, ScrollType, ICursorState, ICommand, INewScrollPosition } from 'vs/editor/common/editorCommon'; +import { EndOfLinePreference, IActiveIndentGuideInfo, ITextModel, TrackedRangeStickiness, TextModelResolvedOptions, IIdentifiedSingleEditOperation, ICursorStateComputer } from 'vs/editor/common/model'; import { ModelDecorationOverviewRulerOptions, ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; import { ColorId, LanguageId, TokenizationRegistry } from 'vs/editor/common/modes'; @@ -25,25 +27,33 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import * as platform from 'vs/base/common/platform'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { Cursor } from 'vs/editor/common/controller/cursor'; +import { PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; +import { ViewModelEventDispatcher, OutgoingViewModelEvent, FocusChangedEvent, ScrollChangedEvent, ViewZonesChangedEvent, ViewModelEventsCollector, ReadOnlyEditAttemptEvent } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; const USE_IDENTITY_LINES_COLLECTION = true; -export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel { +export class ViewModel extends Disposable implements IViewModel { - private readonly editorId: number; - private readonly configuration: IConfiguration; - private readonly model: ITextModel; + private readonly _editorId: number; + private readonly _configuration: IConfiguration; + public readonly model: ITextModel; + private readonly _eventDispatcher: ViewModelEventDispatcher; + public readonly onEvent: Event; + public cursorConfig: CursorConfiguration; private readonly _tokenizeViewportSoon: RunOnceScheduler; private readonly _updateConfigurationViewLineCount: RunOnceScheduler; - private hasFocus: boolean; - private viewportStartLine: number; - private viewportStartLineTrackedRange: string | null; - private viewportStartLineDelta: number; - private readonly lines: IViewModelLinesCollection; + private _hasFocus: boolean; + private _viewportStartLine: number; + private _viewportStartLineTrackedRange: string | null; + private _viewportStartLineDelta: number; + private readonly _lines: IViewModelLinesCollection; public readonly coordinatesConverter: ICoordinatesConverter; public readonly viewLayout: ViewLayout; - public readonly cursor: Cursor; - private readonly decorations: ViewModelDecorations; + private readonly _cursor: Cursor; + private readonly _decorations: ViewModelDecorations; constructor( editorId: number, @@ -55,28 +65,31 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ) { super(); - this.editorId = editorId; - this.configuration = configuration; + this._editorId = editorId; + this._configuration = configuration; this.model = model; + this._eventDispatcher = new ViewModelEventDispatcher(); + this.onEvent = this._eventDispatcher.onEvent; + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); this._tokenizeViewportSoon = this._register(new RunOnceScheduler(() => this.tokenizeViewport(), 50)); this._updateConfigurationViewLineCount = this._register(new RunOnceScheduler(() => this._updateConfigurationViewLineCountNow(), 0)); - this.hasFocus = false; - this.viewportStartLine = -1; - this.viewportStartLineTrackedRange = null; - this.viewportStartLineDelta = 0; + this._hasFocus = false; + this._viewportStartLine = -1; + this._viewportStartLineTrackedRange = null; + this._viewportStartLineDelta = 0; if (USE_IDENTITY_LINES_COLLECTION && this.model.isTooLargeForTokenization()) { - this.lines = new IdentityLinesCollection(this.model); + this._lines = new IdentityLinesCollection(this.model); } else { - const options = this.configuration.options; + const options = this._configuration.options; const fontInfo = options.get(EditorOption.fontInfo); const wrappingStrategy = options.get(EditorOption.wrappingStrategy); const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); - this.lines = new SplitLinesCollection( + this._lines = new SplitLinesCollection( this.model, domLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, @@ -88,48 +101,42 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - this.coordinatesConverter = this.lines.createCoordinatesConverter(); + this.coordinatesConverter = this._lines.createCoordinatesConverter(); - this.cursor = this._register(new Cursor(this.configuration, model, this, this.coordinatesConverter)); - this._register(this.cursor.addViewEventListener((events) => { - try { - const eventsCollector = this._beginEmitViewEvents(); - for (const event of events) { - eventsCollector.emit(event); - } - } finally { - this._endEmitViewEvents(); - } - })); + this._cursor = this._register(new Cursor(model, this, this.coordinatesConverter, this.cursorConfig)); - this.viewLayout = this._register(new ViewLayout(this.configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); + this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); this._register(this.viewLayout.onDidScroll((e) => { if (e.scrollTopChanged) { this._tokenizeViewportSoon.schedule(); } - this._emitSingleViewEvent(new viewEvents.ViewScrollChangedEvent(e)); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewScrollChangedEvent(e)); + this._eventDispatcher.emitOutgoingEvent(new ScrollChangedEvent( + e.oldScrollWidth, e.oldScrollLeft, e.oldScrollHeight, e.oldScrollTop, + e.scrollWidth, e.scrollLeft, e.scrollHeight, e.scrollTop + )); })); this._register(this.viewLayout.onDidContentSizeChange((e) => { - this._emitSingleViewEvent(new viewEvents.ViewContentSizeChangedEvent(e)); + this._eventDispatcher.emitOutgoingEvent(e); })); - this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.lines, this.coordinatesConverter); + this._decorations = new ViewModelDecorations(this._editorId, this.model, this._configuration, this._lines, this.coordinatesConverter); this._registerModelEvents(); - this._register(this.configuration.onDidChange((e) => { + this._register(this._configuration.onDidChangeFast((e) => { try { - const eventsCollector = this._beginEmitViewEvents(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); this._onConfigurationChanged(eventsCollector, e); } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } })); this._register(MinimapTokensColorTracker.getInstance().onDidChange(() => { - this._emitSingleViewEvent(new viewEvents.ViewTokensColorsChangedEvent()); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensColorsChangedEvent()); })); this._updateConfigurationViewLineCountNow(); @@ -139,14 +146,23 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel // First remove listeners, as disposing the lines might end up sending // model decoration changed events ... and we no longer care about them ... super.dispose(); - this.decorations.dispose(); - this.lines.dispose(); + this._decorations.dispose(); + this._lines.dispose(); this.invalidateMinimapColorCache(); - this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, null, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this._viewportStartLineTrackedRange = this.model._setTrackedRange(this._viewportStartLineTrackedRange, null, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this._eventDispatcher.dispose(); + } + + public addViewEventHandler(eventHandler: ViewEventHandler): void { + this._eventDispatcher.addViewEventHandler(eventHandler); + } + + public removeViewEventHandler(eventHandler: ViewEventHandler): void { + this._eventDispatcher.removeViewEventHandler(eventHandler); } private _updateConfigurationViewLineCountNow(): void { - this.configuration.setViewLineCount(this.lines.getViewLineCount()); + this._configuration.setViewLineCount(this._lines.getViewLineCount()); } public tokenizeViewport(): void { @@ -157,31 +173,38 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public setHasFocus(hasFocus: boolean): void { - this.hasFocus = hasFocus; + this._hasFocus = hasFocus; + this._cursor.setHasFocus(hasFocus); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewFocusChangedEvent(hasFocus)); + this._eventDispatcher.emitOutgoingEvent(new FocusChangedEvent(!hasFocus, hasFocus)); } - private _onConfigurationChanged(eventsCollector: viewEvents.ViewEventsCollector, e: ConfigurationChangedEvent): void { + public onDidColorThemeChange(): void { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewThemeChangedEvent()); + } + + private _onConfigurationChanged(eventsCollector: ViewModelEventsCollector, e: ConfigurationChangedEvent): void { // We might need to restore the current centered view range, so save it (if available) let previousViewportStartModelPosition: Position | null = null; - if (this.viewportStartLine !== -1) { - let previousViewportStartViewPosition = new Position(this.viewportStartLine, this.getLineMinColumn(this.viewportStartLine)); + if (this._viewportStartLine !== -1) { + let previousViewportStartViewPosition = new Position(this._viewportStartLine, this.getLineMinColumn(this._viewportStartLine)); previousViewportStartModelPosition = this.coordinatesConverter.convertViewPositionToModelPosition(previousViewportStartViewPosition); } let restorePreviousViewportStart = false; - const options = this.configuration.options; + const options = this._configuration.options; const fontInfo = options.get(EditorOption.fontInfo); const wrappingStrategy = options.get(EditorOption.wrappingStrategy); const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); - if (this.lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent)) { - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(); - this.decorations.onLineMappingChanged(); + if (this._lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent)) { + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); if (this.viewLayout.getCurrentScrollTop() !== 0) { @@ -194,27 +217,30 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (e.hasChanged(EditorOption.readOnly)) { // Must read again all decorations due to readOnly filtering - this.decorations.reset(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); + this._decorations.reset(); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); } - eventsCollector.emit(new viewEvents.ViewConfigurationChangedEvent(e)); + eventsCollector.emitViewEvent(new viewEvents.ViewConfigurationChangedEvent(e)); this.viewLayout.onConfigurationChanged(e); if (restorePreviousViewportStart && previousViewportStartModelPosition) { const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(previousViewportStartModelPosition); const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); - this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this.viewportStartLineDelta }, ScrollType.Immediate); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStartLineDelta }, ScrollType.Immediate); } - this.cursor.onDidChangeConfiguration(e); + if (CursorConfiguration.shouldRecreate(e)) { + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); + } } private _registerModelEvents(): void { this._register(this.model.onDidChangeRawContentFast((e) => { try { - const eventsCollector = this._beginEmitViewEvents(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); let hadOtherModelChange = false; let hadModelLineChangeThatChangedLineMapping = false; @@ -223,7 +249,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const versionId = e.versionId; // Do a first pass to compute line mappings, and a second pass to actually interpret them - const lineBreaksComputer = this.lines.createLineBreaksComputer(); + const lineBreaksComputer = this._lines.createLineBreaksComputer(); for (const change of changes) { switch (change.changeType) { case textModelEvents.RawContentChangedType.LinesInserted: { @@ -245,17 +271,17 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel switch (change.changeType) { case textModelEvents.RawContentChangedType.Flush: { - this.lines.onModelFlushed(); - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - this.decorations.reset(); + this._lines.onModelFlushed(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + this._decorations.reset(); this.viewLayout.onFlushed(this.getLineCount()); hadOtherModelChange = true; break; } case textModelEvents.RawContentChangedType.LinesDeleted: { - const linesDeletedEvent = this.lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber); + const linesDeletedEvent = this._lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber); if (linesDeletedEvent !== null) { - eventsCollector.emit(linesDeletedEvent); + eventsCollector.emitViewEvent(linesDeletedEvent); this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); } hadOtherModelChange = true; @@ -265,9 +291,9 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const insertedLineBreaks = lineBreaks.slice(lineBreaksOffset, lineBreaksOffset + change.detail.length); lineBreaksOffset += change.detail.length; - const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); + const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { - eventsCollector.emit(linesInsertedEvent); + eventsCollector.emitViewEvent(linesInsertedEvent); this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); } hadOtherModelChange = true; @@ -277,17 +303,17 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const changedLineBreakData = lineBreaks[lineBreaksOffset]; lineBreaksOffset++; - const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); + const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this._lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); hadModelLineChangeThatChangedLineMapping = lineMappingChanged; if (linesChangedEvent) { - eventsCollector.emit(linesChangedEvent); + eventsCollector.emitViewEvent(linesChangedEvent); } if (linesInsertedEvent) { - eventsCollector.emit(linesInsertedEvent); + eventsCollector.emitViewEvent(linesInsertedEvent); this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); } if (linesDeletedEvent) { - eventsCollector.emit(linesDeletedEvent); + eventsCollector.emitViewEvent(linesDeletedEvent); this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); } break; @@ -298,35 +324,40 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } } } - this.lines.acceptVersionId(versionId); + this._lines.acceptVersionId(versionId); this.viewLayout.onHeightMaybeChanged(); if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(); - this.decorations.onLineMappingChanged(); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); } } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } // Update the configuration and reset the centered view line - this.viewportStartLine = -1; - this.configuration.setMaxLineNumber(this.model.getLineCount()); + this._viewportStartLine = -1; + this._configuration.setMaxLineNumber(this.model.getLineCount()); this._updateConfigurationViewLineCountNow(); // Recover viewport - if (!this.hasFocus && this.model.getAttachedEditorCount() >= 2 && this.viewportStartLineTrackedRange) { - const modelRange = this.model._getTrackedRange(this.viewportStartLineTrackedRange); + if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && this._viewportStartLineTrackedRange) { + const modelRange = this.model._getTrackedRange(this._viewportStartLineTrackedRange); if (modelRange) { const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition()); const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); - this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this.viewportStartLineDelta }, ScrollType.Immediate); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStartLineDelta }, ScrollType.Immediate); } } - this.cursor.onModelContentChanged(e); + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + this._cursor.onModelContentChanged(eventsCollector, e); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } })); this._register(this.model.onDidChangeTokens((e) => { @@ -340,7 +371,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel toLineNumber: viewEndLineNumber }; } - this._emitSingleViewEvent(new viewEvents.ViewTokensChangedEvent(viewRanges)); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensChangedEvent(viewRanges)); if (e.tokenizationSupportChanged) { this._tokenizeViewportSoon.schedule(); @@ -348,62 +379,65 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel })); this._register(this.model.onDidChangeLanguageConfiguration((e) => { - this._emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent()); - this.cursor.onDidChangeModelLanguageConfiguration(); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent()); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeLanguage((e) => { - this.cursor.onDidChangeModelLanguage(e); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeOptions((e) => { // A tab size change causes a line mapping changed event => all view parts will repaint OK, no further event needed here - if (this.lines.setTabSize(this.model.getOptions().tabSize)) { - this.cursor.onLineMappingChanged(); - this.decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + if (this._lines.setTabSize(this.model.getOptions().tabSize)) { try { - const eventsCollector = this._beginEmitViewEvents(); - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); + this.viewLayout.onFlushed(this.getLineCount()); } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } this._updateConfigurationViewLineCount.schedule(); } - this.cursor.onDidChangeModelOptions(); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeDecorations((e) => { - this.decorations.onModelDecorationsChanged(); - this._emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); + this._decorations.onModelDecorationsChanged(); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); })); } public setHiddenAreas(ranges: Range[]): void { try { - const eventsCollector = this._beginEmitViewEvents(); - let lineMappingChanged = this.lines.setHiddenAreas(ranges); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + let lineMappingChanged = this._lines.setHiddenAreas(ranges); if (lineMappingChanged) { - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(); - this.decorations.onLineMappingChanged(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); this.viewLayout.onHeightMaybeChanged(); } } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } this._updateConfigurationViewLineCount.schedule(); } public getVisibleRangesPlusViewportAboveBelow(): Range[] { - const layoutInfo = this.configuration.options.get(EditorOption.layoutInfo); - const lineHeight = this.configuration.options.get(EditorOption.lineHeight); + const layoutInfo = this._configuration.options.get(EditorOption.layoutInfo); + const lineHeight = this._configuration.options.get(EditorOption.lineHeight); const linesAround = Math.max(20, Math.round(layoutInfo.height / lineHeight)); const partialData = this.viewLayout.getLinesViewportData(); const startViewLineNumber = Math.max(1, partialData.completelyVisibleStartLineNumber - linesAround); @@ -422,7 +456,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel private _toModelVisibleRanges(visibleViewRange: Range): Range[] { const visibleRange = this.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); - const hiddenAreas = this.lines.getHiddenAreas(); + const hiddenAreas = this._lines.getHiddenAreas(); if (hiddenAreas.length === 0) { return [visibleRange]; @@ -527,48 +561,48 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel return this.model.getOptions().tabSize; } - public getOptions(): TextModelResolvedOptions { + public getTextModelOptions(): TextModelResolvedOptions { return this.model.getOptions(); } public getLineCount(): number { - return this.lines.getViewLineCount(); + return this._lines.getViewLineCount(); } /** * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { - this.viewportStartLine = startLineNumber; + this._viewportStartLine = startLineNumber; let position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber))); - this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this._viewportStartLineTrackedRange = this.model._setTrackedRange(this._viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); const viewportStartLineTop = this.viewLayout.getVerticalOffsetForLineNumber(startLineNumber); const scrollTop = this.viewLayout.getCurrentScrollTop(); - this.viewportStartLineDelta = scrollTop - viewportStartLineTop; + this._viewportStartLineDelta = scrollTop - viewportStartLineTop; } public getActiveIndentGuide(lineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo { - return this.lines.getActiveIndentGuide(lineNumber, minLineNumber, maxLineNumber); + return this._lines.getActiveIndentGuide(lineNumber, minLineNumber, maxLineNumber); } public getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[] { - return this.lines.getViewLinesIndentGuides(startLineNumber, endLineNumber); + return this._lines.getViewLinesIndentGuides(startLineNumber, endLineNumber); } public getLineContent(lineNumber: number): string { - return this.lines.getViewLineContent(lineNumber); + return this._lines.getViewLineContent(lineNumber); } public getLineLength(lineNumber: number): number { - return this.lines.getViewLineLength(lineNumber); + return this._lines.getViewLineLength(lineNumber); } public getLineMinColumn(lineNumber: number): number { - return this.lines.getViewLineMinColumn(lineNumber); + return this._lines.getViewLineMinColumn(lineNumber); } public getLineMaxColumn(lineNumber: number): number { - return this.lines.getViewLineMaxColumn(lineNumber); + return this._lines.getViewLineMaxColumn(lineNumber); } public getLineFirstNonWhitespaceColumn(lineNumber: number): number { @@ -588,15 +622,15 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[] { - return this.decorations.getDecorationsViewportData(visibleRange).decorations; + return this._decorations.getDecorationsViewportData(visibleRange).decorations; } public getViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData { let mightContainRTL = this.model.mightContainRTL(); let mightContainNonBasicASCII = this.model.mightContainNonBasicASCII(); let tabSize = this.getTabSize(); - let lineData = this.lines.getViewLineData(lineNumber); - let allInlineDecorations = this.decorations.getDecorationsViewportData(visibleRange).inlineDecorations; + let lineData = this._lines.getViewLineData(lineNumber); + let allInlineDecorations = this._decorations.getDecorationsViewportData(visibleRange).inlineDecorations; let inlineDecorations = allInlineDecorations[lineNumber - visibleRange.startLineNumber]; return new ViewLineRenderingData( @@ -614,11 +648,11 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getViewLineData(lineNumber: number): ViewLineData { - return this.lines.getViewLineData(lineNumber); + return this._lines.getViewLineData(lineNumber); } public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): MinimapLinesRenderingData { - let result = this.lines.getViewLinesData(startLineNumber, endLineNumber, needed); + let result = this._lines.getViewLinesData(startLineNumber, endLineNumber, needed); return new MinimapLinesRenderingData( this.getTabSize(), result @@ -626,7 +660,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getAllOverviewRulerDecorations(theme: EditorTheme): IOverviewRulerDecorations { - return this.lines.getAllOverviewRulerDecorations(this.editorId, filterValidationDecorations(this.configuration.options), theme); + return this._lines.getAllOverviewRulerDecorations(this._editorId, filterValidationDecorations(this._configuration.options), theme); } public invalidateOverviewRulerColorCache(): void { @@ -768,7 +802,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel range = new Range(lineNumber, this.model.getLineMinColumn(lineNumber), lineNumber, this.model.getLineMaxColumn(lineNumber)); } - const fontInfo = this.configuration.options.get(EditorOption.fontInfo); + const fontInfo = this._configuration.options.get(EditorOption.fontInfo); const colorMap = this._getColorMap(); const fontFamily = fontInfo.fontFamily === EDITOR_FONT_DEFAULTS.fontFamily ? fontInfo.fontFamily : `'${fontInfo.fontFamily}', ${EDITOR_FONT_DEFAULTS.fontFamily}`; @@ -826,4 +860,150 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } return result; } + + //#region model + + public pushStackElement(): void { + this.model.pushStackElement(); + } + + //#endregion + + //#region cursor operations + + public getPrimaryCursorState(): CursorState { + return this._cursor.getPrimaryCursorState(); + } + public getLastAddedCursorIndex(): number { + return this._cursor.getLastAddedCursorIndex(); + } + public getCursorStates(): CursorState[] { + return this._cursor.getCursorStates(); + } + public setCursorStates(source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): void { + this._withViewEventsCollector(eventsCollector => this._cursor.setStates(eventsCollector, source, reason, states)); + } + public getCursorColumnSelectData(): IColumnSelectData { + return this._cursor.getCursorColumnSelectData(); + } + public setCursorColumnSelectData(columnSelectData: IColumnSelectData): void { + this._cursor.setCursorColumnSelectData(columnSelectData); + } + public getPrevEditOperationType(): EditOperationType { + return this._cursor.getPrevEditOperationType(); + } + public setPrevEditOperationType(type: EditOperationType): void { + this._cursor.setPrevEditOperationType(type); + } + public getSelection(): Selection { + return this._cursor.getSelection(); + } + public getSelections(): Selection[] { + return this._cursor.getSelections(); + } + public getPosition(): Position { + return this._cursor.getPrimaryCursorState().modelState.position; + } + public setSelections(source: string | null | undefined, selections: readonly ISelection[]): void { + this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections)); + } + public saveCursorState(): ICursorState[] { + return this._cursor.saveState(); + } + public restoreCursorState(states: ICursorState[]): void { + this._withViewEventsCollector(eventsCollector => this._cursor.restoreState(eventsCollector, states)); + } + + private _executeCursorEdit(callback: (eventsCollector: ViewModelEventsCollector) => void): void { + if (this._cursor.context.cursorConfig.readOnly) { + // we cannot edit when read only... + this._eventDispatcher.emitOutgoingEvent(new ReadOnlyEditAttemptEvent()); + return; + } + this._withViewEventsCollector(callback); + } + public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { + this._executeCursorEdit(eventsCollector => this._cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer)); + } + public startComposition(): void { + this._cursor.setIsDoingComposition(true); + this._executeCursorEdit(eventsCollector => this._cursor.startComposition(eventsCollector)); + } + public endComposition(source?: string | null | undefined): void { + this._cursor.setIsDoingComposition(false); + this._executeCursorEdit(eventsCollector => this._cursor.endComposition(eventsCollector, source)); + } + public type(text: string, source?: string | null | undefined): void { + this._executeCursorEdit(eventsCollector => this._cursor.type(eventsCollector, text, source)); + } + public replacePreviousChar(text: string, replaceCharCnt: number, source?: string | null | undefined): void { + this._executeCursorEdit(eventsCollector => this._cursor.replacePreviousChar(eventsCollector, text, replaceCharCnt, source)); + } + public paste(text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void { + this._executeCursorEdit(eventsCollector => this._cursor.paste(eventsCollector, text, pasteOnNewLine, multicursorText, source)); + } + public cut(source?: string | null | undefined): void { + this._executeCursorEdit(eventsCollector => this._cursor.cut(eventsCollector, source)); + } + public executeCommand(command: ICommand, source?: string | null | undefined): void { + this._executeCursorEdit(eventsCollector => this._cursor.executeCommand(eventsCollector, command, source)); + } + public executeCommands(commands: ICommand[], source?: string | null | undefined): void { + this._executeCursorEdit(eventsCollector => this._cursor.executeCommands(eventsCollector, commands, source)); + } + public revealPrimaryCursor(source: string | null | undefined, revealHorizontal: boolean): void { + this._withViewEventsCollector(eventsCollector => this._cursor.revealPrimary(eventsCollector, source, revealHorizontal, ScrollType.Smooth)); + } + public revealTopMostCursor(source: string | null | undefined): void { + const viewPosition = this._cursor.getTopMostViewPosition(); + const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); + this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth))); + } + public revealBottomMostCursor(source: string | null | undefined): void { + const viewPosition = this._cursor.getBottomMostViewPosition(); + const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); + this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth))); + } + public revealRange(source: string | null | undefined, revealHorizontal: boolean, viewRange: Range, verticalType: viewEvents.VerticalRevealType, scrollType: ScrollType): void { + this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, verticalType, revealHorizontal, scrollType))); + } + + //#endregion + + //#region viewLayout + public getVerticalOffsetForLineNumber(viewLineNumber: number): number { + return this.viewLayout.getVerticalOffsetForLineNumber(viewLineNumber); + } + public getScrollTop(): number { + return this.viewLayout.getCurrentScrollTop(); + } + public setScrollTop(newScrollTop: number, scrollType: ScrollType): void { + this.viewLayout.setScrollPosition({ scrollTop: newScrollTop }, scrollType); + } + public setScrollPosition(position: INewScrollPosition, type: ScrollType): void { + this.viewLayout.setScrollPosition(position, type); + } + public deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void { + this.viewLayout.deltaScrollNow(deltaScrollLeft, deltaScrollTop); + } + public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): void { + const hadAChange = this.viewLayout.changeWhitespace(callback); + if (hadAChange) { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewZonesChangedEvent()); + this._eventDispatcher.emitOutgoingEvent(new ViewZonesChangedEvent()); + } + } + public setMaxLineWidth(maxLineWidth: number): void { + this.viewLayout.setMaxLineWidth(maxLineWidth); + } + //#endregion + + private _withViewEventsCollector(callback: (eventsCollector: ViewModelEventsCollector) => void): void { + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + callback(eventsCollector); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } + } } diff --git a/src/vs/platform/files/node/files.ts b/src/vs/editor/contrib/anchorSelect/anchorSelect.css similarity index 79% rename from src/vs/platform/files/node/files.ts rename to src/vs/editor/contrib/anchorSelect/anchorSelect.css index bbb85376228..46ea76d2011 100644 --- a/src/vs/platform/files/node/files.ts +++ b/src/vs/editor/contrib/anchorSelect/anchorSelect.css @@ -3,5 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export const MIN_MAX_MEMORY_SIZE_MB = 2048; -export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; +.monaco-editor .selection-anchor { + background-color: #007ACC; + width: 2px !important; +} diff --git a/src/vs/editor/contrib/anchorSelect/anchorSelect.ts b/src/vs/editor/contrib/anchorSelect/anchorSelect.ts new file mode 100644 index 00000000000..80011cf5909 --- /dev/null +++ b/src/vs/editor/contrib/anchorSelect/anchorSelect.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./anchorSelect'; +import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { localize } from 'vs/nls'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { Selection } from 'vs/editor/common/core/selection'; +import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { TrackedRangeStickiness } from 'vs/editor/common/model'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { alert } from 'vs/base/browser/ui/aria/aria'; + +export const SelectionAnchorSet = new RawContextKey('selectionAnchorSet', false); + +class SelectionAnchorController implements IEditorContribution { + + public static readonly ID = 'editor.contrib.selectionAnchorController'; + + static get(editor: ICodeEditor): SelectionAnchorController { + return editor.getContribution(SelectionAnchorController.ID); + } + + private decorationId: string | undefined; + private selectionAnchorSetContextKey: IContextKey; + private modelChangeListener: IDisposable; + + constructor( + private editor: ICodeEditor, + @IContextKeyService contextKeyService: IContextKeyService + ) { + this.selectionAnchorSetContextKey = SelectionAnchorSet.bindTo(contextKeyService); + this.modelChangeListener = editor.onDidChangeModel(() => this.selectionAnchorSetContextKey.reset()); + } + + setSelectionAnchor(): void { + if (this.editor.hasModel()) { + const position = this.editor.getPosition(); + const previousDecorations = this.decorationId ? [this.decorationId] : []; + const newDecorationId = this.editor.deltaDecorations(previousDecorations, [{ + range: Selection.fromPositions(position, position), + options: { + stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, + hoverMessage: new MarkdownString().appendText(localize('selectionAnchor', "Selection Anchor")), + className: 'selection-anchor' + } + }]); + this.decorationId = newDecorationId[0]; + this.selectionAnchorSetContextKey.set(!!this.decorationId); + alert(localize('anchorSet', "Anchor set at {0}:{1}", position.lineNumber, position.column)); + } + } + + goToSelectionAnchor(): void { + if (this.editor.hasModel() && this.decorationId) { + const anchorPosition = this.editor.getModel().getDecorationRange(this.decorationId); + if (anchorPosition) { + this.editor.setPosition(anchorPosition.getStartPosition()); + } + } + } + + selectFromAnchorToCursor(): void { + if (this.editor.hasModel() && this.decorationId) { + const start = this.editor.getModel().getDecorationRange(this.decorationId); + if (start) { + const end = this.editor.getPosition(); + this.editor.setSelection(Selection.fromPositions(start.getStartPosition(), end)); + this.cancelSelectionAnchor(); + } + } + } + + cancelSelectionAnchor(): void { + if (this.decorationId) { + this.editor.deltaDecorations([this.decorationId], []); + this.decorationId = undefined; + this.selectionAnchorSetContextKey.set(false); + } + } + + dispose(): void { + this.cancelSelectionAnchor(); + this.modelChangeListener.dispose(); + } +} + +class SetSelectionAnchor extends EditorAction { + constructor() { + super({ + id: 'editor.action.setSelectionAnchor', + label: localize('setSelectionAnchor', "Set Selection Anchor"), + alias: 'Set Selection Anchor', + precondition: undefined, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_B), + weight: KeybindingWeight.EditorContrib + } + }); + } + + async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = SelectionAnchorController.get(editor); + controller.setSelectionAnchor(); + } +} + +class GoToSelectionAnchor extends EditorAction { + constructor() { + super({ + id: 'editor.action.goToSelectionAnchor', + label: localize('goToSelectionAnchor', "Go to Selection Anchor"), + alias: 'Go to Selection Anchor', + precondition: SelectionAnchorSet, + }); + } + + async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = SelectionAnchorController.get(editor); + controller.goToSelectionAnchor(); + } +} + +class SelectFromAnchorToCursor extends EditorAction { + constructor() { + super({ + id: 'editor.action.selectFromAnchorToCursor', + label: localize('selectFromAnchorToCursor', "Select from Anchor to Cursor"), + alias: 'Select from Anchor to Cursor', + precondition: SelectionAnchorSet, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K), + weight: KeybindingWeight.EditorContrib + } + }); + } + + async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = SelectionAnchorController.get(editor); + controller.selectFromAnchorToCursor(); + } +} + +class CancelSelectionAnchor extends EditorAction { + constructor() { + super({ + id: 'editor.action.cancelSelectionAnchor', + label: localize('cancelSelectionAnchor', "Cancel Selection Anchor"), + alias: 'Cancel Selection Anchor', + precondition: SelectionAnchorSet, + kbOpts: { + kbExpr: EditorContextKeys.editorTextFocus, + primary: KeyCode.Escape, + weight: KeybindingWeight.EditorContrib + } + }); + } + + async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const controller = SelectionAnchorController.get(editor); + controller.cancelSelectionAnchor(); + } +} + +registerEditorContribution(SelectionAnchorController.ID, SelectionAnchorController); +registerEditorAction(SetSelectionAnchor); +registerEditorAction(GoToSelectionAnchor); +registerEditorAction(SelectFromAnchorToCursor); +registerEditorAction(CancelSelectionAnchor); diff --git a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts index ae17d2f9de1..0a11a1a5f99 100644 --- a/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts +++ b/src/vs/editor/contrib/bracketMatching/test/bracketMatching.test.ts @@ -33,7 +33,7 @@ suite('bracket matching', () => { let mode = new BracketMode(); let model = createTextModel('var x = (3 + (5-7)) + ((5+3)+5);', undefined, mode.getLanguageIdentifier()); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor) => { let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); // start on closing bracket @@ -65,7 +65,7 @@ suite('bracket matching', () => { let mode = new BracketMode(); let model = createTextModel('var x = (3 + (5-7)); y();', undefined, mode.getLanguageIdentifier()); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor) => { let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); // start position between brackets @@ -102,7 +102,7 @@ suite('bracket matching', () => { let mode = new BracketMode(); let model = createTextModel('var x = (3 + (5-7)); y();', undefined, mode.getLanguageIdentifier()); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor) => { let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); @@ -154,7 +154,7 @@ suite('bracket matching', () => { const mode = new BracketMode(); const model = createTextModel(text, undefined, mode.getLanguageIdentifier()); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor) => { const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); editor.setPosition(new Position(3, 5)); @@ -179,7 +179,7 @@ suite('bracket matching', () => { const mode = new BracketMode(); const model = createTextModel(text, undefined, mode.getLanguageIdentifier()); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor) => { const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); editor.setPosition(new Position(3, 5)); @@ -197,7 +197,7 @@ suite('bracket matching', () => { let mode = new BracketMode(); let model = createTextModel('{ } { } { }', undefined, mode.getLanguageIdentifier()); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor) => { let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController); // cursors inside brackets become selections of the entire bracket contents diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index b22036d6b2f..8d2f6668924 100644 --- a/src/vs/editor/contrib/find/findController.ts +++ b/src/vs/editor/contrib/find/findController.ts @@ -359,7 +359,7 @@ export class CommonFindController extends Disposable implements IEditorContribut && this._editor.hasModel() && !this._editor.getModel().isTooLargeForSyncing() ) { - return this._clipboardService.readFindText(); + return this._clipboardService.readFindTextSync(); } return ''; } @@ -370,7 +370,7 @@ export class CommonFindController extends Disposable implements IEditorContribut && this._editor.hasModel() && !this._editor.getModel().isTooLargeForSyncing() ) { - this._clipboardService.writeFindText(text); + this._clipboardService.writeFindTextSync(text); } } } diff --git a/src/vs/editor/contrib/find/findModel.ts b/src/vs/editor/contrib/find/findModel.ts index c083478dd00..897af3ef6b2 100644 --- a/src/vs/editor/contrib/find/findModel.ts +++ b/src/vs/editor/contrib/find/findModel.ts @@ -10,7 +10,7 @@ import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; +import { Range, IRange } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; import { Constants } from 'vs/base/common/uint'; import { ScrollType, ICommand } from 'vs/editor/common/editorCommon'; @@ -23,6 +23,7 @@ import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/repla import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { binarySearch, findFirstInSorted } from 'vs/base/common/arrays'; export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey('findWidgetVisible', false); export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated(); @@ -189,8 +190,17 @@ export class FindModelBoundToEditorModel { let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT); this._decorations.set(findMatches, findScope); + const editorSelection = this._editor.getSelection(); + let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection); + if (currentMatchesPosition === 0 && findMatches.length > 0) { + // current selection is not on top of a match + // try to find its nearest result from the top of the document + const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.range), range => Range.compareRangesUsingStarts(range, editorSelection) >= 0); + currentMatchesPosition = matchAfterSelection > 0 ? matchAfterSelection - 1 + 1 /** match position is one based */ : currentMatchesPosition; + } + this._state.changeMatchInfo( - this._decorations.getCurrentMatchesPosition(this._editor.getSelection()), + currentMatchesPosition, this._decorations.getCount(), undefined ); diff --git a/src/vs/editor/contrib/find/test/find.test.ts b/src/vs/editor/contrib/find/test/find.test.ts index a0e8e987ef3..98f31f328cb 100644 --- a/src/vs/editor/contrib/find/test/find.test.ts +++ b/src/vs/editor/contrib/find/test/find.test.ts @@ -16,7 +16,7 @@ suite('Find', () => { withTestCodeEditor([ 'ABC DEF', '0123 456' - ], {}, (editor, cursor) => { + ], {}, (editor) => { // The cursor is at the very top, of the file, at the first ABC let searchStringAtTop = getSelectionSearchString(editor); @@ -39,7 +39,7 @@ suite('Find', () => { withTestCodeEditor([ 'ABC DEF', '0123 456' - ], {}, (editor, cursor) => { + ], {}, (editor) => { // Select A of ABC editor.setSelection(new Range(1, 1, 1, 2)); @@ -63,7 +63,7 @@ suite('Find', () => { withTestCodeEditor([ 'ABC DEF', '0123 456' - ], {}, (editor, cursor) => { + ], {}, (editor) => { // Select first line and newline editor.setSelection(new Range(1, 1, 2, 1)); diff --git a/src/vs/editor/contrib/find/test/findController.test.ts b/src/vs/editor/contrib/find/test/findController.test.ts index 633a70af76e..44895a2d658 100644 --- a/src/vs/editor/contrib/find/test/findController.test.ts +++ b/src/vs/editor/contrib/find/test/findController.test.ts @@ -83,7 +83,7 @@ suite('FindController', () => { 'ABC', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; if (!platform.isMacintosh) { assert.ok(true); @@ -107,7 +107,7 @@ suite('FindController', () => { 'ABC', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = 'ABC'; if (!platform.isMacintosh) { @@ -134,7 +134,7 @@ suite('FindController', () => { 'ABC', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; if (!platform.isMacintosh) { assert.ok(true); @@ -158,7 +158,7 @@ suite('FindController', () => { 'ABC', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -213,7 +213,7 @@ suite('FindController', () => { test('issue #3090: F3 does not loop with two matches on a single line', () => { withTestCodeEditor([ 'import nls = require(\'vs/nls\');' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); let nextMatchFindAction = new NextMatchFindAction(); @@ -238,7 +238,7 @@ suite('FindController', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); let startFindAction = new StartFindAction(); @@ -262,7 +262,7 @@ suite('FindController', () => { test('issue #41027: Don\'t replace find input value on replace action if find input is active', () => { withTestCodeEditor([ 'test', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { let testRegexString = 'tes.'; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); let nextMatchFindAction = new NextMatchFindAction(); @@ -293,7 +293,7 @@ suite('FindController', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); findController.start({ @@ -322,7 +322,7 @@ suite('FindController', () => { test('issue #18111: Regex replace with single space replaces with no space', () => { withTestCodeEditor([ 'HRESULT OnAmbientPropertyChange(DISPID dispid);' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -349,7 +349,7 @@ suite('FindController', () => { '', 'line2', 'line3' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -376,7 +376,7 @@ suite('FindController', () => { '([funny]', '', '([funny]' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); let nextSelectionMatchFindAction = new NextSelectionMatchFindAction(); @@ -403,7 +403,7 @@ suite('FindController', () => { '([funny]', '', '([funny]' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { clipboardState = ''; let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); let startFindAction = new StartFindAction(); @@ -453,7 +453,7 @@ suite('FindController query options persistence', () => { 'ABC', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { queryState = { 'editor.isRegex': false, 'editor.matchCase': true, 'editor.wholeWord': false }; // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -480,7 +480,7 @@ suite('FindController query options persistence', () => { 'AB', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true }; // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -505,7 +505,7 @@ suite('FindController query options persistence', () => { 'AB', 'XYZ', 'ABC' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true }; // The cursor is at the very top, of the file, at the first ABC let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -521,7 +521,7 @@ suite('FindController query options persistence', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, (editor, cursor) => { + ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, (editor) => { // clipboardState = ''; editor.setSelection(new Range(1, 1, 2, 1)); let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -545,7 +545,7 @@ suite('FindController query options persistence', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, (editor, cursor) => { + ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, (editor) => { // clipboardState = ''; editor.setSelection(new Range(1, 2, 1, 2)); let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -569,7 +569,7 @@ suite('FindController query options persistence', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, (editor, cursor) => { + ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, (editor) => { // clipboardState = ''; editor.setSelection(new Range(1, 2, 1, 3)); let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); @@ -594,7 +594,7 @@ suite('FindController query options persistence', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'multiline', globalFindClipboard: false } }, (editor, cursor) => { + ], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'multiline', globalFindClipboard: false } }, (editor) => { // clipboardState = ''; editor.setSelection(new Range(1, 6, 2, 1)); let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController); diff --git a/src/vs/editor/contrib/find/test/findModel.test.ts b/src/vs/editor/contrib/find/test/findModel.test.ts index d83c9ce4d34..1a515fdddcc 100644 --- a/src/vs/editor/contrib/find/test/findModel.test.ts +++ b/src/vs/editor/contrib/find/test/findModel.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; -import { Cursor } from 'vs/editor/common/controller/cursor'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -21,7 +20,7 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te suite('FindModel', () => { - function findTest(testName: string, callback: (editor: IActiveCodeEditor, cursor: Cursor) => void): void { + function findTest(testName: string, callback: (editor: IActiveCodeEditor) => void): void { test(testName, () => { const textArr = [ '// my cool header', @@ -37,7 +36,7 @@ suite('FindModel', () => { '// blablablaciao', '' ]; - withTestCodeEditor(textArr, {}, (editor, cursor) => callback(editor as unknown as IActiveCodeEditor, cursor)); + withTestCodeEditor(textArr, {}, (editor) => callback(editor as IActiveCodeEditor)); const text = textArr.join('\n'); const ptBuilder = new PieceTreeTextBufferBuilder(); @@ -49,7 +48,7 @@ suite('FindModel', () => { { model: new TextModel(factory, TextModel.DEFAULT_CREATION_OPTIONS, null, null, new UndoRedoService(new TestDialogService(), new TestNotificationService())) }, - (editor, cursor) => callback(editor as unknown as IActiveCodeEditor, cursor) + (editor) => callback(editor as IActiveCodeEditor) ); }); } @@ -91,7 +90,7 @@ suite('FindModel', () => { assert.deepEqual(_getFindState(editor), expectedState, 'state'); } - findTest('incremental find from beginning of file', (editor, cursor) => { + findTest('incremental find from beginning of file', (editor) => { editor.setPosition({ lineNumber: 1, column: 1 }); let findState = new FindReplaceState(); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -241,7 +240,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model removes its decorations', (editor, cursor) => { + findTest('find model removes its decorations', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -271,7 +270,7 @@ suite('FindModel', () => { ); }); - findTest('find model updates state matchesCount', (editor, cursor) => { + findTest('find model updates state matchesCount', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -303,7 +302,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model reacts to position change', (editor, cursor) => { + findTest('find model reacts to position change', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -356,7 +355,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model next', (editor, cursor) => { + findTest('find model next', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -442,7 +441,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model next stays in scope', (editor, cursor) => { + findTest('find model next stays in scope', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 9, 1) }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -494,7 +493,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model prev', (editor, cursor) => { + findTest('find model prev', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -580,7 +579,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model prev stays in scope', (editor, cursor) => { + findTest('find model prev stays in scope', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 9, 1) }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -632,7 +631,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model next/prev with no matches', (editor, cursor) => { + findTest('find model next/prev with no matches', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'helloo', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -664,7 +663,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find model next/prev respects cursor position', (editor, cursor) => { + findTest('find model next/prev respects cursor position', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -713,7 +712,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find ^', (editor, cursor) => { + findTest('find ^', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '^', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -784,7 +783,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find $', (editor, cursor) => { + findTest('find $', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '$', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -876,7 +875,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find next ^$', (editor, cursor) => { + findTest('find next ^$', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '^$', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -928,7 +927,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find .*', (editor, cursor) => { + findTest('find .*', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '.*', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -957,7 +956,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find next ^.*$', (editor, cursor) => { + findTest('find next ^.*$', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '^.*$', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1028,7 +1027,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find prev ^.*$', (editor, cursor) => { + findTest('find prev ^.*$', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '^.*$', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1099,7 +1098,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('find prev ^$', (editor, cursor) => { + findTest('find prev ^$', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: '^$', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1151,7 +1150,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replace hello', (editor, cursor) => { + findTest('replace hello', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: 'hi', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1247,7 +1246,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replace bla', (editor, cursor) => { + findTest('replace bla', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'bla', replaceString: 'ciao' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1312,7 +1311,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll hello', (editor, cursor) => { + findTest('replaceAll hello', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: 'hi', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1360,7 +1359,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll two spaces with one space', (editor, cursor) => { + findTest('replaceAll two spaces with one space', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: ' ', replaceString: ' ' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1402,7 +1401,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll bla', (editor, cursor) => { + findTest('replaceAll bla', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'bla', replaceString: 'ciao' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1431,7 +1430,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll bla with \\t\\n', (editor, cursor) => { + findTest('replaceAll bla with \\t\\n', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'bla', replaceString: '<\\n\\t>', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1463,7 +1462,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #3516: "replace all" moves page/cursor/focus/scroll to the place of the last replacement', (editor, cursor) => { + findTest('issue #3516: "replace all" moves page/cursor/focus/scroll to the place of the last replacement', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'include', replaceString: 'bar' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1493,7 +1492,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('listens to model content changes', (editor, cursor) => { + findTest('listens to model content changes', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: 'hi', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1522,7 +1521,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('selectAllMatches', (editor, cursor) => { + findTest('selectAllMatches', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: 'hi', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1564,7 +1563,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #14143 selectAllMatches should maintain primary cursor if feasible', (editor, cursor) => { + findTest('issue #14143 selectAllMatches should maintain primary cursor if feasible', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: 'hi', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1610,7 +1609,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #1914: NPE when there is only one find match', (editor, cursor) => { + findTest('issue #1914: NPE when there is only one find match', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'cool.h' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1648,7 +1647,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replace when search string has look ahed regex', (editor, cursor) => { + findTest('replace when search string has look ahed regex', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello(?=\\sworld)', replaceString: 'hi', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1714,7 +1713,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replace when search string has look ahed regex and cursor is at the last find match', (editor, cursor) => { + findTest('replace when search string has look ahed regex and cursor is at the last find match', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello(?=\\sworld)', replaceString: 'hi', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1785,7 +1784,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll when search string has look ahed regex', (editor, cursor) => { + findTest('replaceAll when search string has look ahed regex', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello(?=\\sworld)', replaceString: 'hi', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1818,7 +1817,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replace when search string has look ahed regex and replace string has capturing groups', (editor, cursor) => { + findTest('replace when search string has look ahed regex and replace string has capturing groups', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hel(lo)(?=\\sworld)', replaceString: 'hi$1', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1884,7 +1883,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll when search string has look ahed regex and replace string has capturing groups', (editor, cursor) => { + findTest('replaceAll when search string has look ahed regex and replace string has capturing groups', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'wo(rl)d(?=.*;$)', replaceString: 'gi$1', isRegex: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1919,7 +1918,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll when search string is multiline and has look ahed regex and replace string has capturing groups', (editor, cursor) => { + findTest('replaceAll when search string is multiline and has look ahed regex and replace string has capturing groups', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'wo(rl)d(.*;\\n)(?=.*hello)', replaceString: 'gi$1$2', isRegex: true, matchCase: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1950,7 +1949,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('replaceAll preserving case', (editor, cursor) => { + findTest('replaceAll preserving case', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: 'goodbye', isRegex: false, matchCase: false, preserveCase: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -1986,7 +1985,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #18711 replaceAll with empty string', (editor, cursor) => { + findTest('issue #18711 replaceAll with empty string', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', replaceString: '', wholeWord: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -2018,7 +2017,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #32522 replaceAll with ^ on more than 1000 matches', (editor, cursor) => { + findTest('issue #32522 replaceAll with ^ on more than 1000 matches', (editor) => { let initialText = ''; for (let i = 0; i < 1100; i++) { initialText += 'line' + i + '\n'; @@ -2041,7 +2040,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #19740 Find and replace capture group/backreference inserts `undefined` instead of empty string', (editor, cursor) => { + findTest('issue #19740 Find and replace capture group/backreference inserts `undefined` instead of empty string', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello(z)?', replaceString: 'hi$1', isRegex: true, matchCase: true }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -2072,7 +2071,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #27083. search scope works even if it is a single line', (editor, cursor) => { + findTest('issue #27083. search scope works even if it is a single line', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', wholeWord: true, searchScope: new Range(7, 1, 8, 1) }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -2090,7 +2089,7 @@ suite('FindModel', () => { findState.dispose(); }); - findTest('issue #3516: Control behavior of "Next" operations (not looping back to beginning)', (editor, cursor) => { + findTest('issue #3516: Control behavior of "Next" operations (not looping back to beginning)', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello', loop: false }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); @@ -2170,7 +2169,7 @@ suite('FindModel', () => { }); - findTest('issue #3516: Control behavior of "Next" operations (looping back to beginning)', (editor, cursor) => { + findTest('issue #3516: Control behavior of "Next" operations (looping back to beginning)', (editor) => { let findState = new FindReplaceState(); findState.change({ searchString: 'hello' }, false); let findModel = new FindModelBoundToEditorModel(editor, findState); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 4c41acb0056..3b827fb5f53 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts @@ -12,7 +12,7 @@ import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; import { dispose, IDisposable, IReference, DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; -import { basenameOrAuthority, dirname, isEqual } from 'vs/base/common/resources'; +import { basenameOrAuthority, dirname } from 'vs/base/common/resources'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -62,12 +62,13 @@ class DecorationsManager implements IDisposable { private _onModelChanged(): void { this._callOnModelChange.clear(); const model = this._editor.getModel(); - if (model) { - for (const ref of this._model.groups) { - if (isEqual(ref.uri, model.uri)) { - this._addDecorations(ref); - return; - } + if (!model) { + return; + } + for (let ref of this._model.references) { + if (ref.uri.toString() === model.uri.toString()) { + this._addDecorations(ref.parent); + return; } } } @@ -76,7 +77,7 @@ class DecorationsManager implements IDisposable { if (!this._editor.hasModel()) { return; } - this._callOnModelChange.add(this._editor.getModel().onDidChangeDecorations((event) => this._onDecorationChanged())); + this._callOnModelChange.add(this._editor.getModel().onDidChangeDecorations(() => this._onDecorationChanged())); const newDecorations: IModelDeltaDecoration[] = []; const newDecorationsActualIndex: number[] = []; diff --git a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts index c325946c380..5c4bbe4d021 100644 --- a/src/vs/editor/contrib/gotoSymbol/referencesModel.ts +++ b/src/vs/editor/contrib/gotoSymbol/referencesModel.ts @@ -5,7 +5,7 @@ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { basename, compare, isEqual } from 'vs/base/common/resources'; +import { basename, extUri } from 'vs/base/common/resources'; import { IDisposable, dispose, IReference, DisposableStore } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; @@ -151,7 +151,7 @@ export class ReferencesModel implements IDisposable { let current: FileReferences | undefined; for (let link of links) { - if (!current || !isEqual(current.uri, link.uri, false, true)) { + if (!current || !extUri.isEqual(current.uri, link.uri, true)) { // new group current = new FileReferences(this, link.uri); this.groups.push(current); @@ -281,6 +281,6 @@ export class ReferencesModel implements IDisposable { } private static _compareReferences(a: Location, b: Location): number { - return compare(a.uri, b.uri, false, false) || Range.compareRangesUsingStarts(a.range, b.range); + return extUri.compare(a.uri, b.uri) || Range.compareRangesUsingStarts(a.range, b.range); } } diff --git a/src/vs/editor/contrib/hover/hover.ts b/src/vs/editor/contrib/hover/hover.ts index 3a0f4eea252..cb1f9a7a553 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./hover'; import * as nls from 'vs/nls'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; @@ -27,6 +26,7 @@ import { IMarkerDecorationsService } from 'vs/editor/common/services/markersDeco import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { GotoDefinitionAtPositionEditorContribution } from 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; export class ModesHoverController implements IEditorContribution { @@ -57,6 +57,8 @@ export class ModesHoverController implements IEditorContribution { private _isHoverEnabled!: boolean; private _isHoverSticky!: boolean; + private _hoverVisibleKey: IContextKey; + static get(editor: ICodeEditor): ModesHoverController { return editor.getContribution(ModesHoverController.ID); } @@ -66,7 +68,8 @@ export class ModesHoverController implements IEditorContribution { @IModeService private readonly _modeService: IModeService, @IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IThemeService private readonly _themeService: IThemeService + @IThemeService private readonly _themeService: IThemeService, + @IContextKeyService _contextKeyService: IContextKeyService ) { this._isMouseDown = false; this._hoverClicked = false; @@ -80,6 +83,8 @@ export class ModesHoverController implements IEditorContribution { this._hookEvents(); } }); + + this._hoverVisibleKey = EditorContextKeys.hoverVisible.bindTo(_contextKeyService); } private _hookEvents(): void { @@ -205,7 +210,7 @@ export class ModesHoverController implements IEditorContribution { } private _createHoverWidgets() { - this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._markerDecorationsService, this._themeService, this._keybindingService, this._modeService, this._openerService); + this._contentWidget.value = new ModesContentHoverWidget(this._editor, this._hoverVisibleKey, this._markerDecorationsService, this._keybindingService, this._themeService, this._modeService, this._openerService); this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); } @@ -312,29 +317,29 @@ registerThemingParticipant((theme, collector) => { } const hoverBackground = theme.getColor(editorHoverBackground); if (hoverBackground) { - collector.addRule(`.monaco-editor .monaco-editor-hover { background-color: ${hoverBackground}; }`); + collector.addRule(`.monaco-editor .monaco-hover { background-color: ${hoverBackground}; }`); } const hoverBorder = theme.getColor(editorHoverBorder); if (hoverBorder) { - collector.addRule(`.monaco-editor .monaco-editor-hover { border: 1px solid ${hoverBorder}; }`); - collector.addRule(`.monaco-editor .monaco-editor-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); - collector.addRule(`.monaco-editor .monaco-editor-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); - collector.addRule(`.monaco-editor .monaco-editor-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-editor .monaco-hover { border: 1px solid ${hoverBorder}; }`); + collector.addRule(`.monaco-editor .monaco-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-editor .monaco-hover hr { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`); + collector.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${hoverBorder.transparent(0.5)}; }`); } const link = theme.getColor(textLinkForeground); if (link) { - collector.addRule(`.monaco-editor .monaco-editor-hover a { color: ${link}; }`); + collector.addRule(`.monaco-editor .monaco-hover a { color: ${link}; }`); } const hoverForeground = theme.getColor(editorHoverForeground); if (hoverForeground) { - collector.addRule(`.monaco-editor .monaco-editor-hover { color: ${hoverForeground}; }`); + collector.addRule(`.monaco-editor .monaco-hover { color: ${hoverForeground}; }`); } const actionsBackground = theme.getColor(editorHoverStatusBarBackground); if (actionsBackground) { - collector.addRule(`.monaco-editor .monaco-editor-hover .hover-row .actions { background-color: ${actionsBackground}; }`); + collector.addRule(`.monaco-editor .monaco-hover .hover-row .actions { background-color: ${actionsBackground}; }`); } const codeBackground = theme.getColor(textCodeBlockBackground); if (codeBackground) { - collector.addRule(`.monaco-editor .monaco-editor-hover code { background-color: ${codeBackground}; }`); + collector.addRule(`.monaco-editor .monaco-hover code { background-color: ${codeBackground}; }`); } }); diff --git a/src/vs/editor/contrib/hover/hoverWidgets.ts b/src/vs/editor/contrib/hover/hoverWidgets.ts index e3f9f62125a..06278716db2 100644 --- a/src/vs/editor/contrib/hover/hoverWidgets.ts +++ b/src/vs/editor/contrib/hover/hoverWidgets.ts @@ -3,27 +3,28 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { toggleClass } from 'vs/base/browser/dom'; +import * as dom from 'vs/base/browser/dom'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { Widget } from 'vs/base/browser/ui/widget'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IContentWidget, ICodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference, IOverlayWidget, IOverlayWidgetPosition } from 'vs/editor/browser/editorBrowser'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; +import { renderHoverAction, HoverWidget } from 'vs/base/browser/ui/hover/hoverWidget'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; export class ContentHoverWidget extends Widget implements IContentWidget { + protected readonly _hover: HoverWidget; private readonly _id: string; protected _editor: ICodeEditor; private _isVisible: boolean; - private readonly _containerDomNode: HTMLElement; - protected readonly _domNode: HTMLElement; protected _showAtPosition: Position | null; protected _showAtRange: Range | null; private _stoleFocus: boolean; - private readonly scrollbar: DomScrollableElement; // Editor.IContentWidget.allowEditorOverflow public allowEditorOverflow = true; @@ -34,29 +35,24 @@ export class ContentHoverWidget extends Widget implements IContentWidget { protected set isVisible(value: boolean) { this._isVisible = value; - toggleClass(this._containerDomNode, 'hidden', !this._isVisible); + dom.toggleClass(this._hover.containerDomNode, 'hidden', !this._isVisible); } - constructor(id: string, editor: ICodeEditor) { + constructor( + id: string, + editor: ICodeEditor, + private readonly _hoverVisibleKey: IContextKey, + private readonly _keybindingService: IKeybindingService + ) { super(); + + this._hover = this._register(new HoverWidget()); this._id = id; this._editor = editor; this._isVisible = false; this._stoleFocus = false; - this._containerDomNode = document.createElement('div'); - this._containerDomNode.className = 'monaco-editor-hover hidden'; - this._containerDomNode.tabIndex = 0; - this._containerDomNode.setAttribute('role', 'tooltip'); - - this._domNode = document.createElement('div'); - this._domNode.className = 'monaco-editor-hover-content'; - - this.scrollbar = new DomScrollableElement(this._domNode, {}); - this._register(this.scrollbar); - this._containerDomNode.appendChild(this.scrollbar.getDomNode()); - - this.onkeydown(this._containerDomNode, (e: IKeyboardEvent) => { + this.onkeydown(this._hover.containerDomNode, (e: IKeyboardEvent) => { if (e.equals(KeyCode.Escape)) { this.hide(); } @@ -82,13 +78,14 @@ export class ContentHoverWidget extends Widget implements IContentWidget { } public getDomNode(): HTMLElement { - return this._containerDomNode; + return this._hover.containerDomNode; } public showAt(position: Position, range: Range | null, focus: boolean): void { // Position has changed this._showAtPosition = position; this._showAtRange = range; + this._hoverVisibleKey.set(true); this.isVisible = true; this._editor.layoutContentWidget(this); @@ -97,7 +94,7 @@ export class ContentHoverWidget extends Widget implements IContentWidget { this._editor.render(); this._stoleFocus = focus; if (focus) { - this._containerDomNode.focus(); + this._hover.containerDomNode.focus(); } } @@ -106,6 +103,12 @@ export class ContentHoverWidget extends Widget implements IContentWidget { return; } + setTimeout(() => { + // Give commands a chance to see the key + if (!this.isVisible) { + this._hoverVisibleKey.set(false); + } + }, 0); this.isVisible = false; this._editor.layoutContentWidget(this); @@ -134,31 +137,33 @@ export class ContentHoverWidget extends Widget implements IContentWidget { } private updateFont(): void { - const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByClassName('code')); + const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName('code')); codeClasses.forEach(node => this._editor.applyFontInfo(node)); } protected updateContents(node: Node): void { - this._domNode.textContent = ''; - this._domNode.appendChild(node); + this._hover.contentsDomNode.textContent = ''; + this._hover.contentsDomNode.appendChild(node); this.updateFont(); this._editor.layoutContentWidget(this); - this.onContentsChange(); + this._hover.onContentsChanged(); } - protected onContentsChange(): void { - this.scrollbar.scanDomNode(); + protected _renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { + const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); + const keybindingLabel = keybinding ? keybinding.getLabel() : null; + return renderHoverAction(parent, actionOptions, keybindingLabel); } private layout(): void { const height = Math.max(this._editor.getLayoutInfo().height / 4, 250); const { fontSize, lineHeight } = this._editor.getOption(EditorOption.fontInfo); - this._domNode.style.fontSize = `${fontSize}px`; - this._domNode.style.lineHeight = `${lineHeight}px`; - this._domNode.style.maxHeight = `${height}px`; - this._domNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; + this._hover.contentsDomNode.style.fontSize = `${fontSize}px`; + this._hover.contentsDomNode.style.lineHeight = `${lineHeight}px`; + this._hover.contentsDomNode.style.maxHeight = `${height}px`; + this._hover.contentsDomNode.style.maxWidth = `${Math.max(this._editor.getLayoutInfo().width * 0.66, 500)}px`; } } @@ -177,7 +182,7 @@ export class GlyphHoverWidget extends Widget implements IOverlayWidget { this._isVisible = false; this._domNode = document.createElement('div'); - this._domNode.className = 'monaco-editor-hover hidden'; + this._domNode.className = 'monaco-hover hidden'; this._domNode.setAttribute('aria-hidden', 'true'); this._domNode.setAttribute('role', 'tooltip'); @@ -198,7 +203,7 @@ export class GlyphHoverWidget extends Widget implements IOverlayWidget { protected set isVisible(value: boolean) { this._isVisible = value; - toggleClass(this._domNode, 'hidden', !this._isVisible); + dom.toggleClass(this._domNode, 'hidden', !this._isVisible); } public getId(): string { diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 71055810784..1d7fb929b81 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -41,6 +41,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; import { textLinkForeground } from 'vs/platform/theme/common/colorRegistry'; import { Progress } from 'vs/platform/progress/common/progress'; +import { IContextKey } from 'vs/platform/contextkey/common/contextkey'; const $ = dom.$; @@ -212,13 +213,14 @@ export class ModesContentHoverWidget extends ContentHoverWidget { constructor( editor: ICodeEditor, + _hoverVisibleKey: IContextKey, markerDecorationsService: IMarkerDecorationsService, + keybindingService: IKeybindingService, private readonly _themeService: IThemeService, - private readonly _keybindingService: IKeybindingService, private readonly _modeService: IModeService, private readonly _openerService: IOpenerService = NullOpenerService, ) { - super(ModesContentHoverWidget.ID, editor); + super(ModesContentHoverWidget.ID, editor, _hoverVisibleKey, keybindingService); this._messages = []; this._lastRange = null; @@ -249,7 +251,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { })); this._register(TokenizationRegistry.onDidChange((e) => { if (this.isVisible && this._lastRange && this._messages.length > 0) { - this._domNode.textContent = ''; + this._hover.contentsDomNode.textContent = ''; this._renderMessages(this._lastRange, this._messages); } })); @@ -461,7 +463,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { const renderer = markdownDisposeables.add(new MarkdownRenderer(this._editor, this._modeService, this._openerService)); markdownDisposeables.add(renderer.onDidRenderCodeBlock(() => { hoverContentsElement.className = 'hover-contents code-hover-contents'; - this.onContentsChange(); + this._hover.onContentsChanged(); })); const renderedContents = markdownDisposeables.add(renderer.render(contents)); hoverContentsElement.appendChild(renderedContents.element); @@ -562,7 +564,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { const disposables = new DisposableStore(); const actionsElement = dom.append(hoverElement, $('div.actions')); if (markerHover.marker.severity === MarkerSeverity.Error || markerHover.marker.severity === MarkerSeverity.Warning || markerHover.marker.severity === MarkerSeverity.Info) { - disposables.add(this.renderAction(actionsElement, { + disposables.add(this._renderAction(actionsElement, { label: nls.localize('peek problem', "Peek Problem"), commandId: NextMarkerAction.ID, run: () => { @@ -600,7 +602,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { } })); - disposables.add(this.renderAction(actionsElement, { + disposables.add(this._renderAction(actionsElement, { label: nls.localize('quick fixes', "Quick Fix..."), commandId: QuickFixAction.Id, run: (target) => { @@ -633,25 +635,6 @@ export class ModesContentHoverWidget extends ContentHoverWidget { }); } - private renderAction(parent: HTMLElement, actionOptions: { label: string, iconClass?: string, run: (target: HTMLElement) => void, commandId: string }): IDisposable { - const actionContainer = dom.append(parent, $('div.action-container')); - const action = dom.append(actionContainer, $('a.action')); - action.setAttribute('href', '#'); - action.setAttribute('role', 'button'); - if (actionOptions.iconClass) { - dom.append(action, $(`span.icon.${actionOptions.iconClass}`)); - } - const label = dom.append(action, $('span')); - const keybinding = this._keybindingService.lookupKeybinding(actionOptions.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label; - return dom.addDisposableListener(actionContainer, dom.EventType.CLICK, e => { - e.stopPropagation(); - e.preventDefault(); - actionOptions.run(actionContainer); - }); - } - private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({ className: 'hoverHighlight' }); @@ -683,6 +666,6 @@ function hoverContentsEquals(first: HoverPart[], second: HoverPart[]): boolean { registerThemingParticipant((theme, collector) => { const linkFg = theme.getColor(textLinkForeground); if (linkFg) { - collector.addRule(`.monaco-editor-hover .hover-contents a.code-link span:hover { color: ${linkFg}; }`); + collector.addRule(`.monaco-hover .hover-contents a.code-link span:hover { color: ${linkFg}; }`); } }); diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 174302f90a1..bc7fd660d84 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -454,12 +454,12 @@ export class IndentLinesAction extends EditorAction { } public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - const cursors = editor._getCursors(); - if (!cursors) { + const viewModel = editor._getViewModel(); + if (!viewModel) { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.indent(cursors.context.config, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, TypeOperations.indent(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); editor.pushUndoStop(); } } @@ -500,12 +500,12 @@ export class InsertLineBeforeAction extends EditorAction { } public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - const cursors = editor._getCursors(); - if (!cursors) { + const viewModel = editor._getViewModel(); + if (!viewModel) { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertBefore(cursors.context.config, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, TypeOperations.lineInsertBefore(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } @@ -525,12 +525,12 @@ export class InsertLineAfterAction extends EditorAction { } public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { - const cursors = editor._getCursors(); - if (!cursors) { + const viewModel = editor._getViewModel(); + if (!viewModel) { return; } editor.pushUndoStop(); - editor.executeCommands(this.id, TypeOperations.lineInsertAfter(cursors.context.config, editor.getModel(), editor.getSelections())); + editor.executeCommands(this.id, TypeOperations.lineInsertAfter(viewModel.cursorConfig, editor.getModel(), editor.getSelections())); } } diff --git a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts index 446c947cf8d..6777693d4ea 100644 --- a/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/copyLinesCommand.test.ts @@ -204,7 +204,7 @@ suite('Editor Contrib - Duplicate Selection', () => { const duplicateSelectionAction = new DuplicateSelectionAction(); function testDuplicateSelectionAction(lines: string[], selections: Selection[], expectedLines: string[], expectedSelections: Selection[]): void { - withTestCodeEditor(lines.join('\n'), {}, (editor, cursor) => { + withTestCodeEditor(lines.join('\n'), {}, (editor) => { editor.setSelections(selections); duplicateSelectionAction.run(null!, editor, {}); assert.deepEqual(editor.getValue(), expectedLines.join('\n')); diff --git a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts index d0dc6b57285..cdcb7f49128 100644 --- a/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts +++ b/src/vs/editor/contrib/linesOperations/test/linesOperations.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; -import { Cursor } from 'vs/editor/common/controller/cursor'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { Handler } from 'vs/editor/common/editorCommon'; @@ -14,6 +13,7 @@ import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import type { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction } from 'vs/editor/browser/editorExtensions'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; function assertSelection(editor: ICodeEditor, expected: Selection | Selection[]): void { if (!Array.isArray(expected)) { @@ -831,39 +831,39 @@ suite('Editor Contrib - Line Operations', () => { }); test('InsertLineBeforeAction', () => { - function testInsertLineBefore(lineNumber: number, column: number, callback: (model: ITextModel, cursor: Cursor) => void): void { + function testInsertLineBefore(lineNumber: number, column: number, callback: (model: ITextModel, viewModel: ViewModel) => void): void { const TEXT = [ 'First line', 'Second line', 'Third line' ]; - withTestCodeEditor(TEXT, {}, (editor, cursor) => { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { editor.setPosition(new Position(lineNumber, column)); let insertLineBeforeAction = new InsertLineBeforeAction(); executeAction(insertLineBeforeAction, editor); - callback(editor.getModel()!, cursor); + callback(editor.getModel()!, viewModel); }); } - testInsertLineBefore(1, 3, (model, cursor) => { - assert.deepEqual(cursor.getSelection(), new Selection(1, 1, 1, 1)); + testInsertLineBefore(1, 3, (model, viewModel) => { + assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 1)); assert.equal(model.getLineContent(1), ''); assert.equal(model.getLineContent(2), 'First line'); assert.equal(model.getLineContent(3), 'Second line'); assert.equal(model.getLineContent(4), 'Third line'); }); - testInsertLineBefore(2, 3, (model, cursor) => { - assert.deepEqual(cursor.getSelection(), new Selection(2, 1, 2, 1)); + testInsertLineBefore(2, 3, (model, viewModel) => { + assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); assert.equal(model.getLineContent(1), 'First line'); assert.equal(model.getLineContent(2), ''); assert.equal(model.getLineContent(3), 'Second line'); assert.equal(model.getLineContent(4), 'Third line'); }); - testInsertLineBefore(3, 3, (model, cursor) => { - assert.deepEqual(cursor.getSelection(), new Selection(3, 1, 3, 1)); + testInsertLineBefore(3, 3, (model, viewModel) => { + assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); assert.equal(model.getLineContent(1), 'First line'); assert.equal(model.getLineContent(2), 'Second line'); assert.equal(model.getLineContent(3), ''); @@ -872,39 +872,39 @@ suite('Editor Contrib - Line Operations', () => { }); test('InsertLineAfterAction', () => { - function testInsertLineAfter(lineNumber: number, column: number, callback: (model: ITextModel, cursor: Cursor) => void): void { + function testInsertLineAfter(lineNumber: number, column: number, callback: (model: ITextModel, viewModel: ViewModel) => void): void { const TEXT = [ 'First line', 'Second line', 'Third line' ]; - withTestCodeEditor(TEXT, {}, (editor, cursor) => { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { editor.setPosition(new Position(lineNumber, column)); let insertLineAfterAction = new InsertLineAfterAction(); executeAction(insertLineAfterAction, editor); - callback(editor.getModel()!, cursor); + callback(editor.getModel()!, viewModel); }); } - testInsertLineAfter(1, 3, (model, cursor) => { - assert.deepEqual(cursor.getSelection(), new Selection(2, 1, 2, 1)); + testInsertLineAfter(1, 3, (model, viewModel) => { + assert.deepEqual(viewModel.getSelection(), new Selection(2, 1, 2, 1)); assert.equal(model.getLineContent(1), 'First line'); assert.equal(model.getLineContent(2), ''); assert.equal(model.getLineContent(3), 'Second line'); assert.equal(model.getLineContent(4), 'Third line'); }); - testInsertLineAfter(2, 3, (model, cursor) => { - assert.deepEqual(cursor.getSelection(), new Selection(3, 1, 3, 1)); + testInsertLineAfter(2, 3, (model, viewModel) => { + assert.deepEqual(viewModel.getSelection(), new Selection(3, 1, 3, 1)); assert.equal(model.getLineContent(1), 'First line'); assert.equal(model.getLineContent(2), 'Second line'); assert.equal(model.getLineContent(3), ''); assert.equal(model.getLineContent(4), 'Third line'); }); - testInsertLineAfter(3, 3, (model, cursor) => { - assert.deepEqual(cursor.getSelection(), new Selection(4, 1, 4, 1)); + testInsertLineAfter(3, 3, (model, viewModel) => { + assert.deepEqual(viewModel.getSelection(), new Selection(4, 1, 4, 1)); assert.equal(model.getLineContent(1), 'First line'); assert.equal(model.getLineContent(2), 'Second line'); assert.equal(model.getLineContent(3), 'Third line'); diff --git a/src/vs/editor/contrib/multicursor/multicursor.ts b/src/vs/editor/contrib/multicursor/multicursor.ts index abebedd5080..cdb7d0c6190 100644 --- a/src/vs/editor/contrib/multicursor/multicursor.ts +++ b/src/vs/editor/contrib/multicursor/multicursor.ts @@ -9,7 +9,6 @@ import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; -import { RevealTarget } from 'vs/editor/common/controller/cursorCommon'; import { CursorChangeReason, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { CursorMoveCommands } from 'vs/editor/common/controller/cursorMoveCommands'; import { Range } from 'vs/editor/common/core/range'; @@ -61,20 +60,19 @@ export class InsertCursorAbove extends EditorAction { } const useLogicalLine = (args && args.logicalLine === true); - const cursors = editor._getCursors(); - const context = cursors.context; + const viewModel = editor._getViewModel(); - if (context.config.readOnly) { + if (viewModel.cursorConfig.readOnly) { return; } - context.model.pushStackElement(); - cursors.setStates( + viewModel.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.addCursorUp(context, cursors.getAll(), useLogicalLine) + CursorMoveCommands.addCursorUp(viewModel, viewModel.getCursorStates(), useLogicalLine) ); - cursors.reveal(args.source, true, RevealTarget.TopMost, ScrollType.Smooth); + viewModel.revealTopMostCursor(args.source); } } @@ -110,20 +108,19 @@ export class InsertCursorBelow extends EditorAction { } const useLogicalLine = (args && args.logicalLine === true); - const cursors = editor._getCursors(); - const context = cursors.context; + const viewModel = editor._getViewModel(); - if (context.config.readOnly) { + if (viewModel.cursorConfig.readOnly) { return; } - context.model.pushStackElement(); - cursors.setStates( + viewModel.pushStackElement(); + viewModel.setCursorStates( args.source, CursorChangeReason.Explicit, - CursorMoveCommands.addCursorDown(context, cursors.getAll(), useLogicalLine) + CursorMoveCommands.addCursorDown(viewModel, viewModel.getCursorStates(), useLogicalLine) ); - cursors.reveal(args.source, true, RevealTarget.BottomMost, ScrollType.Smooth); + viewModel.revealBottomMostCursor(args.source); } } diff --git a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts index a1276a70c7d..c40fa2be7a6 100644 --- a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts @@ -10,7 +10,7 @@ import { Handler } from 'vs/editor/common/editorCommon'; import { EndOfLineSequence } from 'vs/editor/common/model'; import { CommonFindController } from 'vs/editor/contrib/find/findController'; import { AddSelectionToNextFindMatchAction, InsertCursorAbove, InsertCursorBelow, MultiCursorSelectionController, SelectHighlightsAction } from 'vs/editor/contrib/multicursor/multicursor'; -import { TestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { ITestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IStorageService } from 'vs/platform/storage/common/storage'; @@ -20,12 +20,12 @@ suite('Multicursor', () => { withTestCodeEditor([ 'abc', 'def' - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { let addCursorUpAction = new InsertCursorAbove(); editor.setSelection(new Selection(2, 1, 2, 1)); addCursorUpAction.run(null!, editor, {}); - assert.equal(cursor.getSelections().length, 2); + assert.equal(viewModel.getSelections().length, 2); editor.trigger('test', Handler.Paste, { text: '1\n2', @@ -34,7 +34,7 @@ suite('Multicursor', () => { '2' ] }); - // cursorCommand(cursor, H.Paste, { text: '1\n2' }); + assert.equal(editor.getModel()!.getLineContent(1), '1abc'); assert.equal(editor.getModel()!.getLineContent(2), '2def'); }); @@ -43,10 +43,10 @@ suite('Multicursor', () => { test('issue #1336: Insert cursor below on last line adds a cursor to the end of the current line', () => { withTestCodeEditor([ 'abc' - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { let addCursorDownAction = new InsertCursorBelow(); addCursorDownAction.run(null!, editor, {}); - assert.equal(cursor.getSelections().length, 1); + assert.equal(viewModel.getSelections().length, 1); }); }); @@ -78,7 +78,7 @@ suite('Multicursor selection', () => { 'var x = (3 * 5)', 'var y = (3 * 5)', 'var z = (3 * 5)', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { let findController = editor.registerAndInstantiateContribution(CommonFindController.ID, CommonFindController); let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController.ID, MultiCursorSelectionController); @@ -108,7 +108,7 @@ suite('Multicursor selection', () => { 'someething', 'someeething', 'nothing' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { let findController = editor.registerAndInstantiateContribution(CommonFindController.ID, CommonFindController); let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController.ID, MultiCursorSelectionController); @@ -142,7 +142,7 @@ suite('Multicursor selection', () => { 'rty', 'qwe', 'rty' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { let findController = editor.registerAndInstantiateContribution(CommonFindController.ID, CommonFindController); let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController.ID, MultiCursorSelectionController); @@ -170,7 +170,7 @@ suite('Multicursor selection', () => { 'abcabc', 'abc', 'abcabc', - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { let findController = editor.registerAndInstantiateContribution(CommonFindController.ID, CommonFindController); let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController.ID, MultiCursorSelectionController); @@ -225,7 +225,7 @@ suite('Multicursor selection', () => { 'rty', 'qwe', 'rty' - ], { serviceCollection: serviceCollection }, (editor, cursor) => { + ], { serviceCollection: serviceCollection }, (editor) => { editor.getModel()!.setEOL(EndOfLineSequence.CRLF); @@ -250,8 +250,8 @@ suite('Multicursor selection', () => { }); }); - function testMulticursor(text: string[], callback: (editor: TestCodeEditor, findController: CommonFindController) => void): void { - withTestCodeEditor(text, { serviceCollection: serviceCollection }, (editor, cursor) => { + function testMulticursor(text: string[], callback: (editor: ITestCodeEditor, findController: CommonFindController) => void): void { + withTestCodeEditor(text, { serviceCollection: serviceCollection }, (editor) => { let findController = editor.registerAndInstantiateContribution(CommonFindController.ID, CommonFindController); let multiCursorSelectController = editor.registerAndInstantiateContribution(MultiCursorSelectionController.ID, MultiCursorSelectionController); @@ -262,7 +262,7 @@ suite('Multicursor selection', () => { }); } - function testAddSelectionToNextFindMatchAction(text: string[], callback: (editor: TestCodeEditor, action: AddSelectionToNextFindMatchAction, findController: CommonFindController) => void): void { + function testAddSelectionToNextFindMatchAction(text: string[], callback: (editor: ITestCodeEditor, action: AddSelectionToNextFindMatchAction, findController: CommonFindController) => void): void { testMulticursor(text, (editor, findController) => { let action = new AddSelectionToNextFindMatchAction(); callback(editor, action, findController); diff --git a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts index 809192427fa..e140bdc4cd5 100644 --- a/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts +++ b/src/vs/editor/contrib/parameterHints/parameterHintsWidget.ts @@ -24,6 +24,7 @@ import { HIGH_CONTRAST, registerThemingParticipant } from 'vs/platform/theme/com import { ParameterHintsModel, TriggerContext } from 'vs/editor/contrib/parameterHints/parameterHintsModel'; import { pad } from 'vs/base/common/strings'; import { registerIcon, Codicon } from 'vs/base/common/codicons'; +import { assertIsDefined } from 'vs/base/common/types'; const $ = dom.$; @@ -263,16 +264,16 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { } private hasDocs(signature: modes.SignatureInformation, activeParameter: modes.ParameterInformation | undefined): boolean { - if (activeParameter && typeof (activeParameter.documentation) === 'string' && activeParameter.documentation.length > 0) { + if (activeParameter && typeof activeParameter.documentation === 'string' && assertIsDefined(activeParameter.documentation).length > 0) { return true; } - if (activeParameter && typeof (activeParameter.documentation) === 'object' && activeParameter.documentation.value.length > 0) { + if (activeParameter && typeof activeParameter.documentation === 'object' && assertIsDefined(activeParameter.documentation).value.length > 0) { return true; } - if (typeof (signature.documentation) === 'string' && signature.documentation.length > 0) { + if (signature.documentation && typeof signature.documentation === 'string' && assertIsDefined(signature.documentation).length > 0) { return true; } - if (typeof (signature.documentation) === 'object' && signature.documentation.value.length > 0) { + if (signature.documentation && typeof signature.documentation === 'object' && assertIsDefined(signature.documentation.value).length > 0) { return true; } return false; diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts index 8a3177f139f..a7c6fd064fe 100644 --- a/src/vs/editor/contrib/rename/onTypeRename.ts +++ b/src/vs/editor/contrib/rename/onTypeRename.ts @@ -195,9 +195,9 @@ export class OnTypeRenameContribution extends Disposable implements IEditorContr try { this._ignoreChangeEvent = true; - const prevEditOperationType = this._editor._getCursors().getPrevEditOperationType(); + const prevEditOperationType = this._editor._getViewModel().getPrevEditOperationType(); this._editor.executeEdits('onTypeRename', edits); - this._editor._getCursors().setPrevEditOperationType(prevEditOperationType); + this._editor._getViewModel().setPrevEditOperationType(prevEditOperationType); } finally { this._ignoreChangeEvent = false; } diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 865d05eccdf..ec9fa64d5cc 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -28,7 +28,6 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IdleValue, raceCancellation } from 'vs/base/common/async'; -import { withNullAsUndefined } from 'vs/base/common/types'; import { ILogService } from 'vs/platform/log/common/log'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -38,6 +37,7 @@ import { ITextResourceConfigurationService } from 'vs/editor/common/services/tex class RenameSkeleton { private readonly _providers: RenameProvider[]; + private _providerRenameIdx: number = 0; constructor( private readonly model: ITextModel, @@ -51,30 +51,45 @@ class RenameSkeleton { } async resolveRenameLocation(token: CancellationToken): Promise { - const firstProvider = this._providers[0]; - if (!firstProvider) { - return undefined; - } - let res: RenameLocation & Rejection | undefined; - if (firstProvider.resolveRenameLocation) { - res = withNullAsUndefined(await firstProvider.resolveRenameLocation(this.model, this.position, token)); - } + const rejects: string[] = []; - if (!res) { - const word = this.model.getWordAtPosition(this.position); - if (word) { - return { - range: new Range(this.position.lineNumber, word.startColumn, this.position.lineNumber, word.endColumn), - text: word.word - }; + for (this._providerRenameIdx = 0; this._providerRenameIdx < this._providers.length; this._providerRenameIdx++) { + const provider = this._providers[this._providerRenameIdx]; + if (!provider.resolveRenameLocation) { + break; } + let res = await provider.resolveRenameLocation(this.model, this.position, token); + if (!res) { + continue; + } + if (res.rejectReason) { + rejects.push(res.rejectReason); + continue; + } + return res; } - return res; + const word = this.model.getWordAtPosition(this.position); + if (!word) { + return { + range: Range.fromPositions(this.position), + text: '', + rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined + }; + } + return { + range: new Range(this.position.lineNumber, word.startColumn, this.position.lineNumber, word.endColumn), + text: word.word, + rejectReason: rejects.length > 0 ? rejects.join('\n') : undefined + }; } - async provideRenameEdits(newName: string, i: number, rejects: string[], token: CancellationToken): Promise { + async provideRenameEdits(newName: string, token: CancellationToken): Promise { + return this._provideRenameEdits(newName, this._providerRenameIdx, [], token); + } + + private async _provideRenameEdits(newName: string, i: number, rejects: string[], token: CancellationToken): Promise { const provider = this._providers[i]; if (!provider) { return { @@ -85,16 +100,21 @@ class RenameSkeleton { const result = await provider.provideRenameEdits(this.model, this.position, newName, token); if (!result) { - return this.provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result.")), token); + return this._provideRenameEdits(newName, i + 1, rejects.concat(nls.localize('no result', "No result.")), token); } else if (result.rejectReason) { - return this.provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason), token); + return this._provideRenameEdits(newName, i + 1, rejects.concat(result.rejectReason), token); } return result; } } export async function rename(model: ITextModel, position: Position, newName: string): Promise { - return new RenameSkeleton(model, position).provideRenameEdits(newName, 0, [], CancellationToken.None); + const skeleton = new RenameSkeleton(model, position); + const loc = await skeleton.resolveRenameLocation(CancellationToken.None); + if (loc?.rejectReason) { + return { edits: [], rejectReason: loc.rejectReason }; + } + return skeleton.provideRenameEdits(newName, CancellationToken.None); } // --- register actions and commands @@ -194,7 +214,7 @@ class RenameController implements IEditorContribution { this.editor.focus(); - const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, 0, [], this._cts.token), this._cts.token).then(async renameResult => { + const renameOperation = raceCancellation(skeleton.provideRenameEdits(inputFieldResult.newName, this._cts.token), this._cts.token).then(async renameResult => { if (!renameResult || !this.editor.hasModel()) { return; diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index d527c20ea96..2be443c8c55 100644 --- a/src/vs/editor/contrib/rename/renameInputField.ts +++ b/src/vs/editor/contrib/rename/renameInputField.ts @@ -82,7 +82,7 @@ export class RenameInputField implements IContentWidget { const updateLabel = () => { const [accept, preview] = this._acceptKeybindings; this._keybindingService.lookupKeybinding(accept); - this._label!.innerText = localize('label', "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); + this._label!.innerText = localize({ key: 'label', comment: ['placeholders are keybindings, e.g "F2 to Rename, Shift+F2 to Preview"'] }, "{0} to Rename, {1} to Preview", this._keybindingService.lookupKeybinding(accept)?.getLabel(), this._keybindingService.lookupKeybinding(preview)?.getLabel()); }; updateLabel(); this._disposables.add(this._keybindingService.onDidUpdateKeybindings(updateLabel)); diff --git a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts index f57996f48c3..86177205b90 100644 --- a/src/vs/editor/contrib/rename/test/onTypeRename.test.ts +++ b/src/vs/editor/contrib/rename/test/onTypeRename.test.ts @@ -11,7 +11,7 @@ import { 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, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +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'; @@ -30,7 +30,7 @@ suite('On type rename', () => { disposables.clear(); }); - function createMockEditor(text: string | string[]) { + function createMockEditor(text: string | string[]): ITestCodeEditor { const model = typeof text === 'string' ? createTextModel(text, undefined, undefined, mockFile) : createTextModel(text.join('\n'), undefined, undefined, mockFile); @@ -46,7 +46,7 @@ suite('On type rename', () => { function testCase( name: string, initialState: { text: string | string[], ranges: Range[], stopPattern?: RegExp }, - operations: (editor: TestCodeEditor, contrib: OnTypeRenameContribution) => Promise, + operations: (editor: ITestCodeEditor, contrib: OnTypeRenameContribution) => Promise, expectedEndText: string | string[] ) { test(name, async () => { diff --git a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts index 8a1d8bfa9c2..8c9be33cf11 100644 --- a/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts +++ b/src/vs/editor/contrib/snippet/test/snippetController2.old.test.ts @@ -6,8 +6,7 @@ import * as assert from 'assert'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; -import { TestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { Cursor } from 'vs/editor/common/controller/cursor'; +import { ITestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { NullLogService } from 'vs/platform/log/common/log'; @@ -29,7 +28,7 @@ class TestSnippetController extends SnippetController2 { suite('SnippetController', () => { - function snippetTest(cb: (editor: TestCodeEditor, cursor: Cursor, template: string, snippetController: TestSnippetController) => void, lines?: string[]): void { + function snippetTest(cb: (editor: ITestCodeEditor, template: string, snippetController: TestSnippetController) => void, lines?: string[]): void { if (!lines) { lines = [ @@ -41,7 +40,7 @@ suite('SnippetController', () => { ]; } - withTestCodeEditor(lines, {}, (editor, cursor) => { + withTestCodeEditor(lines, {}, (editor) => { editor.getModel()!.updateOptions({ insertSpaces: false }); @@ -53,13 +52,13 @@ suite('SnippetController', () => { '}' ].join('\n'); - cb(editor, cursor, template, snippetController); + cb(editor, template, snippetController); snippetController.dispose(); }); } test('Simple accepted', () => { - snippetTest((editor, cursor, template, snippetController) => { + snippetTest((editor, template, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); @@ -95,7 +94,7 @@ suite('SnippetController', () => { }); test('Simple canceled', () => { - snippetTest((editor, cursor, template, snippetController) => { + snippetTest((editor, template, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(template); @@ -110,7 +109,7 @@ suite('SnippetController', () => { }); // test('Stops when deleting lines above', () => { - // snippetTest((editor, cursor, codeSnippet, snippetController) => { + // snippetTest((editor, codeSnippet, snippetController) => { // editor.setPosition({ lineNumber: 4, column: 2 }); // snippetController.insert(codeSnippet, 0, 0); @@ -127,7 +126,7 @@ suite('SnippetController', () => { // }); // test('Stops when deleting lines below', () => { - // snippetTest((editor, cursor, codeSnippet, snippetController) => { + // snippetTest((editor, codeSnippet, snippetController) => { // editor.setPosition({ lineNumber: 4, column: 2 }); // snippetController.run(codeSnippet, 0, 0); @@ -144,7 +143,7 @@ suite('SnippetController', () => { // }); // test('Stops when inserting lines above', () => { - // snippetTest((editor, cursor, codeSnippet, snippetController) => { + // snippetTest((editor, codeSnippet, snippetController) => { // editor.setPosition({ lineNumber: 4, column: 2 }); // snippetController.run(codeSnippet, 0, 0); @@ -161,7 +160,7 @@ suite('SnippetController', () => { // }); // test('Stops when inserting lines below', () => { - // snippetTest((editor, cursor, codeSnippet, snippetController) => { + // snippetTest((editor, codeSnippet, snippetController) => { // editor.setPosition({ lineNumber: 4, column: 2 }); // snippetController.run(codeSnippet, 0, 0); @@ -178,7 +177,7 @@ suite('SnippetController', () => { // }); test('Stops when calling model.setValue()', () => { - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(codeSnippet); @@ -189,7 +188,7 @@ suite('SnippetController', () => { }); test('Stops when undoing', () => { - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(codeSnippet); @@ -200,7 +199,7 @@ suite('SnippetController', () => { }); test('Stops when moving cursor outside', () => { - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(codeSnippet); @@ -211,7 +210,7 @@ suite('SnippetController', () => { }); test('Stops when disconnecting editor model', () => { - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(codeSnippet); @@ -222,7 +221,7 @@ suite('SnippetController', () => { }); test('Stops when disposing editor', () => { - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setPosition({ lineNumber: 4, column: 2 }); snippetController.insert(codeSnippet); @@ -233,7 +232,7 @@ suite('SnippetController', () => { }); test('Final tabstop with multiple selections', () => { - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setSelections([ new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), @@ -248,7 +247,7 @@ suite('SnippetController', () => { assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); }); - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setSelections([ new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), @@ -263,7 +262,7 @@ suite('SnippetController', () => { assert.ok(second.equalsRange({ startLineNumber: 2, startColumn: 4, endLineNumber: 2, endColumn: 4 }), second.toString()); }); - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setSelections([ new Selection(1, 1, 1, 1), new Selection(1, 5, 1, 5), @@ -278,7 +277,7 @@ suite('SnippetController', () => { assert.ok(second.equalsRange({ startLineNumber: 1, startColumn: 14, endLineNumber: 1, endColumn: 14 }), second.toString()); }); - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setSelections([ new Selection(1, 1, 1, 1), new Selection(1, 5, 1, 5), @@ -293,7 +292,7 @@ suite('SnippetController', () => { assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); }); - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setSelections([ new Selection(1, 1, 1, 1), new Selection(1, 5, 1, 5), @@ -308,7 +307,7 @@ suite('SnippetController', () => { assert.ok(second.equalsRange({ startLineNumber: 4, startColumn: 1, endLineNumber: 4, endColumn: 1 }), second.toString()); }); - snippetTest((editor, cursor, codeSnippet, snippetController) => { + snippetTest((editor, codeSnippet, snippetController) => { editor.setSelections([ new Selection(2, 7, 2, 7), ]); @@ -322,7 +321,7 @@ suite('SnippetController', () => { }); test('Final tabstop, #11742 simple', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelection(new Selection(1, 19, 1, 19)); @@ -335,7 +334,7 @@ suite('SnippetController', () => { }, ['example example sc']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelection(new Selection(1, 3, 1, 3)); @@ -353,7 +352,7 @@ suite('SnippetController', () => { }, ['af']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelection(new Selection(1, 3, 1, 3)); @@ -371,7 +370,7 @@ suite('SnippetController', () => { }, ['af']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelection(new Selection(1, 9, 1, 9)); @@ -390,7 +389,7 @@ suite('SnippetController', () => { test('Final tabstop, #11742 different indents', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(2, 4, 2, 4), @@ -416,7 +415,7 @@ suite('SnippetController', () => { test('Final tabstop, #11890 stay at the beginning', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 5, 1, 5) @@ -440,7 +439,7 @@ suite('SnippetController', () => { test('Final tabstop, no tabstop', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 3, 1, 3) @@ -457,7 +456,7 @@ suite('SnippetController', () => { test('Multiple cursor and overwriteBefore/After, issue #11060', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 7, 1, 7), @@ -470,7 +469,7 @@ suite('SnippetController', () => { }, ['this._', 'abc']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 7, 1, 7), @@ -483,7 +482,7 @@ suite('SnippetController', () => { }, ['this._', 'abc']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 7, 1, 7), @@ -497,7 +496,7 @@ suite('SnippetController', () => { }, ['this._', 'abc', 'def_']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 7, 1, 7), // primary at `this._` @@ -511,7 +510,7 @@ suite('SnippetController', () => { }, ['this._', 'abc', 'def._']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(3, 6, 3, 6), // primary at `def._` @@ -525,7 +524,7 @@ suite('SnippetController', () => { }, ['this._', 'abc', 'def._']); - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(2, 4, 2, 4), // primary at `abc` @@ -542,7 +541,7 @@ suite('SnippetController', () => { }); test('Multiple cursor and overwriteBefore/After, #16277', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 5, 1, 5), @@ -558,7 +557,7 @@ suite('SnippetController', () => { test('Insert snippet twice, #19449', () => { - snippetTest((editor, cursor, codeSnippet, controller) => { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 1, 1, 1) @@ -571,7 +570,7 @@ suite('SnippetController', () => { }, ['for (var i=0; i { + snippetTest((editor, codeSnippet, controller) => { editor.setSelections([ new Selection(1, 1, 1, 1) diff --git a/src/vs/editor/contrib/suggest/suggestMemory.ts b/src/vs/editor/contrib/suggest/suggestMemory.ts index 9fff61fe466..a33e6e3191f 100644 --- a/src/vs/editor/contrib/suggest/suggestMemory.ts +++ b/src/vs/editor/contrib/suggest/suggestMemory.ts @@ -26,7 +26,7 @@ export abstract class Memory { return 0; } let topScore = items[0].score[0]; - for (let i = 1; i < items.length; i++) { + for (let i = 0; i < items.length; i++) { const { score, completion: suggestion } = items[i]; if (score[0] !== topScore) { // stop when leaving the group of top matches diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index 42611587560..ffeccaf1ba3 100644 --- a/src/vs/editor/contrib/suggest/test/suggestController.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; -import { createTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { createTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { TextModel } from 'vs/editor/common/model/textModel'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -26,13 +26,14 @@ import { IMenuService, IMenu } from 'vs/platform/actions/common/actions'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { Range } from 'vs/editor/common/core/range'; import { timeout } from 'vs/base/common/async'; +import { NullLogService, ILogService } from 'vs/platform/log/common/log'; suite('SuggestController', function () { const disposables = new DisposableStore(); let controller: SuggestController; - let editor: TestCodeEditor; + let editor: ITestCodeEditor; let model: TextModel; setup(function () { @@ -40,6 +41,7 @@ suite('SuggestController', function () { const serviceCollection = new ServiceCollection( [ITelemetryService, NullTelemetryService], + [ILogService, new NullLogService()], [IStorageService, new InMemoryStorageService()], [IKeybindingService, new MockKeybindingService()], [IEditorWorkerService, new class extends mock() { diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index 778ed2ece6f..2b4b06edd41 100644 --- a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts @@ -21,7 +21,7 @@ import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2 import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { LineContext, SuggestModel } from 'vs/editor/contrib/suggest/suggestModel'; import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget'; -import { TestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { ITestCodeEditor, createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; @@ -43,7 +43,7 @@ export function mock(): Ctor { } -function createMockEditor(model: TextModel): TestCodeEditor { +function createMockEditor(model: TextModel): ITestCodeEditor { let editor = createTestCodeEditor({ model: model, serviceCollection: new ServiceCollection( @@ -192,7 +192,7 @@ suite('SuggestModel - TriggerAndCancelOracle', function () { disposables.push(model); }); - function withOracle(callback: (model: SuggestModel, editor: TestCodeEditor) => any): Promise { + function withOracle(callback: (model: SuggestModel, editor: ITestCodeEditor) => any): Promise { return new Promise((resolve, reject) => { const editor = createMockEditor(model); diff --git a/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts b/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts index aeab4fc11cc..0a7464eb0d4 100644 --- a/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts +++ b/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts @@ -27,7 +27,7 @@ class UnusualLineTerminatorsDetector extends Disposable implements IEditorContri public static readonly ID = 'editor.contrib.unusualLineTerminatorsDetector'; - private _enabled: boolean; + private _config: 'off' | 'prompt' | 'auto'; constructor( private readonly _editor: ICodeEditor, @@ -36,10 +36,10 @@ class UnusualLineTerminatorsDetector extends Disposable implements IEditorContri ) { super(); - this._enabled = this._editor.getOption(EditorOption.removeUnusualLineTerminators); + this._config = this._editor.getOption(EditorOption.unusualLineTerminators); this._register(this._editor.onDidChangeConfiguration((e) => { - if (e.hasChanged(EditorOption.removeUnusualLineTerminators)) { - this._enabled = this._editor.getOption(EditorOption.removeUnusualLineTerminators); + if (e.hasChanged(EditorOption.unusualLineTerminators)) { + this._config = this._editor.getOption(EditorOption.unusualLineTerminators); this._checkForUnusualLineTerminators(); } })); @@ -54,7 +54,7 @@ class UnusualLineTerminatorsDetector extends Disposable implements IEditorContri } private async _checkForUnusualLineTerminators(): Promise { - if (!this._enabled) { + if (this._config === 'off') { return; } if (!this._editor.hasModel()) { @@ -74,10 +74,16 @@ class UnusualLineTerminatorsDetector extends Disposable implements IEditorContri return; } + if (this._config === 'auto') { + // just do it! + model.removeUnusualLineTerminators(this._editor.getSelections()); + return; + } + const result = await this._dialogService.confirm({ title: nls.localize('unusualLineTerminators.title', "Unusual Line Terminators"), message: nls.localize('unusualLineTerminators.message', "Detected unusual line terminators"), - detail: nls.localize('unusualLineTerminators.detail', "Your file contains one or more unusual line terminator characters, like Line Separator (LS), Paragraph Separator (PS) or Next Line (NEL).\n\nThese characters can cause subtle problems with language servers, due to how each programming language specifies its line terminators. e.g. what is line 11 for VS Code might be line 12 for a language server.\n\nThis check can be disabled via `editor.removeUnusualLineTerminators`."), + detail: nls.localize('unusualLineTerminators.detail', "This file contains one or more unusual line terminator characters, like Line Separator (LS), Paragraph Separator (PS) or Next Line (NEL).\n\nIt is recommended to remove them from the file. This can be configured via `editor.unusualLineTerminators`."), primaryButton: nls.localize('unusualLineTerminators.fix', "Fix this file"), secondaryButton: nls.localize('unusualLineTerminators.ignore', "Ignore problem for this file") }); diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index 2b1d99f2aa9..ee38c0c8146 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts @@ -11,9 +11,8 @@ import { Selection } from 'vs/editor/common/core/selection'; import { deserializePipePositions, serializePipePositions, testRepeatedActionAndExtractPositions } from 'vs/editor/contrib/wordOperations/test/wordTestUtils'; import { CursorWordEndLeft, CursorWordEndLeftSelect, CursorWordEndRight, CursorWordEndRightSelect, CursorWordLeft, CursorWordLeftSelect, CursorWordRight, CursorWordRightSelect, CursorWordStartLeft, CursorWordStartLeftSelect, CursorWordStartRight, CursorWordStartRightSelect, DeleteWordEndLeft, DeleteWordEndRight, DeleteWordLeft, DeleteWordRight, DeleteWordStartLeft, DeleteWordStartRight, CursorWordAccessibilityLeft, CursorWordAccessibilityLeftSelect, CursorWordAccessibilityRight, CursorWordAccessibilityRightSelect } from 'vs/editor/contrib/wordOperations/wordOperations'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { Handler } from 'vs/editor/common/editorCommon'; -import { Cursor } from 'vs/editor/common/controller/cursor'; import { CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; suite('WordOperations', () => { @@ -113,7 +112,7 @@ suite('WordOperations', () => { ' Third Line🐶', '', '1', - ], {}, (editor, _) => { + ], {}, (editor) => { editor.setPosition(new Position(5, 2)); cursorWordLeft(editor, true); assert.deepEqual(editor.getSelection(), new Selection(5, 2, 5, 1)); @@ -197,23 +196,19 @@ suite('WordOperations', () => { }); test('issue #51275 - cursorWordStartLeft does not push undo/redo stack element', () => { - function cursorCommand(cursor: Cursor, command: string, extraData?: any, overwriteSource?: string) { - cursor.trigger(overwriteSource || 'tests', command, extraData); - } - - function type(cursor: Cursor, text: string) { + function type(viewModel: ViewModel, text: string) { for (let i = 0; i < text.length; i++) { - cursorCommand(cursor, Handler.Type, { text: text.charAt(i) }, 'keyboard'); + viewModel.type(text.charAt(i), 'keyboard'); } } - withTestCodeEditor('', {}, (editor, cursor) => { - type(cursor, 'foo bar baz'); + withTestCodeEditor('', {}, (editor, viewModel) => { + type(viewModel, 'foo bar baz'); assert.equal(editor.getValue(), 'foo bar baz'); cursorWordStartLeft(editor); cursorWordStartLeft(editor); - type(cursor, 'q'); + type(viewModel, 'q'); assert.equal(editor.getValue(), 'foo qbar baz'); diff --git a/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts b/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts index 68a6a2f6738..7ecf61a6925 100644 --- a/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts +++ b/src/vs/editor/contrib/wordOperations/test/wordTestUtils.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Position } from 'vs/editor/common/core/position'; -import { TestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { ITestCodeEditor, withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; export function deserializePipePositions(text: string): [string, Position[]] { let resultText = ''; @@ -58,9 +58,9 @@ export function serializePipePositions(text: string, positions: Position[]): str return resultText; } -export function testRepeatedActionAndExtractPositions(text: string, initialPosition: Position, action: (editor: TestCodeEditor) => void, record: (editor: TestCodeEditor) => Position, stopCondition: (editor: TestCodeEditor) => boolean): Position[] { +export function testRepeatedActionAndExtractPositions(text: string, initialPosition: Position, action: (editor: ITestCodeEditor) => void, record: (editor: ITestCodeEditor) => Position, stopCondition: (editor: ITestCodeEditor) => boolean): Position[] { let actualStops: Position[] = []; - withTestCodeEditor(text, {}, (editor, _) => { + withTestCodeEditor(text, {}, (editor) => { editor.setPosition(initialPosition); while (true) { action(editor); diff --git a/src/vs/editor/contrib/wordOperations/wordOperations.ts b/src/vs/editor/contrib/wordOperations/wordOperations.ts index ae5429ed0cc..791e831aab0 100644 --- a/src/vs/editor/contrib/wordOperations/wordOperations.ts +++ b/src/vs/editor/contrib/wordOperations/wordOperations.ts @@ -53,7 +53,7 @@ export abstract class MoveWordCommand extends EditorCommand { }); model.pushStackElement(); - editor._getCursors().setStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); + editor._getViewModel().setCursorStates('moveWordCommand', CursorChangeReason.NotSet, result.map(r => CursorState.fromModelSelection(r))); if (result.length === 1) { const pos = new Position(result[0].positionLineNumber, result[0].positionColumn); editor.revealPosition(pos, ScrollType.Smooth); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index ce4bc0b5dd5..a591facd8b7 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -7,6 +7,7 @@ import 'vs/editor/browser/controller/coreCommands'; import 'vs/editor/browser/widget/codeEditorWidget'; import 'vs/editor/browser/widget/diffEditorWidget'; import 'vs/editor/browser/widget/diffNavigator'; +import 'vs/editor/contrib/anchorSelect/anchorSelect'; import 'vs/editor/contrib/bracketMatching/bracketMatching'; import 'vs/editor/contrib/caretOperations/caretOperations'; import 'vs/editor/contrib/caretOperations/transpose'; diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 491fd218b9c..c5a48fbeff3 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -108,12 +108,11 @@ function withTypedEditor(widget: IEditor, codeEditorCallback: (editor: ICodeE export class SimpleEditorModelResolverService implements ITextModelService { public _serviceBrand: undefined; - private readonly modelService: IModelService | undefined; private editor?: IEditor; - constructor(modelService: IModelService | undefined) { - this.modelService = modelService; - } + constructor( + @IModelService private readonly modelService: IModelService + ) { } public setEditor(editor: IEditor): void { this.editor = editor; @@ -141,12 +140,12 @@ export class SimpleEditorModelResolverService implements ITextModelService { }; } - public hasTextModelContentProvider(scheme: string): boolean { + public canHandleResource(resource: URI): boolean { return false; } private findModel(editor: ICodeEditor, resource: URI): ITextModel | null { - let model = this.modelService ? this.modelService.getModel(resource) : editor.getModel(); + let model = this.modelService.getModel(resource); if (model && model.uri.toString() !== resource.toString()) { return null; } diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index 4d173457365..d78b42051da 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -18,7 +18,7 @@ stroke-width: 1.2px; } -.monaco-editor-hover p { +.monaco-hover p { margin: 0; } diff --git a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts index 3d4db53c943..db77616779f 100644 --- a/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts +++ b/src/vs/editor/standalone/browser/standaloneThemeServiceImpl.ts @@ -48,6 +48,10 @@ class StandaloneTheme implements IStandaloneTheme { this._tokenTheme = null; } + public get label(): string { + return this.themeName; + } + public get base(): string { return this.themeData.base; } diff --git a/src/vs/editor/standalone/common/monarch/monarchCommon.ts b/src/vs/editor/standalone/common/monarch/monarchCommon.ts index 757e208bade..6dbf14dc803 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCommon.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCommon.ts @@ -24,6 +24,7 @@ export interface ILexerMin { languageId: string; noThrow: boolean; ignoreCase: boolean; + unicode: boolean; usesEmbedded: boolean; defaultToken: string; stateNames: { [stateName: string]: any; }; @@ -34,6 +35,7 @@ export interface ILexer extends ILexerMin { maxStack: number; start: string | null; ignoreCase: boolean; + unicode: boolean; tokenPostfix: string; tokenizer: { [stateName: string]: IRule[]; }; diff --git a/src/vs/editor/standalone/common/monarch/monarchCompile.ts b/src/vs/editor/standalone/common/monarch/monarchCompile.ts index 2c98c6ba429..289d045aba0 100644 --- a/src/vs/editor/standalone/common/monarch/monarchCompile.ts +++ b/src/vs/editor/standalone/common/monarch/monarchCompile.ts @@ -79,7 +79,7 @@ function createKeywordMatcher(arr: string[], caseInsensitive: boolean = false): // Lexer helpers /** - * Compiles a regular expression string, adding the 'i' flag if 'ignoreCase' is set. + * Compiles a regular expression string, adding the 'i' flag if 'ignoreCase' is set, and the 'u' flag if 'unicode' is set. * Also replaces @\w+ or sequences with the content of the specified attribute */ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { @@ -103,7 +103,8 @@ function compileRegExp(lexer: monarchCommon.ILexerMin, str: string): RegExp { }); } - return new RegExp(str, (lexer.ignoreCase ? 'i' : '')); + let flags = (lexer.ignoreCase ? 'i' : '') + (lexer.unicode ? 'u' : ''); + return new RegExp(str, flags); } /** @@ -400,6 +401,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm // Set standard fields: be defensive about types lexer.start = (typeof json.start === 'string' ? json.start : null); lexer.ignoreCase = bool(json.ignoreCase, false); + lexer.unicode = bool(json.unicode, false); lexer.tokenPostfix = string(json.tokenPostfix, '.' + lexer.languageId); lexer.defaultToken = string(json.defaultToken, 'source'); @@ -410,6 +412,7 @@ export function compile(languageId: string, json: IMonarchLanguage): monarchComm let lexerMin: monarchCommon.ILexerMin = json; lexerMin.languageId = languageId; lexerMin.ignoreCase = lexer.ignoreCase; + lexerMin.unicode = lexer.unicode; lexerMin.noThrow = lexer.noThrow; lexerMin.usesEmbedded = lexer.usesEmbedded; lexerMin.stateNames = json.tokenizer; diff --git a/src/vs/editor/standalone/common/monarch/monarchLexer.ts b/src/vs/editor/standalone/common/monarch/monarchLexer.ts index e4ddf8778d4..f120b5383d7 100644 --- a/src/vs/editor/standalone/common/monarch/monarchLexer.ts +++ b/src/vs/editor/standalone/common/monarch/monarchLexer.ts @@ -497,7 +497,8 @@ export class MonarchTokenizer implements modes.ITokenizationSupport { let regex = rule.regex; let regexSource = rule.regex.source; if (regexSource.substr(0, 4) === '^(?:' && regexSource.substr(regexSource.length - 1, 1) === ')') { - regex = new RegExp(regexSource.substr(4, regexSource.length - 5), regex.ignoreCase ? 'i' : ''); + let flags = (regex.ignoreCase ? 'i' : '') + (regex.unicode ? 'u' : ''); + regex = new RegExp(regexSource.substr(4, regexSource.length - 5), flags); } let result = line.search(regex); diff --git a/src/vs/editor/standalone/common/monarch/monarchTypes.ts b/src/vs/editor/standalone/common/monarch/monarchTypes.ts index cd0fd107992..19936be8dc8 100644 --- a/src/vs/editor/standalone/common/monarch/monarchTypes.ts +++ b/src/vs/editor/standalone/common/monarch/monarchTypes.ts @@ -21,6 +21,10 @@ export interface IMonarchLanguage { * is the language case insensitive? */ ignoreCase?: boolean; + /** + * is the language unicode-aware? (i.e., /\u{1D306}/) + */ + unicode?: boolean; /** * if no match in the tokenizer assign this token class (default 'source') */ diff --git a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 07bfa4b14bb..ac779135546 100644 --- a/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts +++ b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts @@ -42,6 +42,8 @@ suite('TokenizationSupport2Adapter', () => { } public getColorTheme(): IStandaloneTheme { return { + label: 'mock', + tokenTheme: new MockTokenTheme(), themeName: LIGHT, diff --git a/src/vs/editor/test/browser/commands/sideEditing.test.ts b/src/vs/editor/test/browser/commands/sideEditing.test.ts index 775a434ddd2..be07a56821e 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -12,16 +12,16 @@ import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; function testCommand(lines: string[], selections: Selection[], edits: IIdentifiedSingleEditOperation[], expectedLines: string[], expectedSelections: Selection[]): void { - withTestCodeEditor(lines, {}, (editor, cursor) => { + withTestCodeEditor(lines, {}, (editor, viewModel) => { const model = editor.getModel()!; - cursor.setSelections('tests', selections); + viewModel.setSelections('tests', selections); model.applyEdits(edits); assert.deepEqual(model.getLinesContent(), expectedLines); - let actualSelections = cursor.getSelections(); + let actualSelections = viewModel.getSelections(); assert.deepEqual(actualSelections.map(s => s.toString()), expectedSelections.map(s => s.toString())); }); @@ -194,14 +194,14 @@ suite('SideEditing', () => { ]; function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void { - withTestCodeEditor(LINES.join('\n'), {}, (editor, cursor) => { - cursor.setSelections('tests', [selection]); - cursor.context.model.applyEdits([{ + withTestCodeEditor(LINES.join('\n'), {}, (editor, viewModel) => { + viewModel.setSelections('tests', [selection]); + editor.getModel().applyEdits([{ range: editRange, text: editText, forceMoveMarkers: editForceMoveMarkers }]); - const actual = cursor.getSelection(); + const actual = viewModel.getSelection(); assert.deepEqual(actual.toString(), expected.toString(), msg); }); } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 8a526a5888a..7f757690c16 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -6,114 +6,104 @@ import * as assert from 'assert'; import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { Cursor, CursorStateChangedEvent } from 'vs/editor/common/controller/cursor'; import { EditOperation } from 'vs/editor/common/core/editOperation'; 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 { TokenizationResult2 } from 'vs/editor/common/core/token'; -import { Handler, ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; +import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, EndOfLineSequence, ITextModel } from 'vs/editor/common/model'; import { TextModel } from 'vs/editor/common/model/textModel'; import { IState, ITokenizationSupport, LanguageIdentifier, TokenizationRegistry } from 'vs/editor/common/modes'; import { IndentAction, IndentationRule } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { NULL_STATE } from 'vs/editor/common/modes/nullMode'; -import { withTestCodeEditor, TestCodeEditorCreationOptions, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { withTestCodeEditor, TestCodeEditorCreationOptions, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { IRelaxedTextModelCreationOptions, createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; - -const H = Handler; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { OutgoingViewModelEventKind } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; // --------- utils -function cursorCommand(cursor: Cursor, command: string, extraData?: any, overwriteSource?: string) { - cursor.trigger(overwriteSource || 'tests', command, extraData); -} - -function cursorCommandAndTokenize(model: TextModel, cursor: Cursor, command: string, extraData?: any, overwriteSource?: string) { - cursor.trigger(overwriteSource || 'tests', command, extraData); - model.forceTokenization(model.getLineCount()); -} - -function moveTo(cursor: Cursor, lineNumber: number, column: number, inSelectionMode: boolean = false) { +function moveTo(editor: ITestCodeEditor, viewModel: ViewModel, lineNumber: number, column: number, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(cursor, { + CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(viewModel, { position: new Position(lineNumber, column) }); } else { - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(lineNumber, column) }); } } -function moveLeft(cursor: Cursor, inSelectionMode: boolean = false) { +function moveLeft(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorLeftSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorLeftSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorLeft.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); } } -function moveRight(cursor: Cursor, inSelectionMode: boolean = false) { +function moveRight(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorRightSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorRightSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorRight.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorRight.runCoreEditorCommand(viewModel, {}); } } -function moveDown(cursor: Cursor, inSelectionMode: boolean = false) { +function moveDown(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorDownSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorDownSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorDown.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorDown.runCoreEditorCommand(viewModel, {}); } } -function moveUp(cursor: Cursor, inSelectionMode: boolean = false) { +function moveUp(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorUpSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorUpSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorUp.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorUp.runCoreEditorCommand(viewModel, {}); } } -function moveToBeginningOfLine(cursor: Cursor, inSelectionMode: boolean = false) { +function moveToBeginningOfLine(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorHomeSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorHomeSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorHome.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorHome.runCoreEditorCommand(viewModel, {}); } } -function moveToEndOfLine(cursor: Cursor, inSelectionMode: boolean = false) { +function moveToEndOfLine(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorEndSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorEndSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorEnd.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorEnd.runCoreEditorCommand(viewModel, {}); } } -function moveToBeginningOfBuffer(cursor: Cursor, inSelectionMode: boolean = false) { +function moveToBeginningOfBuffer(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorTopSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorTopSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorTop.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorTop.runCoreEditorCommand(viewModel, {}); } } -function moveToEndOfBuffer(cursor: Cursor, inSelectionMode: boolean = false) { +function moveToEndOfBuffer(editor: ITestCodeEditor, viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorBottomSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorBottomSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorBottom.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorBottom.runCoreEditorCommand(viewModel, {}); } } -function assertCursor(cursor: Cursor, what: Position | Selection | Selection[]): void { +function assertCursor(viewModel: ViewModel, what: Position | Selection | Selection[]): void { let selections: Selection[]; if (what instanceof Position) { selections = [new Selection(what.lineNumber, what.column, what.lineNumber, what.column)]; @@ -122,7 +112,7 @@ function assertCursor(cursor: Cursor, what: Position | Selection | Selection[]): } else { selections = what; } - let actual = cursor.getSelections().map(s => s.toString()); + let actual = viewModel.getSelections().map(s => s.toString()); let expected = selections.map(s => s.toString()); assert.deepEqual(actual, expected); @@ -169,656 +159,660 @@ suite('Editor Controller - Cursor', () => { // thisConfiguration.dispose(); // }); - function runTest(callback: (editor: TestCodeEditor, cursor: Cursor) => void): void { - withTestCodeEditor(TEXT, {}, (editor, cursor) => { - callback(editor, cursor); + function runTest(callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { + callback(editor, viewModel); }); } test('cursor initialized', () => { - runTest((editor, cursor) => { - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); }); }); // --------- absolute move test('no move', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 1); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 1); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 2); - assertCursor(cursor, new Position(1, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 2); + assertCursor(viewModel, new Position(1, 2)); }); }); test('move in selection mode', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 2, true); - assertCursor(cursor, new Selection(1, 1, 1, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 2, true); + assertCursor(viewModel, new Selection(1, 1, 1, 2)); }); }); test('move beyond line end', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 25); - assertCursor(cursor, new Position(1, LINE1.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 25); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); }); }); test('move empty line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 4, 20); - assertCursor(cursor, new Position(4, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 4, 20); + assertCursor(viewModel, new Position(4, 1)); }); }); test('move one char line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 5, 20); - assertCursor(cursor, new Position(5, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 5, 20); + assertCursor(viewModel, new Position(5, 2)); }); }); test('selection down', () => { - runTest((editor, cursor) => { - moveTo(cursor, 2, 1, true); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 1, true); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); }); }); test('move and then select', () => { - runTest((editor, cursor) => { - moveTo(cursor, 2, 3); - assertCursor(cursor, new Position(2, 3)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 3); + assertCursor(viewModel, new Position(2, 3)); - moveTo(cursor, 2, 15, true); - assertCursor(cursor, new Selection(2, 3, 2, 15)); + moveTo(editor, viewModel, 2, 15, true); + assertCursor(viewModel, new Selection(2, 3, 2, 15)); - moveTo(cursor, 1, 2, true); - assertCursor(cursor, new Selection(2, 3, 1, 2)); + moveTo(editor, viewModel, 1, 2, true); + assertCursor(viewModel, new Selection(2, 3, 1, 2)); }); }); // --------- move left test('move left on top left position', () => { - runTest((editor, cursor) => { - moveLeft(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move left', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 3); - assertCursor(cursor, new Position(1, 3)); - moveLeft(cursor); - assertCursor(cursor, new Position(1, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 3); + assertCursor(viewModel, new Position(1, 3)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(1, 2)); }); }); test('move left with surrogate pair', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 17); - assertCursor(cursor, new Position(3, 17)); - moveLeft(cursor); - assertCursor(cursor, new Position(3, 15)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 17); + assertCursor(viewModel, new Position(3, 17)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(3, 15)); }); }); test('move left goes to previous row', () => { - runTest((editor, cursor) => { - moveTo(cursor, 2, 1); - assertCursor(cursor, new Position(2, 1)); - moveLeft(cursor); - assertCursor(cursor, new Position(1, 21)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 1); + assertCursor(viewModel, new Position(2, 1)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(1, 21)); }); }); test('move left selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 2, 1); - assertCursor(cursor, new Position(2, 1)); - moveLeft(cursor, true); - assertCursor(cursor, new Selection(2, 1, 1, 21)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 1); + assertCursor(viewModel, new Position(2, 1)); + moveLeft(editor, viewModel, true); + assertCursor(viewModel, new Selection(2, 1, 1, 21)); }); }); // --------- move right test('move right on bottom right position', () => { - runTest((editor, cursor) => { - moveTo(cursor, 5, 2); - assertCursor(cursor, new Position(5, 2)); - moveRight(cursor); - assertCursor(cursor, new Position(5, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 5, 2); + assertCursor(viewModel, new Position(5, 2)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(5, 2)); }); }); test('move right', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 3); - assertCursor(cursor, new Position(1, 3)); - moveRight(cursor); - assertCursor(cursor, new Position(1, 4)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 3); + assertCursor(viewModel, new Position(1, 3)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(1, 4)); }); }); test('move right with surrogate pair', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 15); - assertCursor(cursor, new Position(3, 15)); - moveRight(cursor); - assertCursor(cursor, new Position(3, 17)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 15); + assertCursor(viewModel, new Position(3, 15)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(3, 17)); }); }); test('move right goes to next row', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 21); - assertCursor(cursor, new Position(1, 21)); - moveRight(cursor); - assertCursor(cursor, new Position(2, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 21); + assertCursor(viewModel, new Position(1, 21)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(2, 1)); }); }); test('move right selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 21); - assertCursor(cursor, new Position(1, 21)); - moveRight(cursor, true); - assertCursor(cursor, new Selection(1, 21, 2, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 21); + assertCursor(viewModel, new Position(1, 21)); + moveRight(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 21, 2, 1)); }); }); // --------- move down test('move down', () => { - runTest((editor, cursor) => { - moveDown(cursor); - assertCursor(cursor, new Position(2, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(3, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(4, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(5, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(5, 2)); + runTest((editor, viewModel) => { + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(2, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(3, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(4, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(5, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(5, 2)); }); }); test('move down with selection', () => { - runTest((editor, cursor) => { - moveDown(cursor, true); - assertCursor(cursor, new Selection(1, 1, 2, 1)); - moveDown(cursor, true); - assertCursor(cursor, new Selection(1, 1, 3, 1)); - moveDown(cursor, true); - assertCursor(cursor, new Selection(1, 1, 4, 1)); - moveDown(cursor, true); - assertCursor(cursor, new Selection(1, 1, 5, 1)); - moveDown(cursor, true); - assertCursor(cursor, new Selection(1, 1, 5, 2)); + runTest((editor, viewModel) => { + moveDown(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); + moveDown(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 1, 3, 1)); + moveDown(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 1, 4, 1)); + moveDown(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 1, 5, 1)); + moveDown(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 1, 5, 2)); }); }); test('move down with tabs', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 5); - assertCursor(cursor, new Position(1, 5)); - moveDown(cursor); - assertCursor(cursor, new Position(2, 2)); - moveDown(cursor); - assertCursor(cursor, new Position(3, 5)); - moveDown(cursor); - assertCursor(cursor, new Position(4, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(5, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 5); + assertCursor(viewModel, new Position(1, 5)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(2, 2)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(3, 5)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(4, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(5, 2)); }); }); // --------- move up test('move up', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 5); - assertCursor(cursor, new Position(3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 5); + assertCursor(viewModel, new Position(3, 5)); - moveUp(cursor); - assertCursor(cursor, new Position(2, 2)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(2, 2)); - moveUp(cursor); - assertCursor(cursor, new Position(1, 5)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 5)); }); }); test('move up with selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 5); - assertCursor(cursor, new Position(3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 5); + assertCursor(viewModel, new Position(3, 5)); - moveUp(cursor, true); - assertCursor(cursor, new Selection(3, 5, 2, 2)); + moveUp(editor, viewModel, true); + assertCursor(viewModel, new Selection(3, 5, 2, 2)); - moveUp(cursor, true); - assertCursor(cursor, new Selection(3, 5, 1, 5)); + moveUp(editor, viewModel, true); + assertCursor(viewModel, new Selection(3, 5, 1, 5)); }); }); test('move up and down with tabs', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 5); - assertCursor(cursor, new Position(1, 5)); - moveDown(cursor); - moveDown(cursor); - moveDown(cursor); - moveDown(cursor); - assertCursor(cursor, new Position(5, 2)); - moveUp(cursor); - assertCursor(cursor, new Position(4, 1)); - moveUp(cursor); - assertCursor(cursor, new Position(3, 5)); - moveUp(cursor); - assertCursor(cursor, new Position(2, 2)); - moveUp(cursor); - assertCursor(cursor, new Position(1, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 5); + assertCursor(viewModel, new Position(1, 5)); + moveDown(editor, viewModel); + moveDown(editor, viewModel); + moveDown(editor, viewModel); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(5, 2)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(4, 1)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(3, 5)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(2, 2)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 5)); }); }); test('move up and down with end of lines starting from a long one', () => { - runTest((editor, cursor) => { - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); - moveDown(cursor); - assertCursor(cursor, new Position(2, LINE2.length + 1)); - moveDown(cursor); - assertCursor(cursor, new Position(3, LINE3.length + 1)); - moveDown(cursor); - assertCursor(cursor, new Position(4, LINE4.length + 1)); - moveDown(cursor); - assertCursor(cursor, new Position(5, LINE5.length + 1)); - moveUp(cursor); - moveUp(cursor); - moveUp(cursor); - moveUp(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); + runTest((editor, viewModel) => { + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(2, LINE2.length + 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(3, LINE3.length + 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(4, LINE4.length + 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(5, LINE5.length + 1)); + moveUp(editor, viewModel); + moveUp(editor, viewModel); + moveUp(editor, viewModel); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); }); }); test('issue #44465: cursor position not correct when move', () => { - runTest((editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 5, 1, 5)]); + runTest((editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); // going once up on the first line remembers the offset visual columns - moveUp(cursor); - assertCursor(cursor, new Position(1, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(2, 2)); - moveUp(cursor); - assertCursor(cursor, new Position(1, 5)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(2, 2)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 5)); // going twice up on the first line discards the offset visual columns - moveUp(cursor); - assertCursor(cursor, new Position(1, 1)); - moveUp(cursor); - assertCursor(cursor, new Position(1, 1)); - moveDown(cursor); - assertCursor(cursor, new Position(2, 1)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(2, 1)); }); }); // --------- move to beginning of line test('move to beginning of line', () => { - runTest((editor, cursor) => { - moveToBeginningOfLine(cursor); - assertCursor(cursor, new Position(1, 6)); - moveToBeginningOfLine(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveToBeginningOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, 6)); + moveToBeginningOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move to beginning of line from within line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 8); - moveToBeginningOfLine(cursor); - assertCursor(cursor, new Position(1, 6)); - moveToBeginningOfLine(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 8); + moveToBeginningOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, 6)); + moveToBeginningOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move to beginning of line from whitespace at beginning of line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 2); - moveToBeginningOfLine(cursor); - assertCursor(cursor, new Position(1, 6)); - moveToBeginningOfLine(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 2); + moveToBeginningOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, 6)); + moveToBeginningOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move to beginning of line from within line selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 8); - moveToBeginningOfLine(cursor, true); - assertCursor(cursor, new Selection(1, 8, 1, 6)); - moveToBeginningOfLine(cursor, true); - assertCursor(cursor, new Selection(1, 8, 1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 8); + moveToBeginningOfLine(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 8, 1, 6)); + moveToBeginningOfLine(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 8, 1, 1)); }); }); test('move to beginning of line with selection multiline forward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 8); - moveTo(cursor, 3, 9, true); - moveToBeginningOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 5, 3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 8); + moveTo(editor, viewModel, 3, 9, true); + moveToBeginningOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 5, 3, 5)); }); }); test('move to beginning of line with selection multiline backward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 9); - moveTo(cursor, 1, 8, true); - moveToBeginningOfLine(cursor, false); - assertCursor(cursor, new Selection(1, 6, 1, 6)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 9); + moveTo(editor, viewModel, 1, 8, true); + moveToBeginningOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(1, 6, 1, 6)); }); }); test('move to beginning of line with selection single line forward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 2); - moveTo(cursor, 3, 9, true); - moveToBeginningOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 5, 3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 2); + moveTo(editor, viewModel, 3, 9, true); + moveToBeginningOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 5, 3, 5)); }); }); test('move to beginning of line with selection single line backward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 9); - moveTo(cursor, 3, 2, true); - moveToBeginningOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 5, 3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 9); + moveTo(editor, viewModel, 3, 2, true); + moveToBeginningOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 5, 3, 5)); }); }); test('issue #15401: "End" key is behaving weird when text is selected part 1', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 8); - moveTo(cursor, 3, 9, true); - moveToBeginningOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 5, 3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 8); + moveTo(editor, viewModel, 3, 9, true); + moveToBeginningOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 5, 3, 5)); }); }); test('issue #17011: Shift+home/end now go to the end of the selection start\'s line, not the selection\'s end', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 8); - moveTo(cursor, 3, 9, true); - moveToBeginningOfLine(cursor, true); - assertCursor(cursor, new Selection(1, 8, 3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 8); + moveTo(editor, viewModel, 3, 9, true); + moveToBeginningOfLine(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 8, 3, 5)); }); }); // --------- move to end of line test('move to end of line', () => { - runTest((editor, cursor) => { - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); + runTest((editor, viewModel) => { + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); }); }); test('move to end of line from within line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 6); - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 6); + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); }); }); test('move to end of line from whitespace at end of line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 20); - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(cursor); - assertCursor(cursor, new Position(1, LINE1.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 20); + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); + moveToEndOfLine(editor, viewModel); + assertCursor(viewModel, new Position(1, LINE1.length + 1)); }); }); test('move to end of line from within line selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 6); - moveToEndOfLine(cursor, true); - assertCursor(cursor, new Selection(1, 6, 1, LINE1.length + 1)); - moveToEndOfLine(cursor, true); - assertCursor(cursor, new Selection(1, 6, 1, LINE1.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 6); + moveToEndOfLine(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 6, 1, LINE1.length + 1)); + moveToEndOfLine(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 6, 1, LINE1.length + 1)); }); }); test('move to end of line with selection multiline forward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 1); - moveTo(cursor, 3, 9, true); - moveToEndOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 17, 3, 17)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 1); + moveTo(editor, viewModel, 3, 9, true); + moveToEndOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 17, 3, 17)); }); }); test('move to end of line with selection multiline backward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 9); - moveTo(cursor, 1, 1, true); - moveToEndOfLine(cursor, false); - assertCursor(cursor, new Selection(1, 21, 1, 21)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 9); + moveTo(editor, viewModel, 1, 1, true); + moveToEndOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(1, 21, 1, 21)); }); }); test('move to end of line with selection single line forward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 1); - moveTo(cursor, 3, 9, true); - moveToEndOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 17, 3, 17)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 1); + moveTo(editor, viewModel, 3, 9, true); + moveToEndOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 17, 3, 17)); }); }); test('move to end of line with selection single line backward', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 9); - moveTo(cursor, 3, 1, true); - moveToEndOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 17, 3, 17)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 9); + moveTo(editor, viewModel, 3, 1, true); + moveToEndOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 17, 3, 17)); }); }); test('issue #15401: "End" key is behaving weird when text is selected part 2', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 1); - moveTo(cursor, 3, 9, true); - moveToEndOfLine(cursor, false); - assertCursor(cursor, new Selection(3, 17, 3, 17)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 1); + moveTo(editor, viewModel, 3, 9, true); + moveToEndOfLine(editor, viewModel, false); + assertCursor(viewModel, new Selection(3, 17, 3, 17)); }); }); // --------- move to beginning of buffer test('move to beginning of buffer', () => { - runTest((editor, cursor) => { - moveToBeginningOfBuffer(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveToBeginningOfBuffer(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move to beginning of buffer from within first line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 3); - moveToBeginningOfBuffer(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 3); + moveToBeginningOfBuffer(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move to beginning of buffer from within another line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 3); - moveToBeginningOfBuffer(cursor); - assertCursor(cursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 3); + moveToBeginningOfBuffer(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); }); }); test('move to beginning of buffer from within first line selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 1, 3); - moveToBeginningOfBuffer(cursor, true); - assertCursor(cursor, new Selection(1, 3, 1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 3); + moveToBeginningOfBuffer(editor, viewModel, true); + assertCursor(viewModel, new Selection(1, 3, 1, 1)); }); }); test('move to beginning of buffer from within another line selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 3); - moveToBeginningOfBuffer(cursor, true); - assertCursor(cursor, new Selection(3, 3, 1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 3); + moveToBeginningOfBuffer(editor, viewModel, true); + assertCursor(viewModel, new Selection(3, 3, 1, 1)); }); }); // --------- move to end of buffer test('move to end of buffer', () => { - runTest((editor, cursor) => { - moveToEndOfBuffer(cursor); - assertCursor(cursor, new Position(5, LINE5.length + 1)); + runTest((editor, viewModel) => { + moveToEndOfBuffer(editor, viewModel); + assertCursor(viewModel, new Position(5, LINE5.length + 1)); }); }); test('move to end of buffer from within last line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 5, 1); - moveToEndOfBuffer(cursor); - assertCursor(cursor, new Position(5, LINE5.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 5, 1); + moveToEndOfBuffer(editor, viewModel); + assertCursor(viewModel, new Position(5, LINE5.length + 1)); }); }); test('move to end of buffer from within another line', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 3); - moveToEndOfBuffer(cursor); - assertCursor(cursor, new Position(5, LINE5.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 3); + moveToEndOfBuffer(editor, viewModel); + assertCursor(viewModel, new Position(5, LINE5.length + 1)); }); }); test('move to end of buffer from within last line selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 5, 1); - moveToEndOfBuffer(cursor, true); - assertCursor(cursor, new Selection(5, 1, 5, LINE5.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 5, 1); + moveToEndOfBuffer(editor, viewModel, true); + assertCursor(viewModel, new Selection(5, 1, 5, LINE5.length + 1)); }); }); test('move to end of buffer from within another line selection', () => { - runTest((editor, cursor) => { - moveTo(cursor, 3, 3); - moveToEndOfBuffer(cursor, true); - assertCursor(cursor, new Selection(3, 3, 5, LINE5.length + 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 3); + moveToEndOfBuffer(editor, viewModel, true); + assertCursor(viewModel, new Selection(3, 3, 5, LINE5.length + 1)); }); }); // --------- misc test('select all', () => { - runTest((editor, cursor) => { - CoreNavigationCommands.SelectAll.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 5, LINE5.length + 1)); + runTest((editor, viewModel) => { + CoreNavigationCommands.SelectAll.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 5, LINE5.length + 1)); }); }); test('expandLineSelection', () => { - runTest((editor, cursor) => { + runTest((editor, viewModel) => { // 0 1 2 // 01234 56789012345678 0 // let LINE1 = ' \tMy First Line\t '; - moveTo(cursor, 1, 1); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 1); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(cursor, 1, 2); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 2); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(cursor, 1, 5); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 5); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(cursor, 1, 19); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 19); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(cursor, 1, 20); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 20); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(cursor, 1, 21); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 2, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 3, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 4, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 5, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 5, LINE5.length + 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, new Selection(1, 1, 5, LINE5.length + 1)); + moveTo(editor, viewModel, 1, 21); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 3, 1)); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 4, 1)); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 5, 1)); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 5, LINE5.length + 1)); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 5, LINE5.length + 1)); }); }); // --------- eventing test('no move doesn\'t trigger event', () => { - runTest((editor, cursor) => { - cursor.onDidChange((e) => { + runTest((editor, viewModel) => { + viewModel.onEvent((e) => { assert.ok(false, 'was not expecting event'); }); - moveTo(cursor, 1, 1); + moveTo(editor, viewModel, 1, 1); }); }); test('move eventing', () => { - runTest((editor, cursor) => { + runTest((editor, viewModel) => { let events = 0; - cursor.onDidChange((e: CursorStateChangedEvent) => { - events++; - assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + viewModel.onEvent((e) => { + if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { + events++; + assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + } }); - moveTo(cursor, 1, 2); + moveTo(editor, viewModel, 1, 2); assert.equal(events, 1, 'receives 1 event'); }); }); test('move in selection mode eventing', () => { - runTest((editor, cursor) => { + runTest((editor, viewModel) => { let events = 0; - cursor.onDidChange((e: CursorStateChangedEvent) => { - events++; - assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + viewModel.onEvent((e) => { + if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { + events++; + assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + } }); - moveTo(cursor, 1, 2, true); + moveTo(editor, viewModel, 1, 2, true); assert.equal(events, 1, 'receives 1 event'); }); }); @@ -826,28 +820,28 @@ suite('Editor Controller - Cursor', () => { // --------- state save & restore test('saveState & restoreState', () => { - runTest((editor, cursor) => { - moveTo(cursor, 2, 1, true); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 1, true); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - let savedState = JSON.stringify(cursor.saveState()); + let savedState = JSON.stringify(viewModel.saveCursorState()); - moveTo(cursor, 1, 1, false); - assertCursor(cursor, new Position(1, 1)); + moveTo(editor, viewModel, 1, 1, false); + assertCursor(viewModel, new Position(1, 1)); - cursor.restoreState(JSON.parse(savedState)); - assertCursor(cursor, new Selection(1, 1, 2, 1)); + viewModel.restoreCursorState(JSON.parse(savedState)); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); }); }); // --------- updating cursor test('Independent model edit 1', () => { - runTest((editor, cursor) => { - moveTo(cursor, 2, 16, true); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 16, true); - cursor.context.model.applyEdits([EditOperation.delete(new Range(2, 1, 2, 2))]); - assertCursor(cursor, new Selection(1, 1, 2, 15)); + editor.getModel().applyEdits([EditOperation.delete(new Range(2, 1, 2, 2))]); + assertCursor(viewModel, new Selection(1, 1, 2, 15)); }); }); @@ -858,12 +852,12 @@ suite('Editor Controller - Cursor', () => { '\t\t\treturn false;', '\t\t}', '\t}' - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { - moveTo(cursor, 1, 7, false); - assertCursor(cursor, new Position(1, 7)); + moveTo(editor, viewModel, 1, 7, false); + assertCursor(viewModel, new Position(1, 7)); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { position: new Position(4, 4), viewPosition: new Position(4, 4), mouseColumn: 15, @@ -877,7 +871,7 @@ suite('Editor Controller - Cursor', () => { new Selection(4, 4, 4, 4), ]; - assertCursor(cursor, expectedSelections); + assertCursor(viewModel, expectedSelections); }); }); @@ -888,35 +882,35 @@ suite('Editor Controller - Cursor', () => { 'ãããããã', '辻󠄀辻󠄀辻󠄀', 'பு', - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { - cursor.setSelections('test', [new Selection(2, 1, 2, 1)]); - moveRight(cursor); - assertCursor(cursor, new Position(2, 3)); - moveLeft(cursor); - assertCursor(cursor, new Position(2, 1)); + viewModel.setSelections('test', [new Selection(2, 1, 2, 1)]); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(2, 3)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(2, 1)); - cursor.setSelections('test', [new Selection(3, 1, 3, 1)]); - moveRight(cursor); - assertCursor(cursor, new Position(3, 4)); - moveLeft(cursor); - assertCursor(cursor, new Position(3, 1)); + viewModel.setSelections('test', [new Selection(3, 1, 3, 1)]); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(3, 4)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(3, 1)); - cursor.setSelections('test', [new Selection(4, 1, 4, 1)]); - moveRight(cursor); - assertCursor(cursor, new Position(4, 3)); - moveLeft(cursor); - assertCursor(cursor, new Position(4, 1)); + viewModel.setSelections('test', [new Selection(4, 1, 4, 1)]); + moveRight(editor, viewModel); + assertCursor(viewModel, new Position(4, 3)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(4, 1)); - cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); - moveDown(cursor); - assertCursor(cursor, new Position(2, 5)); - moveDown(cursor); - assertCursor(cursor, new Position(3, 4)); - moveUp(cursor); - assertCursor(cursor, new Position(2, 5)); - moveUp(cursor); - assertCursor(cursor, new Position(1, 3)); + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(2, 5)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Position(3, 4)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(2, 5)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 3)); }); }); @@ -930,18 +924,18 @@ suite('Editor Controller - Cursor', () => { 'var merge = require("merge-stream");', 'var concat = require("gulp-concat");', 'var newer = require("gulp-newer");', - ].join('\n'), {}, (editor, cursor) => { - moveTo(cursor, 1, 4, false); - assertCursor(cursor, new Position(1, 4)); + ].join('\n'), {}, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 4, false); + assertCursor(viewModel, new Position(1, 4)); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { position: new Position(4, 1), viewPosition: new Position(4, 1), mouseColumn: 1, doColumnSelect: true }); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 4, 1, 1), new Selection(2, 4, 2, 1), new Selection(3, 4, 3, 1), @@ -962,18 +956,18 @@ suite('Editor Controller - Cursor', () => { '', '', '', - ].join('\n'), {}, (editor, cursor) => { + ].join('\n'), {}, (editor, viewModel) => { - moveTo(cursor, 10, 10, false); - assertCursor(cursor, new Position(10, 10)); + moveTo(editor, viewModel, 10, 10, false); + assertCursor(viewModel, new Position(10, 10)); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 1), viewPosition: new Position(1, 1), mouseColumn: 1, doColumnSelect: true }); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(10, 10, 10, 1), new Selection(9, 10, 9, 1), new Selection(8, 10, 8, 1), @@ -986,13 +980,13 @@ suite('Editor Controller - Cursor', () => { new Selection(1, 10, 1, 1), ]); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 1), viewPosition: new Position(1, 1), mouseColumn: 1, doColumnSelect: true }); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(10, 10, 10, 1), new Selection(9, 10, 9, 1), new Selection(8, 10, 8, 1), @@ -1020,34 +1014,34 @@ suite('Editor Controller - Cursor', () => { '', '', '', - ].join('\n'), {}, (editor, cursor) => { + ].join('\n'), {}, (editor, viewModel) => { - moveTo(cursor, 10, 10, false); - assertCursor(cursor, new Position(10, 10)); + moveTo(editor, viewModel, 10, 10, false); + assertCursor(viewModel, new Position(10, 10)); - CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(10, 10, 10, 9) ]); - CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(10, 10, 10, 8) ]); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(10, 10, 10, 9) ]); - CoreNavigationCommands.CursorColumnSelectUp.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectUp.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(10, 10, 10, 9), new Selection(9, 10, 9, 9), ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(10, 10, 10, 9) ]); }); @@ -1062,34 +1056,34 @@ suite('Editor Controller - Cursor', () => { 'var merge = require("merge-stream");', 'var concat = require("gulp-concat");', 'var newer = require("gulp-newer");', - ].join('\n'), {}, (editor, cursor) => { + ].join('\n'), {}, (editor, viewModel) => { - moveTo(cursor, 1, 4, false); - assertCursor(cursor, new Position(1, 4)); + moveTo(editor, viewModel, 1, 4, false); + assertCursor(viewModel, new Position(1, 4)); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 5) ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 5), new Selection(2, 4, 2, 5) ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 5), new Selection(2, 4, 2, 5), new Selection(3, 4, 3, 5), ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 5), new Selection(2, 4, 2, 5), new Selection(3, 4, 3, 5), @@ -1099,8 +1093,8 @@ suite('Editor Controller - Cursor', () => { new Selection(7, 4, 7, 5), ]); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 6), new Selection(2, 4, 2, 6), new Selection(3, 4, 3, 6), @@ -1111,17 +1105,17 @@ suite('Editor Controller - Cursor', () => { ]); // 10 times - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 16), new Selection(2, 4, 2, 16), new Selection(3, 4, 3, 16), @@ -1132,17 +1126,17 @@ suite('Editor Controller - Cursor', () => { ]); // 10 times - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 26), new Selection(2, 4, 2, 26), new Selection(3, 4, 3, 26), @@ -1153,9 +1147,9 @@ suite('Editor Controller - Cursor', () => { ]); // 2 times => reaching the ending of lines 1 and 2 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 28), @@ -1166,11 +1160,11 @@ suite('Editor Controller - Cursor', () => { ]); // 4 times => reaching the ending of line 3 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1181,9 +1175,9 @@ suite('Editor Controller - Cursor', () => { ]); // 2 times => reaching the ending of line 4 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1194,8 +1188,8 @@ suite('Editor Controller - Cursor', () => { ]); // 1 time => reaching the ending of line 7 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1206,10 +1200,10 @@ suite('Editor Controller - Cursor', () => { ]); // 3 times => reaching the ending of lines 5 & 6 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1220,8 +1214,8 @@ suite('Editor Controller - Cursor', () => { ]); // cannot go anywhere anymore - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1232,11 +1226,11 @@ suite('Editor Controller - Cursor', () => { ]); // cannot go anywhere anymore even if we insist - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1247,8 +1241,8 @@ suite('Editor Controller - Cursor', () => { ]); // can easily go back - CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ + CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ new Selection(1, 4, 1, 28), new Selection(2, 4, 2, 28), new Selection(3, 4, 3, 32), @@ -1312,12 +1306,12 @@ suite('Editor Controller - Regression tests', () => { }, ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 1, 1, 13)]); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 1, 1, 13)]); // Check that indenting maintains the selection start at column 1 CoreEditingCommands.Tab.runEditorCommand(null, editor, null); - assert.deepEqual(cursor.getSelection(), new Selection(1, 1, 1, 14)); + assert.deepEqual(viewModel.getSelection(), new Selection(1, 1, 1, 14)); }); model.dispose(); @@ -1334,20 +1328,20 @@ suite('Editor Controller - Regression tests', () => { }, ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\t', 'assert3'); - cursorCommand(cursor, H.Type, { text: 'x' }); + viewModel.type('x'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert4'); - CoreNavigationCommands.CursorLeft.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -1388,10 +1382,10 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor([ 'Hello', 'world' - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { const model = editor.getModel()!; - assertCursor(cursor, new Position(1, 1)); + assertCursor(viewModel, new Position(1, 1)); model.setEOL(EndOfLineSequence.LF); assert.equal(model.getValue(), 'Hello\nworld'); @@ -1417,10 +1411,10 @@ suite('Editor Controller - Regression tests', () => { const mode = new MyMode(); const model = createTextModel('\'👁\'', undefined, languageId); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelection(new Selection(1, 1, 1, 2)); - cursorCommand(cursor, H.Type, { text: '%' }, 'keyboard'); + viewModel.type('%', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '%\'%👁\'', 'assert1'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); @@ -1434,56 +1428,56 @@ suite('Editor Controller - Regression tests', () => { test('issue #46208: Allow empty selections in the undo/redo stack', () => { let model = createTextModel(''); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursorCommand(cursor, H.Type, { text: 'Hello' }, 'keyboard'); - cursorCommand(cursor, H.Type, { text: ' ' }, 'keyboard'); - cursorCommand(cursor, H.Type, { text: 'world' }, 'keyboard'); - cursorCommand(cursor, H.Type, { text: ' ' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('Hello', 'keyboard'); + viewModel.type(' ', 'keyboard'); + viewModel.type('world', 'keyboard'); + viewModel.type(' ', 'keyboard'); assert.equal(model.getLineContent(1), 'Hello world '); - assertCursor(cursor, new Position(1, 13)); + assertCursor(viewModel, new Position(1, 13)); - moveLeft(cursor); - moveRight(cursor); + moveLeft(editor, viewModel); + moveRight(editor, viewModel); model.pushEditOperations([], [EditOperation.replaceMove(new Range(1, 12, 1, 13), '')], () => []); assert.equal(model.getLineContent(1), 'Hello world'); - assertCursor(cursor, new Position(1, 12)); + assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world '); - assertCursor(cursor, new Selection(1, 12, 1, 13)); + assertCursor(viewModel, new Selection(1, 12, 1, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); - assertCursor(cursor, new Position(1, 12)); + assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello'); - assertCursor(cursor, new Position(1, 6)); + assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ''); - assertCursor(cursor, new Position(1, 1)); + assertCursor(viewModel, new Position(1, 1)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello'); - assertCursor(cursor, new Position(1, 6)); + assertCursor(viewModel, new Position(1, 6)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); - assertCursor(cursor, new Position(1, 12)); + assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world '); - assertCursor(cursor, new Position(1, 13)); + assertCursor(viewModel, new Position(1, 13)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); - assertCursor(cursor, new Position(1, 12)); + assertCursor(viewModel, new Position(1, 12)); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'Hello world'); - assertCursor(cursor, new Position(1, 12)); + assertCursor(viewModel, new Position(1, 12)); }); model.dispose(); @@ -1499,13 +1493,13 @@ suite('Editor Controller - Regression tests', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 1, 6, false); - assertCursor(cursor, new Selection(1, 6, 1, 6)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 6, false); + assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' function baz() {'); - assertCursor(cursor, new Selection(1, 5, 1, 5)); + assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); model.dispose(); @@ -1519,13 +1513,13 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 1, 7, false); - assertCursor(cursor, new Selection(1, 7, 1, 7)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 7, false); + assertCursor(viewModel, new Selection(1, 7, 1, 7)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' '); - assertCursor(cursor, new Selection(1, 5, 1, 5)); + assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); model.dispose(); @@ -1541,13 +1535,13 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor(null, { model: model, useTabStops: false - }, (editor, cursor) => { - moveTo(cursor, 1, 9, false); - assertCursor(cursor, new Selection(1, 9, 1, 9)); + }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 9, false); + assertCursor(viewModel, new Selection(1, 9, 1, 9)); CoreEditingCommands.Outdent.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' '); - assertCursor(cursor, new Selection(1, 5, 1, 5)); + assertCursor(viewModel, new Selection(1, 5, 1, 5)); }); model.dispose(); @@ -1569,13 +1563,13 @@ suite('Editor Controller - Regression tests', () => { }, ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 7, 1, false); - assertCursor(cursor, new Selection(7, 1, 7, 1)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 7, 1, false); + assertCursor(viewModel, new Selection(7, 1, 7, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(7), '\t'); - assertCursor(cursor, new Selection(7, 2, 7, 2)); + assertCursor(viewModel, new Selection(7, 2, 7, 2)); }); model.dispose(); @@ -1587,13 +1581,13 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor([ 'asdasd', 'qwerty' - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { const model = editor.getModel()!; - moveTo(cursor, 2, 1, false); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + moveTo(editor, viewModel, 2, 1, false); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); - cursorCommand(cursor, H.Cut, null, 'keyboard'); + viewModel.cut('keyboard'); assert.equal(model.getLineCount(), 1); assert.equal(model.getLineContent(1), 'asdasd'); @@ -1603,17 +1597,17 @@ suite('Editor Controller - Regression tests', () => { withTestCodeEditor([ 'asdasd', '' - ], {}, (editor, cursor) => { + ], {}, (editor, viewModel) => { const model = editor.getModel()!; - moveTo(cursor, 2, 1, false); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + moveTo(editor, viewModel, 2, 1, false); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); - cursorCommand(cursor, H.Cut, null, 'keyboard'); + viewModel.cut('keyboard'); assert.equal(model.getLineCount(), 1); assert.equal(model.getLineContent(1), 'asdasd'); - cursorCommand(cursor, H.Cut, null, 'keyboard'); + viewModel.cut('keyboard'); assert.equal(model.getLineCount(), 1); assert.equal(model.getLineContent(1), ''); }); @@ -1626,16 +1620,16 @@ suite('Editor Controller - Regression tests', () => { 'hello' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 1, 3, false); - moveTo(cursor, 1, 5, true); - assertCursor(cursor, new Selection(1, 3, 1, 5)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 3, false); + moveTo(editor, viewModel, 1, 5, true); + assertCursor(viewModel, new Selection(1, 3, 1, 5)); - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); - assertCursor(cursor, new Selection(1, 4, 1, 6)); + viewModel.type('(', 'keyboard'); + assertCursor(viewModel, new Selection(1, 4, 1, 6)); - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); - assertCursor(cursor, new Selection(1, 5, 1, 7)); + viewModel.type('(', 'keyboard'); + assertCursor(viewModel, new Selection(1, 5, 1, 7)); }); mode.dispose(); }); @@ -1650,13 +1644,13 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 3, 2, false); - moveTo(cursor, 1, 14, true); - assertCursor(cursor, new Selection(3, 2, 1, 14)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 3, 2, false); + moveTo(editor, viewModel, 1, 14, true); + assertCursor(viewModel, new Selection(3, 2, 1, 14)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assertCursor(cursor, new Selection(1, 14, 1, 14)); + assertCursor(viewModel, new Selection(1, 14, 1, 14)); assert.equal(model.getLineCount(), 1); assert.equal(model.getLineContent(1), 'function baz(;'); }); @@ -1671,11 +1665,11 @@ suite('Editor Controller - Regression tests', () => { 'line1', 'line2' ], - }, (model, cursor) => { - moveTo(cursor, 2, 1, false); - moveTo(cursor, 2, 6, true); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 1, false); + moveTo(editor, viewModel, 2, 6, true); - cursorCommand(cursor, H.Paste, { text: 'line1\n', pasteOnNewLine: true }); + viewModel.paste('line1\n', true); assert.equal(model.getLineContent(1), 'line1'); assert.equal(model.getLineContent(2), 'line1'); @@ -1690,10 +1684,10 @@ suite('Editor Controller - Regression tests', () => { 'line sel 2', 'line3' ], - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(2, 6, 2, 9)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(2, 6, 2, 9)]); - cursorCommand(cursor, H.Paste, { text: 'line1\n', pasteOnNewLine: true }); + viewModel.paste('line1\n', true); assert.equal(model.getLineContent(1), 'line1'); assert.equal(model.getLineContent(2), 'line line1'); @@ -1709,17 +1703,17 @@ suite('Editor Controller - Regression tests', () => { 'line2', 'line3' ], - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1)]); - cursorCommand(cursor, H.Paste, { - text: 'a\nb\nc\nd', - pasteOnNewLine: false, - multicursorText: [ + viewModel.paste( + 'a\nb\nc\nd', + false, + [ 'a\nb', 'c\nd' ] - }); + ); assert.equal(model.getValue(), [ 'a', @@ -1739,19 +1733,19 @@ suite('Editor Controller - Regression tests', () => { 'test', 'test' ], - }, (model, cursor) => { - cursor.setSelections('test', [ + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), new Selection(4, 1, 4, 5), ]); - cursorCommand(cursor, H.Paste, { - text: 'aaa\nbbb\nccc\n', - pasteOnNewLine: false, - multicursorText: null - }); + viewModel.paste( + 'aaa\nbbb\nccc\n', + false, + null + ); assert.equal(model.getValue(), [ 'aaa', @@ -1782,19 +1776,19 @@ suite('Editor Controller - Regression tests', () => { 'test', 'test' ], - }, (model, cursor) => { - cursor.setSelections('test', [ + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [ new Selection(1, 1, 1, 5), new Selection(2, 1, 2, 5), new Selection(3, 1, 3, 5), new Selection(4, 1, 4, 5), ]); - cursorCommand(cursor, H.Paste, { - text: 'aaa\r\nbbb\r\nccc\r\nddd\r\n', - pasteOnNewLine: false, - multicursorText: null - }); + viewModel.paste( + 'aaa\r\nbbb\r\nccc\r\nddd\r\n', + false, + null + ); assert.equal(model.getValue(), [ 'aaa', @@ -1812,14 +1806,14 @@ suite('Editor Controller - Regression tests', () => { 'line2', 'line3' ], - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), new Selection(3, 1, 3, 1)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), new Selection(3, 1, 3, 1)]); - cursorCommand(cursor, H.Paste, { - text: 'a\nb\nc', - pasteOnNewLine: false, - multicursorText: null - }); + viewModel.paste( + 'a\nb\nc', + false, + null + ); assert.equal(model.getValue(), [ 'aline1', @@ -1836,14 +1830,14 @@ suite('Editor Controller - Regression tests', () => { 'line2', 'line3' ], - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), new Selection(3, 1, 3, 1)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 1, 1, 1), new Selection(2, 1, 2, 1), new Selection(3, 1, 3, 1)]); - cursorCommand(cursor, H.Paste, { - text: 'a\nb\nc\n', - pasteOnNewLine: false, - multicursorText: null - }); + viewModel.paste( + 'a\nb\nc\n', + false, + null + ); assert.equal(model.getValue(), [ 'aline1', @@ -1862,15 +1856,15 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 1, 1, false); - moveTo(cursor, 3, 4, true); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 1, false); + moveTo(editor, viewModel, 3, 4, true); let isFirst = true; model.onDidChangeContent(() => { if (isFirst) { isFirst = false; - cursorCommand(cursor, H.Type, { text: '\t' }, 'keyboard'); + viewModel.type('\t', 'keyboard'); } }); @@ -1912,10 +1906,10 @@ suite('Editor Controller - Regression tests', () => { 'just some text', ], languageIdentifier: null - }, (model, cursor) => { - moveTo(cursor, 3, 1, false); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 1, false); - cursorCommand(cursor, H.Type, { text: '😍' }, 'keyboard'); + viewModel.type('😍', 'keyboard'); assert.equal(model.getValue(), [ 'some lines', @@ -1936,8 +1930,8 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 3, 2, false); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 3, 2, false); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(3), '\t \tx: 3'); }); @@ -1956,9 +1950,9 @@ suite('Editor Controller - Regression tests', () => { } ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 1, 15, false); - moveTo(cursor, 1, 22, true); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 15, false); + moveTo(editor, viewModel, 1, 22, true); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'var foo = 123;\t// this is a comment'); }); @@ -1972,8 +1966,8 @@ suite('Editor Controller - Regression tests', () => { text: [ ' /* Just some more text a+= 3 +5-3 + 7 */ ' ], - }, (model, cursor) => { - moveTo(cursor, 1, 1, false); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 1, false); function assertWordRight(col: number, expectedCol: number) { let args = { @@ -1983,13 +1977,13 @@ suite('Editor Controller - Regression tests', () => { } }; if (col === 1) { - CoreNavigationCommands.WordSelect.runCoreEditorCommand(cursor, args); + CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, args); } else { - CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(cursor, args); + CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, args); } - assert.equal(cursor.getSelection().startColumn, 1, 'TEST FOR ' + col); - assert.equal(cursor.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); + assert.equal(viewModel.getSelection().startColumn, 1, 'TEST FOR ' + col); + assert.equal(viewModel.getSelection().endColumn, expectedCol, 'TEST FOR ' + col); } assertWordRight(1, ' '.length + 1); @@ -2052,12 +2046,12 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - CoreNavigationCommands.WordSelect.runCoreEditorCommand(cursor, { position: new Position(1, 8) }); - assert.deepEqual(cursor.getSelection(), new Selection(1, 6, 1, 10)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); + assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); - CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(cursor, { position: new Position(1, 8) }); - assert.deepEqual(cursor.getSelection(), new Selection(1, 6, 1, 10)); + CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(viewModel, { position: new Position(1, 8) }); + assert.deepEqual(viewModel.getSelection(), new Selection(1, 6, 1, 10)); }); model.dispose(); @@ -2070,38 +2064,38 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - CoreNavigationCommands.WordSelect.runCoreEditorCommand(cursor, { position: new Position(1, 5) }); - assert.deepEqual(cursor.getSelection(), new Selection(1, 5, 1, 8)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + CoreNavigationCommands.WordSelect.runCoreEditorCommand(viewModel, { position: new Position(1, 5) }); + assert.deepEqual(viewModel.getSelection(), new Selection(1, 5, 1, 8)); }); model.dispose(); }); test('issue #9675: Undo/Redo adds a stop in between CHN Characters', () => { - withTestCodeEditor([], {}, (editor, cursor) => { + withTestCodeEditor([], {}, (editor, viewModel) => { const model = editor.getModel()!; - assertCursor(cursor, new Position(1, 1)); + assertCursor(viewModel, new Position(1, 1)); // Typing sennsei in Japanese - Hiragana - cursorCommand(cursor, H.Type, { text: 's' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せ', replaceCharCnt: 1 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せn', replaceCharCnt: 1 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せん', replaceCharCnt: 2 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんs', replaceCharCnt: 2 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんせ', replaceCharCnt: 3 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんせ', replaceCharCnt: 3 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんせい', replaceCharCnt: 3 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんせい', replaceCharCnt: 4 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんせい', replaceCharCnt: 4 }); - cursorCommand(cursor, H.ReplacePreviousChar, { text: 'せんせい', replaceCharCnt: 4 }); + viewModel.type('s', 'keyboard'); + viewModel.replacePreviousChar('せ', 1); + viewModel.replacePreviousChar('せn', 1); + viewModel.replacePreviousChar('せん', 2); + viewModel.replacePreviousChar('せんs', 2); + viewModel.replacePreviousChar('せんせ', 3); + viewModel.replacePreviousChar('せんせ', 3); + viewModel.replacePreviousChar('せんせい', 3); + viewModel.replacePreviousChar('せんせい', 4); + viewModel.replacePreviousChar('せんせい', 4); + viewModel.replacePreviousChar('せんせい', 4); assert.equal(model.getLineContent(1), 'せんせい'); - assertCursor(cursor, new Position(1, 5)); + assertCursor(viewModel, new Position(1, 5)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ''); - assertCursor(cursor, new Position(1, 1)); + assertCursor(viewModel, new Position(1, 1)); }); }); @@ -2115,23 +2109,23 @@ suite('Editor Controller - Regression tests', () => { } usingCursor({ text: text - }, (model, cursor) => { + }, (editor, model, viewModel) => { let selections: Selection[] = []; for (let i = 0; i < LINE_CNT; i++) { selections[i] = new Selection(i + 1, 1, i + 1, 1); } - cursor.setSelections('test', selections); + viewModel.setSelections('test', selections); - cursorCommand(cursor, H.Type, { text: 'n' }, 'keyboard'); - cursorCommand(cursor, H.Type, { text: 'n' }, 'keyboard'); + viewModel.type('n', 'keyboard'); + viewModel.type('n', 'keyboard'); for (let i = 0; i < LINE_CNT; i++) { assert.equal(model.getLineContent(i + 1), 'nnasd', 'line #' + (i + 1)); } - assert.equal(cursor.getSelections().length, LINE_CNT); - assert.equal(cursor.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); + assert.equal(viewModel.getSelections().length, LINE_CNT); + assert.equal(viewModel.getSelections()[LINE_CNT - 1].startLineNumber, LINE_CNT); }); }); @@ -2141,13 +2135,13 @@ suite('Editor Controller - Regression tests', () => { 'first line', 'second line' ] - }, (model, cursor) => { + }, (editor, model, viewModel) => { model.setEOL(EndOfLineSequence.CRLF); - cursor.setSelections('test', [new Selection(2, 2, 2, 2)]); + viewModel.setSelections('test', [new Selection(2, 2, 2, 2)]); model.setEOL(EndOfLineSequence.LF); - assertCursor(cursor, new Selection(2, 2, 2, 2)); + assertCursor(viewModel, new Selection(2, 2, 2, 2)); }); }); @@ -2157,17 +2151,17 @@ suite('Editor Controller - Regression tests', () => { 'first line', 'second line' ] - }, (model, cursor) => { + }, (editor, model, viewModel) => { model.setEOL(EndOfLineSequence.CRLF); - cursor.setSelections('test', [new Selection(2, 2, 2, 2)]); + viewModel.setSelections('test', [new Selection(2, 2, 2, 2)]); model.setValue([ 'different first line', 'different second line', 'new third line' ].join('\n')); - assertCursor(cursor, new Selection(1, 1, 1, 1)); + assertCursor(viewModel, new Selection(1, 1, 1, 1)); }); }); @@ -2180,54 +2174,118 @@ suite('Editor Controller - Regression tests', () => { 'consectetur ', 'adipiscing elit', ].join('') - ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 16 }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 7, 1, 7)]); + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 16 }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 7, 1, 7)]); - moveRight(cursor); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); - moveRight(cursor); - assertCursor(cursor, new Selection(1, 9, 1, 9)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 9, 1, 9)); - moveRight(cursor); - assertCursor(cursor, new Selection(1, 10, 1, 10)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 10, 1, 10)); - moveRight(cursor); - assertCursor(cursor, new Selection(1, 11, 1, 11)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 11, 1, 11)); - moveRight(cursor); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); - moveRight(cursor); - assertCursor(cursor, new Selection(1, 13, 1, 13)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 13, 1, 13)); // moving to view line 2 - moveRight(cursor); - assertCursor(cursor, new Selection(1, 14, 1, 14)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(1, 14, 1, 14)); - moveLeft(cursor); - assertCursor(cursor, new Selection(1, 13, 1, 13)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Selection(1, 13, 1, 13)); // moving back to view line 1 - moveLeft(cursor); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + moveLeft(editor, viewModel); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); + }); + }); + + test('issue #98320: Multi-Cursor, Wrap lines and cursorSelectRight ==> cursors out of sync', () => { + // a single model line => 4 view lines + withTestCodeEditor([ + [ + 'lorem_ipsum-1993x11x13', + 'dolor_sit_amet-1998x04x27', + 'consectetur-2007x10x08', + 'adipiscing-2012x07x27', + 'elit-2015x02x27', + ].join('\n') + ], { wordWrap: 'wordWrapColumn', wordWrapColumn: 16 }, (editor, viewModel) => { + viewModel.setSelections('test', [ + new Selection(1, 13, 1, 13), + new Selection(2, 16, 2, 16), + new Selection(3, 13, 3, 13), + new Selection(4, 12, 4, 12), + new Selection(5, 6, 5, 6), + ]); + assertCursor(viewModel, [ + new Selection(1, 13, 1, 13), + new Selection(2, 16, 2, 16), + new Selection(3, 13, 3, 13), + new Selection(4, 12, 4, 12), + new Selection(5, 6, 5, 6), + ]); + + moveRight(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 13, 1, 14), + new Selection(2, 16, 2, 17), + new Selection(3, 13, 3, 14), + new Selection(4, 12, 4, 13), + new Selection(5, 6, 5, 7), + ]); + + moveRight(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 13, 1, 15), + new Selection(2, 16, 2, 18), + new Selection(3, 13, 3, 15), + new Selection(4, 12, 4, 14), + new Selection(5, 6, 5, 8), + ]); + + moveRight(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 13, 1, 16), + new Selection(2, 16, 2, 19), + new Selection(3, 13, 3, 16), + new Selection(4, 12, 4, 15), + new Selection(5, 6, 5, 9), + ]); + + moveRight(editor, viewModel, true); + assertCursor(viewModel, [ + new Selection(1, 13, 1, 17), + new Selection(2, 16, 2, 20), + new Selection(3, 13, 3, 17), + new Selection(4, 12, 4, 16), + new Selection(5, 6, 5, 10), + ]); }); }); test('issue #41573 - delete across multiple lines does not shrink the selection when word wraps', () => { withTestCodeEditor([ 'Authorization: \'Bearer pHKRfCTFSnGxs6akKlb9ddIXcca0sIUSZJutPHYqz7vEeHdMTMh0SGN0IGU3a0n59DXjTLRsj5EJ2u33qLNIFi9fk5XF8pK39PndLYUZhPt4QvHGLScgSkK0L4gwzkzMloTQPpKhqiikiIOvyNNSpd2o8j29NnOmdTUOKi9DVt74PD2ohKxyOrWZ6oZprTkb3eKajcpnS0LABKfaw2rmv4\',' - ].join('\n'), { wordWrap: 'wordWrapColumn', wordWrapColumn: 100 }, (editor, cursor) => { - moveTo(cursor, 1, 43, false); - moveTo(cursor, 1, 147, true); - assertCursor(cursor, new Selection(1, 43, 1, 147)); + ].join('\n'), { wordWrap: 'wordWrapColumn', wordWrapColumn: 100 }, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 43, false); + moveTo(editor, viewModel, 1, 147, true); + assertCursor(viewModel, new Selection(1, 43, 1, 147)); - cursor.context.model.applyEdits([{ + editor.getModel().applyEdits([{ range: new Range(1, 1, 1, 43), text: '' }]); - assertCursor(cursor, new Selection(1, 1, 1, 105)); + assertCursor(viewModel, new Selection(1, 1, 1, 105)); }); }); @@ -2238,20 +2296,20 @@ suite('Editor Controller - Regression tests', () => { '一二三四五六七八九十', '12345678901234567890', ].join('\n') - ], {}, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 5, 1, 5)]); + ], {}, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); - moveDown(cursor); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + moveDown(editor, viewModel); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); - moveRight(cursor); - assertCursor(cursor, new Selection(2, 10, 2, 10)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(2, 10, 2, 10)); - moveRight(cursor); - assertCursor(cursor, new Selection(2, 11, 2, 11)); + moveRight(editor, viewModel); + assertCursor(viewModel, new Selection(2, 11, 2, 11)); - moveUp(cursor); - assertCursor(cursor, new Selection(1, 6, 1, 6)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Selection(1, 6, 1, 6)); }); }); @@ -2262,7 +2320,7 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { readOnly: true, model: model }, (editor, cursor) => { + withTestCodeEditor(null, { readOnly: true, model: model }, (editor, viewModel) => { model.pushEditOperations([new Selection(1, 1, 1, 1)], [{ range: new Range(1, 1, 1, 1), text: 'Hello world!' @@ -2313,7 +2371,7 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { multiCursorMergeOverlapping: false, model: model }, (editor, cursor) => { + withTestCodeEditor(null, { multiCursorMergeOverlapping: false, model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(1, 12, 1, 12), new Selection(1, 16, 1, 16), @@ -2323,14 +2381,14 @@ suite('Editor Controller - Regression tests', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 11, 1, 11), new Selection(1, 14, 1, 14), new Selection(2, 11, 2, 11), new Selection(2, 11, 2, 11), ]); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); + viewModel.type('\'', 'keyboard'); assert.equal(model.getLineContent(1), 'const a = \'foo\';'); assert.equal(model.getLineContent(2), 'const b = \'\''); @@ -2346,7 +2404,7 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(1, 4, 1, 4) ]); @@ -2356,17 +2414,17 @@ suite('Editor Controller - Regression tests', () => { text: '*', forceMoveMarkers: true }]); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 5, 1, 5), ]); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 4, 1, 4), ]); CoreEditingCommands.Redo.runEditorCommand(null, editor, null); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 5, 1, 5), ]); }); @@ -2381,7 +2439,7 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(1, 1, 1, 1) ]); @@ -2390,12 +2448,12 @@ suite('Editor Controller - Regression tests', () => { range: new Range(1, 1, 1, 3), text: '' }]); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 1, 1, 1), ]); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 1, 1, 1), ]); @@ -2403,7 +2461,7 @@ suite('Editor Controller - Regression tests', () => { range: new Range(1, 1, 1, 2), text: '' }]); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 1, 1, 1), ]); }); @@ -2419,17 +2477,17 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(2, 1, 2, 1) ]); - cursorCommand(cursor, H.Paste, { text: 'something\n', pasteOnNewLine: true }); + viewModel.paste('something\n', true); assert.equal(model.getValue(), [ 'abc123', 'something', '' ].join('\n')); - assertCursor(cursor, new Position(3, 1)); + assertCursor(viewModel, new Position(3, 1)); }); model.dispose(); @@ -2442,7 +2500,7 @@ suite('Editor Controller - Regression tests', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(1, 7, 1, 7) ]); @@ -2481,9 +2539,9 @@ suite('Editor Controller - Cursor Configuration', () => { '', '1' ] - }, (model, cursor) => { - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(1, 21), source: 'keyboard' }); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + }, (editor, model, viewModel) => { + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(1, 21), source: 'keyboard' }); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' \tMy First Line\t '); assert.equal(model.getLineContent(2), ' '); }); @@ -2504,58 +2562,58 @@ suite('Editor Controller - Cursor Configuration', () => { } ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { // Tab on column 1 - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 1) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 1) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 2 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 2) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 2) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'M y Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 3 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 3) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 3) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 4 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 4) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 4) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 5) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 5 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 5) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 5) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My S econd Line123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 13 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 13) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 13) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Li ne123'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); // Tab on column 14 assert.equal(model.getLineContent(2), 'My Second Line123'); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { position: new Position(2, 14) }); + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(2, 14) }); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'My Second Lin e123'); }); @@ -2570,11 +2628,11 @@ suite('Editor Controller - Cursor Configuration', () => { '\thello' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 1, 7, false); - assertCursor(cursor, new Selection(1, 7, 1, 7)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 7, false); + assertCursor(viewModel, new Selection(1, 7, 1, 7)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); @@ -2587,11 +2645,11 @@ suite('Editor Controller - Cursor Configuration', () => { '\thello' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 1, 7, false); - assertCursor(cursor, new Selection(1, 7, 1, 7)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 7, false); + assertCursor(viewModel, new Selection(1, 7, 1, 7)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thello\r\n '); }); mode.dispose(); @@ -2604,11 +2662,11 @@ suite('Editor Controller - Cursor Configuration', () => { '\thell()' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 1, 7, false); - assertCursor(cursor, new Selection(1, 7, 1, 7)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 7, false); + assertCursor(viewModel, new Selection(1, 7, 1, 7)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.CRLF), '\thell(\r\n \r\n )'); }); mode.dispose(); @@ -2622,16 +2680,16 @@ suite('Editor Controller - Cursor Configuration', () => { modelOpts: { trimAutoWhitespace: false } - }, (model, cursor) => { + }, (editor, model, viewModel) => { // Move cursor to the end, verify that we do not trim whitespaces if line has values - moveTo(cursor, 1, model.getLineContent(1).length + 1); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' some line abc '); assert.equal(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' some line abc '); assert.equal(model.getLineContent(2), ' '); assert.equal(model.getLineContent(3), ' '); @@ -2643,13 +2701,13 @@ suite('Editor Controller - Cursor Configuration', () => { text: [ ' ' ] - }, (model, cursor) => { - moveTo(cursor, 1, model.getLineContent(1).length + 1); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' '); assert.equal(model.getLineContent(2), ' '); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' '); assert.equal(model.getLineContent(2), ''); assert.equal(model.getLineContent(3), ' '); @@ -2663,10 +2721,10 @@ suite('Editor Controller - Cursor Configuration', () => { 'function foo (params: string) {}' ], languageIdentifier: mode.getLanguageIdentifier(), - }, (model, cursor) => { + }, (editor, model, viewModel) => { - moveTo(cursor, 1, 32); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + moveTo(editor, viewModel, 1, 32); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), 'function foo (params: string) {'); assert.equal(model.getLineContent(2), ' '); assert.equal(model.getLineContent(3), '}'); @@ -2677,7 +2735,7 @@ suite('Editor Controller - Cursor Configuration', () => { public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void { builder.addEditOperation(new Range(1, 13, 1, 14), ''); - this._selectionId = builder.trackSelection(cursor.getSelection()); + this._selectionId = builder.trackSelection(viewModel.getSelection()); } public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection { @@ -2686,7 +2744,7 @@ suite('Editor Controller - Cursor Configuration', () => { } - cursor.trigger('autoFormat', Handler.ExecuteCommand, new TestCommand()); + viewModel.executeCommand(new TestCommand(), 'autoFormat'); assert.equal(model.getLineContent(1), 'function foo(params: string) {'); assert.equal(model.getLineContent(2), ' '); assert.equal(model.getLineContent(3), '}'); @@ -2705,9 +2763,9 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { - moveTo(cursor, 3, 1); + moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' if (a) {'); assert.equal(model.getLineContent(2), ' '); @@ -2715,7 +2773,7 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(4), ''); assert.equal(model.getLineContent(5), ' }'); - moveTo(cursor, 4, 1); + moveTo(editor, viewModel, 4, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' if (a) {'); assert.equal(model.getLineContent(2), ' '); @@ -2723,8 +2781,8 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(4), ' '); assert.equal(model.getLineContent(5), ' }'); - moveTo(cursor, 5, model.getLineMaxColumn(5)); - cursorCommand(cursor, H.Type, { text: 'something' }, 'keyboard'); + moveTo(editor, viewModel, 5, model.getLineMaxColumn(5)); + viewModel.type('something', 'keyboard'); assert.equal(model.getLineContent(1), ' if (a) {'); assert.equal(model.getLineContent(2), ' '); assert.equal(model.getLineContent(3), ''); @@ -2742,16 +2800,16 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { // Move cursor to the end, verify that we do not trim whitespaces if line has values - moveTo(cursor, 1, model.getLineContent(1).length + 1); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + moveTo(editor, viewModel, 1, model.getLineContent(1).length + 1); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' some line abc '); assert.equal(model.getLineContent(2), ' '); // Try to enter again, we should trimmed previous line - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' some line abc '); assert.equal(model.getLineContent(2), ''); assert.equal(model.getLineContent(3), ' '); @@ -2763,15 +2821,15 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(3), ' '); // Enter and verify that trimmed again - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' some line abc '); assert.equal(model.getLineContent(2), ''); assert.equal(model.getLineContent(3), ''); assert.equal(model.getLineContent(4), ' '); // Trimmed if we will keep only text - moveTo(cursor, 1, 5); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + moveTo(editor, viewModel, 1, 5); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' '); assert.equal(model.getLineContent(2), ' some line abc '); assert.equal(model.getLineContent(3), ''); @@ -2779,9 +2837,9 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(5), ''); // Trimmed if we will keep only text by selection - moveTo(cursor, 2, 5); - moveTo(cursor, 3, 1, true); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + moveTo(editor, viewModel, 2, 5); + moveTo(editor, viewModel, 3, 1, true); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(1), ' '); assert.equal(model.getLineContent(2), ' '); assert.equal(model.getLineContent(3), ' '); @@ -2802,10 +2860,10 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { - moveTo(cursor, 3, model.getLineMaxColumn(3)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + moveTo(editor, viewModel, 3, model.getLineMaxColumn(3)); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(), [ ' function f() {', @@ -2814,9 +2872,9 @@ suite('Editor Controller - Cursor Configuration', () => { ' ', ' }', ].join('\n')); - assertCursor(cursor, new Position(4, model.getLineMaxColumn(4))); + assertCursor(viewModel, new Position(4, model.getLineMaxColumn(4))); - cursorCommand(cursor, H.Paste, { text: ' // I\'m gonna copy this line\n', pasteOnNewLine: true }); + viewModel.paste(' // I\'m gonna copy this line\n', true); assert.equal(model.getValue(), [ ' function f() {', ' // I\'m gonna copy this line', @@ -2825,7 +2883,7 @@ suite('Editor Controller - Cursor Configuration', () => { '', ' }', ].join('\n')); - assertCursor(cursor, new Position(5, 1)); + assertCursor(viewModel, new Position(5, 1)); }); model.dispose(); @@ -2842,10 +2900,10 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([new Selection(4, 10, 4, 10)]); - cursorCommand(cursor, H.Paste, { text: ' // I\'m gonna copy this line\n', pasteOnNewLine: true }); + viewModel.paste(' // I\'m gonna copy this line\n', true); assert.equal(model.getValue(), [ ' function f() {', @@ -2855,7 +2913,7 @@ suite('Editor Controller - Cursor Configuration', () => { ' return 3;', ' }', ].join('\n')); - assertCursor(cursor, new Position(5, 10)); + assertCursor(viewModel, new Position(5, 10)); }); model.dispose(); @@ -2870,9 +2928,9 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, cursor) => { + withTestCodeEditor(null, { model: model, useTabStops: false }, (editor, viewModel) => { // DeleteLeft removes just one whitespace - moveTo(cursor, 2, 9); + moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' a '); }); @@ -2889,14 +2947,14 @@ suite('Editor Controller - Cursor Configuration', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model, useTabStops: true }, (editor, cursor) => { + withTestCodeEditor(null, { model: model, useTabStops: true }, (editor, viewModel) => { // DeleteLeft does not remove tab size, because some text exists before - moveTo(cursor, 2, model.getLineContent(2).length + 1); + moveTo(editor, viewModel, 2, model.getLineContent(2).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' a '); // DeleteLeft removes tab size = 4 - moveTo(cursor, 2, 9); + moveTo(editor, viewModel, 2, 9); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' a '); @@ -2909,12 +2967,12 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(2), ' a '); // Nothing is broken when cursor is in (1,1) - moveTo(cursor, 1, 1); + moveTo(editor, viewModel, 1, 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' \t \t x'); // DeleteLeft stops at tab stops even in mixed whitespace case - moveTo(cursor, 1, 10); + moveTo(editor, viewModel, 1, 10); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' \t \t x'); @@ -2928,7 +2986,7 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getLineContent(1), 'x'); // DeleteLeft on last line - moveTo(cursor, 3, model.getLineContent(3).length + 1); + moveTo(editor, viewModel, 3, model.getLineContent(3).length + 1); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(3), ''); @@ -2937,8 +2995,8 @@ suite('Editor Controller - Cursor Configuration', () => { assert.equal(model.getValue(EndOfLinePreference.LF), 'x\n a '); // In case of selection DeleteLeft only deletes selected text - moveTo(cursor, 2, 3); - moveTo(cursor, 2, 4, true); + moveTo(editor, viewModel, 2, 3); + moveTo(editor, viewModel, 2, 4, true); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' a '); }); @@ -2956,23 +3014,23 @@ suite('Editor Controller - Cursor Configuration', () => { } ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n', 'assert1'); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\t', 'assert2'); - cursorCommand(cursor, H.Type, { text: 'y' }, 'keyboard'); + viewModel.type('y', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty', 'assert2'); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\t', 'assert3'); - cursorCommand(cursor, H.Type, { text: 'x' }); + viewModel.type('x'); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert4'); - CoreNavigationCommands.CursorLeft.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorLeft.runCoreEditorCommand(viewModel, {}); assert.equal(model.getValue(EndOfLinePreference.LF), '\n\ty\n\tx', 'assert5'); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -3022,10 +3080,10 @@ suite('Editor Controller - Cursor Configuration', () => { } ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { const beforeVersion = model.getVersionId(); const beforeAltVersion = model.getAlternativeVersionId(); - cursorCommand(cursor, H.Type, { text: 'Hello' }, 'keyboard'); + viewModel.type('Hello', 'keyboard'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); const afterVersion = model.getVersionId(); const afterAltVersion = model.getAlternativeVersionId(); @@ -3057,18 +3115,19 @@ suite('Editor Controller - Indentation Rules', () => { languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 1, 12, false); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 12, false); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); - cursorCommandAndTokenize(model, cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(2, 2, 2, 2)); + viewModel.type('\n', 'keyboard'); + model.forceTokenization(model.getLineCount()); + assertCursor(viewModel, new Selection(2, 2, 2, 2)); - moveTo(cursor, 3, 13, false); - assertCursor(cursor, new Selection(3, 13, 3, 13)); + moveTo(editor, viewModel, 3, 13, false); + assertCursor(viewModel, new Selection(3, 13, 3, 13)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); }); }); @@ -3080,12 +3139,12 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 2, 2, false); - assertCursor(cursor, new Selection(2, 2, 2, 2)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 2, false); + assertCursor(viewModel, new Selection(2, 2, 2, 2)); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); - assertCursor(cursor, new Selection(2, 2, 2, 2)); + viewModel.type('}', 'keyboard'); + assertCursor(viewModel, new Selection(2, 2, 2, 2)); assert.equal(model.getLineContent(2), '}', '001'); }); }); @@ -3099,12 +3158,12 @@ suite('Editor Controller - Indentation Rules', () => { languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 2, 15, false); - assertCursor(cursor, new Selection(2, 15, 2, 15)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 15, false); + assertCursor(viewModel, new Selection(2, 15, 2, 15)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(3, 2, 3, 2)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(3, 2, 3, 2)); }); }); @@ -3119,18 +3178,19 @@ suite('Editor Controller - Indentation Rules', () => { languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 2, 14, false); - assertCursor(cursor, new Selection(2, 14, 2, 14)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 14, false); + assertCursor(viewModel, new Selection(2, 14, 2, 14)); - cursorCommandAndTokenize(model, cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(3, 1, 3, 1)); + viewModel.type('\n', 'keyboard'); + model.forceTokenization(model.getLineCount()); + assertCursor(viewModel, new Selection(3, 1, 3, 1)); - moveTo(cursor, 5, 16, false); - assertCursor(cursor, new Selection(5, 16, 5, 16)); + moveTo(editor, viewModel, 5, 16, false); + assertCursor(viewModel, new Selection(5, 16, 5, 16)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(6, 2, 6, 2)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(6, 2, 6, 2)); }); }); @@ -3146,16 +3206,17 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, cursor) => { - moveTo(cursor, 2, 11, false); - assertCursor(cursor, new Selection(2, 11, 2, 11)); + withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { + moveTo(editor, viewModel, 2, 11, false); + assertCursor(viewModel, new Selection(2, 11, 2, 11)); - cursorCommandAndTokenize(model, cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(3, 3, 3, 3)); + viewModel.type('\n', 'keyboard'); + model.forceTokenization(model.getLineCount()); + assertCursor(viewModel, new Selection(3, 3, 3, 3)); - cursorCommand(cursor, H.Type, { text: 'console.log();' }, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 1, 4, 1)); + viewModel.type('console.log();', 'keyboard'); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 1, 4, 1)); }); model.dispose(); @@ -3171,12 +3232,12 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 3, 13, false); - assertCursor(cursor, new Selection(3, 13, 3, 13)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 13, false); + assertCursor(viewModel, new Selection(3, 13, 3, 13)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 1, 4, 1)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 1, 4, 1)); assert.equal(model.getLineContent(3), 'return true;', '001'); }); }); @@ -3191,13 +3252,13 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 4, 3, false); - moveTo(cursor, 4, 4, true); - assertCursor(cursor, new Selection(4, 3, 4, 4)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 4, 3, false); + moveTo(editor, viewModel, 4, 4, true); + assertCursor(viewModel, new Selection(4, 3, 4, 4)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(5, 1, 5, 1)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(5, 1, 5, 1)); assert.equal(model.getLineContent(4), '\t}', '001'); }); }); @@ -3210,16 +3271,16 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 2, 12, false); - moveTo(cursor, 2, 13, true); - assertCursor(cursor, new Selection(2, 12, 2, 13)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 12, false); + moveTo(editor, viewModel, 2, 13, true); + assertCursor(viewModel, new Selection(2, 12, 2, 13)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(3, 3, 3, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(3, 3, 3, 3)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); }); }); @@ -3230,20 +3291,20 @@ suite('Editor Controller - Indentation Rules', () => { '\tif (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - }, (model, cursor) => { - moveTo(cursor, 1, 12, false); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 12, false); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(2, 5, 2, 5)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(2, 5, 2, 5)); model.forceTokenization(model.getLineCount()); - moveTo(cursor, 3, 13, false); - assertCursor(cursor, new Selection(3, 13, 3, 13)); + moveTo(editor, viewModel, 3, 13, false); + assertCursor(viewModel, new Selection(3, 13, 3, 13)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 9, 4, 9)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 9, 4, 9)); }); }); @@ -3254,19 +3315,20 @@ suite('Editor Controller - Indentation Rules', () => { ' if (true) {' ], languageIdentifier: mode.getLanguageIdentifier(), - }, (model, cursor) => { - moveTo(cursor, 1, 12, false); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 12, false); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); - cursorCommandAndTokenize(model, cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(2, 5, 2, 5)); + viewModel.type('\n', 'keyboard'); + model.forceTokenization(model.getLineCount()); + assertCursor(viewModel, new Selection(2, 5, 2, 5)); - moveTo(cursor, 3, 16, false); - assertCursor(cursor, new Selection(3, 16, 3, 16)); + moveTo(editor, viewModel, 3, 16, false); + assertCursor(viewModel, new Selection(3, 16, 3, 16)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(3), ' if (true) {'); - assertCursor(cursor, new Selection(4, 9, 4, 9)); + assertCursor(viewModel, new Selection(4, 9, 4, 9)); }); }); @@ -3278,19 +3340,20 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 1, 12, false); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 12, false); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); - cursorCommandAndTokenize(model, cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(2, 2, 2, 2)); + viewModel.type('\n', 'keyboard'); + model.forceTokenization(model.getLineCount()); + assertCursor(viewModel, new Selection(2, 2, 2, 2)); - moveTo(cursor, 3, 16, false); - assertCursor(cursor, new Selection(3, 16, 3, 16)); + moveTo(editor, viewModel, 3, 16, false); + assertCursor(viewModel, new Selection(3, 16, 3, 16)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(3), ' if (true) {'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); }); }); @@ -3307,13 +3370,13 @@ suite('Editor Controller - Indentation Rules', () => { languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false }, editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 5, 4, false); - assertCursor(cursor, new Selection(5, 4, 5, 4)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 5, 4, false); + assertCursor(viewModel, new Selection(5, 4, 5, 4)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(5), '\t\t}'); - assertCursor(cursor, new Selection(6, 3, 6, 3)); + assertCursor(viewModel, new Selection(6, 3, 6, 3)); }); }); @@ -3327,12 +3390,12 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 3, 9, false); - assertCursor(cursor, new Selection(3, 9, 3, 9)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 9, false); + assertCursor(viewModel, new Selection(3, 9, 3, 9)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); assert.equal(model.getLineContent(4), '\t\t true;', '001'); }); }); @@ -3347,12 +3410,12 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 3, 3, false); - assertCursor(cursor, new Selection(3, 3, 3, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 3, false); + assertCursor(viewModel, new Selection(3, 3, 3, 3)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); }); }); @@ -3366,12 +3429,12 @@ suite('Editor Controller - Indentation Rules', () => { ' }a}' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 3, 11, false); - assertCursor(cursor, new Selection(3, 11, 3, 11)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 11, false); + assertCursor(viewModel, new Selection(3, 11, 3, 11)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 5, 4, 5)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 5, 4, 5)); assert.equal(model.getLineContent(4), ' true;', '001'); }); }); @@ -3386,19 +3449,19 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 3, 2, false); - assertCursor(cursor, new Selection(3, 2, 3, 2)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 2, false); + assertCursor(viewModel, new Selection(3, 2, 3, 2)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 2, 4, 2)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 2, 4, 2)); assert.equal(model.getLineContent(4), '\t\treturn true;', '001'); - moveTo(cursor, 4, 1, false); - assertCursor(cursor, new Selection(4, 1, 4, 1)); + moveTo(editor, viewModel, 4, 1, false); + assertCursor(viewModel, new Selection(4, 1, 4, 1)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(5, 1, 5, 1)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(5, 1, 5, 1)); assert.equal(model.getLineContent(5), '\t\treturn true;', '002'); }); }); @@ -3413,19 +3476,19 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 3, 4, false); - assertCursor(cursor, new Selection(3, 4, 3, 4)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 4, false); + assertCursor(viewModel, new Selection(3, 4, 3, 4)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); assert.equal(model.getLineContent(4), '\t\t\treturn true;', '001'); - moveTo(cursor, 4, 1, false); - assertCursor(cursor, new Selection(4, 1, 4, 1)); + moveTo(editor, viewModel, 4, 1, false); + assertCursor(viewModel, new Selection(4, 1, 4, 1)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(5, 1, 5, 1)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(5, 1, 5, 1)); assert.equal(model.getLineContent(5), '\t\t\treturn true;', '002'); }); }); @@ -3439,17 +3502,17 @@ suite('Editor Controller - Indentation Rules', () => { '}a}' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 3, 2, false); - assertCursor(cursor, new Selection(3, 2, 3, 2)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 2, false); + assertCursor(viewModel, new Selection(3, 2, 3, 2)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 2, 4, 2)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 2, 4, 2)); assert.equal(model.getLineContent(4), ' return true;', '001'); - moveTo(cursor, 4, 3, false); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(5, 3, 5, 3)); + moveTo(editor, viewModel, 4, 3, false); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(5, 3, 5, 3)); assert.equal(model.getLineContent(5), ' return true;', '002'); }); }); @@ -3472,17 +3535,17 @@ suite('Editor Controller - Indentation Rules', () => { tabSize: 2, indentSize: 2 } - }, (model, cursor) => { - moveTo(cursor, 3, 3, false); - assertCursor(cursor, new Selection(3, 3, 3, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 3, false); + assertCursor(viewModel, new Selection(3, 3, 3, 3)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 4, 4, 4)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 4, 4, 4)); assert.equal(model.getLineContent(4), ' return true;', '001'); - moveTo(cursor, 9, 4, false); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(10, 5, 10, 5)); + moveTo(editor, viewModel, 9, 4, false); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(10, 5, 10, 5)); assert.equal(model.getLineContent(10), ' return true;', '001'); }); }); @@ -3498,13 +3561,13 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), modelOpts: { tabSize: 2 } - }, (model, cursor) => { - moveTo(cursor, 3, 5, false); - moveTo(cursor, 4, 3, true); - assertCursor(cursor, new Selection(3, 5, 4, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 5, false); + moveTo(editor, viewModel, 4, 3, true); + assertCursor(viewModel, new Selection(3, 5, 4, 3)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); assert.equal(model.getLineContent(4), ' return true;', '001'); }); }); @@ -3521,14 +3584,14 @@ suite('Editor Controller - Indentation Rules', () => { insertSpaces: false, }, languageIdentifier: mode.getLanguageIdentifier(), - }, (model, cursor) => { - moveTo(cursor, 3, 8, false); - moveTo(cursor, 2, 12, true); - assertCursor(cursor, new Selection(3, 8, 2, 12)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 8, false); + moveTo(editor, viewModel, 2, 12, true); + assertCursor(viewModel, new Selection(3, 8, 2, 12)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(3), '\treturn x;'); - assertCursor(cursor, new Position(3, 2)); + assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3544,14 +3607,14 @@ suite('Editor Controller - Indentation Rules', () => { insertSpaces: false, }, languageIdentifier: mode.getLanguageIdentifier(), - }, (model, cursor) => { - moveTo(cursor, 2, 12, false); - moveTo(cursor, 3, 8, true); - assertCursor(cursor, new Selection(2, 12, 3, 8)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 12, false); + moveTo(editor, viewModel, 3, 8, true); + assertCursor(viewModel, new Selection(2, 12, 3, 8)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(3), '\treturn x;'); - assertCursor(cursor, new Position(3, 2)); + assertCursor(viewModel, new Position(3, 2)); }); }); @@ -3566,13 +3629,13 @@ suite('Editor Controller - Indentation Rules', () => { '?>' ], modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 5, 3, false); - assertCursor(cursor, new Selection(5, 3, 5, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 5, 3, false); + assertCursor(viewModel, new Selection(5, 3, 5, 3)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getLineContent(6), '\t'); - assertCursor(cursor, new Selection(6, 2, 6, 2)); + assertCursor(viewModel, new Selection(6, 2, 6, 2)); assert.equal(model.getLineContent(5), '\t}'); }); }); @@ -3585,12 +3648,12 @@ suite('Editor Controller - Indentation Rules', () => { ' ' ], modelOpts: { insertSpaces: false } - }, (model, cursor) => { - moveTo(cursor, 3, 2, false); - assertCursor(cursor, new Selection(3, 2, 3, 2)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 2, false); + assertCursor(viewModel, new Selection(3, 2, 3, 2)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); - assertCursor(cursor, new Selection(4, 2, 4, 2)); + viewModel.type('\n', 'keyboard'); + assertCursor(viewModel, new Selection(4, 2, 4, 2)); assert.equal(model.getLineContent(4), '\t'); }); }); @@ -3611,9 +3674,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 4, 1, false); - assertCursor(cursor, new Selection(4, 1, 4, 1)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 4, 1, false); + assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(4), '\t\t'); @@ -3639,9 +3702,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 4, 2, false); - assertCursor(cursor, new Selection(4, 2, 4, 2)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 4, 2, false); + assertCursor(viewModel, new Selection(4, 2, 4, 2)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(4), '\t\t\t'); @@ -3667,9 +3730,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 4, 1, false); - assertCursor(cursor, new Selection(4, 1, 4, 1)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 4, 1, false); + assertCursor(viewModel, new Selection(4, 1, 4, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(4), '\t\t\t'); @@ -3694,9 +3757,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 4, 3, false); - assertCursor(cursor, new Selection(4, 3, 4, 3)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 4, 3, false); + assertCursor(viewModel, new Selection(4, 3, 4, 3)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(4), '\t\t\t\t'); @@ -3721,9 +3784,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - moveTo(cursor, 4, 4, false); - assertCursor(cursor, new Selection(4, 4, 4, 4)); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + moveTo(editor, viewModel, 4, 4, false); + assertCursor(viewModel, new Selection(4, 4, 4, 4)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(4), '\t\t\t\t\t'); @@ -3746,9 +3809,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { - moveTo(cursor, 3, 1); + moveTo(editor, viewModel, 3, 1); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), ' if (a) {'); assert.equal(model.getLineContent(2), ' '); @@ -3776,11 +3839,11 @@ suite('Editor Controller - Indentation Rules', () => { rubyMode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, cursor) => { - moveTo(cursor, 4, 7, false); - assertCursor(cursor, new Selection(4, 7, 4, 7)); + withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { + moveTo(editor, viewModel, 4, 7, false); + assertCursor(viewModel, new Selection(4, 7, 4, 7)); - cursorCommand(cursor, H.Type, { text: 'd' }, 'keyboard'); + viewModel.type('d', 'keyboard'); assert.equal(model.getLineContent(4), ' end'); }); @@ -3798,12 +3861,12 @@ suite('Editor Controller - Indentation Rules', () => { '\t}' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 5, 3, false); - assertCursor(cursor, new Selection(5, 3, 5, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 5, 3, false); + assertCursor(viewModel, new Selection(5, 3, 5, 3)); - cursorCommand(cursor, H.Type, { text: 'e' }, 'keyboard'); - assertCursor(cursor, new Selection(5, 4, 5, 4)); + viewModel.type('e', 'keyboard'); + assertCursor(viewModel, new Selection(5, 4, 5, 4)); assert.equal(model.getLineContent(5), '\t}e', 'This line should not decrease indent'); }); }); @@ -3819,12 +3882,12 @@ suite('Editor Controller - Indentation Rules', () => { '}' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 3, false); - assertCursor(cursor, new Selection(2, 3, 2, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 3, false); + assertCursor(viewModel, new Selection(2, 3, 2, 3)); - cursorCommand(cursor, H.Type, { text: ' ' }, 'keyboard'); - assertCursor(cursor, new Selection(2, 4, 2, 4)); + viewModel.type(' ', 'keyboard'); + assertCursor(viewModel, new Selection(2, 4, 2, 4)); assert.equal(model.getLineContent(2), '\t ) {', 'This line should not decrease indent'); }); }); @@ -3838,12 +3901,12 @@ suite('Editor Controller - Indentation Rules', () => { ], languageIdentifier: mode.getLanguageIdentifier(), editorOpts: { autoIndent: 'full' } - }, (model, cursor) => { - moveTo(cursor, 3, 3, false); - assertCursor(cursor, new Selection(3, 3, 3, 3)); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 3, false); + assertCursor(viewModel, new Selection(3, 3, 3, 3)); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); - assertCursor(cursor, new Selection(3, 2, 3, 2)); + viewModel.type('}', 'keyboard'); + assertCursor(viewModel, new Selection(3, 2, 3, 2)); assert.equal(model.getLineContent(3), '}'); }); }); @@ -3886,11 +3949,11 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, cursor) => { - moveTo(cursor, 7, 6, false); - assertCursor(cursor, new Selection(7, 6, 7, 6)); + withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { + moveTo(editor, viewModel, 7, 6, false); + assertCursor(viewModel, new Selection(7, 6, 7, 6)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.equal(model.getValue(), [ 'class ItemCtrl {', @@ -3904,7 +3967,7 @@ suite('Editor Controller - Indentation Rules', () => { '}', ].join('\n') ); - assertCursor(cursor, new Selection(8, 5, 8, 5)); + assertCursor(viewModel, new Selection(8, 5, 8, 5)); }); model.dispose(); @@ -3950,9 +4013,9 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, cursor) => { - moveTo(cursor, 8, 1, false); - assertCursor(cursor, new Selection(8, 1, 8, 1)); + withTestCodeEditor(null, { model: model, autoIndent: 'advanced' }, (editor, viewModel) => { + moveTo(editor, viewModel, 8, 1, false); + assertCursor(viewModel, new Selection(8, 1, 8, 1)); CoreEditingCommands.Tab.runEditorCommand(null, editor, null); assert.equal(model.getValue(), @@ -3968,7 +4031,7 @@ suite('Editor Controller - Indentation Rules', () => { ')', ].join('\n') ); - assert.deepEqual(cursor.getSelection(), new Selection(8, 3, 8, 3)); + assert.deepEqual(viewModel.getSelection(), new Selection(8, 3, 8, 3)); }); model.dispose(); @@ -4013,30 +4076,30 @@ suite('Editor Controller - Indentation Rules', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, cursor) => { - moveTo(cursor, 3, 19, false); - assertCursor(cursor, new Selection(3, 19, 3, 19)); + withTestCodeEditor(null, { model: model, autoIndent: 'full' }, (editor, viewModel) => { + moveTo(editor, viewModel, 3, 19, false); + assertCursor(viewModel, new Selection(3, 19, 3, 19)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.deepEqual(model.getLineContent(4), ' '); - moveTo(cursor, 5, 18, false); - assertCursor(cursor, new Selection(5, 18, 5, 18)); + moveTo(editor, viewModel, 5, 18, false); + assertCursor(viewModel, new Selection(5, 18, 5, 18)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.deepEqual(model.getLineContent(6), ' '); - moveTo(cursor, 7, 15, false); - assertCursor(cursor, new Selection(7, 15, 7, 15)); + moveTo(editor, viewModel, 7, 15, false); + assertCursor(viewModel, new Selection(7, 15, 7, 15)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.deepEqual(model.getLineContent(8), ' '); assert.deepEqual(model.getLineContent(9), ' ]'); - moveTo(cursor, 10, 18, false); - assertCursor(cursor, new Selection(10, 18, 10, 18)); + moveTo(editor, viewModel, 10, 18, false); + assertCursor(viewModel, new Selection(10, 18, 10, 18)); - cursorCommand(cursor, H.Type, { text: '\n' }, 'keyboard'); + viewModel.type('\n', 'keyboard'); assert.deepEqual(model.getLineContent(11), ' ]'); }); @@ -4052,12 +4115,12 @@ interface ICursorOpts { editorOpts?: IEditorOptions; } -function usingCursor(opts: ICursorOpts, callback: (model: TextModel, cursor: Cursor) => void): void { +function usingCursor(opts: ICursorOpts, callback: (editor: ITestCodeEditor, model: TextModel, viewModel: ViewModel) => void): void { const model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); const editorOptions: TestCodeEditorCreationOptions = opts.editorOpts || {}; editorOptions.model = model; - withTestCodeEditor(null, editorOptions, (editor, cursor) => { - callback(model, cursor); + withTestCodeEditor(null, editorOptions, (editor, viewModel) => { + callback(editor, model, viewModel); }); } @@ -4089,9 +4152,9 @@ suite('ElectricCharacter', () => { '' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 1); - cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 1); + viewModel.type('*', 'keyboard'); assert.deepEqual(model.getLineContent(2), '*'); }); mode.dispose(); @@ -4105,9 +4168,9 @@ suite('ElectricCharacter', () => { '' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 1); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 1); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(2), ' }'); }); mode.dispose(); @@ -4121,9 +4184,9 @@ suite('ElectricCharacter', () => { ' ' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 5); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 5); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(2), ' }'); }); mode.dispose(); @@ -4139,9 +4202,9 @@ suite('ElectricCharacter', () => { ' ' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 4, 1); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 4, 1); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(4), ' } '); }); mode.dispose(); @@ -4157,9 +4220,9 @@ suite('ElectricCharacter', () => { ' } ' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 4, 6); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 4, 6); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(4), ' } }'); }); mode.dispose(); @@ -4173,9 +4236,9 @@ suite('ElectricCharacter', () => { '// hello' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 1); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 1); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(2), ' }// hello'); }); mode.dispose(); @@ -4189,9 +4252,9 @@ suite('ElectricCharacter', () => { ' ' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 3); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 3); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(2), ' }'); }); mode.dispose(); @@ -4205,9 +4268,9 @@ suite('ElectricCharacter', () => { 'a' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 2); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 2); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(2), 'a}'); }); mode.dispose(); @@ -4222,9 +4285,9 @@ suite('ElectricCharacter', () => { '})' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 13); - cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 13); + viewModel.type('*', 'keyboard'); assert.deepEqual(model.getLineContent(2), ' ( 1 + 2 ) *'); }); mode.dispose(); @@ -4237,13 +4300,13 @@ suite('ElectricCharacter', () => { '(div', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 1, 5); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 1, 5); let changeText: string | null = null; model.onDidChangeContent(e => { changeText = e.changes[0].text; }); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.deepEqual(model.getLineContent(1), '(div)'); assert.deepEqual(changeText, ')'); }); @@ -4259,9 +4322,9 @@ suite('ElectricCharacter', () => { '\t3' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 3, 3); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 3, 3); + viewModel.type(')', 'keyboard'); assert.deepEqual(model.getLineContent(3), '\t3)'); }); mode.dispose(); @@ -4275,9 +4338,9 @@ suite('ElectricCharacter', () => { '/*' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 3); - cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 3); + viewModel.type('*', 'keyboard'); assert.deepEqual(model.getLineContent(2), '/** */'); }); mode.dispose(); @@ -4291,9 +4354,9 @@ suite('ElectricCharacter', () => { ' /*' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 5); - cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 5); + viewModel.type('*', 'keyboard'); assert.deepEqual(model.getLineContent(2), ' /** */'); }); mode.dispose(); @@ -4307,10 +4370,10 @@ suite('ElectricCharacter', () => { 'word' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - moveTo(cursor, 2, 5); - moveTo(cursor, 2, 1, true); - cursorCommand(cursor, H.Type, { text: '}' }, 'keyboard'); + }, (editor, model, viewModel) => { + moveTo(editor, viewModel, 2, 5); + moveTo(editor, viewModel, 2, 1, true); + viewModel.type('}', 'keyboard'); assert.deepEqual(model.getLineContent(2), '}'); }); mode.dispose(); @@ -4382,11 +4445,11 @@ suite('autoClosingPairs', () => { return result; } - function assertType(model: TextModel, cursor: Cursor, lineNumber: number, column: number, chr: string, expectedInsert: string, message: string): void { + function assertType(editor: ITestCodeEditor, model: TextModel, viewModel: ViewModel, lineNumber: number, column: number, chr: string, expectedInsert: string, message: string): void { let lineContent = model.getLineContent(lineNumber); let expected = lineContent.substr(0, column - 1) + expectedInsert + lineContent.substr(column - 1); - moveTo(cursor, lineNumber, column); - cursorCommand(cursor, H.Type, { text: chr }, 'keyboard'); + moveTo(editor, viewModel, lineNumber, column); + viewModel.type(chr, 'keyboard'); assert.deepEqual(model.getLineContent(lineNumber), expected, message); model.undo(); } @@ -4405,7 +4468,7 @@ suite('autoClosingPairs', () => { 'var h = { a: \'value\' };', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var| a| |=| [|]|;|', @@ -4424,9 +4487,9 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); } } } @@ -4451,7 +4514,7 @@ suite('autoClosingPairs', () => { editorOpts: { autoClosingBrackets: 'beforeWhitespace' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var| a| =| [|];|', @@ -4470,9 +4533,9 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); } } } @@ -4491,7 +4554,7 @@ suite('autoClosingPairs', () => { autoClosingBrackets: 'beforeWhitespace', autoClosingQuotes: 'never' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var| a| =| [|];|', @@ -4503,11 +4566,11 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); } - assertType(model, cursor, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); } } }); @@ -4521,7 +4584,7 @@ suite('autoClosingPairs', () => { autoClosingBrackets: 'never', autoClosingQuotes: 'beforeWhitespace' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var b =| [|];|', @@ -4533,11 +4596,11 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); } - assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); } } }); @@ -4562,7 +4625,7 @@ suite('autoClosingPairs', () => { editorOpts: { autoClosingBrackets: 'languageDefined' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'v|ar |a = [|];|', @@ -4581,9 +4644,9 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); } } } @@ -4609,7 +4672,7 @@ suite('autoClosingPairs', () => { autoClosingBrackets: 'never', autoClosingQuotes: 'never' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var a = [];', @@ -4628,11 +4691,11 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); - assertType(model, cursor, lineNumber, column, '"', '""', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '()', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '"', '""', `auto closes @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); - assertType(model, cursor, lineNumber, column, '"', '"', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '(', '(', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '"', '"', `does not auto close @ (${lineNumber}, ${column})`); } } } @@ -4647,20 +4710,20 @@ suite('autoClosingPairs', () => { 'var a = asd' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [ + viewModel.setSelections('test', [ new Selection(1, 1, 1, 4), new Selection(1, 9, 1, 12), ]); // type a ` - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + viewModel.type('`', 'keyboard'); assert.equal(model.getValue(), '`var` a = `asd`'); // type a ( - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + viewModel.type('(', 'keyboard'); assert.equal(model.getValue(), '`(var)` a = `(asd)`'); }); @@ -4673,14 +4736,14 @@ suite('autoClosingPairs', () => { editorOpts: { autoSurround: 'never' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [ + viewModel.setSelections('test', [ new Selection(1, 1, 1, 4), ]); // type a ` - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + viewModel.type('`', 'keyboard'); assert.equal(model.getValue(), '` a = asd'); }); @@ -4693,18 +4756,18 @@ suite('autoClosingPairs', () => { editorOpts: { autoSurround: 'quotes' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [ + viewModel.setSelections('test', [ new Selection(1, 1, 1, 4), ]); // type a ` - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + viewModel.type('`', 'keyboard'); assert.equal(model.getValue(), '`var` a = asd'); // type a ( - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + viewModel.type('(', 'keyboard'); assert.equal(model.getValue(), '`(` a = asd'); }); @@ -4716,18 +4779,18 @@ suite('autoClosingPairs', () => { editorOpts: { autoSurround: 'brackets' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [ + viewModel.setSelections('test', [ new Selection(1, 1, 1, 4), ]); // type a ( - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + viewModel.type('(', 'keyboard'); assert.equal(model.getValue(), '(var) a = asd'); // type a ` - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + viewModel.type('`', 'keyboard'); assert.equal(model.getValue(), '(`) a = asd'); }); mode.dispose(); @@ -4747,7 +4810,7 @@ suite('autoClosingPairs', () => { 'var h = { a: \'value\' };', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var a |=| [|]|;|', @@ -4766,11 +4829,11 @@ suite('autoClosingPairs', () => { for (let column = 1; column < autoCloseColumns.length; column++) { model.forceTokenization(lineNumber); if (autoCloseColumns[column] === ColumnType.Special1) { - assertType(model, cursor, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '\'', '\'\'', `auto closes @ (${lineNumber}, ${column})`); } else if (autoCloseColumns[column] === ColumnType.Special2) { - assertType(model, cursor, lineNumber, column, '\'', '', `over types @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '\'', '', `over types @ (${lineNumber}, ${column})`); } else { - assertType(model, cursor, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); + assertType(editor, model, viewModel, lineNumber, column, '\'', '\'', `does not auto close @ (${lineNumber}, ${column})`); } } } @@ -4785,16 +4848,16 @@ suite('autoClosingPairs', () => { '', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { model.setValue('begi'); - cursor.setSelections('test', [new Selection(1, 5, 1, 5)]); - cursorCommand(cursor, H.Type, { text: 'n' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.type('n', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'beginend'); model.setValue('/*'); - cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); - cursorCommand(cursor, H.Type, { text: '*' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('*', 'keyboard'); assert.strictEqual(model.getLineContent(1), '/** */'); }); mode.dispose(); @@ -4830,17 +4893,17 @@ suite('autoClosingPairs', () => { 'Big LAMB' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { model.forceTokenization(model.getLineCount()); - assertType(model, cursor, 1, 4, '"', '"', `does not double quote when ending with open`); + assertType(editor, model, viewModel, 1, 4, '"', '"', `does not double quote when ending with open`); model.forceTokenization(model.getLineCount()); - assertType(model, cursor, 2, 4, '"', '"', `does not double quote when ending with open`); + assertType(editor, model, viewModel, 2, 4, '"', '"', `does not double quote when ending with open`); model.forceTokenization(model.getLineCount()); - assertType(model, cursor, 3, 4, '"', '"', `does not double quote when ending with open`); + assertType(editor, model, viewModel, 3, 4, '"', '"', `does not double quote when ending with open`); model.forceTokenization(model.getLineCount()); - assertType(model, cursor, 4, 2, '"', '"', `does not double quote when ending with open`); + assertType(editor, model, viewModel, 4, 2, '"', '"', `does not double quote when ending with open`); model.forceTokenization(model.getLineCount()); - assertType(model, cursor, 4, 3, '"', '"', `does not double quote when ending with open`); + assertType(editor, model, viewModel, 4, 3, '"', '"', `does not double quote when ending with open`); }); mode.dispose(); }); @@ -4852,8 +4915,8 @@ suite('autoClosingPairs', () => { 'var arr = ["b", "c"];' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertType(model, cursor, 1, 12, '"', '""', `does not over type and will auto close`); + }, (editor, model, viewModel) => { + assertType(editor, model, viewModel, 1, 12, '"', '""', `does not over type and will auto close`); }); mode.dispose(); }); @@ -4865,60 +4928,60 @@ suite('autoClosingPairs', () => { '', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { - function typeCharacters(cursor: Cursor, chars: string): void { + function typeCharacters(viewModel: ViewModel, chars: string): void { for (let i = 0, len = chars.length; i < len; i++) { - cursorCommand(cursor, H.Type, { text: chars[i] }, 'keyboard'); + viewModel.type(chars[i], 'keyboard'); } } // First gif model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste1 = teste\' ok'); + typeCharacters(viewModel, 'teste1 = teste\' ok'); assert.equal(model.getLineContent(1), 'teste1 = teste\' ok'); - cursor.setSelections('test', [new Selection(1, 1000, 1, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(1, 1000, 1, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste2 = teste \'ok'); + typeCharacters(viewModel, 'teste2 = teste \'ok'); assert.equal(model.getLineContent(2), 'teste2 = teste \'ok\''); - cursor.setSelections('test', [new Selection(2, 1000, 2, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(2, 1000, 2, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste3 = teste" ok'); + typeCharacters(viewModel, 'teste3 = teste" ok'); assert.equal(model.getLineContent(3), 'teste3 = teste" ok'); - cursor.setSelections('test', [new Selection(3, 1000, 3, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(3, 1000, 3, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste4 = teste "ok'); + typeCharacters(viewModel, 'teste4 = teste "ok'); assert.equal(model.getLineContent(4), 'teste4 = teste "ok"'); // Second gif - cursor.setSelections('test', [new Selection(4, 1000, 4, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(4, 1000, 4, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste \''); + typeCharacters(viewModel, 'teste \''); assert.equal(model.getLineContent(5), 'teste \'\''); - cursor.setSelections('test', [new Selection(5, 1000, 5, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(5, 1000, 5, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste "'); + typeCharacters(viewModel, 'teste "'); assert.equal(model.getLineContent(6), 'teste ""'); - cursor.setSelections('test', [new Selection(6, 1000, 6, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(6, 1000, 6, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste\''); + typeCharacters(viewModel, 'teste\''); assert.equal(model.getLineContent(7), 'teste\''); - cursor.setSelections('test', [new Selection(7, 1000, 7, 1000)]); - typeCharacters(cursor, '\n'); + viewModel.setSelections('test', [new Selection(7, 1000, 7, 1000)]); + typeCharacters(viewModel, '\n'); model.forceTokenization(model.getLineCount()); - typeCharacters(cursor, 'teste"'); + typeCharacters(viewModel, 'teste"'); assert.equal(model.getLineContent(8), 'teste"'); }); mode.dispose(); @@ -4932,22 +4995,22 @@ suite('autoClosingPairs', () => { 'y=();' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); - cursorCommand(cursor, H.Type, { text: 'x=(' }, 'keyboard'); + viewModel.type('x=(', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursorCommand(cursor, H.Type, { text: 'asd' }, 'keyboard'); + viewModel.type('asd', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=(asd)'); // overtype! - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=(asd)'); // do not overtype! - cursor.setSelections('test', [new Selection(2, 4, 2, 4)]); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(2, 4, 2, 4)]); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(2), 'y=());'); }); @@ -4962,14 +5025,14 @@ suite('autoClosingPairs', () => { 'y=();' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); - cursorCommand(cursor, H.Type, { text: 'x=(' }, 'keyboard'); + viewModel.type('x=(', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursor.setSelections('test', [new Selection(1, 5, 1, 5)]); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=())'); }); mode.dispose(); @@ -4983,17 +5046,17 @@ suite('autoClosingPairs', () => { 'y=();' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); - cursorCommand(cursor, H.Type, { text: 'x=(' }, 'keyboard'); + viewModel.type('x=(', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursor.setSelections('test', [new Selection(1, 4, 1, 4)]); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 4, 1, 4)]); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=())'); }); mode.dispose(); @@ -5007,19 +5070,19 @@ suite('autoClosingPairs', () => { 'y=();' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); - cursorCommand(cursor, H.Type, { text: 'x=(' }, 'keyboard'); + viewModel.type('x=(', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + viewModel.type('(', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=(())'); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=(())'); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=(())'); }); mode.dispose(); @@ -5032,22 +5095,22 @@ suite('autoClosingPairs', () => { 'std::cout << \'"\' << entryMap' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(1, 29, 1, 29)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 29, 1, 29)]); - cursorCommand(cursor, H.Type, { text: '[' }, 'keyboard'); + viewModel.type('[', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'std::cout << \'"\' << entryMap[]'); - cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard'); + viewModel.type('"', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'std::cout << \'"\' << entryMap[""]'); - cursorCommand(cursor, H.Type, { text: 'a' }, 'keyboard'); + viewModel.type('a', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'std::cout << \'"\' << entryMap["a"]'); - cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard'); + viewModel.type('"', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'std::cout << \'"\' << entryMap["a"]'); - cursorCommand(cursor, H.Type, { text: ']' }, 'keyboard'); + viewModel.type(']', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'std::cout << \'"\' << entryMap["a"]'); }); mode.dispose(); @@ -5092,8 +5155,8 @@ suite('autoClosingPairs', () => { 'foo\'hello\'' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertType(model, cursor, 1, 4, '(', '(', `does not auto close @ (1, 4)`); + }, (editor, model, viewModel) => { + assertType(editor, model, viewModel, 1, 4, '(', '(', `does not auto close @ (1, 4)`); }); mode.dispose(); }); @@ -5105,16 +5168,16 @@ suite('autoClosingPairs', () => { '
{ - cursor.setSelections('test', [new Selection(1, 8, 1, 8)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 8, 1, 8)]); - cursor.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)]); + viewModel.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)]); assert.strictEqual(model.getLineContent(1), '
{ editorOpts: { autoClosingOvertype: 'always' } - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); - cursorCommand(cursor, H.Type, { text: 'x=(' }, 'keyboard'); + viewModel.type('x=(', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursor.setSelections('test', [new Selection(1, 4, 1, 4)]); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 4, 1, 4)]); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(1), 'x=()'); - cursor.setSelections('test', [new Selection(2, 4, 2, 4)]); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.setSelections('test', [new Selection(2, 4, 2, 4)]); + viewModel.type(')', 'keyboard'); assert.strictEqual(model.getLineContent(2), 'y=();'); }); mode.dispose(); @@ -5157,14 +5220,14 @@ suite('autoClosingPairs', () => { text: [ ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); // Typing ` + e on the mac US intl kb layout - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: 'è' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.startComposition(); + viewModel.type('`', 'keyboard'); + viewModel.replacePreviousChar('è', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), 'è'); }); @@ -5178,15 +5241,15 @@ suite('autoClosingPairs', () => { 'test' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(1, 1, 1, 5)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 1, 1, 5)]); // Typing ` + e on the mac US intl kb layout - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '\'test\''); }); @@ -5200,20 +5263,20 @@ suite('autoClosingPairs', () => { 'console.log();' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [new Selection(1, 13, 1, 13)]); + viewModel.setSelections('test', [new Selection(1, 13, 1, 13)]); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); + viewModel.type('\'', 'keyboard'); assert.equal(model.getValue(), 'console.log(\'\');'); - cursorCommand(cursor, H.Type, { text: 'it' }, 'keyboard'); + viewModel.type('it', 'keyboard'); assert.equal(model.getValue(), 'console.log(\'it\');'); - cursorCommand(cursor, H.Type, { text: '\\' }, 'keyboard'); + viewModel.type('\\', 'keyboard'); assert.equal(model.getValue(), 'console.log(\'it\\\');'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); + viewModel.type('\'', 'keyboard'); assert.equal(model.getValue(), 'console.log(\'it\\\'\'\');'); }); mode.dispose(); @@ -5226,23 +5289,23 @@ suite('autoClosingPairs', () => { '' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [new Selection(1, 1, 1, 1)]); + viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); - cursorCommand(cursor, H.Type, { text: '\\' }, 'keyboard'); + viewModel.type('\\', 'keyboard'); assert.equal(model.getValue(), '\\'); - cursorCommand(cursor, H.Type, { text: '(' }, 'keyboard'); + viewModel.type('(', 'keyboard'); assert.equal(model.getValue(), '\\()'); - cursorCommand(cursor, H.Type, { text: 'abc' }, 'keyboard'); + viewModel.type('abc', 'keyboard'); assert.equal(model.getValue(), '\\(abc)'); - cursorCommand(cursor, H.Type, { text: '\\' }, 'keyboard'); + viewModel.type('\\', 'keyboard'); assert.equal(model.getValue(), '\\(abc\\)'); - cursorCommand(cursor, H.Type, { text: ')' }, 'keyboard'); + viewModel.type(')', 'keyboard'); assert.equal(model.getValue(), '\\(abc\\)'); }); mode.dispose(); @@ -5256,20 +5319,20 @@ suite('autoClosingPairs', () => { 'world' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); // Typing ` and pressing shift+down on the mac US intl kb layout // Here we're just replaying what the cursor gets - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); - moveDown(cursor, true); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '`' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '`' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.startComposition(); + viewModel.type('`', 'keyboard'); + moveDown(editor, viewModel, true); + viewModel.replacePreviousChar('`', 1, 'keyboard'); + viewModel.replacePreviousChar('`', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '`hello\nworld'); - assertCursor(cursor, new Selection(1, 2, 2, 2)); + assertCursor(viewModel, new Selection(1, 2, 2, 2)); }); mode.dispose(); }); @@ -5281,60 +5344,60 @@ suite('autoClosingPairs', () => { '' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - assertCursor(cursor, new Position(1, 1)); + }, (editor, model, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); // on the mac US intl kb layout // Typing ' + space - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '\'\''); // Typing one more ' + space - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '\'\''); // Typing ' as a closing tag model.setValue('\'abc'); - cursor.setSelections('test', [new Selection(1, 5, 1, 5)]); - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 5, 1, 5)]); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '\'abc\''); // quotes before the newly added character are all paired. model.setValue('\'abc\'def '); - cursor.setSelections('test', [new Selection(1, 10, 1, 10)]); - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 10, 1, 10)]); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '\'abc\'def \'\''); // No auto closing if there is non-whitespace character after the cursor model.setValue('abc'); - cursor.setSelections('test', [new Selection(1, 1, 1, 1)]); - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 1, 1, 1)]); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); // No auto closing if it's after a word. model.setValue('abc'); - cursor.setSelections('test', [new Selection(1, 4, 1, 4)]); - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '\'' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.setSelections('test', [new Selection(1, 4, 1, 4)]); + viewModel.startComposition(); + viewModel.type('\'', 'keyboard'); + viewModel.replacePreviousChar('\'', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), 'abc\''); }); @@ -5348,14 +5411,14 @@ suite('autoClosingPairs', () => { '{}' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { - cursor.setSelections('test', [new Selection(1, 2, 1, 2)]); + }, (editor, model, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 2, 1, 2)]); // Typing a + backspace - cursorCommand(cursor, H.CompositionStart, null, 'keyboard'); - cursorCommand(cursor, H.Type, { text: 'a' }, 'keyboard'); - cursorCommand(cursor, H.ReplacePreviousChar, { replaceCharCnt: 1, text: '' }, 'keyboard'); - cursorCommand(cursor, H.CompositionEnd, null, 'keyboard'); + viewModel.startComposition(); + viewModel.type('a', 'keyboard'); + viewModel.replacePreviousChar('', 1, 'keyboard'); + viewModel.endComposition('keyboard'); assert.equal(model.getValue(), '{}'); }); mode.dispose(); @@ -5368,15 +5431,15 @@ suite('autoClosingPairs', () => { 'var a = asd' ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { - cursor.setSelections('test', [ + viewModel.setSelections('test', [ new Selection(1, 9, 1, 9), new Selection(1, 12, 1, 12), ]); // type a ` - cursorCommand(cursor, H.Type, { text: '`' }, 'keyboard'); + viewModel.type('`', 'keyboard'); assert.equal(model.getValue(), 'var a = `asd`'); }); @@ -5400,19 +5463,19 @@ suite('autoClosingPairs', () => { const mode = new MyMode(); const model = createTextModel('var x = \'hi\';', undefined, languageId); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { editor.setSelections([ new Selection(1, 9, 1, 10), new Selection(1, 12, 1, 13) ]); - cursorCommand(cursor, H.Type, { text: '"' }, 'keyboard'); + viewModel.type('"', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = "hi";', 'assert1'); editor.setSelections([ new Selection(1, 9, 1, 10), new Selection(1, 12, 1, 13) ]); - cursorCommand(cursor, H.Type, { text: '\'' }, 'keyboard'); + viewModel.type('\'', 'keyboard'); assert.equal(model.getValue(EndOfLinePreference.LF), 'var x = \'hi\';', 'assert2'); }); @@ -5430,8 +5493,8 @@ suite('autoClosingPairs', () => { mode.getLanguageIdentifier() ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [ + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [ new Selection(1, 4, 1, 4), new Selection(1, 10, 1, 10), ]); @@ -5459,16 +5522,16 @@ suite('autoClosingPairs', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { CoreNavigationCommands.WordSelect.runEditorCommand(null, editor, { position: new Position(3, 7) }); - assertCursor(cursor, new Selection(3, 7, 3, 7)); + assertCursor(viewModel, new Selection(3, 7, 3, 7)); CoreNavigationCommands.WordSelectDrag.runEditorCommand(null, editor, { position: new Position(4, 7) }); - assertCursor(cursor, new Selection(3, 7, 4, 7)); + assertCursor(viewModel, new Selection(3, 7, 4, 7)); }); }); }); @@ -5483,24 +5546,24 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); - cursorCommand(cursor, H.Type, { text: 'first' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('first', 'keyboard'); assert.equal(model.getLineContent(1), 'A first line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A fir line'); - assertCursor(cursor, new Selection(1, 6, 1, 6)); + assertCursor(viewModel, new Selection(1, 6, 1, 6)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A line'); - assertCursor(cursor, new Selection(1, 3, 1, 3)); + assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5512,24 +5575,24 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); - cursorCommand(cursor, H.Type, { text: 'first' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('first', 'keyboard'); assert.equal(model.getLineContent(1), 'A first line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A firstine'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A line'); - assertCursor(cursor, new Selection(1, 3, 1, 3)); + assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5541,8 +5604,8 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(2, 8, 2, 8)]); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(2, 8, 2, 8)]); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -5551,19 +5614,19 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' line'); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); - cursorCommand(cursor, H.Type, { text: 'Second' }, 'keyboard'); + viewModel.type('Second', 'keyboard'); assert.equal(model.getLineContent(2), 'Second line'); - assertCursor(cursor, new Selection(2, 7, 2, 7)); + assertCursor(viewModel, new Selection(2, 7, 2, 7)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' line'); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); - assertCursor(cursor, new Selection(2, 8, 2, 8)); + assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5575,8 +5638,8 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(2, 8, 2, 8)]); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(2, 8, 2, 8)]); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -5585,7 +5648,7 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' line'); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); @@ -5593,15 +5656,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ''); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), ' line'); - assertCursor(cursor, new Selection(2, 1, 2, 1)); + assertCursor(viewModel, new Selection(2, 1, 2, 1)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); - assertCursor(cursor, new Selection(2, 8, 2, 8)); + assertCursor(viewModel, new Selection(2, 8, 2, 8)); }); }); @@ -5613,26 +5676,26 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(2, 9, 2, 9)]); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(2, 9, 2, 9)]); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another '); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); - cursorCommand(cursor, H.Type, { text: 'text' }, 'keyboard'); + viewModel.type('text', 'keyboard'); assert.equal(model.getLineContent(2), 'Another text'); - assertCursor(cursor, new Selection(2, 13, 2, 13)); + assertCursor(viewModel, new Selection(2, 13, 2, 13)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another '); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5644,14 +5707,14 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(2, 9, 2, 9)]); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(2, 9, 2, 9)]); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another '); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); @@ -5660,15 +5723,15 @@ suite('Undo stops', () => { CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); CoreEditingCommands.DeleteLeft.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'An'); - assertCursor(cursor, new Selection(2, 3, 2, 3)); + assertCursor(viewModel, new Selection(2, 3, 2, 3)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another '); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(2), 'Another line'); - assertCursor(cursor, new Selection(2, 9, 2, 9)); + assertCursor(viewModel, new Selection(2, 9, 2, 9)); }); }); @@ -5680,23 +5743,23 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); - cursorCommand(cursor, H.Type, { text: 'first and interesting' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('first and interesting', 'keyboard'); assert.equal(model.getLineContent(1), 'A first and interesting line'); - assertCursor(cursor, new Selection(1, 24, 1, 24)); + assertCursor(viewModel, new Selection(1, 24, 1, 24)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first and line'); - assertCursor(cursor, new Selection(1, 12, 1, 12)); + assertCursor(viewModel, new Selection(1, 12, 1, 12)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A first line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getLineContent(1), 'A line'); - assertCursor(cursor, new Selection(1, 3, 1, 3)); + assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5708,19 +5771,19 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [new Selection(1, 3, 1, 3)]); - cursorCommand(cursor, H.Type, { text: 'first' }, 'keyboard'); + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [new Selection(1, 3, 1, 3)]); + viewModel.type('first', 'keyboard'); assert.equal(model.getValue(), 'A first line\nAnother line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); model.pushEOL(EndOfLineSequence.CRLF); assert.equal(model.getValue(), 'A first line\r\nAnother line'); - assertCursor(cursor, new Selection(1, 8, 1, 8)); + assertCursor(viewModel, new Selection(1, 8, 1, 8)); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); assert.equal(model.getValue(), 'A line\nAnother line'); - assertCursor(cursor, new Selection(1, 3, 1, 3)); + assertCursor(viewModel, new Selection(1, 3, 1, 3)); }); }); @@ -5732,12 +5795,12 @@ suite('Undo stops', () => { ].join('\n') ); - withTestCodeEditor(null, { model: model }, (editor, cursor) => { - cursor.setSelections('test', [ + withTestCodeEditor(null, { model: model }, (editor, viewModel) => { + viewModel.setSelections('test', [ new Selection(2, 7, 2, 12), new Selection(1, 7, 1, 12), ]); - cursorCommand(cursor, H.Type, { text: 'no' }, 'keyboard'); + viewModel.type('no', 'keyboard'); assert.equal(model.getValue(), 'hello no\nhello no'); CoreEditingCommands.Undo.runEditorCommand(null, editor, null); diff --git a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts index b940bedc584..3c85cc22eb2 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -5,13 +5,12 @@ import * as assert from 'assert'; import { CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; -import { Cursor } from 'vs/editor/common/controller/cursor'; import { CursorMove } from 'vs/editor/common/controller/cursorMoveCommands'; 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 { withTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { withTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; suite('Cursor move command test', () => { @@ -23,470 +22,465 @@ suite('Cursor move command test', () => { '1' ].join('\n'); - function executeTest(callback: (editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor) => void): void { - withTestCodeEditor(TEXT, {}, (editor, cursor) => { - const viewModel = editor._getViewModel(); - if (!viewModel) { - assert.ok(false); - return; - } - callback(editor, viewModel, cursor); + function executeTest(callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { + callback(editor, viewModel); }); } test('move left should move to left character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveLeft(editor, viewModel, cursor); - cursorEqual(cursor, 1, 7); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveLeft(viewModel); + cursorEqual(viewModel, 1, 7); }); }); test('move left should move to left by n characters', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveLeft(editor, viewModel, cursor, 3); - cursorEqual(cursor, 1, 5); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveLeft(viewModel, 3); + cursorEqual(viewModel, 1, 5); }); }); test('move left should move to left by half line', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveLeft(editor, viewModel, cursor, 1, CursorMove.RawUnit.HalfLine); - cursorEqual(cursor, 1, 1); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveLeft(viewModel, 1, CursorMove.RawUnit.HalfLine); + cursorEqual(viewModel, 1, 1); }); }); test('move left moves to previous line', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 2, 3); - moveLeft(editor, viewModel, cursor, 10); - cursorEqual(cursor, 1, 21); + executeTest((editor, viewModel) => { + moveTo(viewModel, 2, 3); + moveLeft(viewModel, 10); + cursorEqual(viewModel, 1, 21); }); }); test('move right should move to right character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 5); - moveRight(editor, viewModel, cursor); - cursorEqual(cursor, 1, 6); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 5); + moveRight(viewModel); + cursorEqual(viewModel, 1, 6); }); }); test('move right should move to right by n characters', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 2); - moveRight(editor, viewModel, cursor, 6); - cursorEqual(cursor, 1, 8); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 2); + moveRight(viewModel, 6); + cursorEqual(viewModel, 1, 8); }); }); test('move right should move to right by half line', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 4); - moveRight(editor, viewModel, cursor, 1, CursorMove.RawUnit.HalfLine); - cursorEqual(cursor, 1, 14); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 4); + moveRight(viewModel, 1, CursorMove.RawUnit.HalfLine); + cursorEqual(viewModel, 1, 14); }); }); test('move right moves to next line', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveRight(editor, viewModel, cursor, 100); - cursorEqual(cursor, 2, 1); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveRight(viewModel, 100); + cursorEqual(viewModel, 2, 1); }); }); test('move to first character of line from middle', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveToLineStart(editor, viewModel, cursor); - cursorEqual(cursor, 1, 1); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveToLineStart(viewModel); + cursorEqual(viewModel, 1, 1); }); }); test('move to first character of line from first non white space character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 6); - moveToLineStart(editor, viewModel, cursor); - cursorEqual(cursor, 1, 1); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 6); + moveToLineStart(viewModel); + cursorEqual(viewModel, 1, 1); }); }); test('move to first character of line from first character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 1); - moveToLineStart(editor, viewModel, cursor); - cursorEqual(cursor, 1, 1); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 1); + moveToLineStart(viewModel); + cursorEqual(viewModel, 1, 1); }); }); test('move to first non white space character of line from middle', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveToLineFirstNonWhitespaceCharacter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 6); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveToLineFirstNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 6); }); }); test('move to first non white space character of line from first non white space character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 6); - moveToLineFirstNonWhitespaceCharacter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 6); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 6); + moveToLineFirstNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 6); }); }); test('move to first non white space character of line from first character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 1); - moveToLineFirstNonWhitespaceCharacter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 6); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 1); + moveToLineFirstNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 6); }); }); test('move to end of line from middle', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveToLineEnd(editor, viewModel, cursor); - cursorEqual(cursor, 1, 21); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveToLineEnd(viewModel); + cursorEqual(viewModel, 1, 21); }); }); test('move to end of line from last non white space character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 19); - moveToLineEnd(editor, viewModel, cursor); - cursorEqual(cursor, 1, 21); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 19); + moveToLineEnd(viewModel); + cursorEqual(viewModel, 1, 21); }); }); test('move to end of line from line end', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 21); - moveToLineEnd(editor, viewModel, cursor); - cursorEqual(cursor, 1, 21); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 21); + moveToLineEnd(viewModel); + cursorEqual(viewModel, 1, 21); }); }); test('move to last non white space character from middle', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveToLineLastNonWhitespaceCharacter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 19); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveToLineLastNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 19); }); }); test('move to last non white space character from last non white space character', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 19); - moveToLineLastNonWhitespaceCharacter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 19); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 19); + moveToLineLastNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 19); }); }); test('move to last non white space character from line end', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 21); - moveToLineLastNonWhitespaceCharacter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 19); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 21); + moveToLineLastNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 19); }); }); test('move to center of line not from center', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 8); - moveToLineCenter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 11); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); }); }); test('move to center of line from center', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 11); - moveToLineCenter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 11); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 11); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); }); }); test('move to center of line from start', () => { - executeTest((editor, viewModel, cursor) => { - moveToLineStart(editor, viewModel, cursor); - moveToLineCenter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 11); + executeTest((editor, viewModel) => { + moveToLineStart(viewModel); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); }); }); test('move to center of line from end', () => { - executeTest((editor, viewModel, cursor) => { - moveToLineEnd(editor, viewModel, cursor); - moveToLineCenter(editor, viewModel, cursor); - cursorEqual(cursor, 1, 11); + executeTest((editor, viewModel) => { + moveToLineEnd(viewModel); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); }); }); test('move up by cursor move command', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 3, 5); - cursorEqual(cursor, 3, 5); + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveUp(editor, viewModel, cursor, 2); - cursorEqual(cursor, 1, 5); + moveUp(viewModel, 2); + cursorEqual(viewModel, 1, 5); - moveUp(editor, viewModel, cursor, 1); - cursorEqual(cursor, 1, 1); + moveUp(viewModel, 1); + cursorEqual(viewModel, 1, 1); }); }); test('move up by model line cursor move command', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 3, 5); - cursorEqual(cursor, 3, 5); + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveUpByModelLine(editor, viewModel, cursor, 2); - cursorEqual(cursor, 1, 5); + moveUpByModelLine(viewModel, 2); + cursorEqual(viewModel, 1, 5); - moveUpByModelLine(editor, viewModel, cursor, 1); - cursorEqual(cursor, 1, 1); + moveUpByModelLine(viewModel, 1); + cursorEqual(viewModel, 1, 1); }); }); test('move down by model line cursor move command', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 3, 5); - cursorEqual(cursor, 3, 5); + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveDownByModelLine(editor, viewModel, cursor, 2); - cursorEqual(cursor, 5, 2); + moveDownByModelLine(viewModel, 2); + cursorEqual(viewModel, 5, 2); - moveDownByModelLine(editor, viewModel, cursor, 1); - cursorEqual(cursor, 5, 2); + moveDownByModelLine(viewModel, 1); + cursorEqual(viewModel, 5, 2); }); }); test('move up with selection by cursor move command', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 3, 5); - cursorEqual(cursor, 3, 5); + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveUp(editor, viewModel, cursor, 1, true); - cursorEqual(cursor, 2, 2, 3, 5); + moveUp(viewModel, 1, true); + cursorEqual(viewModel, 2, 2, 3, 5); - moveUp(editor, viewModel, cursor, 1, true); - cursorEqual(cursor, 1, 5, 3, 5); + moveUp(viewModel, 1, true); + cursorEqual(viewModel, 1, 5, 3, 5); }); }); test('move up and down with tabs by cursor move command', () => { - executeTest((editor, viewModel, cursor) => { - moveTo(cursor, 1, 5); - cursorEqual(cursor, 1, 5); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 5); + cursorEqual(viewModel, 1, 5); - moveDown(editor, viewModel, cursor, 4); - cursorEqual(cursor, 5, 2); + moveDown(viewModel, 4); + cursorEqual(viewModel, 5, 2); - moveUp(editor, viewModel, cursor, 1); - cursorEqual(cursor, 4, 1); + moveUp(viewModel, 1); + cursorEqual(viewModel, 4, 1); - moveUp(editor, viewModel, cursor, 1); - cursorEqual(cursor, 3, 5); + moveUp(viewModel, 1); + cursorEqual(viewModel, 3, 5); - moveUp(editor, viewModel, cursor, 1); - cursorEqual(cursor, 2, 2); + moveUp(viewModel, 1); + cursorEqual(viewModel, 2, 2); - moveUp(editor, viewModel, cursor, 1); - cursorEqual(cursor, 1, 5); + moveUp(viewModel, 1); + cursorEqual(viewModel, 1, 5); }); }); test('move up and down with end of lines starting from a long one by cursor move command', () => { - executeTest((editor, viewModel, cursor) => { - moveToEndOfLine(cursor); - cursorEqual(cursor, 1, 21); + executeTest((editor, viewModel) => { + moveToEndOfLine(viewModel); + cursorEqual(viewModel, 1, 21); - moveToEndOfLine(cursor); - cursorEqual(cursor, 1, 21); + moveToEndOfLine(viewModel); + cursorEqual(viewModel, 1, 21); - moveDown(editor, viewModel, cursor, 2); - cursorEqual(cursor, 3, 17); + moveDown(viewModel, 2); + cursorEqual(viewModel, 3, 17); - moveDown(editor, viewModel, cursor, 1); - cursorEqual(cursor, 4, 1); + moveDown(viewModel, 1); + cursorEqual(viewModel, 4, 1); - moveDown(editor, viewModel, cursor, 1); - cursorEqual(cursor, 5, 2); + moveDown(viewModel, 1); + cursorEqual(viewModel, 5, 2); - moveUp(editor, viewModel, cursor, 4); - cursorEqual(cursor, 1, 21); + moveUp(viewModel, 4); + cursorEqual(viewModel, 1, 21); }); }); test('move to view top line moves to first visible line if it is first line', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 10, 1); - moveTo(cursor, 2, 2); - moveToTop(editor, viewModel, cursor); + moveTo(viewModel, 2, 2); + moveToTop(viewModel); - cursorEqual(cursor, 1, 6); + cursorEqual(viewModel, 1, 6); }); }); test('move to view top line moves to top visible line when first line is not visible', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 10, 1); - moveTo(cursor, 4, 1); - moveToTop(editor, viewModel, cursor); + moveTo(viewModel, 4, 1); + moveToTop(viewModel); - cursorEqual(cursor, 2, 2); + cursorEqual(viewModel, 2, 2); }); }); test('move to view top line moves to nth line from top', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 10, 1); - moveTo(cursor, 4, 1); - moveToTop(editor, viewModel, cursor, 3); + moveTo(viewModel, 4, 1); + moveToTop(viewModel, 3); - cursorEqual(cursor, 3, 5); + cursorEqual(viewModel, 3, 5); }); }); test('move to view top line moves to last line if n is greater than last visible line number', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 3, 1); - moveTo(cursor, 2, 2); - moveToTop(editor, viewModel, cursor, 4); + moveTo(viewModel, 2, 2); + moveToTop(viewModel, 4); - cursorEqual(cursor, 3, 5); + cursorEqual(viewModel, 3, 5); }); }); test('move to view center line moves to the center line', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(3, 1, 3, 1); - moveTo(cursor, 2, 2); - moveToCenter(editor, viewModel, cursor); + moveTo(viewModel, 2, 2); + moveToCenter(viewModel); - cursorEqual(cursor, 3, 5); + cursorEqual(viewModel, 3, 5); }); }); test('move to view bottom line moves to last visible line if it is last line', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 5, 1); - moveTo(cursor, 2, 2); - moveToBottom(editor, viewModel, cursor); + moveTo(viewModel, 2, 2); + moveToBottom(viewModel); - cursorEqual(cursor, 5, 1); + cursorEqual(viewModel, 5, 1); }); }); test('move to view bottom line moves to last visible line when last line is not visible', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 3, 1); - moveTo(cursor, 2, 2); - moveToBottom(editor, viewModel, cursor); + moveTo(viewModel, 2, 2); + moveToBottom(viewModel); - cursorEqual(cursor, 3, 5); + cursorEqual(viewModel, 3, 5); }); }); test('move to view bottom line moves to nth line from bottom', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 5, 1); - moveTo(cursor, 4, 1); - moveToBottom(editor, viewModel, cursor, 3); + moveTo(viewModel, 4, 1); + moveToBottom(viewModel, 3); - cursorEqual(cursor, 3, 5); + cursorEqual(viewModel, 3, 5); }); }); test('move to view bottom line moves to first line if n is lesser than first visible line number', () => { - executeTest((editor, viewModel, cursor) => { + executeTest((editor, viewModel) => { viewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 5, 1); - moveTo(cursor, 4, 1); - moveToBottom(editor, viewModel, cursor, 5); + moveTo(viewModel, 4, 1); + moveToBottom(viewModel, 5); - cursorEqual(cursor, 2, 2); + cursorEqual(viewModel, 2, 2); }); }); }); // Move command -function move(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, args: any) { - CoreNavigationCommands.CursorMove.runCoreEditorCommand(editor, viewModel, cursor, args); +function move(viewModel: ViewModel, args: any) { + CoreNavigationCommands.CursorMove.runCoreEditorCommand(viewModel, args); } -function moveToLineStart(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.WrappedLineStart }); +function moveToLineStart(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineStart }); } -function moveToLineFirstNonWhitespaceCharacter(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.WrappedLineFirstNonWhitespaceCharacter }); +function moveToLineFirstNonWhitespaceCharacter(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineFirstNonWhitespaceCharacter }); } -function moveToLineCenter(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.WrappedLineColumnCenter }); +function moveToLineCenter(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineColumnCenter }); } -function moveToLineEnd(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.WrappedLineEnd }); +function moveToLineEnd(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineEnd }); } -function moveToLineLastNonWhitespaceCharacter(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.WrappedLineLastNonWhitespaceCharacter }); +function moveToLineLastNonWhitespaceCharacter(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineLastNonWhitespaceCharacter }); } -function moveLeft(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, value?: number, by?: string, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.Left, by: by, value: value, select: select }); +function moveLeft(viewModel: ViewModel, value?: number, by?: string, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.Left, by: by, value: value, select: select }); } -function moveRight(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, value?: number, by?: string, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.Right, by: by, value: value, select: select }); +function moveRight(viewModel: ViewModel, value?: number, by?: string, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.Right, by: by, value: value, select: select }); } -function moveUp(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.Up, by: CursorMove.RawUnit.WrappedLine, value: noOfLines, select: select }); +function moveUp(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.Up, by: CursorMove.RawUnit.WrappedLine, value: noOfLines, select: select }); } -function moveUpByModelLine(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.Up, value: noOfLines, select: select }); +function moveUpByModelLine(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.Up, value: noOfLines, select: select }); } -function moveDown(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.Down, by: CursorMove.RawUnit.WrappedLine, value: noOfLines, select: select }); +function moveDown(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.Down, by: CursorMove.RawUnit.WrappedLine, value: noOfLines, select: select }); } -function moveDownByModelLine(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.Down, value: noOfLines, select: select }); +function moveDownByModelLine(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.Down, value: noOfLines, select: select }); } -function moveToTop(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.ViewPortTop, value: noOfLines, select: select }); +function moveToTop(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.ViewPortTop, value: noOfLines, select: select }); } -function moveToCenter(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.ViewPortCenter, select: select }); +function moveToCenter(viewModel: ViewModel, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.ViewPortCenter, select: select }); } -function moveToBottom(editor: TestCodeEditor, viewModel: IViewModel, cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(editor, viewModel, cursor, { to: CursorMove.RawDirection.ViewPortBottom, value: noOfLines, select: select }); +function moveToBottom(viewModel: ViewModel, noOfLines: number = 1, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.ViewPortBottom, value: noOfLines, select: select }); } -function cursorEqual(cursor: Cursor, posLineNumber: number, posColumn: number, selLineNumber: number = posLineNumber, selColumn: number = posColumn) { - positionEqual(cursor.getPosition(), posLineNumber, posColumn); - selectionEqual(cursor.getSelection(), posLineNumber, posColumn, selLineNumber, selColumn); +function cursorEqual(viewModel: ViewModel, posLineNumber: number, posColumn: number, selLineNumber: number = posLineNumber, selColumn: number = posColumn) { + positionEqual(viewModel.getPosition(), posLineNumber, posColumn); + selectionEqual(viewModel.getSelection(), posLineNumber, posColumn, selLineNumber, selColumn); } function positionEqual(position: Position, lineNumber: number, column: number) { @@ -507,22 +501,22 @@ function selectionEqual(selection: Selection, posLineNumber: number, posColumn: }, 'selection equal'); } -function moveTo(cursor: Cursor, lineNumber: number, column: number, inSelectionMode: boolean = false) { +function moveTo(viewModel: ViewModel, lineNumber: number, column: number, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(cursor, { + CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(viewModel, { position: new Position(lineNumber, column) }); } else { - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursor, { + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: new Position(lineNumber, column) }); } } -function moveToEndOfLine(cursor: Cursor, inSelectionMode: boolean = false) { +function moveToEndOfLine(viewModel: ViewModel, inSelectionMode: boolean = false) { if (inSelectionMode) { - CoreNavigationCommands.CursorEndSelect.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorEndSelect.runCoreEditorCommand(viewModel, {}); } else { - CoreNavigationCommands.CursorEnd.runCoreEditorCommand(cursor, {}); + CoreNavigationCommands.CursorEnd.runCoreEditorCommand(viewModel, {}); } } diff --git a/src/vs/editor/test/browser/testCodeEditor.ts b/src/vs/editor/test/browser/testCodeEditor.ts index dbefa695010..183f79e20f2 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -3,13 +3,12 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorContributionCtor } from 'vs/editor/browser/editorExtensions'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { View } from 'vs/editor/browser/view/viewImpl'; import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; -import { Cursor } from 'vs/editor/common/controller/cursor'; import { IConfiguration, IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; @@ -27,7 +26,12 @@ import { TestNotificationService } from 'vs/platform/notification/test/common/te import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { +export interface ITestCodeEditor extends IActiveCodeEditor { + getViewModel(): ViewModel | undefined; + registerAndInstantiateContribution(id: string, ctor: new (editor: ICodeEditor, ...services: Services) => T): T; +} + +class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#region testing overrides protected _createConfiguration(options: editorOptions.IEditorConstructionOptions): IConfiguration { @@ -40,8 +44,8 @@ export class TestCodeEditor extends CodeEditorWidget implements ICodeEditor { //#endregion //#region Testing utils - public getCursor(): Cursor | undefined { - return this._modelData ? this._modelData.viewModel.cursor : undefined; + public getViewModel(): ViewModel | undefined { + return this._modelData ? this._modelData.viewModel : undefined; } public registerAndInstantiateContribution(id: string, ctor: new (editor: ICodeEditor, ...services: Services) => T): T { const r: T = this._instantiationService.createInstance(ctor as IEditorContributionCtor, this); @@ -74,7 +78,7 @@ export interface TestCodeEditorCreationOptions extends editorOptions.IEditorOpti serviceCollection?: ServiceCollection; } -export function withTestCodeEditor(text: string | string[] | null, options: TestCodeEditorCreationOptions, callback: (editor: TestCodeEditor, cursor: Cursor) => void): void { +export function withTestCodeEditor(text: string | string[] | null, options: TestCodeEditorCreationOptions, callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { // create a model if necessary and remember it in order to dispose it. if (!options.model) { if (typeof text === 'string') { @@ -84,14 +88,15 @@ export function withTestCodeEditor(text: string | string[] | null, options: Test } } - let editor = createTestCodeEditor(options); - editor.getCursor()!.setHasFocus(true); - callback(editor, editor.getCursor()!); + const editor = createTestCodeEditor(options); + const viewModel = editor.getViewModel()!; + viewModel.setHasFocus(true); + callback(editor, editor.getViewModel()!); editor.dispose(); } -export function createTestCodeEditor(options: TestCodeEditorCreationOptions): TestCodeEditor { +export function createTestCodeEditor(options: TestCodeEditorCreationOptions): ITestCodeEditor { const model = options.model; delete options.model; @@ -127,5 +132,5 @@ export function createTestCodeEditor(options: TestCodeEditorCreationOptions): Te codeEditorWidgetOptions ); editor.setModel(model); - return editor; + return editor; } diff --git a/src/vs/editor/test/browser/testCommand.ts b/src/vs/editor/test/browser/testCommand.ts index 4c280c1d960..c3f73be0274 100644 --- a/src/vs/editor/test/browser/testCommand.ts +++ b/src/vs/editor/test/browser/testCommand.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { IRange } from 'vs/editor/common/core/range'; import { Selection, ISelection } from 'vs/editor/common/core/selection'; -import { ICommand, Handler, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; +import { ICommand, IEditOperationBuilder } from 'vs/editor/common/editorCommon'; import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; import { LanguageIdentifier } from 'vs/editor/common/modes'; @@ -33,7 +33,7 @@ export function testCommand( cursor.setSelections('tests', [selection]); - cursor.trigger('tests', Handler.ExecuteCommand, commandFactory(cursor.getSelection())); + cursor.executeCommand(commandFactory(cursor.getSelection()), 'tests'); assert.deepEqual(model.getLinesContent(), expectedLines); diff --git a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts index e210f797ebb..3205a10ce2b 100644 --- a/src/vs/editor/test/common/viewLayout/linesLayout.test.ts +++ b/src/vs/editor/test/common/viewLayout/linesLayout.test.ts @@ -8,9 +8,11 @@ import { LinesLayout, EditorWhitespace } from 'vs/editor/common/viewLayout/lines suite('Editor ViewLayout - LinesLayout', () => { function insertWhitespace(linesLayout: LinesLayout, afterLineNumber: number, ordinal: number, heightInPx: number, minWidth: number): string { - return linesLayout.changeWhitespace((accessor) => { - return accessor.insertWhitespace(afterLineNumber, ordinal, heightInPx, minWidth); + let id: string; + linesLayout.changeWhitespace((accessor) => { + id = accessor.insertWhitespace(afterLineNumber, ordinal, heightInPx, minWidth); }); + return id!; } function changeOneWhitespace(linesLayout: LinesLayout, id: string, newAfterLineNumber: number, newHeight: number): void { diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 14110739815..6fe30a1284a 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -7,6 +7,8 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLineSequence } from 'vs/editor/common/model'; import { testViewModel } from 'vs/editor/test/common/viewModel/testViewModel'; +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; +import { ViewEvent } from 'vs/editor/common/view/viewEvents'; suite('ViewModel', () => { @@ -63,9 +65,11 @@ suite('ViewModel', () => { let viewLineCount: number[] = []; viewLineCount.push(viewModel.getLineCount()); - viewModel.addViewEventListener((events) => { - // Access the view model - viewLineCount.push(viewModel.getLineCount()); + viewModel.addViewEventHandler(new class extends ViewEventHandler { + handleEvents(events: ViewEvent[]): void { + // Access the view model + viewLineCount.push(viewModel.getLineCount()); + } }); model.undo(); viewLineCount.push(viewModel.getLineCount()); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index ccfb97e62f1..40fc25bdca9 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2630,9 +2630,9 @@ declare namespace monaco.editor { renderFinalNewline?: boolean; /** * Remove unusual line terminators like LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL). - * Defaults to true. + * Defaults to 'prompt'. */ - removeUnusualLineTerminators?: boolean; + unusualLineTerminators?: 'off' | 'prompt' | 'auto'; /** * Should the corresponding line be selected when clicking on the line number? * Defaults to true. @@ -3906,36 +3906,36 @@ declare namespace monaco.editor { quickSuggestions = 70, quickSuggestionsDelay = 71, readOnly = 72, - removeUnusualLineTerminators = 73, - renameOnType = 74, - renderControlCharacters = 75, - renderIndentGuides = 76, - renderFinalNewline = 77, - renderLineHighlight = 78, - renderLineHighlightOnlyWhenFocus = 79, - renderValidationDecorations = 80, - renderWhitespace = 81, - revealHorizontalRightPadding = 82, - roundedSelection = 83, - rulers = 84, - scrollbar = 85, - scrollBeyondLastColumn = 86, - scrollBeyondLastLine = 87, - scrollPredominantAxis = 88, - selectionClipboard = 89, - selectionHighlight = 90, - selectOnLineNumbers = 91, - showFoldingControls = 92, - showUnused = 93, - snippetSuggestions = 94, - smoothScrolling = 95, - stopRenderingLineAfter = 96, - suggest = 97, - suggestFontSize = 98, - suggestLineHeight = 99, - suggestOnTriggerCharacters = 100, - suggestSelection = 101, - tabCompletion = 102, + renameOnType = 73, + renderControlCharacters = 74, + renderIndentGuides = 75, + renderFinalNewline = 76, + renderLineHighlight = 77, + renderLineHighlightOnlyWhenFocus = 78, + renderValidationDecorations = 79, + renderWhitespace = 80, + revealHorizontalRightPadding = 81, + roundedSelection = 82, + rulers = 83, + scrollbar = 84, + scrollBeyondLastColumn = 85, + scrollBeyondLastLine = 86, + scrollPredominantAxis = 87, + selectionClipboard = 88, + selectionHighlight = 89, + selectOnLineNumbers = 90, + showFoldingControls = 91, + showUnused = 92, + snippetSuggestions = 93, + smoothScrolling = 94, + stopRenderingLineAfter = 95, + suggest = 96, + suggestFontSize = 97, + suggestLineHeight = 98, + suggestOnTriggerCharacters = 99, + suggestSelection = 100, + tabCompletion = 101, + unusualLineTerminators = 102, useTabStops = 103, wordSeparators = 104, wordWrap = 105, @@ -4025,7 +4025,6 @@ declare namespace monaco.editor { quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; readOnly: IEditorOption; - removeUnusualLineTerminators: IEditorOption; renameOnType: IEditorOption; renderControlCharacters: IEditorOption; renderIndentGuides: IEditorOption; @@ -4055,6 +4054,7 @@ declare namespace monaco.editor { suggestOnTriggerCharacters: IEditorOption; suggestSelection: IEditorOption; tabCompletion: IEditorOption; + unusualLineTerminators: IEditorOption; useTabStops: IEditorOption; wordSeparators: IEditorOption; wordWrap: IEditorOption; @@ -6286,6 +6286,10 @@ declare namespace monaco.languages { * is the language case insensitive? */ ignoreCase?: boolean; + /** + * is the language unicode-aware? (i.e., /\u{1D306}/) + */ + unicode?: boolean; /** * if no match in the tokenizer assign this token class (default 'source') */ diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index 3a025be247a..65fcf4abaf6 100644 --- a/src/vs/platform/accessibility/common/accessibility.ts +++ b/src/vs/platform/accessibility/common/accessibility.ts @@ -32,3 +32,8 @@ export const enum AccessibilitySupport { } export const CONTEXT_ACCESSIBILITY_MODE_ENABLED = new RawContextKey('accessibilityModeEnabled', false); + +export interface IAccessibilityInformation { + label: string; + role?: string; +} diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 4f8a451d742..7a229a28343 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -111,6 +111,7 @@ export class MenuId { static readonly TunnelInline = new MenuId('TunnelInline'); static readonly TunnelTitle = new MenuId('TunnelTitle'); static readonly ViewItemContext = new MenuId('ViewItemContext'); + static readonly ViewContainerTitleContext = new MenuId('ViewContainerTitleContext'); static readonly ViewTitle = new MenuId('ViewTitle'); static readonly ViewTitleContext = new MenuId('ViewTitleContext'); static readonly CommentThreadTitle = new MenuId('CommentThreadTitle'); diff --git a/src/vs/platform/authentication/common/authentication.ts b/src/vs/platform/authentication/common/authentication.ts index d3142ae022a..399f7d271b0 100644 --- a/src/vs/platform/authentication/common/authentication.ts +++ b/src/vs/platform/authentication/common/authentication.ts @@ -17,11 +17,12 @@ export interface IUserDataSyncAuthToken { export interface IAuthenticationTokenService { _serviceBrand: undefined; + readonly token: IUserDataSyncAuthToken | undefined; readonly onDidChangeToken: Event; - readonly onTokenFailed: Event; - getToken(): Promise; setToken(userDataSyncAuthToken: IUserDataSyncAuthToken | undefined): Promise; + + readonly onTokenFailed: Event; sendTokenFailed(): void; } @@ -29,21 +30,14 @@ export class AuthenticationTokenService extends Disposable implements IAuthentic _serviceBrand: any; + private _token: IUserDataSyncAuthToken | undefined; + get token(): IUserDataSyncAuthToken | undefined { return this._token; } private _onDidChangeToken = this._register(new Emitter()); readonly onDidChangeToken = this._onDidChangeToken.event; private _onTokenFailed: Emitter = this._register(new Emitter()); readonly onTokenFailed: Event = this._onTokenFailed.event; - private _token: IUserDataSyncAuthToken | undefined; - - constructor() { - super(); - } - - async getToken(): Promise { - return this._token; - } async setToken(token: IUserDataSyncAuthToken | undefined): Promise { if (token && this._token ? token.token !== this._token.token || token.authenticationProviderId !== this._token.authenticationProviderId : token !== this._token) { diff --git a/src/vs/platform/authentication/common/authenticationIpc.ts b/src/vs/platform/authentication/electron-browser/authenticationIpc.ts similarity index 95% rename from src/vs/platform/authentication/common/authenticationIpc.ts rename to src/vs/platform/authentication/electron-browser/authenticationIpc.ts index 9836ee14682..2b9411b9465 100644 --- a/src/vs/platform/authentication/common/authenticationIpc.ts +++ b/src/vs/platform/authentication/electron-browser/authenticationIpc.ts @@ -7,7 +7,6 @@ import { IServerChannel } from 'vs/base/parts/ipc/common/ipc'; import { Event } from 'vs/base/common/event'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; - export class AuthenticationTokenServiceChannel implements IServerChannel { constructor(private readonly service: IAuthenticationTokenService) { } @@ -22,7 +21,6 @@ export class AuthenticationTokenServiceChannel implements IServerChannel { call(context: any, command: string, args?: any): Promise { switch (command) { case 'setToken': return this.service.setToken(args); - case 'getToken': return this.service.getToken(); } throw new Error('Invalid call'); } diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index 8cbddd37004..85855d06dd8 100644 --- a/src/vs/platform/clipboard/browser/clipboardService.ts +++ b/src/vs/platform/clipboard/browser/clipboardService.ts @@ -11,11 +11,15 @@ export class BrowserClipboardService implements IClipboardService { _serviceBrand: undefined; - private _internalResourcesClipboard: URI[] | undefined; + private readonly mapTextToType = new Map(); // unsupported in web (only in-memory) async writeText(text: string, type?: string): Promise { + + // With type: only in-memory is supported if (type) { - return; // TODO@sbatten support for writing a specific type into clipboard is unsupported + this.mapTextToType.set(type, text); + + return; } // Guard access to navigator.clipboard with try/catch @@ -52,8 +56,10 @@ export class BrowserClipboardService implements IClipboardService { } async readText(type?: string): Promise { + + // With type: only in-memory is supported if (type) { - return ''; // TODO@sbatten support for reading a specific type from clipboard is unsupported + return this.mapTextToType.get(type) || ''; } // Guard access to navigator.clipboard with try/catch @@ -68,26 +74,42 @@ export class BrowserClipboardService implements IClipboardService { } } + private findText = ''; // unsupported in web (only in-memory) + + async readFindText(): Promise { + return this.findText; + } + + async writeFindText(text: string): Promise { + this.findText = text; + } + + private resources: URI[] = []; // unsupported in web (only in-memory) + + async writeResources(resources: URI[]): Promise { + this.resources = resources; + } + + async readResources(): Promise { + return this.resources; + } + + async hasResources(): Promise { + return this.resources.length > 0; + } + + /** @deprecated */ readTextSync(): string | undefined { return undefined; } - readFindText(): string { - // @ts-expect-error - return undefined; + /** @deprecated */ + readFindTextSync(): string { + return this.findText; } - writeFindText(text: string): void { } - - writeResources(resources: URI[]): void { - this._internalResourcesClipboard = resources; - } - - readResources(): URI[] { - return this._internalResourcesClipboard || []; - } - - hasResources(): boolean { - return this._internalResourcesClipboard !== undefined && this._internalResourcesClipboard.length > 0; + /** @deprecated */ + writeFindTextSync(text: string): void { + this.findText = text; } } diff --git a/src/vs/platform/clipboard/common/clipboardService.ts b/src/vs/platform/clipboard/common/clipboardService.ts index 11bf563393a..73cc53f0ae2 100644 --- a/src/vs/platform/clipboard/common/clipboardService.ts +++ b/src/vs/platform/clipboard/common/clipboardService.ts @@ -22,30 +22,38 @@ export interface IClipboardService { */ readText(type?: string): Promise; - readTextSync(): string | undefined; - /** * Reads text from the system find pasteboard. */ - readFindText(): string; + readFindText(): Promise; /** * Writes text to the system find pasteboard. */ - writeFindText(text: string): void; + writeFindText(text: string): Promise; /** * Writes resources to the system clipboard. */ - writeResources(resources: URI[]): void; + writeResources(resources: URI[]): Promise; /** * Reads resources from the system clipboard. */ - readResources(): URI[]; + readResources(): Promise; /** * Find out if resources are copied to the clipboard. */ - hasResources(): boolean; + hasResources(): Promise; + + + /** @deprecated */ + readTextSync(): string | undefined; + + /** @deprecated */ + readFindTextSync(): string; + + /** @deprecated */ + writeFindTextSync(text: string): void; } diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.css b/src/vs/platform/contextview/browser/contextMenuHandler.css index 97ac10bc5be..ef8a5236187 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.css +++ b/src/vs/platform/contextview/browser/contextMenuHandler.css @@ -9,9 +9,9 @@ .context-view-block { position: fixed; + cursor: initial; left:0; top:0; - z-index: -1; width: 100%; height: 100%; -} \ No newline at end of file +} diff --git a/src/vs/platform/contextview/browser/contextMenuHandler.ts b/src/vs/platform/contextview/browser/contextMenuHandler.ts index 921a5f810a5..3daf186140d 100644 --- a/src/vs/platform/contextview/browser/contextMenuHandler.ts +++ b/src/vs/platform/contextview/browser/contextMenuHandler.ts @@ -14,7 +14,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; -import { EventType, $, removeNode } from 'vs/base/browser/dom'; +import { EventType, $, removeNode, isHTMLElement } from 'vs/base/browser/dom'; import { attachMenuStyler } from 'vs/platform/theme/common/styler'; import { domEvent } from 'vs/base/browser/event'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; @@ -50,6 +50,7 @@ export class ContextMenuHandler { let menu: Menu | undefined; + const anchor = delegate.getAnchor(); this.contextViewService.showContextView({ getAnchor: () => delegate.getAnchor(), canRelayout: false, @@ -65,6 +66,7 @@ export class ContextMenuHandler { // Render invisible div to block mouse interaction in the rest of the UI if (this.options.blockMouse) { this.block = container.appendChild($('.context-view-block')); + domEvent(this.block, EventType.MOUSE_DOWN)((e: MouseEvent) => e.stopPropagation()); } const menuDisposables = new DisposableStore(); @@ -131,7 +133,7 @@ export class ContextMenuHandler { this.focusToReturn.focus(); } } - }); + }, !!delegate.anchorAsContainer && isHTMLElement(anchor) ? anchor : undefined); } private onActionRun(e: IRunEvent): void { diff --git a/src/vs/platform/contextview/browser/contextView.ts b/src/vs/platform/contextview/browser/contextView.ts index 2104e66c0b4..a4b225a173d 100644 --- a/src/vs/platform/contextview/browser/contextView.ts +++ b/src/vs/platform/contextview/browser/contextView.ts @@ -15,7 +15,7 @@ export interface IContextViewService extends IContextViewProvider { _serviceBrand: undefined; - showContextView(delegate: IContextViewDelegate): void; + showContextView(delegate: IContextViewDelegate, container?: HTMLElement): void; hideContextView(data?: any): void; layout(): void; anchorAlignment?: AnchorAlignment; diff --git a/src/vs/platform/contextview/browser/contextViewService.ts b/src/vs/platform/contextview/browser/contextViewService.ts index 1cbd4908c2d..9e06e73db61 100644 --- a/src/vs/platform/contextview/browser/contextViewService.ts +++ b/src/vs/platform/contextview/browser/contextViewService.ts @@ -12,13 +12,15 @@ export class ContextViewService extends Disposable implements IContextViewServic _serviceBrand: undefined; private contextView: ContextView; + private container: HTMLElement; constructor( @ILayoutService readonly layoutService: ILayoutService ) { super(); - this.contextView = this._register(new ContextView(layoutService.container)); + this.container = layoutService.container; + this.contextView = this._register(new ContextView(this.container, false)); this.layout(); this._register(layoutService.onLayout(() => this.layout())); @@ -26,11 +28,24 @@ export class ContextViewService extends Disposable implements IContextViewServic // ContextView - setContainer(container: HTMLElement): void { - this.contextView.setContainer(container); + setContainer(container: HTMLElement, useFixedPosition?: boolean): void { + this.contextView.setContainer(container, !!useFixedPosition); } - showContextView(delegate: IContextViewDelegate): void { + showContextView(delegate: IContextViewDelegate, container?: HTMLElement): void { + + if (container) { + if (container !== this.container) { + this.container = container; + this.setContainer(container, true); + } + } else { + if (this.container !== this.layoutService.container) { + this.container = this.layoutService.container; + this.setContainer(this.container, false); + } + } + this.contextView.show(delegate); } @@ -41,4 +56,4 @@ export class ContextViewService extends Disposable implements IContextViewServic hideContextView(data?: any): void { this.contextView.hide(data); } -} \ No newline at end of file +} diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index ece49da3e7c..50ef02af982 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -272,3 +272,12 @@ export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI message.push(''); return message.join('\n'); } + +export interface INativeOpenDialogOptions { + forceNewWindow?: boolean; + + defaultPath?: string; + + telemetryEventName?: string; + telemetryExtraData?: ITelemetryData; +} diff --git a/src/vs/platform/dialogs/electron-main/dialogs.ts b/src/vs/platform/dialogs/electron-main/dialogs.ts index c694c003e9b..8f1b34aead4 100644 --- a/src/vs/platform/dialogs/electron-main/dialogs.ts +++ b/src/vs/platform/dialogs/electron-main/dialogs.ts @@ -11,7 +11,7 @@ import { isMacintosh } from 'vs/base/common/platform'; import { dirname } from 'vs/base/common/path'; import { normalizeNFC } from 'vs/base/common/normalization'; import { exists } from 'vs/base/node/pfs'; -import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; +import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; import { localize } from 'vs/nls'; import { WORKSPACE_FILTER } from 'vs/platform/workspaces/common/workspaces'; diff --git a/src/vs/platform/driver/electron-browser/driver.ts b/src/vs/platform/driver/electron-browser/driver.ts index 2594eef1361..9e39b4d6eb0 100644 --- a/src/vs/platform/driver/electron-browser/driver.ts +++ b/src/vs/platform/driver/electron-browser/driver.ts @@ -6,11 +6,11 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; -import * as electron from 'electron'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { remote } from 'electron'; import { timeout } from 'vs/base/common/async'; import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; class WindowDriver extends BaseWindowDriver { @@ -32,7 +32,7 @@ class WindowDriver extends BaseWindowDriver { private async _click(selector: string, clickCount: number, offset?: { x: number, y: number }): Promise { const { x, y } = await this._getElementXY(selector, offset); - const webContents: electron.WebContents = (electron as any).remote.getCurrentWebContents(); + const webContents = remote.getCurrentWebContents(); webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any); await timeout(10); diff --git a/src/vs/platform/electron/node/electron.ts b/src/vs/platform/electron/common/electron.ts similarity index 73% rename from src/vs/platform/electron/node/electron.ts rename to src/vs/platform/electron/common/electron.ts index 43203239b81..7dda5eb0f94 100644 --- a/src/vs/platform/electron/node/electron.ts +++ b/src/vs/platform/electron/common/electron.ts @@ -4,19 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions } from 'electron'; -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; -import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; +import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes'; +import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; +import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; -import { INativeOpenWindowOptions, IOpenedWindow } from 'vs/platform/windows/node/window'; -export const IElectronService = createDecorator('electronService'); - -export interface IElectronService { +export interface ICommonElectronService { _serviceBrand: undefined; + // Properties + readonly windowId: number; + // Events readonly onWindowOpen: Event; @@ -32,7 +31,7 @@ export interface IElectronService { getActiveWindowId(): Promise; openWindow(options?: IOpenEmptyWindowOptions): Promise; - openWindow(toOpen: IWindowOpenable[], options?: INativeOpenWindowOptions): Promise; + openWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; toggleFullScreen(): Promise; @@ -61,6 +60,16 @@ export interface IElectronService { setDocumentEdited(edited: boolean): Promise; openExternal(url: string): Promise; updateTouchBar(items: ISerializableCommandAction[][]): Promise; + moveItemToTrash(fullPath: string, deleteOnFail?: boolean): Promise; + + // clipboard + readClipboardText(type?: 'selection' | 'clipboard'): Promise; + writeClipboardText(text: string, type?: 'selection' | 'clipboard'): Promise; + readClipboardFindText(): Promise; + writeClipboardFindText(text: string): Promise; + writeClipboardBuffer(format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise; + readClipboardBuffer(format: string): Promise; + hasClipboard(format: string, type?: 'selection' | 'clipboard'): Promise; // macOS Touchbar newWindowTab(): Promise; diff --git a/src/vs/platform/electron/electron-main/electronMainService.ts b/src/vs/platform/electron/electron-main/electronMainService.ts index c21fb966278..01087f97b01 100644 --- a/src/vs/platform/electron/electron-main/electronMainService.ts +++ b/src/vs/platform/electron/electron-main/electronMainService.ts @@ -5,13 +5,13 @@ import { Event } from 'vs/base/common/event'; import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows'; -import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app } from 'electron'; -import { INativeOpenWindowOptions, IOpenedWindow, OpenContext } from 'vs/platform/windows/node/window'; +import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app, clipboard } from 'electron'; +import { OpenContext } from 'vs/platform/windows/node/window'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -import { IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; -import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; +import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; +import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { isMacintosh } from 'vs/base/common/platform'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { ICommonElectronService } from 'vs/platform/electron/common/electron'; import { ISerializableCommandAction } from 'vs/platform/actions/common/actions'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { AddFirstParameterToFunctions } from 'vs/base/common/types'; @@ -23,9 +23,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ILogService } from 'vs/platform/log/common/log'; import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -export interface IElectronMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } +export interface IElectronMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } -export const IElectronMainService = createDecorator('electronMainService'); +export const IElectronMainService = createDecorator('electronMainService'); export class ElectronMainService implements IElectronMainService { @@ -41,6 +41,12 @@ export class ElectronMainService implements IElectronMainService { ) { } + //#region Properties + + get windowId(): never { throw new Error('Not implemented in electron-main'); } + + //#endregion + //#region Events readonly onWindowOpen: Event = Event.filter(Event.fromNodeEventEmitter(app, 'browser-window-created', (_, window: BrowserWindow) => window.id), windowId => !!this.windowsMainService.getWindowById(windowId)); @@ -85,8 +91,8 @@ export class ElectronMainService implements IElectronMainService { } openWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise; - openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: INativeOpenWindowOptions): Promise; - openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: INativeOpenWindowOptions): Promise { + openWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise; + openWindow(windowId: number | undefined, arg1?: IOpenEmptyWindowOptions | IWindowOpenable[], arg2?: IOpenWindowOptions): Promise { if (Array.isArray(arg1)) { return this.doOpenWindow(windowId, arg1, arg2); } @@ -94,7 +100,7 @@ export class ElectronMainService implements IElectronMainService { return this.doOpenEmptyWindow(windowId, arg1); } - private async doOpenWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options: INativeOpenWindowOptions = Object.create(null)): Promise { + private async doOpenWindow(windowId: number | undefined, toOpen: IWindowOpenable[], options: IOpenWindowOptions = Object.create(null)): Promise { if (toOpen.length > 0) { this.windowsMainService.open({ context: OpenContext.API, @@ -293,6 +299,43 @@ export class ElectronMainService implements IElectronMainService { } } + async moveItemToTrash(windowId: number | undefined, fullPath: string): Promise { + return shell.moveItemToTrash(fullPath); + } + + //#endregion + + + //#region clipboard + + async readClipboardText(windowId: number | undefined, type?: 'selection' | 'clipboard'): Promise { + return clipboard.readText(type); + } + + async writeClipboardText(windowId: number | undefined, text: string, type?: 'selection' | 'clipboard'): Promise { + return clipboard.writeText(text, type); + } + + async readClipboardFindText(windowId: number | undefined,): Promise { + return clipboard.readFindText(); + } + + async writeClipboardFindText(windowId: number | undefined, text: string): Promise { + return clipboard.writeFindText(text); + } + + async writeClipboardBuffer(windowId: number | undefined, format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise { + return clipboard.writeBuffer(format, buffer as Buffer, type); + } + + async readClipboardBuffer(windowId: number | undefined, format: string): Promise { + return clipboard.readBuffer(format); + } + + async hasClipboard(windowId: number | undefined, format: string, type?: 'selection' | 'clipboard'): Promise { + return clipboard.has(format, type); + } + //#endregion //#region macOS Touchbar diff --git a/src/vs/platform/electron/electron-sandbox/electron.ts b/src/vs/platform/electron/electron-sandbox/electron.ts new file mode 100644 index 00000000000..aa17d948100 --- /dev/null +++ b/src/vs/platform/electron/electron-sandbox/electron.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICommonElectronService } from 'vs/platform/electron/common/electron'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; +import { createChannelSender } from 'vs/base/parts/ipc/common/ipc'; + +export const IElectronService = createDecorator('electronService'); + +export interface IElectronService extends ICommonElectronService { } + +export class ElectronService { + + _serviceBrand: undefined; + + constructor( + readonly windowId: number, + @IMainProcessService mainProcessService: IMainProcessService + ) { + return createChannelSender(mainProcessService.getChannel('electron'), { + context: windowId, + properties: (() => { + const properties = new Map(); + properties.set('windowId', windowId); + + return properties; + })() + }); + } +} diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 61c2b16ad7a..c9c362f066e 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -72,6 +72,7 @@ export interface ParsedArgs { remote?: string; 'disable-user-env-probe'?: boolean; 'force'?: boolean; + 'donot-sync'?: boolean; 'force-user-env'?: boolean; 'sync'?: 'on' | 'off'; @@ -187,6 +188,7 @@ export const OPTIONS: OptionDescriptions> = { 'file-chmod': { type: 'boolean' }, 'driver-verbose': { type: 'boolean' }, 'force': { type: 'boolean' }, + 'donot-sync': { type: 'boolean' }, 'trace': { type: 'boolean' }, 'trace-category-filter': { type: 'string' }, 'trace-options': { type: 'string' }, diff --git a/src/vs/platform/environment/node/argvHelper.ts b/src/vs/platform/environment/node/argvHelper.ts index 11e54c68b8a..eda11907549 100644 --- a/src/vs/platform/environment/node/argvHelper.ts +++ b/src/vs/platform/environment/node/argvHelper.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { firstIndex } from 'vs/base/common/arrays'; import { localize } from 'vs/nls'; -import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/node/files'; +import { MIN_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { parseArgs, ErrorReporter, OPTIONS, ParsedArgs } from 'vs/platform/environment/node/argv'; function parseAndValidate(cmdLineArgs: string[], reportWarnings: boolean): ParsedArgs { diff --git a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts index 25c274ca05f..2df5bfb9329 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -362,10 +362,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } const { id, uuid } = extension ? extension.identifier : arg1; let query = new Query() - .withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.ExcludeNonValidated) + .withFlags(Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, 1) - .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') - .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)); + .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code'); if (uuid) { query = query.withFilter(FilterType.ExtensionId, uuid); @@ -426,8 +425,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService { let query = new Query() .withFlags(Flags.IncludeLatestVersionOnly, Flags.IncludeAssetUri, Flags.IncludeStatistics, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, pageSize) - .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') - .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)); + .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code'); if (text) { // Use category filter instead of "category:themes" @@ -484,6 +482,11 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } private queryGallery(query: Query, token: CancellationToken): Promise<{ galleryExtensions: IRawGalleryExtension[], total: number; }> { + // Always exclude non validated and unpublished extensions + query = query + .withFlags(query.flags, Flags.ExcludeNonValidated) + .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)); + if (!this.isEnabled()) { return Promise.reject(new Error('No extension gallery service configured.')); } @@ -602,10 +605,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise { let query = new Query() - .withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties, Flags.ExcludeNonValidated) + .withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties) .withPage(1, 1) - .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code') - .withFilter(FilterType.ExcludeWithFlags, flagsToString(Flags.Unpublished)); + .withFilter(FilterType.Target, 'Microsoft.VisualStudio.Code'); if (extension.identifier.uuid) { query = query.withFilter(FilterType.ExtensionId, extension.identifier.uuid); diff --git a/src/vs/platform/extensionManagement/common/extensionManagement.ts b/src/vs/platform/extensionManagement/common/extensionManagement.ts index f26af4def98..d5bbcec2963 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -92,7 +92,9 @@ export interface IGalleryMetadata { export interface ILocalExtension extends IExtension { readonly manifest: IExtensionManifest; - metadata: IGalleryMetadata; + isMachineScoped: boolean; + publisherId: string | null; + publisherDisplayName: string | null; readmeUrl: URI | null; changelogUrl: URI | null; } @@ -187,6 +189,12 @@ export const INSTALL_ERROR_NOT_SUPPORTED = 'notsupported'; export const INSTALL_ERROR_MALICIOUS = 'malicious'; export const INSTALL_ERROR_INCOMPATIBLE = 'incompatible'; +export class ExtensionManagementError extends Error { + constructor(message: string, readonly code: string) { + super(message); + } +} + export interface IExtensionManagementService { _serviceBrand: undefined; @@ -196,10 +204,10 @@ export interface IExtensionManagementService { onDidUninstallExtension: Event; zip(extension: ILocalExtension): Promise; - unzip(zipLocation: URI, type: ExtensionType): Promise; + unzip(zipLocation: URI): Promise; getManifest(vsix: URI): Promise; - install(vsix: URI): Promise; - installFromGallery(extension: IGalleryExtension): Promise; + install(vsix: URI, isMachineScoped?: boolean): Promise; + installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise; uninstall(extension: ILocalExtension, force?: boolean): Promise; reinstallFromGallery(extension: ILocalExtension): Promise; getInstalled(type?: ExtensionType): Promise; diff --git a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts index 23efae34eef..48abfb96d37 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementIpc.ts @@ -60,7 +60,7 @@ export class ExtensionManagementChannel implements IServerChannel { const uriTransformer: IURITransformer | null = this.getUriTransformer(context); switch (command) { case 'zip': return this.service.zip(transformIncomingExtension(args[0], uriTransformer)).then(uri => transformOutgoingURI(uri, uriTransformer)); - case 'unzip': return this.service.unzip(transformIncomingURI(args[0], uriTransformer), args[1]); + 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 'installFromGallery': return this.service.installFromGallery(args[0]); @@ -92,8 +92,8 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(result))); } - unzip(zipLocation: URI, type: ExtensionType): Promise { - return Promise.resolve(this.channel.call('unzip', [zipLocation, type])); + unzip(zipLocation: URI): Promise { + return Promise.resolve(this.channel.call('unzip', [zipLocation])); } install(vsix: URI): Promise { diff --git a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts index bad92e00651..fef60bf20b8 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -69,9 +69,9 @@ export function getLocalExtensionTelemetryData(extension: ILocalExtension): any id: extension.identifier.id, name: extension.manifest.name, galleryId: null, - publisherId: extension.metadata ? extension.metadata.publisherId : null, + publisherId: extension.publisherId, publisherName: extension.manifest.publisher, - publisherDisplayName: extension.metadata ? extension.metadata.publisherDisplayName : null, + publisherDisplayName: extension.publisherDisplayName, dependencies: extension.manifest.extensionDependencies && extension.manifest.extensionDependencies.length > 0 }; } @@ -116,4 +116,4 @@ export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set { - return new Promise((c, e) => { - try { - const manifest = JSON.parse(raw); - const metadata = manifest.__metadata || null; - delete manifest.__metadata; - c({ manifest, metadata }); - } catch (err) { - e(new Error(nls.localize('invalidManifest', "Extension invalid: package.json is not a JSON file."))); - } - }); -} - -function readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> { - const promises = [ - pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') - .then(raw => parseManifest(raw)), - pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') - .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') - .then(raw => JSON.parse(raw)) - ]; - - return Promise.all(promises).then(([{ manifest, metadata }, translations]) => { - return { - manifest: localizeManifest(manifest, translations), - metadata - }; - }); -} - interface InstallableExtension { zipPath: string; identifierWithVersion: ExtensionIdentifierWithVersion; - metadata: IGalleryMetadata | null; + metadata?: IMetadata; } export class ExtensionManagementService extends Disposable implements IExtensionManagementService { _serviceBrand: undefined; - private systemExtensionsPath: string; - private extensionsPath: string; - private uninstalledPath: string; - private uninstalledFileLimiter: Queue; + private readonly extensionsScanner: ExtensionsScanner; private reportedExtensions: Promise | undefined; private lastReportTimestamp = 0; private readonly installingExtensions: Map> = new Map>(); private readonly uninstallingExtensions: Map> = new Map>(); private readonly manifestCache: ExtensionsManifestCache; private readonly extensionsDownloader: ExtensionsDownloader; - private readonly extensionLifecycle: ExtensionsLifecycle; private readonly _onInstallExtension = this._register(new Emitter()); readonly onInstallExtension: Event = this._onInstallExtension.event; @@ -130,7 +85,7 @@ export class ExtensionManagementService extends Disposable implements IExtension onDidUninstallExtension: Event = this._onDidUninstallExtension.event; constructor( - @IEnvironmentService private readonly environmentService: INativeEnvironmentService, + @IEnvironmentService environmentService: INativeEnvironmentService, @IExtensionGalleryService private readonly galleryService: IExtensionGalleryService, @ILogService private readonly logService: ILogService, @optional(IDownloadService) private downloadService: IDownloadService, @@ -138,13 +93,9 @@ export class ExtensionManagementService extends Disposable implements IExtension @IInstantiationService instantiationService: IInstantiationService, ) { super(); - this.systemExtensionsPath = environmentService.builtinExtensionsPath; - this.extensionsPath = environmentService.extensionsPath!; - this.uninstalledPath = path.join(this.extensionsPath, '.obsolete'); - this.uninstalledFileLimiter = new Queue(); + this.extensionsScanner = this._register(instantiationService.createInstance(ExtensionsScanner)); this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this)); this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader)); - this.extensionLifecycle = this._register(new ExtensionsLifecycle(environmentService, this.logService)); this._register(toDisposable(() => { this.installingExtensions.forEach(promise => promise.cancel()); @@ -152,6 +103,9 @@ export class ExtensionManagementService extends Disposable implements IExtension this.installingExtensions.clear(); this.uninstallingExtensions.clear(); })); + + const extensionLifecycle = this._register(new ExtensionsLifecycle(environmentService, this.logService)); + this._register(this.extensionsScanner.onDidRemoveExtension(extension => extensionLifecycle.postUninstall(extension))); } zip(extension: ILocalExtension): Promise { @@ -161,9 +115,9 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(path => URI.file(path)); } - unzip(zipLocation: URI, type: ExtensionType): Promise { + unzip(zipLocation: URI): Promise { this.logService.trace('ExtensionManagementService#unzip', zipLocation.toString()); - return this.install(zipLocation, type).then(local => local.identifier); + return this.install(zipLocation).then(local => local.identifier); } async getManifest(vsix: URI): Promise { @@ -198,7 +152,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } - install(vsix: URI, type: ExtensionType = ExtensionType.User): Promise { + install(vsix: URI, isMachineScoped?: boolean): Promise { this.logService.trace('ExtensionManagementService#install', vsix.toString()); return createCancelablePromise(token => { return this.downloadVsix(vsix).then(downloadLocation => { @@ -216,9 +170,10 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(installedExtensions => { const existing = installedExtensions.filter(i => areSameExtensions(identifier, i.identifier))[0]; if (existing) { + isMachineScoped = isMachineScoped || existing.isMachineScoped; operation = InstallOperation.Update; if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) { - return this.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); + return this.extensionsScanner.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); } else if (semver.gt(existing.manifest.version, manifest.version)) { return this.uninstall(existing, true); } @@ -228,7 +183,7 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.unsetUninstalledAndGetLocal(identifierWithVersion) .then(existing => { if (existing) { - return this.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); + return this.extensionsScanner.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart VS Code before reinstalling {0}.", manifest.displayName || manifest.name)))); } return undefined; }); @@ -238,10 +193,10 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(() => { this.logService.info('Installing the extension:', identifier.id); this._onInstallExtension.fire({ identifier, zipPath }); - return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) + return this.getGalleryMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) .then( - metadata => this.installFromZipPath(identifierWithVersion, zipPath, metadata, type, operation, token), - () => this.installFromZipPath(identifierWithVersion, zipPath, null, type, operation, token)) + metadata => this.installFromZipPath(identifierWithVersion, zipPath, { ...metadata, isMachineScoped }, operation, token), + () => this.installFromZipPath(identifierWithVersion, zipPath, isMachineScoped ? { isMachineScoped } : undefined, operation, token)) .then( local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, e => { @@ -265,9 +220,9 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.downloadService.download(vsix, URI.file(downloadedLocation)).then(() => URI.file(downloadedLocation)); } - private installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IGalleryMetadata | null, type: ExtensionType, operation: InstallOperation, token: CancellationToken): Promise { - return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, type, token) - .then(local => this.installDependenciesAndPackExtensions(local, null) + private installFromZipPath(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, metadata: IMetadata | undefined, operation: InstallOperation, token: CancellationToken): Promise { + return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, token) + .then(local => this.installDependenciesAndPackExtensions(local, undefined) .then( () => local, error => { @@ -285,7 +240,7 @@ export class ExtensionManagementService extends Disposable implements IExtension )); } - async installFromGallery(extension: IGalleryExtension): Promise { + async installFromGallery(extension: IGalleryExtension, isMachineScoped?: boolean): Promise { if (!this.galleryService.isEnabled()) { return Promise.reject(new Error(nls.localize('MarketPlaceDisabled', "Marketplace is not enabled"))); } @@ -327,14 +282,17 @@ export class ExtensionManagementService extends Disposable implements IExtension this.installingExtensions.set(key, cancellablePromise); try { const installed = await this.getInstalled(ExtensionType.User); - const existingExtension = installed.filter(i => areSameExtensions(i.identifier, extension.identifier))[0]; + const existingExtension = installed.find(i => areSameExtensions(i.identifier, extension.identifier)); if (existingExtension) { operation = InstallOperation.Update; } this.downloadInstallableExtension(extension, operation) - .then(installableExtension => this.installExtension(installableExtension, ExtensionType.User, cancellationToken) - .then(local => this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)).finally(() => { }).then(() => local))) + .then(installableExtension => { + installableExtension.metadata.isMachineScoped = isMachineScoped || existingExtension?.isMachineScoped; + return this.installExtension(installableExtension, cancellationToken) + .then(local => this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)).finally(() => { }).then(() => local)); + }) .then(local => this.installDependenciesAndPackExtensions(local, existingExtension) .then(() => local, error => this.uninstall(local, true).then(() => Promise.reject(error), () => Promise.reject(error)))) .then( @@ -386,7 +344,7 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(galleryExtension => { if (galleryExtension) { return this.setUninstalled(extension) - .then(() => this.removeUninstalledExtension(extension) + .then(() => this.extensionsScanner.removeUninstalledExtension(extension) .then( () => this.installFromGallery(galleryExtension).then(), e => Promise.reject(new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e)))))); @@ -404,7 +362,7 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(report => getMaliciousExtensionsSet(report).has(extension.identifier.id)); } - private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise { + private downloadInstallableExtension(extension: IGalleryExtension, operation: InstallOperation): Promise> { const metadata = { id: extension.identifier.uuid, publisherId: extension.publisherId, @@ -419,21 +377,21 @@ export class ExtensionManagementService extends Disposable implements IExtension this.logService.info('Downloaded extension:', extension.identifier.id, zipPath); return getManifest(zipPath) .then( - manifest => ({ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }), + manifest => (>{ zipPath, identifierWithVersion: new ExtensionIdentifierWithVersion(extension.identifier, manifest.version), metadata }), error => Promise.reject(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING)) ); }, error => Promise.reject(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING))); } - private installExtension(installableExtension: InstallableExtension, type: ExtensionType, token: CancellationToken): Promise { + private installExtension(installableExtension: InstallableExtension, token: CancellationToken): Promise { return this.unsetUninstalledAndGetLocal(installableExtension.identifierWithVersion) .then( local => { if (local) { return local; } - return this.extractAndInstall(installableExtension, type, token); + return this.extractAndInstall(installableExtension, token); }, e => { if (isMacintosh) { @@ -460,63 +418,17 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, type: ExtensionType, token: CancellationToken): Promise { + private async extractAndInstall({ zipPath, identifierWithVersion, metadata }: InstallableExtension, token: CancellationToken): Promise { const { identifier } = identifierWithVersion; - const location = type === ExtensionType.User ? this.extensionsPath : this.systemExtensionsPath; - const folderName = identifierWithVersion.key(); - const tempPath = path.join(location, `.${folderName}`); - const extensionPath = path.join(location, folderName); - return pfs.rimraf(extensionPath) - .then(() => this.extractAndRename(identifier, zipPath, tempPath, extensionPath, token), e => Promise.reject(new ExtensionManagementError(nls.localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifier.id), INSTALL_ERROR_DELETING))) - .then(() => this.scanExtension(folderName, location, type)) - .then(local => { - if (!local) { - return Promise.reject(nls.localize('cannot read', "Cannot read the extension from {0}", location)); - } - this.logService.info('Installation completed.', identifier.id); - if (metadata) { - this.setMetadata(local, metadata); - return this.saveMetadataForLocalExtension(local); - } - return local; - }, error => pfs.rimraf(extensionPath).then(() => Promise.reject(error), () => Promise.reject(error))); + let local = await this.extensionsScanner.extractUserExtension(identifierWithVersion, zipPath, token); + this.logService.info('Installation completed.', identifier.id); + if (metadata) { + local = await this.extensionsScanner.saveMetadataForLocalExtension(local, metadata); + } + return local; } - private extractAndRename(identifier: IExtensionIdentifier, zipPath: string, extractPath: string, renamePath: string, token: CancellationToken): Promise { - return this.extract(identifier, zipPath, extractPath, token) - .then(() => this.rename(identifier, extractPath, renamePath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */) - .then( - () => this.logService.info('Renamed to', renamePath), - e => { - this.logService.info('Rename failed. Deleting from extracted location', extractPath); - return pfs.rimraf(extractPath).finally(() => { }).then(() => Promise.reject(e)); - })); - } - - private extract(identifier: IExtensionIdentifier, zipPath: string, extractPath: string, token: CancellationToken): Promise { - this.logService.trace(`Started extracting the extension from ${zipPath} to ${extractPath}`); - return pfs.rimraf(extractPath) - .then( - () => extract(zipPath, extractPath, { sourcePath: 'extension', overwrite: true }, token) - .then( - () => this.logService.info(`Extracted extension to ${extractPath}:`, identifier.id), - e => pfs.rimraf(extractPath).finally(() => { }) - .then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))), - e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); - } - - private rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { - return pfs.rename(extractPath, renamePath) - .then(undefined, error => { - if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { - this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); - return this.rename(identifier, extractPath, renamePath, retryUntil); - } - return Promise.reject(new ExtensionManagementError(error.message || nls.localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING)); - }); - } - - private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | null): Promise { + private async installDependenciesAndPackExtensions(installed: ILocalExtension, existing: ILocalExtension | undefined): Promise { if (this.galleryService.isEnabled()) { const dependenciesAndPackExtensions: string[] = installed.manifest.extensionDependencies || []; if (installed.manifest.extensionPack) { @@ -570,31 +482,16 @@ export class ExtensionManagementService extends Disposable implements IExtension })); } - updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata): Promise { this.logService.trace('ExtensionManagementService#updateMetadata', local.identifier.id); - local.metadata = metadata; - return this.saveMetadataForLocalExtension(local) - .then(localExtension => { - this.manifestCache.invalidate(); - return localExtension; - }); + local = await this.extensionsScanner.saveMetadataForLocalExtension(local, { ...metadata, isMachineScoped: local.isMachineScoped }); + this.manifestCache.invalidate(); + return local; } - private saveMetadataForLocalExtension(local: ILocalExtension): Promise { - if (!local.metadata) { - return Promise.resolve(local); - } - const manifestPath = path.join(local.location.fsPath, 'package.json'); - return pfs.readFile(manifestPath, 'utf8') - .then(raw => parseManifest(raw)) - .then(({ manifest }) => assign(manifest, { __metadata: local.metadata })) - .then(manifest => pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t'))) - .then(() => local); - } - - private getMetadata(extensionName: string): Promise { + private getGalleryMetadata(extensionName: string): Promise { return this.findGalleryExtensionByName(extensionName) - .then(galleryExtension => galleryExtension ? { id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : null); + .then(galleryExtension => galleryExtension ? { id: galleryExtension.identifier.uuid, publisherDisplayName: galleryExtension.publisherDisplayName, publisherId: galleryExtension.publisherId } : undefined); } private findGalleryExtension(local: ILocalExtension): Promise { @@ -707,7 +604,7 @@ export class ExtensionManagementService extends Disposable implements IExtension let promise = this.uninstallingExtensions.get(local.identifier.id); if (!promise) { // Set all versions of the extension as uninstalled - promise = createCancelablePromise(token => this.scanUserExtensions(false) + promise = createCancelablePromise(token => this.extensionsScanner.scanUserExtensions(false) .then(userExtensions => this.setUninstalled(...userExtensions.filter(u => areSameExtensions(u.identifier, local.identifier)))) .then(() => { this.uninstallingExtensions.delete(local.identifier.id); })); this.uninstallingExtensions.set(local.identifier.id, promise); @@ -731,142 +628,11 @@ export class ExtensionManagementService extends Disposable implements IExtension } getInstalled(type: ExtensionType | null = null): Promise { - const promises: Promise[] = []; - - if (type === null || type === ExtensionType.System) { - promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_SYS_EXTENSIONS)))); - } - - if (type === null || type === ExtensionType.User) { - promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS)))); - } - - return Promise.all(promises).then(flatten, errors => Promise.reject(this.joinErrors(errors))); + return this.extensionsScanner.scanExtensions(type); } - private scanSystemExtensions(): Promise { - this.logService.trace('Started scanning system extensions'); - const systemExtensionsPromise = this.scanExtensions(this.systemExtensionsPath, ExtensionType.System) - .then(result => { - this.logService.trace('Scanned system extensions:', result.length); - return result; - }); - if (this.environmentService.isBuilt) { - return systemExtensionsPromise; - } - - // Scan other system extensions during development - const devSystemExtensionsPromise = this.getDevSystemExtensionsList() - .then(devSystemExtensionsList => { - if (devSystemExtensionsList.length) { - return this.scanExtensions(this.devSystemExtensionsPath, ExtensionType.System) - .then(result => { - this.logService.trace('Scanned dev system extensions:', result.length); - return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id }))); - }); - } else { - return []; - } - }); - return Promise.all([systemExtensionsPromise, devSystemExtensionsPromise]) - .then(([systemExtensions, devSystemExtensions]) => [...systemExtensions, ...devSystemExtensions]); - } - - private scanUserExtensions(excludeOutdated: boolean): Promise { - this.logService.trace('Started scanning user extensions'); - return Promise.all([this.getUninstalledExtensions(), this.scanExtensions(this.extensionsPath, ExtensionType.User)]) - .then(([uninstalled, extensions]) => { - extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); - if (excludeOutdated) { - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]); - } - this.logService.trace('Scanned user extensions:', extensions.length); - return extensions; - }); - } - - private scanExtensions(root: string, type: ExtensionType): Promise { - const limiter = new Limiter(10); - return pfs.readdir(root) - .then(extensionsFolders => Promise.all(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, root, type))))) - .then(extensions => extensions.filter(e => e && e.identifier)); - } - - private scanExtension(folderName: string, root: string, type: ExtensionType): Promise { - if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.` - return Promise.resolve(null); - } - const extensionPath = path.join(root, folderName); - return pfs.readdir(extensionPath) - .then(children => readManifest(extensionPath) - .then(({ manifest, metadata }) => { - const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; - const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : null; - const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; - const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : null; - const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; - const local = { type, identifier, manifest, metadata, location: URI.file(extensionPath), readmeUrl, changelogUrl }; - if (metadata) { - this.setMetadata(local, metadata); - } - return local; - })) - .then(undefined, () => null); - } - - private setMetadata(local: ILocalExtension, metadata: IGalleryMetadata): void { - local.metadata = metadata; - local.identifier.uuid = metadata.id; - } - - async removeDeprecatedExtensions(): Promise { - await this.removeUninstalledExtensions(); - await this.removeOutdatedExtensions(); - } - - private async removeUninstalledExtensions(): Promise { - const uninstalled = await this.getUninstalledExtensions(); - const extensions = await this.scanExtensions(this.extensionsPath, ExtensionType.User); // All user extensions - const installed: Set = new Set(); - for (const e of extensions) { - if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) { - installed.add(e.identifier.id.toLowerCase()); - } - } - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - await Promise.all(byExtension.map(async e => { - const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; - if (!installed.has(latest.identifier.id.toLowerCase())) { - await this.extensionLifecycle.postUninstall(latest); - } - })); - const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); - await Promise.all(toRemove.map(e => this.removeUninstalledExtension(e))); - } - - private removeOutdatedExtensions(): Promise { - return this.scanExtensions(this.extensionsPath, ExtensionType.User) // All user extensions - .then(extensions => { - const toRemove: ILocalExtension[] = []; - - // Outdated extensions - const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); - toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1)))); - - return Promise.all(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); - }).then(() => undefined); - } - - private removeUninstalledExtension(extension: ILocalExtension): Promise { - return this.removeExtension(extension, 'uninstalled') - .then(() => this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()])) - .then(() => undefined); - } - - private removeExtension(extension: ILocalExtension, type: string): Promise { - this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); - return pfs.rimraf(extension.location.fsPath).then(() => this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath)); + removeDeprecatedExtensions(): Promise { + return this.extensionsScanner.cleanUp(); } private isUninstalled(identifier: ExtensionIdentifierWithVersion): Promise { @@ -874,7 +640,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } private filterUninstalled(...identifiers: ExtensionIdentifierWithVersion[]): Promise { - return this.withUninstalledExtensions(allUninstalled => { + return this.extensionsScanner.withUninstalledExtensions(allUninstalled => { const uninstalled: string[] = []; for (const identifier of identifiers) { if (!!allUninstalled[identifier.key()]) { @@ -887,34 +653,11 @@ export class ExtensionManagementService extends Disposable implements IExtension private setUninstalled(...extensions: ILocalExtension[]): Promise<{ [id: string]: boolean }> { const ids: ExtensionIdentifierWithVersion[] = extensions.map(e => new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version)); - return this.withUninstalledExtensions(uninstalled => assign(uninstalled, ids.reduce((result, id) => { result[id.key()] = true; return result; }, {} as { [id: string]: boolean }))); + return this.extensionsScanner.withUninstalledExtensions(uninstalled => assign(uninstalled, ids.reduce((result, id) => { result[id.key()] = true; return result; }, {} as { [id: string]: boolean }))); } private unsetUninstalled(extensionIdentifier: ExtensionIdentifierWithVersion): Promise { - return this.withUninstalledExtensions(uninstalled => delete uninstalled[extensionIdentifier.key()]); - } - - private getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> { - return this.withUninstalledExtensions(uninstalled => uninstalled); - } - - private async withUninstalledExtensions(fn: (uninstalled: { [id: string]: boolean; }) => T): Promise { - return await this.uninstalledFileLimiter.queue(() => { - let result: T | null = null; - return pfs.readFile(this.uninstalledPath, 'utf8') - .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) - .then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) - .then(uninstalled => { result = fn(uninstalled); return uninstalled; }) - .then(uninstalled => { - if (Object.keys(uninstalled).length === 0) { - return pfs.rimraf(this.uninstalledPath); - } else { - const raw = JSON.stringify(uninstalled); - return pfs.writeFile(this.uninstalledPath, raw); - } - }) - .then(() => result); - }); + return this.extensionsScanner.withUninstalledExtensions(uninstalled => delete uninstalled[extensionIdentifier.key()]); } getExtensionsReport(): Promise { @@ -941,18 +684,6 @@ export class ExtensionManagementService extends Disposable implements IExtension }); } - private _devSystemExtensionsPath: string | null = null; - private get devSystemExtensionsPath(): string { - if (!this._devSystemExtensionsPath) { - this._devSystemExtensionsPath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', '.build', 'builtInExtensions')); - } - return this._devSystemExtensionsPath; - } - - private getDevSystemExtensionsList(): Promise { - return Promise.resolve(product.builtInExtensions ? product.builtInExtensions.map(e => e.name) : []); - } - private toNonCancellablePromise(promise: Promise): Promise { return new Promise((c, e) => promise.then(result => c(result), error => e(error))); } diff --git a/src/vs/platform/extensionManagement/node/extensionsScanner.ts b/src/vs/platform/extensionManagement/node/extensionsScanner.ts new file mode 100644 index 00000000000..8c9d4ce94dd --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -0,0 +1,351 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as semver from 'semver-umd'; +import { Disposable } from 'vs/base/common/lifecycle'; +import * as pfs from 'vs/base/node/pfs'; +import * as path from 'vs/base/common/path'; +import { ILogService } from 'vs/platform/log/common/log'; +import { ILocalExtension, IGalleryMetadata, ExtensionManagementError } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { ExtensionType, IExtensionManifest, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { areSameExtensions, ExtensionIdentifierWithVersion, groupByExtension, getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; +import { Limiter, Queue } from 'vs/base/common/async'; +import { URI } from 'vs/base/common/uri'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { localizeManifest } from 'vs/platform/extensionManagement/common/extensionNls'; +import { localize } from 'vs/nls'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { CancellationToken } from 'vscode'; +import { extract, ExtractError } from 'vs/base/node/zip'; +import { isWindows } from 'vs/base/common/platform'; +import { flatten } from 'vs/base/common/arrays'; +import { Emitter } from 'vs/base/common/event'; +import { assign } from 'vs/base/common/objects'; + +const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; +const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser'; +const INSTALL_ERROR_EXTRACTING = 'extracting'; +const INSTALL_ERROR_DELETING = 'deleting'; +const INSTALL_ERROR_RENAMING = 'renaming'; + +export type IMetadata = Partial; + +export class ExtensionsScanner extends Disposable { + + private readonly systemExtensionsPath: string; + private readonly extensionsPath: string; + private readonly uninstalledPath: string; + private readonly uninstalledFileLimiter: Queue; + + private _onDidRemoveExtension = new Emitter(); + readonly onDidRemoveExtension = this._onDidRemoveExtension.event; + + constructor( + @ILogService private readonly logService: ILogService, + @IEnvironmentService private readonly environmentService: INativeEnvironmentService, + @IProductService private readonly productService: IProductService, + ) { + super(); + this.systemExtensionsPath = environmentService.builtinExtensionsPath; + this.extensionsPath = environmentService.extensionsPath!; + this.uninstalledPath = path.join(this.extensionsPath, '.obsolete'); + this.uninstalledFileLimiter = new Queue(); + } + + async cleanUp(): Promise { + await this.removeUninstalledExtensions(); + await this.removeOutdatedExtensions(); + } + + async scanExtensions(type: ExtensionType | null): Promise { + const promises: Promise[] = []; + + if (type === null || type === ExtensionType.System) { + promises.push(this.scanSystemExtensions().then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_SYS_EXTENSIONS)))); + } + + if (type === null || type === ExtensionType.User) { + promises.push(this.scanUserExtensions(true).then(null, e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, ERROR_SCANNING_USER_EXTENSIONS)))); + } + + return Promise.all(promises).then(flatten, errors => Promise.reject(this.joinErrors(errors))); + } + + async scanUserExtensions(excludeOutdated: boolean): Promise { + this.logService.trace('Started scanning user extensions'); + let [uninstalled, extensions] = await Promise.all([this.getUninstalledExtensions(), this.scanAllUserExtensions()]); + extensions = extensions.filter(e => !uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); + if (excludeOutdated) { + const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); + extensions = byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]); + } + this.logService.trace('Scanned user extensions:', extensions.length); + return extensions; + } + + async scanAllUserExtensions(): Promise { + return this.scanExtensionsInDir(this.extensionsPath, ExtensionType.User); + } + + async extractUserExtension(identifierWithVersion: ExtensionIdentifierWithVersion, zipPath: string, token: CancellationToken): Promise { + const { identifier } = identifierWithVersion; + const folderName = identifierWithVersion.key(); + const tempPath = path.join(this.extensionsPath, `.${folderName}`); + const extensionPath = path.join(this.extensionsPath, folderName); + + try { + await pfs.rimraf(extensionPath); + } catch (error) { + try { + await pfs.rimraf(extensionPath); + } catch (e) { /* ignore */ } + throw new ExtensionManagementError(localize('errorDeleting', "Unable to delete the existing folder '{0}' while installing the extension '{1}'. Please delete the folder manually and try again", extensionPath, identifier.id), INSTALL_ERROR_DELETING); + } + + await this.extractAtLocation(identifier, zipPath, tempPath, token); + try { + await this.rename(identifier, tempPath, extensionPath, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */); + this.logService.info('Renamed to', extensionPath); + } catch (error) { + this.logService.info('Rename failed. Deleting from extracted location', tempPath); + try { + pfs.rimraf(tempPath); + } catch (e) { /* ignore */ } + throw error; + } + + let local: ILocalExtension | null = null; + try { + local = await this.scanExtension(folderName, this.extensionsPath, ExtensionType.User); + } catch (e) { /*ignore */ } + + if (local) { + return local; + } + throw new Error(localize('cannot read', "Cannot read the extension from {0}", this.extensionsPath)); + } + + async saveMetadataForLocalExtension(local: ILocalExtension, metadata: IMetadata): Promise { + this.setMetadata(local, metadata); + + // unset if false + metadata.isMachineScoped = metadata.isMachineScoped || undefined; + const manifestPath = path.join(local.location.fsPath, 'package.json'); + const raw = await pfs.readFile(manifestPath, 'utf8'); + const { manifest } = await this.parseManifest(raw); + assign(manifest, { __metadata: metadata }); + await pfs.writeFile(manifestPath, JSON.stringify(manifest, null, '\t')); + return local; + } + + getUninstalledExtensions(): Promise<{ [id: string]: boolean; }> { + return this.withUninstalledExtensions(uninstalled => uninstalled); + } + + async withUninstalledExtensions(fn: (uninstalled: { [id: string]: boolean; }) => T): Promise { + return this.uninstalledFileLimiter.queue(async () => { + let result: T | null = null; + return pfs.readFile(this.uninstalledPath, 'utf8') + .then(undefined, err => err.code === 'ENOENT' ? Promise.resolve('{}') : Promise.reject(err)) + .then<{ [id: string]: boolean }>(raw => { try { return JSON.parse(raw); } catch (e) { return {}; } }) + .then(uninstalled => { result = fn(uninstalled); return uninstalled; }) + .then(uninstalled => { + if (Object.keys(uninstalled).length === 0) { + return pfs.rimraf(this.uninstalledPath); + } else { + const raw = JSON.stringify(uninstalled); + return pfs.writeFile(this.uninstalledPath, raw); + } + }) + .then(() => result); + }); + } + + async removeExtension(extension: ILocalExtension, type: string): Promise { + this.logService.trace(`Deleting ${type} extension from disk`, extension.identifier.id, extension.location.fsPath); + await pfs.rimraf(extension.location.fsPath); + this.logService.info('Deleted from disk', extension.identifier.id, extension.location.fsPath); + } + + async removeUninstalledExtension(extension: ILocalExtension): Promise { + await this.removeExtension(extension, 'uninstalled'); + await this.withUninstalledExtensions(uninstalled => delete uninstalled[new ExtensionIdentifierWithVersion(extension.identifier, extension.manifest.version).key()]); + } + + private extractAtLocation(identifier: IExtensionIdentifier, zipPath: string, location: string, token: CancellationToken): Promise { + this.logService.trace(`Started extracting the extension from ${zipPath} to ${location}`); + return pfs.rimraf(location) + .then( + () => extract(zipPath, location, { sourcePath: 'extension', overwrite: true }, token) + .then( + () => this.logService.info(`Extracted extension to ${location}:`, identifier.id), + e => pfs.rimraf(location).finally(() => { }) + .then(() => Promise.reject(new ExtensionManagementError(e.message, e instanceof ExtractError && e.type ? e.type : INSTALL_ERROR_EXTRACTING)))), + e => Promise.reject(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); + } + + private rename(identifier: IExtensionIdentifier, extractPath: string, renamePath: string, retryUntil: number): Promise { + return pfs.rename(extractPath, renamePath) + .then(undefined, error => { + if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) { + this.logService.info(`Failed renaming ${extractPath} to ${renamePath} with 'EPERM' error. Trying again...`, identifier.id); + return this.rename(identifier, extractPath, renamePath, retryUntil); + } + return Promise.reject(new ExtensionManagementError(error.message || localize('renameError', "Unknown error while renaming {0} to {1}", extractPath, renamePath), error.code || INSTALL_ERROR_RENAMING)); + }); + } + + private async scanSystemExtensions(): Promise { + this.logService.trace('Started scanning system extensions'); + const systemExtensionsPromise = this.scanDefaultSystemExtensions(); + if (this.environmentService.isBuilt) { + return systemExtensionsPromise; + } + + // Scan other system extensions during development + const devSystemExtensionsPromise = this.scanDevSystemExtensions(); + const [systemExtensions, devSystemExtensions] = await Promise.all([systemExtensionsPromise, devSystemExtensionsPromise]); + return [...systemExtensions, ...devSystemExtensions]; + } + + private async scanExtensionsInDir(dir: string, type: ExtensionType): Promise { + const limiter = new Limiter(10); + const extensionsFolders = await pfs.readdir(dir); + const extensions = await Promise.all(extensionsFolders.map(extensionFolder => limiter.queue(() => this.scanExtension(extensionFolder, dir, type)))); + return extensions.filter(e => e && e.identifier); + } + + private async scanExtension(folderName: string, root: string, type: ExtensionType): Promise { + if (type === ExtensionType.User && folderName.indexOf('.') === 0) { // Do not consider user extension folder starting with `.` + return null; + } + const extensionPath = path.join(root, folderName); + try { + const children = await pfs.readdir(extensionPath); + const { manifest, metadata } = await this.readManifest(extensionPath); + const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0]; + const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : null; + const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0]; + const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : null; + const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) }; + const local = { type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false }; + if (metadata) { + this.setMetadata(local, metadata); + } + return local; + } catch (e) { + this.logService.trace(e); + return null; + } + } + + private async scanDefaultSystemExtensions(): Promise { + const result = await this.scanExtensionsInDir(this.systemExtensionsPath, ExtensionType.System); + this.logService.trace('Scanned system extensions:', result.length); + return result; + } + + private async scanDevSystemExtensions(): Promise { + const devSystemExtensionsList = this.getDevSystemExtensionsList(); + if (devSystemExtensionsList.length) { + const result = await this.scanExtensionsInDir(this.devSystemExtensionsPath, ExtensionType.System); + this.logService.trace('Scanned dev system extensions:', result.length); + return result.filter(r => devSystemExtensionsList.some(id => areSameExtensions(r.identifier, { id }))); + } else { + return []; + } + } + + private setMetadata(local: ILocalExtension, metadata: IMetadata): void { + local.publisherDisplayName = metadata.publisherDisplayName || null; + local.publisherId = metadata.publisherId || null; + local.identifier.uuid = metadata.id; + local.isMachineScoped = !!metadata.isMachineScoped; + } + + private async removeUninstalledExtensions(): Promise { + const uninstalled = await this.getUninstalledExtensions(); + const extensions = await this.scanAllUserExtensions(); // All user extensions + const installed: Set = new Set(); + for (const e of extensions) { + if (!uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]) { + installed.add(e.identifier.id.toLowerCase()); + } + } + const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); + await Promise.all(byExtension.map(async e => { + const latest = e.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version))[0]; + if (!installed.has(latest.identifier.id.toLowerCase())) { + this._onDidRemoveExtension.fire(latest); + } + })); + const toRemove: ILocalExtension[] = extensions.filter(e => uninstalled[new ExtensionIdentifierWithVersion(e.identifier, e.manifest.version).key()]); + await Promise.all(toRemove.map(e => this.removeUninstalledExtension(e))); + } + + private async removeOutdatedExtensions(): Promise { + const extensions = await this.scanAllUserExtensions(); + const toRemove: ILocalExtension[] = []; + + // Outdated extensions + const byExtension: ILocalExtension[][] = groupByExtension(extensions, e => e.identifier); + toRemove.push(...flatten(byExtension.map(p => p.sort((a, b) => semver.rcompare(a.manifest.version, b.manifest.version)).slice(1)))); + + await Promise.all(toRemove.map(extension => this.removeExtension(extension, 'outdated'))); + } + + private getDevSystemExtensionsList(): string[] { + return (this.productService.builtInExtensions || []).map(e => e.name); + } + + private joinErrors(errorOrErrors: (Error | string) | (Array)): Error { + const errors = Array.isArray(errorOrErrors) ? errorOrErrors : [errorOrErrors]; + if (errors.length === 1) { + return errors[0] instanceof Error ? errors[0] : new Error(errors[0]); + } + return errors.reduce((previousValue: Error, currentValue: Error | string) => { + return new Error(`${previousValue.message}${previousValue.message ? ',' : ''}${currentValue instanceof Error ? currentValue.message : currentValue}`); + }, new Error('')); + } + + private _devSystemExtensionsPath: string | null = null; + private get devSystemExtensionsPath(): string { + if (!this._devSystemExtensionsPath) { + this._devSystemExtensionsPath = path.normalize(path.join(getPathFromAmdModule(require, ''), '..', '.build', 'builtInExtensions')); + } + return this._devSystemExtensionsPath; + } + + private async readManifest(extensionPath: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> { + const promises = [ + pfs.readFile(path.join(extensionPath, 'package.json'), 'utf8') + .then(raw => this.parseManifest(raw)), + pfs.readFile(path.join(extensionPath, 'package.nls.json'), 'utf8') + .then(undefined, err => err.code !== 'ENOENT' ? Promise.reject(err) : '{}') + .then(raw => JSON.parse(raw)) + ]; + + const [{ manifest, metadata }, translations] = await Promise.all(promises); + return { + manifest: localizeManifest(manifest, translations), + metadata + }; + } + + private parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IMetadata | null; }> { + return new Promise((c, e) => { + try { + const manifest = JSON.parse(raw); + const metadata = manifest.__metadata || null; + delete manifest.__metadata; + c({ manifest, metadata }); + } catch (err) { + e(new Error(localize('invalidManifest', "Extension invalid: package.json is not a JSON file."))); + } + }); + } +} diff --git a/src/vs/platform/extensions/common/extensions.ts b/src/vs/platform/extensions/common/extensions.ts index ac6ec196a03..3c9bede84fe 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -149,6 +149,7 @@ export interface IExtensionManifest { readonly engines: { vscode: string }; readonly description?: string; readonly main?: string; + readonly browser?: string; readonly icon?: string; readonly categories?: string[]; readonly keywords?: string[]; diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 073855d2890..8a6c86f690c 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -7,7 +7,7 @@ import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; -import { isAbsolutePath, dirname, basename, joinPath, isEqual, isEqualOrParent } from 'vs/base/common/resources'; +import { isAbsolutePath, dirname, basename, joinPath, IExtUri, extUri, extUriIgnorePathCase } from 'vs/base/common/resources'; import { localize } from 'vs/nls'; import { TernarySearchTree } from 'vs/base/common/map'; import { isNonEmptyArray, coalesce } from 'vs/base/common/arrays'; @@ -54,7 +54,7 @@ export class FileService extends Disposable implements IFileService { // Forward events from provider const providerDisposables = new DisposableStore(); - providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes)))); + providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, this.getExtUri(provider).extUri)))); providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme }))); if (typeof provider.onDidErrorOccur === 'function') { providerDisposables.add(provider.onDidErrorOccur(error => this._onError.fire(new Error(error)))); @@ -540,6 +540,29 @@ export class FileService extends Disposable implements IFileService { //#region Move/Copy/Delete/Create Folder + async canMove(source: URI, target: URI, overwrite?: boolean): Promise { + return this.doCanMoveCopy(source, target, 'move', overwrite); + } + + async canCopy(source: URI, target: URI, overwrite?: boolean): Promise { + return this.doCanMoveCopy(source, target, 'copy', overwrite); + } + + private async doCanMoveCopy(source: URI, target: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise { + if (source.toString() !== target.toString()) { + try { + const sourceProvider = mode === 'move' ? this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source) : await this.withReadProvider(source); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); + + await this.doValidateMoveCopy(sourceProvider, source, targetProvider, target, mode, overwrite); + } catch (error) { + return error; + } + } + + return true; + } + async move(source: URI, target: URI, overwrite?: boolean): Promise { const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source); const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); @@ -673,16 +696,16 @@ export class FileService extends Disposable implements IFileService { // Check if source is equal or parent to target (requires providers to be the same) if (sourceProvider === targetProvider) { - const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + const { extUri, isPathCaseSensitive } = this.getExtUri(sourceProvider); if (!isPathCaseSensitive) { - isSameResourceWithDifferentPathCase = isEqual(source, target, true /* ignore case */); + isSameResourceWithDifferentPathCase = extUri.isEqual(source, target); } if (isSameResourceWithDifferentPathCase && mode === 'copy') { throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target))); } - if (!isSameResourceWithDifferentPathCase && isEqualOrParent(target, source, !isPathCaseSensitive)) { + if (!isSameResourceWithDifferentPathCase && extUri.isEqualOrParent(target, source)) { throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } @@ -699,8 +722,8 @@ export class FileService extends Disposable implements IFileService { // Special case: if the target is a parent of the source, we cannot delete // it as it would delete the source as well. In this case we have to throw if (sourceProvider === targetProvider) { - const isPathCaseSensitive = !!(sourceProvider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); - if (isEqualOrParent(source, target, !isPathCaseSensitive)) { + const { extUri } = this.getExtUri(sourceProvider); + if (extUri.isEqualOrParent(source, target)) { throw new Error(localize('unableToMoveCopyError4', "Unable to move/copy '{0}' into '{1}' since a file would replace the folder it is contained in.", this.resourceForError(source), this.resourceForError(target))); } } @@ -709,6 +732,15 @@ export class FileService extends Disposable implements IFileService { return { exists, isSameResourceWithDifferentPathCase }; } + private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } { + const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); + + return { + extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase, + isPathCaseSensitive + }; + } + async createFolder(resource: URI): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); @@ -726,7 +758,8 @@ export class FileService extends Disposable implements IFileService { const directoriesToCreate: string[] = []; // mkdir until we reach root - while (!isEqual(directory, dirname(directory))) { + const { extUri } = this.getExtUri(provider); + while (!extUri.isEqual(directory, dirname(directory))) { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { @@ -771,7 +804,17 @@ export class FileService extends Disposable implements IFileService { } } - async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { + async canDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { + try { + await this.doValidateDelete(resource, options); + } catch (error) { + return error; + } + + return true; + } + + private async doValidateDelete(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); // Validate trash support @@ -795,6 +838,15 @@ export class FileService extends Disposable implements IFileService { } } + return provider; + } + + async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { + const provider = await this.doValidateDelete(resource, options); + + const useTrash = !!options?.useTrash; + const recursive = !!options?.recursive; + // Delete through provider await provider.delete(resource, { recursive, useTrash }); diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 7438ec84efe..45cb79ba716 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -11,7 +11,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { Event } from 'vs/base/common/event'; import { startsWithIgnoreCase } from 'vs/base/common/strings'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { isEqualOrParent, isEqual } from 'vs/base/common/resources'; +import { IExtUri } from 'vs/base/common/resources'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer'; import { ReadableStreamEvents } from 'vs/base/common/stream'; @@ -121,6 +121,12 @@ export interface IFileService { */ move(source: URI, target: URI, overwrite?: boolean): Promise; + /** + * Find out if a move operation is possible given the arguments. No changes on disk will + * be performed. Returns an Error if the operation cannot be done. + */ + canMove(source: URI, target: URI, overwrite?: boolean): Promise; + /** * Copies the file/folder to a path identified by the resource. * @@ -128,6 +134,12 @@ export interface IFileService { */ copy(source: URI, target: URI, overwrite?: boolean): Promise; + /** + * Find out if a copy operation is possible given the arguments. No changes on disk will + * be performed. Returns an Error if the operation cannot be done. + */ + canCopy(source: URI, target: URI, overwrite?: boolean): Promise; + /** * Creates a new file with the given path and optional contents. The returned promise * will have the stat model object as a result. @@ -149,6 +161,12 @@ export interface IFileService { */ del(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; + /** + * Find out if a delete operation is possible given the arguments. No changes on disk will + * be performed. Returns an Error if the operation cannot be done. + */ + canDelete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise; + /** * Allows to start a watcher that reports file/folder change events on the provided resource. * @@ -473,7 +491,7 @@ export interface IFileChange { export class FileChangesEvent { - constructor(public readonly changes: readonly IFileChange[]) { } + constructor(public readonly changes: readonly IFileChange[], private readonly extUri: IExtUri) { } /** * Returns true if this change event contains the provided file with the given change type (if provided). In case of @@ -494,10 +512,10 @@ export class FileChangesEvent { // For deleted also return true when deleted folder is parent of target path if (change.type === FileChangeType.DELETED) { - return isEqualOrParent(resource, change.resource); + return this.extUri.isEqualOrParent(resource, change.resource); } - return isEqual(resource, change.resource); + return this.extUri.isEqual(resource, change.resource); }); } @@ -552,6 +570,10 @@ export class FileChangesEvent { return change.type === type; }); } + + filter(filterFn: (change: IFileChange) => boolean): FileChangesEvent { + return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.extUri); + } } export function isParent(path: string, candidate: string, ignoreCase?: boolean): boolean { @@ -824,7 +846,6 @@ export function etag(stat: { mtime: number | undefined, size: number | undefined return stat.mtime.toString(29) + stat.size.toString(31); } - export function whenProviderRegistered(file: URI, fileService: IFileService): Promise { if (fileService.canHandleResource(URI.from({ scheme: file.scheme }))) { return Promise.resolve(); @@ -838,3 +859,9 @@ export function whenProviderRegistered(file: URI, fileService: IFileService): Pr }); }); } + +/** + * Desktop only: limits for memory sizes + */ +export const MIN_MAX_MEMORY_SIZE_MB = 2048; +export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096; diff --git a/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts b/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts index 2c23af873f1..de3211dae17 100644 --- a/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts +++ b/src/vs/platform/files/electron-browser/diskFileSystemProvider.ts @@ -3,15 +3,24 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { shell } from 'electron'; -import { DiskFileSystemProvider as NodeDiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; +import { DiskFileSystemProvider as NodeDiskFileSystemProvider, IDiskFileSystemProviderOptions } from 'vs/platform/files/node/diskFileSystemProvider'; import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/path'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { + constructor( + logService: ILogService, + private electronService: IElectronService, + options?: IDiskFileSystemProviderOptions + ) { + super(logService, options); + } + get capabilities(): FileSystemProviderCapabilities { if (!this._capabilities) { this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash; @@ -25,9 +34,9 @@ export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { return super.doDelete(filePath, opts); } - const result = shell.moveItemToTrash(filePath); + const result = await this.electronService.moveItemToTrash(filePath); if (!result) { throw new Error(isWindows ? localize('binFailed', "Failed to move '{0}' to the recycle bin", basename(filePath)) : localize('trashFailed', "Failed to move '{0}' to the trash", basename(filePath))); } } -} \ No newline at end of file +} diff --git a/src/vs/platform/files/node/diskFileSystemProvider.ts b/src/vs/platform/files/node/diskFileSystemProvider.ts index 1524e074b99..cf70ec2f0f6 100644 --- a/src/vs/platform/files/node/diskFileSystemProvider.ts +++ b/src/vs/platform/files/node/diskFileSystemProvider.ts @@ -472,12 +472,11 @@ export class DiskFileSystemProvider extends Disposable implements } private async validateTargetDeleted(from: URI, to: URI, mode: 'move' | 'copy', overwrite?: boolean): Promise { - const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); - const fromFilePath = this.toFilePath(from); const toFilePath = this.toFilePath(to); let isSameResourceWithDifferentPathCase = false; + const isPathCaseSensitive = !!(this.capabilities & FileSystemProviderCapabilities.PathCaseSensitive); if (!isPathCaseSensitive) { isSameResourceWithDifferentPathCase = isEqual(fromFilePath, toFilePath, true /* ignore case */); } diff --git a/src/vs/platform/files/test/common/files.test.ts b/src/vs/platform/files/test/common/files.test.ts index 5956d293b4c..41cd8b4ccb6 100644 --- a/src/vs/platform/files/test/common/files.test.ts +++ b/src/vs/platform/files/test/common/files.test.ts @@ -8,8 +8,8 @@ import { URI } from 'vs/base/common/uri'; import { isEqual, isEqualOrParent } from 'vs/base/common/extpath'; import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files'; import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform'; -// eslint-disable-next-line code-import-patterns import { toResource } from 'vs/base/test/common/utils'; +import { extUri } from 'vs/base/common/resources'; suite('Files', () => { @@ -22,7 +22,7 @@ suite('Files', () => { { resource: toResource.call(this, '/bar/folder'), type: FileChangeType.DELETED } ]; - let r1 = new FileChangesEvent(changes); + let r1 = new FileChangesEvent(changes, extUri); assert(!r1.contains(toResource.call(this, '/foo'), FileChangeType.UPDATED)); assert(r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED)); diff --git a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 803ff185ab9..0ea2e2b1c8b 100644 --- a/src/vs/platform/files/test/electron-browser/diskFileService.test.ts +++ b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { tmpdir } from 'os'; import { FileService } from 'vs/platform/files/common/fileService'; import { Schemas } from 'vs/base/common/network'; -import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider'; +import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { generateUuid } from 'vs/base/common/uuid'; import { join, basename, dirname, posix } from 'vs/base/common/path'; @@ -463,6 +463,7 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep', 'conway.js')); const source = await service.resolve(resource); + assert.equal(await service.canDelete(source.resource, { useTrash }), true); await service.del(source.resource, { useTrash }); assert.equal(existsSync(source.resource.fsPath), false); @@ -492,6 +493,7 @@ suite('Disk File Service', function () { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); + assert.equal(await service.canDelete(source.resource), true); await service.del(source.resource); assert.equal(existsSync(source.resource.fsPath), false); @@ -511,6 +513,7 @@ suite('Disk File Service', function () { let event: FileOperationEvent; disposables.add(service.onDidRunOperation(e => event = e)); + assert.equal(await service.canDelete(link), true); await service.del(link); assert.equal(existsSync(link.fsPath), false); @@ -535,6 +538,7 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); + assert.equal(await service.canDelete(source.resource, { recursive: true, useTrash }), true); await service.del(source.resource, { recursive: true, useTrash }); assert.equal(existsSync(source.resource.fsPath), false); @@ -547,6 +551,8 @@ suite('Disk File Service', function () { const resource = URI.file(join(testDir, 'deep')); const source = await service.resolve(resource); + assert.ok((await service.canDelete(source.resource)) instanceof Error); + let error; try { await service.del(source.resource); @@ -566,6 +572,7 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'other.html')); + assert.equal(await service.canMove(source, target), true); const renamed = await service.move(source, target); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -646,6 +653,7 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'other.html')).with({ scheme: testSchema }); + assert.equal(await service.canMove(source, target), true); const renamed = await service.move(source, target); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -670,6 +678,7 @@ suite('Disk File Service', function () { const source = URI.file(join(testDir, 'index.html')); + assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), renameToPath))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), renameToPath))); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -686,6 +695,7 @@ suite('Disk File Service', function () { const source = URI.file(join(testDir, 'deep')); + assert.equal(await service.canMove(source, URI.file(join(dirname(source.fsPath), 'deeper'))), true); const renamed = await service.move(source, URI.file(join(dirname(source.fsPath), 'deeper'))); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -733,6 +743,7 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.fsPath), 'deeper')).with({ scheme: testSchema }); + assert.equal(await service.canMove(source, target), true); const renamed = await service.move(source, target); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -757,6 +768,7 @@ suite('Disk File Service', function () { assert.ok(source.size > 0); const renamedResource = URI.file(join(dirname(source.resource.fsPath), 'INDEX.html')); + assert.equal(await service.canMove(source.resource, renamedResource), true); let renamed = await service.move(source.resource, renamedResource); assert.equal(existsSync(renamedResource.fsPath), true); @@ -777,6 +789,7 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); + assert.equal(await service.canMove(source.resource, URI.file(source.resource.fsPath)), true); let renamed = await service.move(source.resource, URI.file(source.resource.fsPath)); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -800,6 +813,7 @@ suite('Disk File Service', function () { const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); + assert.equal(await service.canMove(source.resource, target), true); let renamed = await service.move(source.resource, target); assert.equal(existsSync(renamed.resource.fsPath), true); @@ -821,6 +835,8 @@ suite('Disk File Service', function () { const originalSize = source.size; assert.ok(originalSize > 0); + assert.ok((await service.canMove(URI.file(testDir), URI.file(join(testDir, 'binary.txt'))) instanceof Error)); + let error; try { await service.move(URI.file(testDir), URI.file(join(testDir, 'binary.txt'))); @@ -843,6 +859,8 @@ suite('Disk File Service', function () { const originalSize = source.size; assert.ok(originalSize > 0); + assert.ok((await service.canMove(source.resource, URI.file(join(testDir, 'binary.txt'))) instanceof Error)); + let error; try { await service.move(source.resource, URI.file(join(testDir, 'binary.txt'))); @@ -876,6 +894,7 @@ suite('Disk File Service', function () { const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); + assert.equal(await service.canMove(source, f.resource, true), true); const moved = await service.move(source, f.resource, true); assert.equal(existsSync(moved.resource.fsPath), true); @@ -930,6 +949,7 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, sourceName))); const target = URI.file(join(testDir, 'other.html')); + assert.equal(await service.canCopy(source.resource, target), true); const copied = await service.copy(source.resource, target); assert.equal(existsSync(copied.resource.fsPath), true); @@ -965,6 +985,7 @@ suite('Disk File Service', function () { const f = await service.createFolder(folderResource); const source = URI.file(join(testDir, 'deep', 'conway.js')); + assert.equal(await service.canCopy(source, f.resource, true), true); const copied = await service.copy(source, f.resource, true); assert.equal(existsSync(copied.resource.fsPath), true); @@ -984,6 +1005,8 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.resource.fsPath), 'INDEX.html')); + const canCopy = await service.canCopy(source.resource, target); + let error; let copied: IFileStatWithMetadata; try { @@ -994,12 +1017,14 @@ suite('Disk File Service', function () { if (isLinux) { assert.ok(!error); + assert.equal(canCopy, true); assert.equal(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); assert.equal(source.size, copied!.size); } else { assert.ok(error); + assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); assert.equal(originalSize, source.size); @@ -1013,6 +1038,8 @@ suite('Disk File Service', function () { const target = URI.file(join(dirname(source.resource.fsPath), 'INDEX.html')); + const canCopy = await service.canCopy(source.resource, target, true); + let error; let copied: IFileStatWithMetadata; try { @@ -1023,12 +1050,14 @@ suite('Disk File Service', function () { if (isLinux) { assert.ok(!error); + assert.equal(canCopy, true); assert.equal(existsSync(copied!.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'INDEX.html')); assert.equal(source.size, copied!.size); } else { assert.ok(error); + assert.ok(canCopy instanceof Error); source = await service.resolve(source.resource, { resolveMetadata: true }); assert.equal(originalSize, source.size); @@ -1036,21 +1065,22 @@ suite('Disk File Service', function () { }); test('copy - MIX CASE different taget - overwrite', async () => { - const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); - assert.ok(source.size > 0); + const source1 = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); + assert.ok(source1.size > 0); - const renamed = await service.move(source.resource, URI.file(join(dirname(source.resource.fsPath), 'CONWAY.js'))); + const renamed = await service.move(source1.resource, URI.file(join(dirname(source1.resource.fsPath), 'CONWAY.js'))); assert.equal(existsSync(renamed.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'CONWAY.js')); - assert.equal(source.size, renamed.size); + assert.equal(source1.size, renamed.size); - const source_1 = await service.resolve(URI.file(join(testDir, 'deep', 'conway.js')), { resolveMetadata: true }); - const target = URI.file(join(testDir, basename(source_1.resource.path))); + const source2 = await service.resolve(URI.file(join(testDir, 'deep', 'conway.js')), { resolveMetadata: true }); + const target = URI.file(join(testDir, basename(source2.resource.path))); - const res = await service.copy(source_1.resource, target, true); + assert.equal(await service.canCopy(source2.resource, target, true), true); + const res = await service.copy(source2.resource, target, true); assert.equal(existsSync(res.resource.fsPath), true); assert.ok(readdirSync(testDir).some(f => f === 'conway.js')); - assert.equal(source_1.size, res.size); + assert.equal(source2.size, res.size); }); test('copy - same file', async () => { @@ -1060,6 +1090,7 @@ suite('Disk File Service', function () { const source = await service.resolve(URI.file(join(testDir, 'index.html')), { resolveMetadata: true }); assert.ok(source.size > 0); + assert.equal(await service.canCopy(source.resource, URI.file(source.resource.fsPath)), true); let copied = await service.copy(source.resource, URI.file(source.resource.fsPath)); assert.equal(existsSync(copied.resource.fsPath), true); @@ -1083,6 +1114,7 @@ suite('Disk File Service', function () { const targetParent = URI.file(testDir); const target = targetParent.with({ path: posix.join(targetParent.path, posix.basename(source.resource.path)) }); + assert.equal(await service.canCopy(source.resource, URI.file(target.fsPath)), true); let copied = await service.copy(source.resource, URI.file(target.fsPath)); assert.equal(existsSync(copied.resource.fsPath), true); diff --git a/src/vs/platform/files/test/electron-browser/normalizer.test.ts b/src/vs/platform/files/test/electron-browser/normalizer.test.ts index aade9f0879e..0aa61f7ce03 100644 --- a/src/vs/platform/files/test/electron-browser/normalizer.test.ts +++ b/src/vs/platform/files/test/electron-browser/normalizer.test.ts @@ -9,9 +9,10 @@ import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files import { URI as uri } from 'vs/base/common/uri'; import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/node/watcher/watcher'; import { Event, Emitter } from 'vs/base/common/event'; +import { ExtUri } from 'vs/base/common/resources'; function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent { - return new FileChangesEvent(toFileChanges(changes)); + return new FileChangesEvent(toFileChanges(changes), new ExtUri(() => !platform.isLinux)); } class TestFileWatcher { diff --git a/src/vs/platform/ipc/electron-browser/mainProcessService.ts b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts similarity index 94% rename from src/vs/platform/ipc/electron-browser/mainProcessService.ts rename to src/vs/platform/ipc/electron-sandbox/mainProcessService.ts index c72b1c703d3..b8ffa053a08 100644 --- a/src/vs/platform/ipc/electron-browser/mainProcessService.ts +++ b/src/vs/platform/ipc/electron-sandbox/mainProcessService.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc'; -import { Client } from 'vs/base/parts/ipc/electron-browser/ipc.electron-browser'; +import { Client } from 'vs/base/parts/ipc/electron-sandbox/ipc.electron-sandbox'; import { Disposable } from 'vs/base/common/lifecycle'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; export const IMainProcessService = createDecorator('mainProcessService'); diff --git a/src/vs/platform/issue/node/issue.ts b/src/vs/platform/issue/common/issue.ts similarity index 93% rename from src/vs/platform/issue/node/issue.ts rename to src/vs/platform/issue/common/issue.ts index 08a9c5d456f..17d3fe40830 100644 --- a/src/vs/platform/issue/node/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -3,10 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; - -export const IIssueService = createDecorator('issueService'); - // Since data sent through the service is serialized to JSON, functions will be lost, so Color objects // should not be sent as their 'toString' method will be stripped. Instead convert to strings before sending. export interface WindowStyles { @@ -91,7 +87,7 @@ export interface ProcessExplorerData extends WindowData { styles: ProcessExplorerStyles; } -export interface IIssueService { +export interface ICommonIssueService { _serviceBrand: undefined; openReporter(data: IssueReporterData): Promise; openProcessExplorer(data: ProcessExplorerData): Promise; diff --git a/src/vs/platform/issue/electron-main/issueMainService.ts b/src/vs/platform/issue/electron-main/issueMainService.ts index 193ab70b12f..fcd734b4305 100644 --- a/src/vs/platform/issue/electron-main/issueMainService.ts +++ b/src/vs/platform/issue/electron-main/issueMainService.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import * as objects from 'vs/base/common/objects'; import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; -import { IIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/node/issue'; +import { ICommonIssueService, IssueReporterData, IssueReporterFeatures, ProcessExplorerData } from 'vs/platform/issue/common/issue'; import { BrowserWindow, ipcMain, screen, IpcMainEvent, Display, shell } from 'electron'; import { ILaunchMainService } from 'vs/platform/launch/electron-main/launchMainService'; import { PerformanceInfo, isRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics'; @@ -18,10 +18,16 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IWindowState } from 'vs/platform/windows/electron-main/windows'; import { listProcesses } from 'vs/base/node/ps'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; +import { URI } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; const DEFAULT_BACKGROUND_COLOR = '#1E1E1E'; -export class IssueMainService implements IIssueService { +export const IIssueMainService = createDecorator('issueMainService'); + +export interface IIssueMainService extends ICommonIssueService { } + +export class IssueMainService implements ICommonIssueService { _serviceBrand: undefined; _issueWindow: BrowserWindow | null = null; _issueParentWindow: BrowserWindow | null = null; @@ -163,12 +169,11 @@ export class IssueMainService implements IIssueService { } }); - ipcMain.on('windowsInfoRequest', (event: IpcMainEvent) => { + ipcMain.on('vscode:windowsInfoRequest', (event: IpcMainEvent) => { this.launchMainService.getMainProcessInfo().then(info => { event.sender.send('vscode:windowsInfoResponse', info.windows); }); }); - } openReporter(data: IssueReporterData): Promise { @@ -189,7 +194,9 @@ export class IssueMainService implements IIssueService { title: localize('issueReporter', "Issue Reporter"), backgroundColor: data.styles.backgroundColor || DEFAULT_BACKGROUND_COLOR, webPreferences: { - nodeIntegration: true + preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, + nodeIntegration: true, + enableWebSQL: false } }); @@ -224,7 +231,7 @@ export class IssueMainService implements IIssueService { if (!this._processExplorerWindow) { this._processExplorerParentWindow = BrowserWindow.getFocusedWindow(); if (this._processExplorerParentWindow) { - const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 300); + const position = this.getWindowPosition(this._processExplorerParentWindow, 800, 500); this._processExplorerWindow = new BrowserWindow({ skipTaskbar: true, resizable: true, @@ -238,7 +245,9 @@ export class IssueMainService implements IIssueService { backgroundColor: data.styles.backgroundColor, title: localize('processExplorer', "Process Explorer"), webPreferences: { - nodeIntegration: true + preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath, + nodeIntegration: true, + enableWebSQL: false } }); diff --git a/src/vs/platform/issue/electron-sandbox/issue.ts b/src/vs/platform/issue/electron-sandbox/issue.ts new file mode 100644 index 00000000000..4e84d42806e --- /dev/null +++ b/src/vs/platform/issue/electron-sandbox/issue.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 { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICommonIssueService } from 'vs/platform/issue/common/issue'; + +export const IIssueService = createDecorator('issueService'); + +export interface IIssueService extends ICommonIssueService { } diff --git a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts index 16e096e564b..a321d9b38bb 100644 --- a/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts +++ b/src/vs/platform/keybinding/test/common/keybindingLabels.test.ts @@ -167,4 +167,7 @@ suite('KeybindingLabels', () => { assertElectronAcceleratorLabel(OperatingSystem.Macintosh, KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_A, KeyMod.CtrlCmd | KeyCode.KEY_B), 'cmd+a cmd+b'); }); + test('issue #91235: Do not end with a +', () => { + assertUSLabel(OperatingSystem.Windows, KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Alt, 'Ctrl+Alt'); + }); }); diff --git a/src/vs/platform/label/common/label.ts b/src/vs/platform/label/common/label.ts index 080f1ff0994..e23c8f001da 100644 --- a/src/vs/platform/label/common/label.ts +++ b/src/vs/platform/label/common/label.ts @@ -12,8 +12,12 @@ import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderW import { localize } from 'vs/nls'; import { isEqualOrParent, basename } from 'vs/base/common/resources'; +export const ILabelService = createDecorator('labelService'); + export interface ILabelService { + _serviceBrand: undefined; + /** * Gets the human readable label for a uri. * If relative is passed returns a label relative to the workspace root that the uri belongs to. @@ -24,6 +28,7 @@ export interface ILabelService { getWorkspaceLabel(workspace: (IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | IWorkspace), options?: { verbose: boolean }): string; getHostLabel(scheme: string, authority?: string): string; getSeparator(scheme: string, authority?: string): '/' | '\\'; + registerFormatter(formatter: ResourceLabelFormatter): IDisposable; onDidChangeFormatters: Event; } @@ -48,12 +53,11 @@ export interface ResourceLabelFormatting { authorityPrefix?: string; } -const LABEL_SERVICE_ID = 'label'; - export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, workspaceHome: URI): string { if (isSingleFolderWorkspaceIdentifier(workspace)) { return basename(workspace); } + // Workspace: Untitled if (isEqualOrParent(workspace.configPath, workspaceHome)) { return localize('untitledWorkspace', "Untitled (Workspace)"); @@ -63,8 +67,6 @@ export function getSimpleWorkspaceLabel(workspace: IWorkspaceIdentifier | URI, w if (filename.endsWith(WORKSPACE_EXTENSION)) { filename = filename.substr(0, filename.length - WORKSPACE_EXTENSION.length - 1); } + return localize('workspaceName', "{0} (Workspace)", filename); } - - -export const ILabelService = createDecorator(LABEL_SERVICE_ID); diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index f81f8534749..235c3a7e78c 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -124,6 +124,7 @@ export const keyboardNavigationSettingKey = 'workbench.list.keyboardNavigation'; export const automaticKeyboardNavigationSettingKey = 'workbench.list.automaticKeyboardNavigation'; const treeIndentKey = 'workbench.tree.indent'; const treeRenderIndentGuidesKey = 'workbench.tree.renderIndentGuides'; +const listSmoothScrolling = 'workbench.list.smoothScrolling'; function getHorizontalScrollingSetting(configurationService: IConfigurationService): boolean { return getMigratedSettingValue(configurationService, horizontalScrollingKey, 'workbench.tree.horizontalScrolling'); @@ -823,6 +824,7 @@ function workbenchTreeDataPreamble(treeIndentKey), renderIndentGuides: configurationService.getValue(treeRenderIndentGuidesKey), + smoothScrolling: configurationService.getValue(listSmoothScrolling), automaticKeyboardNavigation: getAutomaticKeyboardNavigation(), simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter', @@ -898,25 +900,33 @@ class WorkbenchTreeInternals { this.hasSelectionOrFocus.set(selection.length > 0 || focus.length > 0); }), configurationService.onDidChangeConfiguration(e => { + let options: any = {}; if (e.affectsConfiguration(openModeSettingKey)) { - tree.updateOptions({ openOnSingleClick: useSingleClickToOpen(configurationService) }); + options = { ...options, openOnSingleClick: useSingleClickToOpen(configurationService) }; } if (e.affectsConfiguration(multiSelectModifierSettingKey)) { this._useAltAsMultipleSelectionModifier = useAltAsMultipleSelectionModifier(configurationService); } if (e.affectsConfiguration(treeIndentKey)) { const indent = configurationService.getValue(treeIndentKey); - tree.updateOptions({ indent }); + options = { ...options, indent }; } if (e.affectsConfiguration(treeRenderIndentGuidesKey)) { const renderIndentGuides = configurationService.getValue(treeRenderIndentGuidesKey); - tree.updateOptions({ renderIndentGuides }); + options = { ...options, renderIndentGuides }; + } + if (e.affectsConfiguration(listSmoothScrolling)) { + const smoothScrolling = configurationService.getValue(listSmoothScrolling); + options = { ...options, smoothScrolling }; } if (e.affectsConfiguration(keyboardNavigationSettingKey)) { updateKeyboardNavigation(); } if (e.affectsConfiguration(automaticKeyboardNavigationSettingKey)) { - tree.updateOptions({ automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }); + options = { ...options, automaticKeyboardNavigation: getAutomaticKeyboardNavigation() }; + } + if (Object.keys(options).length > 0) { + tree.updateOptions(options); } }), this.contextKeyService.onDidChangeContext(e => { @@ -1001,6 +1011,11 @@ configurationRegistry.registerConfiguration({ default: 'onHover', description: localize('render tree indent guides', "Controls whether the tree should render indent guides.") }, + [listSmoothScrolling]: { + type: 'boolean', + default: false, + description: localize('list smoothScrolling setting', "Controls whether lists and trees have smooth scrolling."), + }, [keyboardNavigationSettingKey]: { 'type': 'string', 'enum': ['simple', 'highlight', 'filter'], diff --git a/src/vs/platform/menubar/node/menubar.ts b/src/vs/platform/menubar/common/menubar.ts similarity index 90% rename from src/vs/platform/menubar/node/menubar.ts rename to src/vs/platform/menubar/common/menubar.ts index db246d1b487..69b74dfbe33 100644 --- a/src/vs/platform/menubar/node/menubar.ts +++ b/src/vs/platform/menubar/common/menubar.ts @@ -3,14 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; -export const IMenubarService = createDecorator('menubarService'); - -export interface IMenubarService { - _serviceBrand: undefined; - +export interface ICommonMenubarService { updateMenubar(windowId: number, menuData: IMenubarData): Promise; } diff --git a/src/vs/platform/menubar/electron-main/menubar.ts b/src/vs/platform/menubar/electron-main/menubar.ts index ac66913edbf..46e88cd5537 100644 --- a/src/vs/platform/menubar/electron-main/menubar.ts +++ b/src/vs/platform/menubar/electron-main/menubar.ts @@ -18,7 +18,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { mnemonicMenuLabel as baseMnemonicLabel } from 'vs/base/common/labels'; import { IWindowsMainService, IWindowsCountChangedEvent } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; -import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/node/menubar'; +import { IMenubarData, IMenubarKeybinding, MenubarMenuItem, isMenubarMenuItemSeparator, isMenubarMenuItemSubmenu, isMenubarMenuItemAction, IMenubarMenu, isMenubarMenuItemUriAction } from 'vs/platform/menubar/common/menubar'; import { URI } from 'vs/base/common/uri'; import { IStateService } from 'vs/platform/state/node/state'; import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; diff --git a/src/vs/platform/menubar/electron-main/menubarMainService.ts b/src/vs/platform/menubar/electron-main/menubarMainService.ts index 7cadd57ecdb..243bc205d7b 100644 --- a/src/vs/platform/menubar/electron-main/menubarMainService.ts +++ b/src/vs/platform/menubar/electron-main/menubarMainService.ts @@ -3,13 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IMenubarService, IMenubarData } from 'vs/platform/menubar/node/menubar'; +import { ICommonMenubarService, IMenubarData } from 'vs/platform/menubar/common/menubar'; import { Menubar } from 'vs/platform/menubar/electron-main/menubar'; import { ILogService } from 'vs/platform/log/common/log'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { ILifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; -export class MenubarMainService implements IMenubarService { +export const IMenubarMainService = createDecorator('menubarMainService'); + +export interface IMenubarMainService extends ICommonMenubarService { + _serviceBrand: undefined; +} + +export class MenubarMainService implements IMenubarMainService { _serviceBrand: undefined; diff --git a/src/vs/platform/menubar/electron-sandbox/menubar.ts b/src/vs/platform/menubar/electron-sandbox/menubar.ts new file mode 100644 index 00000000000..51ef554c932 --- /dev/null +++ b/src/vs/platform/menubar/electron-sandbox/menubar.ts @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { ICommonMenubarService } from 'vs/platform/menubar/common/menubar'; + +export const IMenubarService = createDecorator('menubarService'); + +export interface IMenubarService extends ICommonMenubarService { + _serviceBrand: undefined; +} diff --git a/src/vs/platform/product/common/product.ts b/src/vs/platform/product/common/product.ts index 87a3b99c703..c413616e472 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.45.0-dev', + version: '1.46.0-dev', nameLong: 'Visual Studio Code Web Dev', nameShort: 'VSCode Web Dev', urlProtocol: 'code-oss' diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index b817188c26c..3b682f00add 100644 --- a/src/vs/platform/quickinput/common/quickAccess.ts +++ b/src/vs/platform/quickinput/common/quickAccess.ts @@ -24,7 +24,7 @@ export interface IQuickAccessOptions { itemActivation?: ItemActivation; /** - * Wether to take the input value as is and not restore it + * Whether to take the input value as is and not restore it * from any existing value if quick access is visible. */ preserveValue?: boolean; diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index 77f4795576f..6ece94f8379 100644 --- a/src/vs/platform/theme/common/themeService.ts +++ b/src/vs/platform/theme/common/themeService.ts @@ -87,8 +87,11 @@ export interface ITokenStyle { } export interface IColorTheme { + readonly type: ThemeType; + readonly label: string; + /** * Resolves the color of the given color identifier. If the theme does not * specify the color, the default color is returned unless useDefault is set to false. diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index 82dbe518768..8f7ab1d68bf 100644 --- a/src/vs/platform/theme/test/common/testThemeService.ts +++ b/src/vs/platform/theme/test/common/testThemeService.ts @@ -9,6 +9,8 @@ import { Color } from 'vs/base/common/color'; export class TestColorTheme implements IColorTheme { + public readonly label = 'test'; + constructor(private colors: { [id: string]: string; } = {}, public type = DARK) { } diff --git a/src/vs/platform/undoRedo/common/undoRedo.ts b/src/vs/platform/undoRedo/common/undoRedo.ts index 935e3ffcb22..92f147d8efd 100644 --- a/src/vs/platform/undoRedo/common/undoRedo.ts +++ b/src/vs/platform/undoRedo/common/undoRedo.ts @@ -5,6 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { URI } from 'vs/base/common/uri'; +import { IDisposable } from 'vs/base/common/lifecycle'; export const IUndoRedoService = createDecorator('undoRedoService'); @@ -28,6 +29,13 @@ export interface IWorkspaceUndoRedoElement { undo(): Promise | void; redo(): Promise | void; split(): IResourceUndoRedoElement[]; + + /** + * If implemented, will be invoked before calling `undo()` or `redo()`. + * This is a good place to prepare everything such that the calls to `undo()` or `redo()` are synchronous. + * If a disposable is returned, it will be invoked to clean things up. + */ + prepareUndoRedo?(): Promise | IDisposable | void; } export type IUndoRedoElement = IResourceUndoRedoElement | IWorkspaceUndoRedoElement; @@ -37,9 +45,20 @@ export interface IPastFutureElements { future: IUndoRedoElement[]; } +export interface UriComparisonKeyComputer { + /** + * Return `null` if you don't own this URI. + */ + getComparisonKey(uri: URI): string | null; +} + export interface IUndoRedoService { _serviceBrand: undefined; + registerUriComparisonKeyComputer(uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable; + + getUriComparisonKey(resource: URI): string; + /** * Add a new element to the `undo` stack. * This will destroy the `redo` stack. @@ -55,7 +74,7 @@ export interface IUndoRedoService { hasElements(resource: URI): boolean; - setElementsIsValid(resource: URI, isValid: boolean): void; + setElementsValidFlag(resource: URI, isValid: boolean, filter: (element: IUndoRedoElement) => boolean): void; /** * Remove elements that target `resource`. diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index 9858c9eb6af..7fe30a153cb 100644 --- a/src/vs/platform/undoRedo/common/undoRedoService.ts +++ b/src/vs/platform/undoRedo/common/undoRedoService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { IUndoRedoService, IResourceUndoRedoElement, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements } from 'vs/platform/undoRedo/common/undoRedo'; +import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, UriComparisonKeyComputer } from 'vs/platform/undoRedo/common/undoRedo'; import { URI } from 'vs/base/common/uri'; import { onUnexpectedError } from 'vs/base/common/errors'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; @@ -12,28 +12,29 @@ import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import Severity from 'vs/base/common/severity'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IDisposable, Disposable, isDisposable } from 'vs/base/common/lifecycle'; -function uriGetComparisonKey(resource: URI): string { - return resource.toString(); +function getResourceLabel(resource: URI): string { + return resource.scheme === Schemas.file ? resource.fsPath : resource.path; } class ResourceStackElement { public readonly type = UndoRedoElementType.Resource; - public readonly actual: IResourceUndoRedoElement; + public readonly actual: IUndoRedoElement; public readonly label: string; - public readonly resource: URI; + public readonly resourceLabel: string; public readonly strResource: string; - public readonly resources: URI[]; + public readonly resourceLabels: string[]; public readonly strResources: string[]; public isValid: boolean; - constructor(actual: IResourceUndoRedoElement) { + constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string) { this.actual = actual; this.label = actual.label; - this.resource = actual.resource; - this.strResource = uriGetComparisonKey(this.resource); - this.resources = [this.resource]; + this.resourceLabel = resourceLabel; + this.strResource = strResource; + this.resourceLabels = [this.resourceLabel]; this.strResources = [this.strResource]; this.isValid = true; } @@ -50,7 +51,7 @@ const enum RemovedResourceReason { class ResourceReasonPair { constructor( - public readonly resource: URI, + public readonly resourceLabel: string, public readonly reason: RemovedResourceReason ) { } } @@ -58,10 +59,6 @@ class ResourceReasonPair { class RemovedResources { private readonly elements = new Map(); - private _getPath(resource: URI): string { - return resource.scheme === Schemas.file ? resource.fsPath : resource.path; - } - public createMessage(): string { const externalRemoval: string[] = []; const noParallelUniverses: string[] = []; @@ -71,12 +68,12 @@ class RemovedResources { ? externalRemoval : noParallelUniverses ); - dest.push(this._getPath(element.resource)); + dest.push(element.resourceLabel); } let messages: string[] = []; if (externalRemoval.length > 0) { - messages.push(nls.localize('externalRemoval', "The following files have been closed: {0}.", externalRemoval.join(', '))); + messages.push(nls.localize('externalRemoval', "The following files have been closed and modified on disk: {0}.", externalRemoval.join(', '))); } if (noParallelUniverses.length > 0) { messages.push(nls.localize('noParallelUniverses', "The following files have been modified in an incompatible way: {0}.", noParallelUniverses.join(', '))); @@ -106,30 +103,30 @@ class WorkspaceStackElement { public readonly actual: IWorkspaceUndoRedoElement; public readonly label: string; - public readonly resources: URI[]; + public readonly resourceLabels: string[]; public readonly strResources: string[]; public removedResources: RemovedResources | null; public invalidatedResources: RemovedResources | null; - constructor(actual: IWorkspaceUndoRedoElement) { + constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[]) { this.actual = actual; this.label = actual.label; - this.resources = actual.resources.slice(0); - this.strResources = this.resources.map(resource => uriGetComparisonKey(resource)); + this.resourceLabels = resourceLabels; + this.strResources = strResources; this.removedResources = null; this.invalidatedResources = null; } - public removeResource(resource: URI, strResource: string, reason: RemovedResourceReason): void { + public removeResource(resourceLabel: string, strResource: string, reason: RemovedResourceReason): void { if (!this.removedResources) { this.removedResources = new RemovedResources(); } if (!this.removedResources.has(strResource)) { - this.removedResources.set(strResource, new ResourceReasonPair(resource, reason)); + this.removedResources.set(strResource, new ResourceReasonPair(resourceLabel, reason)); } } - public setValid(resource: URI, strResource: string, isValid: boolean): void { + public setValid(resourceLabel: string, strResource: string, isValid: boolean): void { if (isValid) { if (this.invalidatedResources) { this.invalidatedResources.delete(strResource); @@ -142,7 +139,7 @@ class WorkspaceStackElement { this.invalidatedResources = new RemovedResources(); } if (!this.invalidatedResources.has(strResource)) { - this.invalidatedResources.set(strResource, new ResourceReasonPair(resource, RemovedResourceReason.ExternalRemoval)); + this.invalidatedResources.set(strResource, new ResourceReasonPair(resourceLabel, RemovedResourceReason.ExternalRemoval)); } } } @@ -151,14 +148,200 @@ class WorkspaceStackElement { type StackElement = ResourceStackElement | WorkspaceStackElement; class ResourceEditStack { - public resource: URI; - public past: StackElement[]; - public future: StackElement[]; + public readonly resourceLabel: string; + private readonly strResource: string; + private _past: StackElement[]; + private _future: StackElement[]; + public locked: boolean; + public versionId: number; - constructor(resource: URI) { - this.resource = resource; - this.past = []; - this.future = []; + constructor(resourceLabel: string, strResource: string) { + this.resourceLabel = resourceLabel; + this.strResource = strResource; + this._past = []; + this._future = []; + this.locked = false; + this.versionId = 1; + } + + public dispose(): void { + for (const element of this._past) { + if (element.type === UndoRedoElementType.Workspace) { + element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval); + } + } + for (const element of this._future) { + if (element.type === UndoRedoElementType.Workspace) { + element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval); + } + } + this.versionId++; + } + + public flushAllElements(): void { + this._past = []; + this._future = []; + this.versionId++; + } + + public setElementsIsValid(isValid: boolean): void { + for (const element of this._past) { + if (element.type === UndoRedoElementType.Workspace) { + element.setValid(this.resourceLabel, this.strResource, isValid); + } else { + element.setValid(isValid); + } + } + for (const element of this._future) { + if (element.type === UndoRedoElementType.Workspace) { + element.setValid(this.resourceLabel, this.strResource, isValid); + } else { + element.setValid(isValid); + } + } + } + + private _setElementValidFlag(element: StackElement, isValid: boolean): void { + if (element.type === UndoRedoElementType.Workspace) { + element.setValid(this.resourceLabel, this.strResource, isValid); + } else { + element.setValid(isValid); + } + } + + public setElementsValidFlag(isValid: boolean, filter: (element: IUndoRedoElement) => boolean): void { + for (const element of this._past) { + if (filter(element.actual)) { + this._setElementValidFlag(element, isValid); + } + } + for (const element of this._future) { + if (filter(element.actual)) { + this._setElementValidFlag(element, isValid); + } + } + } + + public pushElement(element: StackElement): void { + // remove the future + for (const futureElement of this._future) { + if (futureElement.type === UndoRedoElementType.Workspace) { + futureElement.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.NoParallelUniverses); + } + } + this._future = []; + if (this._past.length > 0) { + const lastElement = this._past[this._past.length - 1]; + if (lastElement.type === UndoRedoElementType.Resource && !lastElement.isValid) { + // clear undo stack + this._past = []; + } + } + this._past.push(element); + this.versionId++; + } + + public getElements(): IPastFutureElements { + const past: IUndoRedoElement[] = []; + const future: IUndoRedoElement[] = []; + + for (const element of this._past) { + past.push(element.actual); + } + for (const element of this._future) { + future.push(element.actual); + } + + return { past, future }; + } + + public getClosestPastElement(): StackElement | null { + if (this._past.length === 0) { + return null; + } + return this._past[this._past.length - 1]; + } + + public getClosestFutureElement(): StackElement | null { + if (this._future.length === 0) { + return null; + } + return this._future[this._future.length - 1]; + } + + public hasPastElements(): boolean { + return (this._past.length > 0); + } + + public hasFutureElements(): boolean { + return (this._future.length > 0); + } + + public splitPastWorkspaceElement(toRemove: WorkspaceStackElement, individualMap: Map): void { + for (let j = this._past.length - 1; j >= 0; j--) { + if (this._past[j] === toRemove) { + if (individualMap.has(this.strResource)) { + // gets replaced + this._past[j] = individualMap.get(this.strResource)!; + } else { + // gets deleted + this._past.splice(j, 1); + } + break; + } + } + this.versionId++; + } + + public splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, individualMap: Map): void { + for (let j = this._future.length - 1; j >= 0; j--) { + if (this._future[j] === toRemove) { + if (individualMap.has(this.strResource)) { + // gets replaced + this._future[j] = individualMap.get(this.strResource)!; + } else { + // gets deleted + this._future.splice(j, 1); + } + break; + } + } + this.versionId++; + } + + public moveBackward(element: StackElement): void { + this._past.pop(); + this._future.push(element); + this.versionId++; + } + + public moveForward(element: StackElement): void { + this._future.pop(); + this._past.push(element); + this.versionId++; + } +} + +class EditStackSnapshot { + + public readonly editStacks: ResourceEditStack[]; + private readonly _versionIds: number[]; + + constructor(editStacks: ResourceEditStack[]) { + this.editStacks = editStacks; + this._versionIds = []; + for (let i = 0, len = this.editStacks.length; i < len; i++) { + this._versionIds[i] = this.editStacks[i].versionId; + } + } + + public isValid(): boolean { + for (let i = 0, len = this.editStacks.length; i < len; i++) { + if (this._versionIds[i] !== this.editStacks[i].versionId) { + return false; + } + } + return true; } } @@ -166,57 +349,95 @@ export class UndoRedoService implements IUndoRedoService { _serviceBrand: undefined; private readonly _editStacks: Map; + private readonly _uriComparisonKeyComputers: UriComparisonKeyComputer[]; constructor( @IDialogService private readonly _dialogService: IDialogService, @INotificationService private readonly _notificationService: INotificationService, ) { this._editStacks = new Map(); + this._uriComparisonKeyComputers = []; } - public pushElement(_element: IUndoRedoElement): void { - const element: StackElement = (_element.type === UndoRedoElementType.Resource ? new ResourceStackElement(_element) : new WorkspaceStackElement(_element)); - for (let i = 0, len = element.resources.length; i < len; i++) { - const resource = element.resources[i]; + public registerUriComparisonKeyComputer(uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable { + this._uriComparisonKeyComputers.push(uriComparisonKeyComputer); + return { + dispose: () => { + for (let i = 0, len = this._uriComparisonKeyComputers.length; i < len; i++) { + if (this._uriComparisonKeyComputers[i] === uriComparisonKeyComputer) { + this._uriComparisonKeyComputers.splice(i, 1); + return; + } + } + } + }; + } + + public getUriComparisonKey(resource: URI): string { + for (const uriComparisonKeyComputer of this._uriComparisonKeyComputers) { + const result = uriComparisonKeyComputer.getComparisonKey(resource); + if (result !== null) { + return result; + } + } + return resource.toString(); + } + + public pushElement(element: IUndoRedoElement): void { + if (element.type === UndoRedoElementType.Resource) { + const resourceLabel = getResourceLabel(element.resource); + const strResource = this.getUriComparisonKey(element.resource); + this._pushElement(new ResourceStackElement(element, resourceLabel, strResource)); + } else { + const seen = new Set(); + const resourceLabels: string[] = []; + const strResources: string[] = []; + for (const resource of element.resources) { + const resourceLabel = getResourceLabel(resource); + const strResource = this.getUriComparisonKey(resource); + + if (seen.has(strResource)) { + continue; + } + seen.add(strResource); + resourceLabels.push(resourceLabel); + strResources.push(strResource); + } + + if (resourceLabels.length === 1) { + this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0])); + } else { + this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources)); + } + } + } + + private _pushElement(element: StackElement): void { + for (let i = 0, len = element.strResources.length; i < len; i++) { + const resourceLabel = element.resourceLabels[i]; const strResource = element.strResources[i]; let editStack: ResourceEditStack; if (this._editStacks.has(strResource)) { editStack = this._editStacks.get(strResource)!; } else { - editStack = new ResourceEditStack(resource); + editStack = new ResourceEditStack(resourceLabel, strResource); this._editStacks.set(strResource, editStack); } - // remove the future - for (const futureElement of editStack.future) { - if (futureElement.type === UndoRedoElementType.Workspace) { - futureElement.removeResource(resource, strResource, RemovedResourceReason.NoParallelUniverses); - } - } - editStack.future = []; - if (editStack.past.length > 0) { - const lastElement = editStack.past[editStack.past.length - 1]; - if (lastElement.type === UndoRedoElementType.Resource && !lastElement.isValid) { - // clear undo stack - editStack.past = []; - } - } - editStack.past.push(element); + editStack.pushElement(element); } } public getLastElement(resource: URI): IUndoRedoElement | null { - const strResource = uriGetComparisonKey(resource); + const strResource = this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - if (editStack.future.length > 0) { + if (editStack.hasFutureElements()) { return null; } - if (editStack.past.length === 0) { - return null; - } - return editStack.past[editStack.past.length - 1].actual; + const closestPastElement = editStack.getClosestPastElement(); + return closestPastElement ? closestPastElement.actual : null; } return null; } @@ -225,7 +446,9 @@ export class UndoRedoService implements IUndoRedoService { const individualArr = toRemove.actual.split(); const individualMap = new Map(); for (const _element of individualArr) { - const element = new ResourceStackElement(_element); + const resourceLabel = getResourceLabel(_element.resource); + const strResource = this.getUriComparisonKey(_element.resource); + const element = new ResourceStackElement(_element, resourceLabel, strResource); individualMap.set(element.strResource, element); } @@ -234,18 +457,7 @@ export class UndoRedoService implements IUndoRedoService { continue; } const editStack = this._editStacks.get(strResource)!; - for (let j = editStack.past.length - 1; j >= 0; j--) { - if (editStack.past[j] === toRemove) { - if (individualMap.has(strResource)) { - // gets replaced - editStack.past[j] = individualMap.get(strResource)!; - } else { - // gets deleted - editStack.past.splice(j, 1); - } - break; - } - } + editStack.splitPastWorkspaceElement(toRemove, individualMap); } } @@ -253,7 +465,9 @@ export class UndoRedoService implements IUndoRedoService { const individualArr = toRemove.actual.split(); const individualMap = new Map(); for (const _element of individualArr) { - const element = new ResourceStackElement(_element); + const resourceLabel = getResourceLabel(_element.resource); + const strResource = this.getUriComparisonKey(_element.resource); + const element = new ResourceStackElement(_element, resourceLabel, strResource); individualMap.set(element.strResource, element); } @@ -262,94 +476,50 @@ export class UndoRedoService implements IUndoRedoService { continue; } const editStack = this._editStacks.get(strResource)!; - for (let j = editStack.future.length - 1; j >= 0; j--) { - if (editStack.future[j] === toRemove) { - if (individualMap.has(strResource)) { - // gets replaced - editStack.future[j] = individualMap.get(strResource)!; - } else { - // gets deleted - editStack.future.splice(j, 1); - } - break; - } - } + editStack.splitFutureWorkspaceElement(toRemove, individualMap); } } - public removeElements(resource: URI): void { - const strResource = uriGetComparisonKey(resource); + public removeElements(resource: URI | string): void { + const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - for (const element of editStack.past) { - if (element.type === UndoRedoElementType.Workspace) { - element.removeResource(resource, strResource, RemovedResourceReason.ExternalRemoval); - } - } - for (const element of editStack.future) { - if (element.type === UndoRedoElementType.Workspace) { - element.removeResource(resource, strResource, RemovedResourceReason.ExternalRemoval); - } - } + editStack.dispose(); this._editStacks.delete(strResource); } } - public setElementsIsValid(resource: URI, isValid: boolean): void { - const strResource = uriGetComparisonKey(resource); + public setElementsValidFlag(resource: URI, isValid: boolean, filter: (element: IUndoRedoElement) => boolean): void { + const strResource = this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - for (const element of editStack.past) { - if (element.type === UndoRedoElementType.Workspace) { - element.setValid(resource, strResource, isValid); - } else { - element.setValid(isValid); - } - } - for (const element of editStack.future) { - if (element.type === UndoRedoElementType.Workspace) { - element.setValid(resource, strResource, isValid); - } else { - element.setValid(isValid); - } - } + editStack.setElementsValidFlag(isValid, filter); } } - // resource - public hasElements(resource: URI): boolean { - const strResource = uriGetComparisonKey(resource); + const strResource = this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - return (editStack.past.length > 0 || editStack.future.length > 0); + return (editStack.hasPastElements() || editStack.hasFutureElements()); } return false; } public getElements(resource: URI): IPastFutureElements { - const past: IUndoRedoElement[] = []; - const future: IUndoRedoElement[] = []; - - const strResource = uriGetComparisonKey(resource); + const strResource = this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - for (const element of editStack.past) { - past.push(element.actual); - } - for (const element of editStack.future) { - future.push(element.actual); - } + return editStack.getElements(); } - - return { past, future }; + return { past: [], future: [] }; } public canUndo(resource: URI): boolean { - const strResource = uriGetComparisonKey(resource); + const strResource = this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - return (editStack.past.length > 0); + return editStack.hasPastElements(); } return false; } @@ -357,200 +527,392 @@ export class UndoRedoService implements IUndoRedoService { private _onError(err: Error, element: StackElement): void { onUnexpectedError(err); // An error occured while undoing or redoing => drop the undo/redo stack for all affected resources - for (const resource of element.resources) { - this.removeElements(resource); + for (const strResource of element.strResources) { + this.removeElements(strResource); } this._notificationService.error(err); } - private _safeInvoke(element: StackElement, invoke: () => Promise | void): Promise | void { + private _acquireLocks(editStackSnapshot: EditStackSnapshot): () => void { + // first, check if all locks can be acquired + for (const editStack of editStackSnapshot.editStacks) { + if (editStack.locked) { + throw new Error('Cannot acquire edit stack lock'); + } + } + + // can acquire all locks + for (const editStack of editStackSnapshot.editStacks) { + editStack.locked = true; + } + + return () => { + // release all locks + for (const editStack of editStackSnapshot.editStacks) { + editStack.locked = false; + } + }; + } + + private _safeInvokeWithLocks(element: StackElement, invoke: () => Promise | void, editStackSnapshot: EditStackSnapshot, cleanup: IDisposable = Disposable.None): Promise | void { + const releaseLocks = this._acquireLocks(editStackSnapshot); + let result: Promise | void; try { result = invoke(); } catch (err) { + releaseLocks(); + cleanup.dispose(); return this._onError(err, element); } if (result) { - return result.then(undefined, (err) => this._onError(err, element)); + // result is Promise + return result.then( + () => { + releaseLocks(); + cleanup.dispose(); + }, + (err) => { + releaseLocks(); + cleanup.dispose(); + return this._onError(err, element); + } + ); + } else { + // result is void + releaseLocks(); + cleanup.dispose(); } } - private _workspaceUndo(resource: URI, element: WorkspaceStackElement): Promise | void { + private async _invokeWorkspacePrepare(element: WorkspaceStackElement): Promise { + if (typeof element.actual.prepareUndoRedo === 'undefined') { + return Disposable.None; + } + const result = element.actual.prepareUndoRedo(); + if (typeof result === 'undefined') { + return Disposable.None; + } + return result; + } + + private _invokeResourcePrepare(element: ResourceStackElement, callback: (disposable: IDisposable) => void): void | Promise { + if (element.actual.type !== UndoRedoElementType.Workspace || typeof element.actual.prepareUndoRedo === 'undefined') { + // no preparation needed + callback(Disposable.None); + return; + } + + const r = element.actual.prepareUndoRedo(); + if (!r) { + // nothing to clean up + callback(Disposable.None); + return; + } + + if (isDisposable(r)) { + callback(r); + return; + } + + return r.then((disposable) => { + callback(disposable); + }); + } + + private _getAffectedEditStacks(element: WorkspaceStackElement): EditStackSnapshot { + const affectedEditStacks: ResourceEditStack[] = []; + for (const strResource of element.strResources) { + affectedEditStacks.push(this._editStacks.get(strResource)!); + } + return new EditStackSnapshot(affectedEditStacks); + } + + private _checkWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null { if (element.removedResources) { this._splitPastWorkspaceElement(element, element.removedResources); const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); this._notificationService.info(message); - return this.undo(resource); + return new WorkspaceVerificationError(this.undo(strResource)); } - if (element.invalidatedResources) { + if (checkInvalidatedResources && element.invalidatedResources) { this._splitPastWorkspaceElement(element, element.invalidatedResources); const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()); this._notificationService.info(message); - return this.undo(resource); + return new WorkspaceVerificationError(this.undo(strResource)); } // this must be the last past element in all the impacted resources! - let affectedEditStacks: ResourceEditStack[] = []; - for (const strResource of element.strResources) { - affectedEditStacks.push(this._editStacks.get(strResource)!); - } - - let cannotUndoDueToResources: URI[] = []; - for (const editStack of affectedEditStacks) { - if (editStack.past.length === 0 || editStack.past[editStack.past.length - 1] !== element) { - cannotUndoDueToResources.push(editStack.resource); + const cannotUndoDueToResources: string[] = []; + for (const editStack of editStackSnapshot.editStacks) { + if (editStack.getClosestPastElement() !== element) { + cannotUndoDueToResources.push(editStack.resourceLabel); } } - if (cannotUndoDueToResources.length > 0) { this._splitPastWorkspaceElement(element, null); - const paths = cannotUndoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path); - const message = nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, paths.join(', ')); + const message = nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', ')); this._notificationService.info(message); - return this.undo(resource); + return new WorkspaceVerificationError(this.undo(strResource)); } - return this._dialogService.show( + const cannotLockDueToResources: string[] = []; + for (const editStack of editStackSnapshot.editStacks) { + if (editStack.locked) { + cannotLockDueToResources.push(editStack.resourceLabel); + } + } + if (cannotLockDueToResources.length > 0) { + this._splitPastWorkspaceElement(element, null); + const message = nls.localize('cannotWorkspaceUndoDueToInProgressUndoRedo', "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')); + this._notificationService.info(message); + return new WorkspaceVerificationError(this.undo(strResource)); + } + + // check if new stack elements were added in the meantime... + if (!editStackSnapshot.isValid()) { + this._splitPastWorkspaceElement(element, null); + const message = nls.localize('cannotWorkspaceUndoDueToInMeantimeUndoRedo', "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label); + this._notificationService.info(message); + return new WorkspaceVerificationError(this.undo(strResource)); + } + + return null; + } + + private _workspaceUndo(strResource: string, element: WorkspaceStackElement): Promise | void { + const affectedEditStacks = this._getAffectedEditStacks(element); + const verificationError = this._checkWorkspaceUndo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false); + if (verificationError) { + return verificationError.returnValue; + } + return this._confirmAndExecuteWorkspaceUndo(strResource, element, affectedEditStacks); + } + + private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise { + + const result = await this._dialogService.show( Severity.Info, nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label), [ - nls.localize('ok', "Undo in {0} Files", affectedEditStacks.length), + nls.localize({ key: 'ok', comment: ['{0} denotes a number that is > 1'] }, "Undo in {0} Files", editStackSnapshot.editStacks.length), nls.localize('nok', "Undo this File"), nls.localize('cancel', "Cancel"), ], { cancelId: 2 } - ).then((result) => { - if (result.choice === 2) { - // cancel - return; - } else if (result.choice === 0) { - for (const editStack of affectedEditStacks) { - editStack.past.pop(); - editStack.future.push(element); - } - return this._safeInvoke(element, () => element.actual.undo()); - } else { - this._splitPastWorkspaceElement(element, null); - return this.undo(resource); - } - }); + ); + + if (result.choice === 2) { + // choice: cancel + return; + } + + if (result.choice === 1) { + // choice: undo this file + this._splitPastWorkspaceElement(element, null); + return this.undo(strResource); + } + + // choice: undo in all files + + // At this point, it is possible that the element has been made invalid in the meantime (due to the confirmation await) + const verificationError1 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*invalidated resources will be checked after the prepare call*/false); + if (verificationError1) { + return verificationError1.returnValue; + } + + // prepare + let cleanup: IDisposable; + try { + cleanup = await this._invokeWorkspacePrepare(element); + } catch (err) { + return this._onError(err, element); + } + + // At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await) + const verificationError2 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*now also check that there are no more invalidated resources*/true); + if (verificationError2) { + cleanup.dispose(); + return verificationError2.returnValue; + } + + for (const editStack of editStackSnapshot.editStacks) { + editStack.moveBackward(element); + } + return this._safeInvokeWithLocks(element, () => element.actual.undo(), editStackSnapshot, cleanup); } private _resourceUndo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { if (!element.isValid) { // invalid element => immediately flush edit stack! - editStack.past = []; - editStack.future = []; + editStack.flushAllElements(); return; } - editStack.past.pop(); - editStack.future.push(element); - return this._safeInvoke(element, () => element.actual.undo()); + if (editStack.locked) { + const message = nls.localize('cannotResourceUndoDueToInProgressUndoRedo', "Could not undo '{0}' because there is already an undo or redo operation running.", element.label); + this._notificationService.info(message); + return; + } + return this._invokeResourcePrepare(element, (cleanup) => { + editStack.moveBackward(element); + return this._safeInvokeWithLocks(element, () => element.actual.undo(), new EditStackSnapshot([editStack]), cleanup); + }); } - public undo(resource: URI): Promise | void { - const strResource = uriGetComparisonKey(resource); + public undo(resource: URI | string): Promise | void { + const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource); if (!this._editStacks.has(strResource)) { return; } const editStack = this._editStacks.get(strResource)!; - if (editStack.past.length === 0) { + const element = editStack.getClosestPastElement(); + if (!element) { return; } - const element = editStack.past[editStack.past.length - 1]; if (element.type === UndoRedoElementType.Workspace) { - return this._workspaceUndo(resource, element); + return this._workspaceUndo(strResource, element); } else { return this._resourceUndo(editStack, element); } } public canRedo(resource: URI): boolean { - const strResource = uriGetComparisonKey(resource); + const strResource = this.getUriComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - return (editStack.future.length > 0); + return editStack.hasFutureElements(); } return false; } - private _workspaceRedo(resource: URI, element: WorkspaceStackElement): Promise | void { + private _checkWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null { if (element.removedResources) { this._splitFutureWorkspaceElement(element, element.removedResources); const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()); this._notificationService.info(message); - return this.redo(resource); + return new WorkspaceVerificationError(this.redo(strResource)); } - if (element.invalidatedResources) { + if (checkInvalidatedResources && element.invalidatedResources) { this._splitFutureWorkspaceElement(element, element.invalidatedResources); const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()); this._notificationService.info(message); - return this.redo(resource); + return new WorkspaceVerificationError(this.redo(strResource)); } // this must be the last future element in all the impacted resources! - let affectedEditStacks: ResourceEditStack[] = []; - for (const strResource of element.strResources) { - affectedEditStacks.push(this._editStacks.get(strResource)!); - } - - let cannotRedoDueToResources: URI[] = []; - for (const editStack of affectedEditStacks) { - if (editStack.future.length === 0 || editStack.future[editStack.future.length - 1] !== element) { - cannotRedoDueToResources.push(editStack.resource); + const cannotRedoDueToResources: string[] = []; + for (const editStack of editStackSnapshot.editStacks) { + if (editStack.getClosestFutureElement() !== element) { + cannotRedoDueToResources.push(editStack.resourceLabel); } } - if (cannotRedoDueToResources.length > 0) { this._splitFutureWorkspaceElement(element, null); - const paths = cannotRedoDueToResources.map(r => r.scheme === Schemas.file ? r.fsPath : r.path); - const message = nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, paths.join(', ')); + const message = nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', ')); this._notificationService.info(message); - return this.redo(resource); + return new WorkspaceVerificationError(this.redo(strResource)); } - for (const editStack of affectedEditStacks) { - editStack.future.pop(); - editStack.past.push(element); + const cannotLockDueToResources: string[] = []; + for (const editStack of editStackSnapshot.editStacks) { + if (editStack.locked) { + cannotLockDueToResources.push(editStack.resourceLabel); + } } - return this._safeInvoke(element, () => element.actual.redo()); + if (cannotLockDueToResources.length > 0) { + this._splitFutureWorkspaceElement(element, null); + const message = nls.localize('cannotWorkspaceRedoDueToInProgressUndoRedo', "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')); + this._notificationService.info(message); + return new WorkspaceVerificationError(this.redo(strResource)); + } + + // check if new stack elements were added in the meantime... + if (!editStackSnapshot.isValid()) { + this._splitPastWorkspaceElement(element, null); + const message = nls.localize('cannotWorkspaceRedoDueToInMeantimeUndoRedo', "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label); + this._notificationService.info(message); + return new WorkspaceVerificationError(this.redo(strResource)); + } + + return null; + } + + private _workspaceRedo(strResource: string, element: WorkspaceStackElement): Promise | void { + const affectedEditStacks = this._getAffectedEditStacks(element); + const verificationError = this._checkWorkspaceRedo(strResource, element, affectedEditStacks, /*invalidated resources will be checked after the prepare call*/false); + if (verificationError) { + return verificationError.returnValue; + } + return this._executeWorkspaceRedo(strResource, element, affectedEditStacks); + } + + private async _executeWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise { + // prepare + let cleanup: IDisposable; + try { + cleanup = await this._invokeWorkspacePrepare(element); + } catch (err) { + return this._onError(err, element); + } + + // At this point, it is possible that the element has been made invalid in the meantime (due to the prepare await) + const verificationError = this._checkWorkspaceRedo(strResource, element, editStackSnapshot, /*now also check that there are no more invalidated resources*/true); + if (verificationError) { + cleanup.dispose(); + return verificationError.returnValue; + } + + for (const editStack of editStackSnapshot.editStacks) { + editStack.moveForward(element); + } + return this._safeInvokeWithLocks(element, () => element.actual.redo(), editStackSnapshot, cleanup); } private _resourceRedo(editStack: ResourceEditStack, element: ResourceStackElement): Promise | void { if (!element.isValid) { // invalid element => immediately flush edit stack! - editStack.past = []; - editStack.future = []; + editStack.flushAllElements(); return; } - editStack.future.pop(); - editStack.past.push(element); - return this._safeInvoke(element, () => element.actual.redo()); + if (editStack.locked) { + const message = nls.localize('cannotResourceRedoDueToInProgressUndoRedo', "Could not redo '{0}' because there is already an undo or redo operation running.", element.label); + this._notificationService.info(message); + return; + } + + return this._invokeResourcePrepare(element, (cleanup) => { + editStack.moveForward(element); + return this._safeInvokeWithLocks(element, () => element.actual.redo(), new EditStackSnapshot([editStack]), cleanup); + }); } - public redo(resource: URI): Promise | void { - const strResource = uriGetComparisonKey(resource); + public redo(resource: URI | string): Promise | void { + const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource); if (!this._editStacks.has(strResource)) { return; } const editStack = this._editStacks.get(strResource)!; - if (editStack.future.length === 0) { + const element = editStack.getClosestFutureElement(); + if (!element) { return; } - const element = editStack.future[editStack.future.length - 1]; if (element.type === UndoRedoElementType.Workspace) { - return this._workspaceRedo(resource, element); + return this._workspaceRedo(strResource, element); } else { return this._resourceRedo(editStack, element); } } } +class WorkspaceVerificationError { + constructor(public readonly returnValue: Promise | void) { } +} + registerSingleton(IUndoRedoService, UndoRedoService); diff --git a/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts new file mode 100644 index 00000000000..86714f8d471 --- /dev/null +++ b/src/vs/platform/undoRedo/test/common/undoRedoService.test.ts @@ -0,0 +1,211 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { UndoRedoService } from 'vs/platform/undoRedo/common/undoRedoService'; +import { TestDialogService } from 'vs/platform/dialogs/test/common/testDialogService'; +import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; +import { UndoRedoElementType, IUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo'; +import { URI } from 'vs/base/common/uri'; +import { mock } from 'vs/base/test/common/mock'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + +suite('UndoRedoService', () => { + + function createUndoRedoService(dialogService: IDialogService = new TestDialogService()): UndoRedoService { + const notificationService = new TestNotificationService(); + return new UndoRedoService(dialogService, notificationService); + } + + test('simple single resource elements', () => { + const resource = URI.file('test.txt'); + const service = createUndoRedoService(); + + assert.equal(service.canUndo(resource), false); + assert.equal(service.canRedo(resource), false); + assert.equal(service.hasElements(resource), false); + assert.ok(service.getLastElement(resource) === null); + + let undoCall1 = 0; + let redoCall1 = 0; + const element1: IUndoRedoElement = { + type: UndoRedoElementType.Resource, + resource: resource, + label: 'typing 1', + undo: () => { undoCall1++; }, + redo: () => { redoCall1++; } + }; + service.pushElement(element1); + + assert.equal(undoCall1, 0); + assert.equal(redoCall1, 0); + assert.equal(service.canUndo(resource), true); + assert.equal(service.canRedo(resource), false); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === element1); + + service.undo(resource); + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 0); + assert.equal(service.canUndo(resource), false); + assert.equal(service.canRedo(resource), true); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === null); + + service.redo(resource); + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 1); + assert.equal(service.canUndo(resource), true); + assert.equal(service.canRedo(resource), false); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === element1); + + let undoCall2 = 0; + let redoCall2 = 0; + const element2: IUndoRedoElement = { + type: UndoRedoElementType.Resource, + resource: resource, + label: 'typing 2', + undo: () => { undoCall2++; }, + redo: () => { redoCall2++; } + }; + service.pushElement(element2); + + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 1); + assert.equal(undoCall2, 0); + assert.equal(redoCall2, 0); + assert.equal(service.canUndo(resource), true); + assert.equal(service.canRedo(resource), false); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === element2); + + service.undo(resource); + + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 1); + assert.equal(undoCall2, 1); + assert.equal(redoCall2, 0); + assert.equal(service.canUndo(resource), true); + assert.equal(service.canRedo(resource), true); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === null); + + let undoCall3 = 0; + let redoCall3 = 0; + const element3: IUndoRedoElement = { + type: UndoRedoElementType.Resource, + resource: resource, + label: 'typing 2', + undo: () => { undoCall3++; }, + redo: () => { redoCall3++; } + }; + service.pushElement(element3); + + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 1); + assert.equal(undoCall2, 1); + assert.equal(redoCall2, 0); + assert.equal(undoCall3, 0); + assert.equal(redoCall3, 0); + assert.equal(service.canUndo(resource), true); + assert.equal(service.canRedo(resource), false); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === element3); + + service.undo(resource); + + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 1); + assert.equal(undoCall2, 1); + assert.equal(redoCall2, 0); + assert.equal(undoCall3, 1); + assert.equal(redoCall3, 0); + assert.equal(service.canUndo(resource), true); + assert.equal(service.canRedo(resource), true); + assert.equal(service.hasElements(resource), true); + assert.ok(service.getLastElement(resource) === null); + }); + + test('multi resource elements', async () => { + const resource1 = URI.file('test1.txt'); + const resource2 = URI.file('test2.txt'); + const service = createUndoRedoService(new class extends mock() { + async show() { + return { + choice: 0 // confirm! + }; + } + }); + + let undoCall1 = 0, undoCall11 = 0, undoCall12 = 0; + let redoCall1 = 0, redoCall11 = 0, redoCall12 = 0; + const element1: IUndoRedoElement = { + type: UndoRedoElementType.Workspace, + resources: [resource1, resource2], + label: 'typing 1', + undo: () => { undoCall1++; }, + redo: () => { redoCall1++; }, + split: () => { + return [ + { + type: UndoRedoElementType.Resource, + resource: resource1, + label: 'typing 1.1', + undo: () => { undoCall11++; }, + redo: () => { redoCall11++; } + }, + { + type: UndoRedoElementType.Resource, + resource: resource2, + label: 'typing 1.2', + undo: () => { undoCall12++; }, + redo: () => { redoCall12++; } + } + ]; + } + }; + service.pushElement(element1); + + assert.equal(service.canUndo(resource1), true); + assert.equal(service.canRedo(resource1), false); + assert.equal(service.hasElements(resource1), true); + assert.ok(service.getLastElement(resource1) === element1); + assert.equal(service.canUndo(resource2), true); + assert.equal(service.canRedo(resource2), false); + assert.equal(service.hasElements(resource2), true); + assert.ok(service.getLastElement(resource2) === element1); + + await service.undo(resource1); + + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 0); + assert.equal(service.canUndo(resource1), false); + assert.equal(service.canRedo(resource1), true); + assert.equal(service.hasElements(resource1), true); + assert.ok(service.getLastElement(resource1) === null); + assert.equal(service.canUndo(resource2), false); + assert.equal(service.canRedo(resource2), true); + assert.equal(service.hasElements(resource2), true); + assert.ok(service.getLastElement(resource2) === null); + + await service.redo(resource2); + assert.equal(undoCall1, 1); + assert.equal(redoCall1, 1); + assert.equal(undoCall11, 0); + assert.equal(redoCall11, 0); + assert.equal(undoCall12, 0); + assert.equal(redoCall12, 0); + assert.equal(service.canUndo(resource1), true); + assert.equal(service.canRedo(resource1), false); + assert.equal(service.hasElements(resource1), true); + assert.ok(service.getLastElement(resource1) === element1); + assert.equal(service.canUndo(resource2), true); + assert.equal(service.canRedo(resource2), false); + assert.equal(service.hasElements(resource2), true); + assert.ok(service.getLastElement(resource2) === element1); + + }); +}); diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index d8ff2bb11e5..8d78ddddd01 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -93,8 +93,8 @@ export class Win32UpdateService extends AbstractUpdateService { protected buildUpdateFeedUrl(quality: string): string | undefined { let platform = 'win32'; - if (process.arch === 'x64') { - platform += '-x64'; + if (process.arch !== 'ia32') { + platform += `-${process.arch}`; } if (getUpdateType() === UpdateType.Archive) { diff --git a/src/vs/platform/url/common/urlService.ts b/src/vs/platform/url/common/urlService.ts index 030d52b587e..dbcac93deff 100644 --- a/src/vs/platform/url/common/urlService.ts +++ b/src/vs/platform/url/common/urlService.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { values } from 'vs/base/common/map'; import { first } from 'vs/base/common/async'; import { toDisposable, IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import product from 'vs/platform/product/common/product'; export abstract class AbstractURLService extends Disposable implements IURLService { @@ -27,3 +28,16 @@ export abstract class AbstractURLService extends Disposable implements IURLServi return toDisposable(() => this.handlers.delete(handler)); } } + +export class NativeURLService extends AbstractURLService { + + create(options?: Partial): URI { + let { authority, path, query, fragment } = options ? options : { authority: undefined, path: undefined, query: undefined, fragment: undefined }; + + if (authority && path && path.indexOf('/') !== 0) { + path = `/${path}`; // URI validation requires a path if there is an authority + } + + return URI.from({ scheme: product.urlProtocol, authority, path, query, fragment }); + } +} diff --git a/src/vs/platform/url/node/urlService.ts b/src/vs/platform/url/node/urlService.ts deleted file mode 100644 index a0c9249dd7d..00000000000 --- a/src/vs/platform/url/node/urlService.ts +++ /dev/null @@ -1,21 +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 product from 'vs/platform/product/common/product'; -import { AbstractURLService } from 'vs/platform/url/common/urlService'; - -export class URLService extends AbstractURLService { - - create(options?: Partial): URI { - let { authority, path, query, fragment } = options ? options : { authority: undefined, path: undefined, query: undefined, fragment: undefined }; - - if (authority && path && path.indexOf('/') !== 0) { - path = `/${path}`; // URI validation requires a path if there is an authority - } - - return URI.from({ scheme: product.urlProtocol, authority, path, query, fragment }); - } -} diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 47e10a2c34e..68d508d54a7 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -10,7 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreviewResult, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources'; -import { CancelablePromise } from 'vs/base/common/async'; +import { CancelablePromise, RunOnceScheduler } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ParseError, parse } from 'vs/base/common/json'; @@ -21,6 +21,8 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { isString } from 'vs/base/common/types'; import { uppercaseFirstLetter } from 'vs/base/common/strings'; import { equals } from 'vs/base/common/arrays'; +import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; +import { IStorageService } from 'vs/platform/storage/common/storage'; type SyncSourceClassification = { source?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; @@ -33,19 +35,34 @@ export interface IRemoteUserData { export interface ISyncData { version: number; + machineId?: string; content: string; } function isSyncData(thing: any): thing is ISyncData { - return thing - && (thing.version && typeof thing.version === 'number') - && (thing.content && typeof thing.content === 'string') - && Object.keys(thing).length === 2; + if (thing + && (thing.version !== undefined && typeof thing.version === 'number') + && (thing.content !== undefined && typeof thing.content === 'string')) { + + // backward compatibility + if (Object.keys(thing).length === 2) { + return true; + } + + if (Object.keys(thing).length === 3 + && (thing.machineId !== undefined && typeof thing.machineId === 'string')) { + return true; + } + } + + return false; } + export abstract class AbstractSynchroniser extends Disposable { protected readonly syncFolder: URI; + private readonly currentMachineIdPromise: Promise; private _status: SyncStatus = SyncStatus.Idle; get status(): SyncStatus { return this._status; } @@ -57,7 +74,8 @@ export abstract class AbstractSynchroniser extends Disposable { private _onDidChangeConflicts: Emitter = this._register(new Emitter()); readonly onDidChangeConflicts: Event = this._onDidChangeConflicts.event; - protected readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); + private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50); + private readonly _onDidChangeLocal: Emitter = this._register(new Emitter()); readonly onDidChangeLocal: Event = this._onDidChangeLocal.event; protected readonly lastSyncResource: URI; @@ -67,10 +85,11 @@ export abstract class AbstractSynchroniser extends Disposable { readonly resource: SyncResource, @IFileService protected readonly fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @ITelemetryService protected readonly telemetryService: ITelemetryService, @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, @IConfigurationService protected readonly configurationService: IConfigurationService, ) { @@ -78,6 +97,22 @@ export abstract class AbstractSynchroniser extends Disposable { this.syncResourceLogLabel = uppercaseFirstLetter(this.resource); this.syncFolder = joinPath(environmentService.userDataSyncHome, resource); this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`); + this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService); + } + + protected async triggerLocalChange(): Promise { + if (this.isEnabled()) { + this.localChangeTriggerScheduler.schedule(); + } + } + + protected async doTriggerLocalChange(): Promise { + this.logService.trace(`${this.syncResourceLogLabel}: Checking for local changes...`); + const lastSyncUserData = await this.getLastSyncUserData(); + const hasRemoteChanged = lastSyncUserData ? (await this.generatePreview(lastSyncUserData, lastSyncUserData)).hasRemoteChanged : true; + if (hasRemoteChanged) { + this._onDidChangeLocal.fire(); + } } protected setStatus(status: SyncStatus): void { @@ -191,7 +226,7 @@ export abstract class AbstractSynchroniser extends Disposable { async getSyncPreview(): Promise { if (!this.isEnabled()) { - return { hasLocalChanged: false, hasRemoteChanged: false }; + return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false }; } const lastSyncUserData = await this.getLastSyncUserData(); @@ -203,7 +238,7 @@ export abstract class AbstractSynchroniser extends Disposable { if (remoteUserData.syncData && remoteUserData.syncData.version > this.version) { // current version is not compatible with cloud version this.telemetryService.publicLog2<{ source: string }, SyncSourceClassification>('sync/incompatible', { source: this.resource }); - throw new UserDataSyncError(localize('incompatible', "Cannot sync {0} as its version {1} is not compatible with cloud {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource); + throw new UserDataSyncError(localize({ key: 'incompatible', comment: ['This is an error while syncing a resource that its local version is not compatible with its remote version.'] }, "Cannot sync {0} as its local version {1} is not compatible with its remote version {2}", this.resource, this.version, remoteUserData.syncData.version), UserDataSyncErrorCode.Incompatible, this.resource); } try { const status = await this.performSync(remoteUserData, lastSyncUserData); @@ -211,7 +246,7 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (e) { if (e instanceof UserDataSyncError) { switch (e.code) { - case UserDataSyncErrorCode.RemotePreconditionFailed: + 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...`); @@ -234,6 +269,11 @@ export abstract class AbstractSynchroniser extends Disposable { return !!lastSyncData; } + protected async isLastSyncFromCurrentMachine(remoteUserData: IRemoteUserData): Promise { + const machineId = await this.currentMachineIdPromise; + return !!remoteUserData.syncData?.machineId && remoteUserData.syncData.machineId === machineId; + } + async getRemoteSyncResourceHandles(): Promise { const handles = await this.userDataSyncStoreService.getAllRefs(this.resource); return handles.map(({ created, ref }) => ({ created, uri: this.toRemoteBackupResource(ref) })); @@ -252,6 +292,18 @@ export abstract class AbstractSynchroniser extends Disposable { return URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local-backup', path: `/${this.resource}/${ref}` }); } + async getMachineId({ uri }: ISyncResourceHandle): Promise { + const ref = basename(uri); + if (isEqual(uri, this.toRemoteBackupResource(ref))) { + const { content } = await this.getUserData(ref); + if (content) { + const syncData = this.parseSyncData(content); + return syncData?.machineId; + } + } + return undefined; + } + async resolveContent(uri: URI): Promise { const ref = basename(uri); if (isEqual(uri, this.toRemoteBackupResource(ref))) { @@ -278,14 +330,13 @@ export abstract class AbstractSynchroniser extends Disposable { if (userData.content === null) { return { ref: parsed.ref, syncData: null } as T; } - let syncData: ISyncData = JSON.parse(userData.content); + const syncData: ISyncData = JSON.parse(userData.content); - // Migration from old content to sync data - if (!isSyncData(syncData)) { - syncData = { version: this.version, content: userData.content }; + /* Check if syncData is of expected type. Return only if matches */ + if (isSyncData(syncData)) { + return { ...parsed, ...{ syncData, content: undefined } }; } - return { ...parsed, ...{ syncData, content: undefined } }; } catch (error) { if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { // log error always except when file does not exist @@ -309,20 +360,16 @@ export abstract class AbstractSynchroniser extends Disposable { return { ref, syncData }; } - protected parseSyncData(content: string): ISyncData | null { - let syncData: ISyncData | null = null; + protected parseSyncData(content: string): ISyncData { try { - syncData = JSON.parse(content); - - // Migration from old content to sync data - if (!isSyncData(syncData)) { - syncData = { version: this.version, content }; + const syncData: ISyncData = JSON.parse(content); + if (isSyncData(syncData)) { + return syncData; } - - } catch (e) { - this.logService.error(e); + } catch (error) { + this.logService.error(error); } - return syncData; + throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.Incompatible, this.resource); } private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { @@ -336,7 +383,8 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async updateRemoteUserData(content: string, ref: string | null): Promise { - const syncData: ISyncData = { version: this.version, content }; + const machineId = await this.currentMachineIdPromise; + const syncData: ISyncData = { version: this.version, machineId, content }; ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref); return { ref, syncData }; } @@ -371,6 +419,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { resource: SyncResource, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @@ -378,7 +427,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { @IUserDataSyncLogService logService: IUserDataSyncLogService, @IConfigurationService configurationService: IConfigurationService, ) { - super(resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(file))); this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e))); } @@ -453,7 +502,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { // Otherwise fire change event else { - this._onDidChangeLocal.fire(); + this.triggerLocalChange(); } } @@ -476,6 +525,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni resource: SyncResource, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @@ -484,7 +534,7 @@ export abstract class AbstractJsonFileSynchroniser extends AbstractFileSynchroni @IUserDataSyncUtilService protected readonly userDataSyncUtilService: IUserDataSyncUtilService, @IConfigurationService configurationService: IConfigurationService, ) { - super(file, resource, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(file, resource, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); } protected hasErrors(content: string): boolean { diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index 43f246b5733..07c8a7bd733 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -7,6 +7,10 @@ import { values, keys } from 'vs/base/common/map'; import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { startsWith } from 'vs/base/common/strings'; +import { deepClone } from 'vs/base/common/objects'; +import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { distinct } from 'vs/base/common/arrays'; export interface IMergeResult { added: ISyncExtension[]; @@ -30,8 +34,6 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync }; } - // massage incoming extension - add disabled property - const massageIncomingExtension = (extension: ISyncExtension): ISyncExtension => ({ ...extension, ...{ disabled: !!extension.disabled } }); localExtensions = localExtensions.map(massageIncomingExtension); remoteExtensions = remoteExtensions.map(massageIncomingExtension); lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null; @@ -54,7 +56,14 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync }; const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map()); const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map()); - const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map()); + const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map, extension: ISyncExtension) => { + const key = getKey(extension); + extension = deepClone(extension); + if (localExtensionsMap.get(key)?.installed) { + extension.installed = true; + } + return addExtensionToMap(map, extension); + }, new Map()); const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map()) : null; const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map()); const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => { @@ -63,90 +72,82 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync }, new Set()); const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet); - if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { - // No changes found between local and remote. - return { added: [], removed: [], updated: [], remote: null }; - } + if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) { - const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet); - const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet); + const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet); + const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet); - // massage outgoing extension - remove disabled property - const massageOutgoingExtension = (extension: ISyncExtension, key: string): ISyncExtension => { - const massagedExtension: ISyncExtension = { - identifier: { - id: extension.identifier.id, - uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined - }, - }; - if (extension.disabled) { - massagedExtension.disabled = true; - } - if (extension.version) { - massagedExtension.version = extension.version; - } - return massagedExtension; - }; - - // Remotely removed extension. - for (const key of values(baseToRemote.removed)) { - const e = localExtensionsMap.get(key); - if (e) { - removed.push(e.identifier); - } - } - - // Remotely added extension - for (const key of values(baseToRemote.added)) { - // Got added in local - if (baseToLocal.added.has(key)) { - // Is different from local to remote - if (localToRemote.updated.has(key)) { - updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); + // Remotely removed extension. + for (const key of values(baseToRemote.removed)) { + const e = localExtensionsMap.get(key); + if (e) { + removed.push(e.identifier); } - } else { - // Add to local - added.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); - } - } - - // Remotely updated extensions - for (const key of values(baseToRemote.updated)) { - // Update in local always - updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); - } - - // Locally added extensions - for (const key of values(baseToLocal.added)) { - // Not there in remote - if (!baseToRemote.added.has(key)) { - newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!); - } - } - - // Locally updated extensions - for (const key of values(baseToLocal.updated)) { - // If removed in remote - if (baseToRemote.removed.has(key)) { - continue; } - // If not updated in remote - if (!baseToRemote.updated.has(key)) { - newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!); + // Remotely added extension + for (const key of values(baseToRemote.added)) { + // Got added in local + if (baseToLocal.added.has(key)) { + // Is different from local to remote + if (localToRemote.updated.has(key)) { + updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); + } + } else { + // Add only installed extension to local + const remoteExtension = remoteExtensionsMap.get(key)!; + if (remoteExtension.installed) { + added.push(massageOutgoingExtension(remoteExtension, key)); + } + } } - } - // Locally removed extensions - for (const key of values(baseToLocal.removed)) { - // If not skipped and not updated in remote - if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) { - newRemoteExtensionsMap.delete(key); + // Remotely updated extensions + for (const key of values(baseToRemote.updated)) { + // Update in local always + updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); + } + + // Locally added extensions + for (const key of values(baseToLocal.added)) { + // Not there in remote + if (!baseToRemote.added.has(key)) { + newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!); + } + } + + // Locally updated extensions + for (const key of values(baseToLocal.updated)) { + // If removed in remote + if (baseToRemote.removed.has(key)) { + continue; + } + + // If not updated in remote + if (!baseToRemote.updated.has(key)) { + const extension = deepClone(localExtensionsMap.get(key)!); + // Retain installed property + if (newRemoteExtensionsMap.get(key)?.installed) { + extension.installed = true; + } + newRemoteExtensionsMap.set(key, extension); + } + } + + // Locally removed extensions + for (const key of values(baseToLocal.removed)) { + // If not skipped and not updated in remote + if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) { + // Remove only if it is an installed extension + if (lastSyncExtensionsMap?.get(key)?.installed) { + newRemoteExtensionsMap.delete(key); + } + } } } const remote: ISyncExtension[] = []; - const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set()); + const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set(), { checkInstalledProperty: true }); if (remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0) { newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key))); } @@ -154,7 +155,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync return { added, removed, updated, remote: remote.length ? remote : null }; } -function compare(from: Map | null, to: Map, ignoredExtensions: Set): { added: Set, removed: Set, updated: Set } { +function compare(from: Map | null, to: Map, ignoredExtensions: Set, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set, removed: Set, updated: Set } { const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : []; const toKeys = keys(to).filter(key => !ignoredExtensions.has(key)); const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); @@ -170,6 +171,7 @@ function compare(from: Map | null, to: Map | null, to: Map i.isMachineScoped).map(i => i.identifier.id.toLowerCase()); + const value = (configurationService.getValue('sync.ignoredExtensions') || []).map(id => id.toLowerCase()); + const added: string[] = [], removed: string[] = []; + if (Array.isArray(value)) { + for (const key of value) { + if (startsWith(key, '-')) { + removed.push(key.substring(1)); + } else { + added.push(key); + } + } + } + return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1)); +} diff --git a/src/vs/platform/userDataSync/common/extensionsSync.ts b/src/vs/platform/userDataSync/common/extensionsSync.ts index 0aa08c4aa3a..866ad0804f3 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -6,13 +6,12 @@ import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService } from 'vs/platform/extensionManagement/common/extensionManagement'; +import { IExtensionManagementService, IExtensionGalleryService, IGlobalExtensionEnablementService, ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement'; import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; -import { isNonEmptyArray } from 'vs/base/common/arrays'; +import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; @@ -20,6 +19,7 @@ import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources'; import { format } from 'vs/base/common/jsonFormatter'; import { applyEdits } from 'vs/base/common/jsonEdit'; import { compare } from 'vs/base/common/strings'; +import { IStorageService } from 'vs/platform/storage/common/storage'; interface IExtensionsSyncPreviewResult extends ISyncPreviewResult { readonly localExtensions: ISyncExtension[]; @@ -40,12 +40,16 @@ interface ILastSyncUserData extends IRemoteUserData { export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` }); - protected readonly version: number = 2; + /* + Version 3 - Introduce installed property to skip installing built in extensions + */ + protected readonly version: number = 3; protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); } constructor( @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @@ -56,14 +60,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(SyncResource.Extensions, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register( Event.debounce( Event.any( Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)), Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)), this.extensionEnablementService.onDidChangeEnablement), - () => undefined, 500)(() => this._onDidChangeLocal.fire())); + () => undefined, 500)(() => this.triggerLocalChange())); } async pull(): Promise { @@ -82,13 +86,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse const remoteUserData = await this.getRemoteUserData(lastSyncUserData); if (remoteUserData.syncData !== null) { - const localExtensions = await this.getLocalExtensions(); - const remoteExtensions = this.parseExtensions(remoteUserData.syncData); - const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions()); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); + const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions); await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine: false }); } @@ -115,14 +122,17 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse this.logService.info(`${this.syncResourceLogLabel}: Started pushing extensions...`); this.setStatus(SyncStatus.Syncing); - const localExtensions = await this.getLocalExtensions(); - const { added, removed, updated, remote } = merge(localExtensions, null, null, [], this.getIgnoredExtensions()); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const { added, removed, updated, remote } = merge(localExtensions, null, null, [], ignoredExtensions); const lastSyncUserData = await this.getLastSyncUserData(); const remoteUserData = await this.getRemoteUserData(lastSyncUserData); await this.apply({ added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine: false }, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing extensions.`); @@ -140,7 +150,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async resolveContent(uri: URI): Promise { if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { - const localExtensions = await this.getLocalExtensions(); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); return this.format(localExtensions); } @@ -184,8 +195,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async hasLocalData(): Promise { try { - const localExtensions = await this.getLocalExtensions(); - if (isNonEmptyArray(localExtensions)) { + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); + if (localExtensions.some(e => e.installed || e.disabled)) { return true; } } catch (error) { @@ -201,23 +213,36 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const localExtensions = await this.getLocalExtensions(); - const syncExtensions = this.parseExtensions(syncData); - const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], this.getIgnoredExtensions()); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); + const syncExtensions = await this.parseAndMigrateExtensions(syncData); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); + const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions); await this.apply({ added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData, hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, - hasRemoteChanged: true + hasRemoteChanged: true, + isLastSyncFromCurrentMachine: false }); } protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null; - const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null; + const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; + const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); + let lastSyncExtensions: ISyncExtension[] | null = null; + if (lastSyncUserData === null) { + if (isLastSyncFromCurrentMachine) { + lastSyncExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData!); + } + } else { + lastSyncExtensions = await this.parseAndMigrateExtensions(lastSyncUserData.syncData!); + } - const localExtensions = await this.getLocalExtensions(); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); + const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService); if (remoteExtensions) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote extensions with local extensions...`); @@ -225,7 +250,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`); } - const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, this.getIgnoredExtensions()); + const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions); return { added, @@ -237,14 +262,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse localExtensions, lastSyncUserData, hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine }; } - private getIgnoredExtensions() { - return this.configurationService.getValue('sync.ignoredExtensions') || []; - } - private async apply({ added, removed, updated, remote, remoteUserData, skippedExtensions, lastSyncUserData, localExtensions, hasLocalChanged, hasRemoteChanged }: IExtensionsSyncPreviewResult, forcePush?: boolean): Promise { if (!hasLocalChanged && !hasRemoteChanged) { @@ -351,31 +373,49 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private parseExtensions(syncData: ISyncData): ISyncExtension[] { - let extensions: ISyncExtension[] = JSON.parse(syncData.content); - if (syncData.version !== this.version) { - extensions = extensions.map(e => { + 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 (!(e).enabled) { - e.disabled = true; + if (syncData.version === 1) { + if ((extension).enabled === false) { + extension.disabled = true; + } + delete (extension).enabled; } - delete (e).enabled; // #endregion - return e; - }); + + // #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 async getLocalExtensions(): Promise { - const installedExtensions = await this.extensionManagementService.getInstalled(); + private parseExtensions(syncData: ISyncData): ISyncExtension[] { + return JSON.parse(syncData.content); + } + + private getLocalExtensions(installedExtensions: ILocalExtension[]): ISyncExtension[] { const disabledExtensions = this.extensionEnablementService.getDisabledExtensions(); return installedExtensions - .map(({ identifier }) => { + .map(({ identifier, type }) => { const syncExntesion: ISyncExtension = { identifier }; if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) { syncExntesion.disabled = true; } + if (type === ExtensionType.User) { + syncExntesion.installed = true; + } return syncExntesion; }); } diff --git a/src/vs/platform/userDataSync/common/globalStateMerge.ts b/src/vs/platform/userDataSync/common/globalStateMerge.ts index 9b7d47a307c..9ed9649b68c 100644 --- a/src/vs/platform/userDataSync/common/globalStateMerge.ts +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -40,7 +40,7 @@ export function merge(localStorage: IStringDictionary, remoteStor const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; if (!storageKey) { skipped.push(key); - logService.info(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`); + logService.trace(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`); continue; } if (storageKey.version !== remoteValue.version) { @@ -64,7 +64,7 @@ export function merge(localStorage: IStringDictionary, remoteStor const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; if (!storageKey) { skipped.push(key); - logService.info(`GlobalState: Skipped updating ${key} in local storage as is not registered.`); + logService.trace(`GlobalState: Skipped updating ${key} in local storage as is not registered.`); continue; } if (storageKey.version !== remoteValue.version) { @@ -82,7 +82,7 @@ export function merge(localStorage: IStringDictionary, remoteStor for (const key of values(baseToRemote.removed)) { const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0]; if (!storageKey) { - logService.info(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`); + logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`); continue; } local.removed.push(key); @@ -120,7 +120,7 @@ export function merge(localStorage: IStringDictionary, remoteStor // do not remove from remote if storage key is not found if (!storageKey) { skipped.push(key); - logService.info(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`); + logService.trace(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`); continue; } diff --git a/src/vs/platform/userDataSync/common/globalStateSync.ts b/src/vs/platform/userDataSync/common/globalStateSync.ts index ad2acedf184..f19f415fe67 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -56,7 +56,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs @IStorageService private readonly storageService: IStorageService, @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, ) { - super(SyncResource.GlobalState, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this._register(this.fileService.watch(dirname(this.environmentService.argvResource))); this._register( Event.any( @@ -66,7 +66,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)), /* Storage key registered */ this.storageKeysSyncRegistryService.onDidChangeStorageKeys - )((() => this._onDidChangeLocal.fire())) + )((() => this.triggerLocalChange())) ); } @@ -93,7 +93,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs local, remote, remoteUserData, localUserData: localGlobalState, lastSyncUserData, skippedStorageKeys: skipped, hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine: false }); } @@ -127,7 +128,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, remoteUserData, localUserData, lastSyncUserData, skippedStorageKeys: [], hasLocalChanged: false, - hasRemoteChanged: true + hasRemoteChanged: true, + isLastSyncFromCurrentMachine: false }, true); this.logService.info(`${this.syncResourceLogLabel}: Finished pushing UI State.`); @@ -208,13 +210,22 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData, skippedStorageKeys: skipped, hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0, - hasRemoteChanged: true + hasRemoteChanged: true, + isLastSyncFromCurrentMachine: false, }); } protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; - const lastSyncGlobalState: IGlobalState = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; + const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); + let lastSyncGlobalState: IGlobalState | null = null; + if (lastSyncUserData === null) { + if (isLastSyncFromCurrentMachine) { + lastSyncGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null; + } + } else { + lastSyncGlobalState = lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null; + } const localGloablState = await this.getLocalGlobalState(); @@ -230,7 +241,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData, skippedStorageKeys: skipped, hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine }; } diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index b2d71b4cfac..07368f5c685 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -20,6 +20,7 @@ import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, 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'; interface ISyncContent { mac?: string; @@ -42,10 +43,11 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, @IUserDataSyncUtilService userDataSyncUtilService: IUserDataSyncUtilService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); + super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } async pull(): Promise { @@ -74,6 +76,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem hasConflicts: false, hasLocalChanged: true, hasRemoteChanged: false, + isLastSyncFromCurrentMachine: false })); await this.apply(); } @@ -115,6 +118,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem hasLocalChanged: false, hasRemoteChanged: true, hasConflicts: false, + isLastSyncFromCurrentMachine: false })); await this.apply(true); } @@ -225,6 +229,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem hasConflicts: false, hasLocalChanged: true, hasRemoteChanged: true, + isLastSyncFromCurrentMachine: false })); await this.apply(); } @@ -269,7 +274,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 || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null; - await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, content: lastSyncContent } : null }); + await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null }); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`); } @@ -285,7 +290,16 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken = CancellationToken.None): Promise { const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; - const lastSyncContent = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; + const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); + let lastSyncContent: string | null = null; + if (lastSyncUserData === null) { + if (isLastSyncFromCurrentMachine) { + lastSyncContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null; + } + } else { + lastSyncContent = lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null; + } + // Get file content last to get the latest const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); @@ -330,7 +344,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []); - return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; + return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine }; } getKeybindingsContentFromSyncContent(syncContent: string): string | null { diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index 184a9a38fa8..9b3e0bd2282 100644 --- a/src/vs/platform/userDataSync/common/settingsMerge.ts +++ b/src/vs/platform/userDataSync/common/settingsMerge.ts @@ -429,26 +429,34 @@ function getEditToInsertAtLocation(content: string, key: string, value: any, loc if (location.insertAfter) { + const edits: Edit[] = []; + /* Insert after a setting */ if (node.setting) { - return [{ offset: node.endOffset, length: 0, content: ',' + newProperty }]; + edits.push({ offset: node.endOffset, length: 0, content: ',' + newProperty }); } - /* - Insert after a comment and before a setting (or) - Insert between comments and there is a setting after - */ - if (tree[location.index + 1] && - (tree[location.index + 1].setting || findNextSettingNode(location.index, tree))) { - return [{ offset: node.endOffset, length: 0, content: eol + newProperty + ',' }]; + /* Insert after a comment */ + else { + + const nextSettingNode = findNextSettingNode(location.index, tree); + const previousSettingNode = findPreviousSettingNode(location.index, tree); + const previousSettingCommaOffset = previousSettingNode?.setting?.commaOffset; + + /* If there is a previous setting and it does not has comma then add it */ + if (previousSettingNode && previousSettingCommaOffset === undefined) { + edits.push({ offset: previousSettingNode.endOffset, length: 0, content: ',' }); + } + + const isPreviouisSettingIncludesComment = previousSettingCommaOffset !== undefined && previousSettingCommaOffset > node.endOffset; + edits.push({ + offset: isPreviouisSettingIncludesComment ? previousSettingCommaOffset! + 1 : node.endOffset, + length: 0, + content: nextSettingNode ? eol + newProperty + ',' : eol + newProperty + }); } - /* Insert after the comment at the end */ - const edits = [{ offset: node.endOffset, length: 0, content: eol + newProperty }]; - const previousSettingNode = findPreviousSettingNode(location.index, tree); - if (previousSettingNode && !previousSettingNode.setting!.hasCommaSeparator) { - edits.splice(0, 0, { offset: previousSettingNode.endOffset, length: 0, content: ',' }); - } + return edits; } @@ -516,7 +524,7 @@ interface INode { readonly value: string; readonly setting?: { readonly key: string; - readonly hasCommaSeparator: boolean; + readonly commaOffset: number | undefined; }; readonly comment?: string; } @@ -547,7 +555,7 @@ function parseSettings(content: string): INode[] { value: content.substring(startOffset, offset + length), setting: { key, - hasCommaSeparator: false + commaOffset: undefined } }); } @@ -564,7 +572,7 @@ function parseSettings(content: string): INode[] { value: content.substring(startOffset, offset + length), setting: { key, - hasCommaSeparator: false + commaOffset: undefined } }); } @@ -577,7 +585,7 @@ function parseSettings(content: string): INode[] { value: content.substring(startOffset, offset + length), setting: { key, - hasCommaSeparator: false + commaOffset: undefined } }); } @@ -585,15 +593,21 @@ function parseSettings(content: string): INode[] { onSeparator: (sep: string, offset: number, length: number) => { if (hierarchyLevel === 0) { if (sep === ',') { - const node = nodes.pop(); + let index = nodes.length - 1; + for (; index >= 0; index--) { + if (nodes[index].setting) { + break; + } + } + const node = nodes[index]; if (node) { - nodes.push({ + nodes.splice(index, 1, { startOffset: node.startOffset, endOffset: node.endOffset, value: node.value, setting: { key: node.setting!.key, - hasCommaSeparator: true + commaOffset: offset } }); } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 4a7b0151ba3..5e87e009449 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -19,6 +19,9 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Edit } from 'vs/base/common/jsonFormatter'; +import { setProperty, applyEdits } from 'vs/base/common/jsonEdit'; export interface ISettingsSyncContent { settings: string; @@ -41,6 +44,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { constructor( @IFileService fileService: IFileService, @IEnvironmentService environmentService: IEnvironmentService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -50,7 +54,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { @ITelemetryService telemetryService: ITelemetryService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, ) { - super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); + super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService); } protected setStatus(status: SyncStatus): void { @@ -90,6 +94,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { hasLocalChanged: true, hasRemoteChanged: false, hasConflicts: false, + isLastSyncFromCurrentMachine: false })); await this.apply(); @@ -136,6 +141,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { hasRemoteChanged: true, hasLocalChanged: false, hasConflicts: false, + isLastSyncFromCurrentMachine: false })); await this.apply(true); @@ -272,6 +278,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { hasLocalChanged: true, hasRemoteChanged: true, hasConflicts: false, + isLastSyncFromCurrentMachine: false })); await this.apply(); @@ -336,7 +343,15 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { const fileContent = await this.getLocalFileContent(); const formattingOptions = await this.getFormattingOptions(); const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); - const lastSettingsSyncContent = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null; + const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); + let lastSettingsSyncContent: ISettingsSyncContent | null = null; + if (lastSyncUserData === null) { + if (isLastSyncFromCurrentMachine) { + lastSettingsSyncContent = this.getSettingsSyncContent(remoteUserData); + } + } else { + lastSettingsSyncContent = this.getSettingsSyncContent(lastSyncUserData); + } let content: string | null = null; let hasLocalChanged: boolean = false; @@ -371,7 +386,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []); - return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; + return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine }; } private getSettingsSyncContent(remoteUserData: IRemoteUserData): ISettingsSyncContent | null { @@ -412,4 +427,49 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } } + + async recoverSettings(): Promise { + try { + const fileContent = await this.getLocalFileContent(); + if (!fileContent) { + return; + } + + const syncData: ISyncData = JSON.parse(fileContent.value.toString()); + if (!isSyncData(syncData)) { + return; + } + + this.telemetryService.publicLog2('sync/settingsCorrupted'); + const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + if (!settingsSyncContent || !settingsSyncContent.settings) { + return; + } + + let settings = settingsSyncContent.settings; + const formattingOptions = await this.getFormattingOptions(); + for (const key in syncData) { + if (['version', 'content', 'machineId'].indexOf(key) === -1 && (syncData as any)[key] !== undefined) { + const edits: Edit[] = setProperty(settings, [key], (syncData as any)[key], formattingOptions); + if (edits.length) { + settings = applyEdits(settings, edits); + } + } + } + + await this.fileService.writeFile(this.file, VSBuffer.fromString(settings)); + } catch (e) {/* ignore */ } + } +} + +function isSyncData(thing: any): thing is ISyncData { + if (thing + && (thing.version !== undefined && typeof thing.version === 'number') + && (thing.content !== undefined && typeof thing.content === 'string') + && (thing.machineId !== undefined && typeof thing.machineId === 'string') + ) { + return true; + } + + return false; } diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 8871c78c7cd..b28535ea524 100644 --- a/src/vs/platform/userDataSync/common/snippetsSync.ts +++ b/src/vs/platform/userDataSync/common/snippetsSync.ts @@ -16,6 +16,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { merge } from 'vs/platform/userDataSync/common/snippetsMerge'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IStorageService } from 'vs/platform/storage/common/storage'; interface ISinppetsSyncPreviewResult extends ISyncPreviewResult { readonly local: IStringDictionary; @@ -39,6 +40,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD constructor( @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, @IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncLogService logService: IUserDataSyncLogService, @@ -46,7 +48,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD @IUserDataSyncEnablementService userDataSyncEnablementService: IUserDataSyncEnablementService, @ITelemetryService telemetryService: ITelemetryService, ) { - super(SyncResource.Snippets, fileService, environmentService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); + super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncEnablementService, telemetryService, logService, configurationService); this.snippetsFolder = environmentService.snippetsHome; this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME); this._register(this.fileService.watch(environmentService.userRoamingDataHome)); @@ -70,7 +72,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } // Otherwise fire change event else { - this._onDidChangeLocal.fire(); + this.triggerLocalChange(); } } @@ -97,7 +99,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine: false })); await this.apply(); } @@ -133,7 +136,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, - hasRemoteChanged: remote !== null + hasRemoteChanged: remote !== null, + isLastSyncFromCurrentMachine: false })); await this.apply(true); @@ -264,7 +268,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0, - hasRemoteChanged: true + hasRemoteChanged: true, + isLastSyncFromCurrentMachine: false })); await this.apply(); } @@ -298,7 +303,16 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD private async doGeneratePreview(local: IStringDictionary, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resolvedConflicts: IStringDictionary = {}, token: CancellationToken = CancellationToken.None): Promise { const localSnippets = this.toSnippetsContents(local); const remoteSnippets: IStringDictionary | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; - const lastSyncSnippets: IStringDictionary | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; + const isLastSyncFromCurrentMachine = await this.isLastSyncFromCurrentMachine(remoteUserData); + + let lastSyncSnippets: IStringDictionary | null = null; + if (lastSyncUserData === null) { + if (isLastSyncFromCurrentMachine) { + lastSyncSnippets = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null; + } + } else { + lastSyncSnippets = lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; + } if (remoteSnippets) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); @@ -340,7 +354,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD remote: mergeResult.remote, resolvedConflicts, hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0, - hasRemoteChanged: mergeResult.remote !== null + hasRemoteChanged: mergeResult.remote !== null, + isLastSyncFromCurrentMachine }; } diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 5c5b984a302..bdb95740b0a 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -3,24 +3,29 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { timeout, Delayer } from 'vs/base/common/async'; +import { Delayer, disposableTimeout } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; -import { Disposable } from 'vs/base/common/lifecycle'; -import { IUserDataSyncLogService, IUserDataSyncService, SyncStatus, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; +import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle'; +import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncEnablementService, ALL_SYNC_RESOURCES, getUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -type AutoSyncTriggerClassification = { - source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +type AutoSyncClassification = { + sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; +export const RESOURCE_ENABLEMENT_SOURCE = 'resourceEnablement'; + export class UserDataAutoSyncService extends Disposable implements IUserDataAutoSyncService { _serviceBrand: any; - private enabled: boolean = false; + private readonly autoSync = this._register(new MutableDisposable()); private successiveFailures: number = 0; - private readonly syncDelayer: Delayer; + private lastSyncTriggerTime: number | undefined = undefined; + private readonly syncTriggerDelayer: Delayer; private readonly _onError: Emitter = this._register(new Emitter()); readonly onError: Event = this._onError.event; @@ -31,100 +36,157 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IAuthenticationTokenService private readonly authTokenService: IAuthenticationTokenService, @ITelemetryService private readonly telemetryService: ITelemetryService, + @IProductService private readonly productService: IProductService, + @IConfigurationService private readonly configurationService: IConfigurationService, ) { super(); - this.updateEnablement(false, true); - this.syncDelayer = this._register(new Delayer(0)); - this._register(Event.any(authTokenService.onDidChangeToken)(() => this.updateEnablement(true, true))); - this._register(Event.any(userDataSyncService.onDidChangeStatus)(() => this.updateEnablement(true, true))); - this._register(this.userDataSyncEnablementService.onDidChangeEnablement(() => this.updateEnablement(true, false))); - this._register(this.userDataSyncEnablementService.onDidChangeResourceEnablement(() => this.triggerAutoSync(['resourceEnablement']))); + this.syncTriggerDelayer = this._register(new Delayer(0)); + + if (getUserDataSyncStore(this.productService, this.configurationService)) { + this.updateAutoSync(); + this._register(Event.any(authTokenService.onDidChangeToken, this.userDataSyncEnablementService.onDidChangeEnablement)(() => this.updateAutoSync())); + this._register(Event.filter(this.userDataSyncEnablementService.onDidChangeResourceEnablement, ([, enabled]) => enabled)(() => this.triggerAutoSync([RESOURCE_ENABLEMENT_SOURCE]))); + } } - private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise { - const { enabled, reason } = await this.isAutoSyncEnabled(); - if (this.enabled === enabled) { - return; - } - - this.enabled = enabled; - if (this.enabled) { - this.logService.info('Auto Sync: Started'); - this.sync(true, auto); - return; - } else { - this.resetFailures(); - if (stopIfDisabled) { - this.userDataSyncService.stop(); - this.logService.info('Auto Sync: stopped because', reason); - } - } - - } - - private async sync(loop: boolean, auto: boolean): Promise { - if (this.enabled) { - try { - await this.userDataSyncService.sync(); - this.resetFailures(); - } catch (e) { - const error = UserDataSyncError.toUserDataSyncError(e); - if (error.code === UserDataSyncErrorCode.TurnedOff || error.code === UserDataSyncErrorCode.SessionExpired) { - this.logService.info('Auto Sync: Sync is turned off in the cloud.'); - this.logService.info('Auto Sync: Resetting the local sync state.'); - await this.userDataSyncService.resetLocal(); - this.logService.info('Auto Sync: Completed resetting the local sync state.'); - if (auto) { - this.userDataSyncEnablementService.setEnablement(false); - this._onError.fire(error); - return; - } else { - return this.sync(loop, auto); - } + private updateAutoSync(): void { + const { enabled, reason } = this.isAutoSyncEnabled(); + if (enabled) { + if (this.autoSync.value === undefined) { + this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncService, this.logService); + this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime())); + this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e))); + if (this.startAutoSync()) { + this.autoSync.value.start(); } - this.logService.error(error); - this.successiveFailures++; - this._onError.fire(error); - } - if (loop) { - await timeout(1000 * 60 * 5); - this.sync(loop, true); } } else { - this.logService.trace('Auto Sync: Not syncing as it is disabled.'); + if (this.autoSync.value !== undefined) { + this.logService.info('Auto Sync: Disabled because', reason); + this.autoSync.clear(); + } } } - private async isAutoSyncEnabled(): Promise<{ enabled: boolean, reason?: string }> { + // For tests purpose only + protected startAutoSync(): boolean { return true; } + + private isAutoSyncEnabled(): { enabled: boolean, reason?: string } { if (!this.userDataSyncEnablementService.isEnabled()) { return { enabled: false, reason: 'sync is disabled' }; } - if (this.userDataSyncService.status === SyncStatus.Uninitialized) { - return { enabled: false, reason: 'sync is not initialized' }; - } - const token = await this.authTokenService.getToken(); - if (!token) { + if (!this.authTokenService.token) { return { enabled: false, reason: 'token is not avaialable' }; } return { enabled: true }; } - private resetFailures(): void { - this.successiveFailures = 0; + private async onDidFinishSync(error: Error | undefined): Promise { + if (!error) { + // Sync finished without errors + this.successiveFailures = 0; + return; + } + + // Error while syncing + const userDataSyncError = UserDataSyncError.toUserDataSyncError(error); + if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff || userDataSyncError.code === UserDataSyncErrorCode.SessionExpired) { + this.logService.info('Auto Sync: Sync is turned off in the cloud.'); + await this.userDataSyncService.resetLocal(); + this.logService.info('Auto Sync: Did reset the local sync state.'); + this.userDataSyncEnablementService.setEnablement(false); + this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud'); + } else if (userDataSyncError.code === UserDataSyncErrorCode.LocalTooManyRequests) { + this.userDataSyncEnablementService.setEnablement(false); + this.logService.info('Auto Sync: Turned off sync because of making too many requests to server'); + } else { + this.logService.error(userDataSyncError); + this.successiveFailures++; + } + this._onError.fire(userDataSyncError); } + private sources: string[] = []; async triggerAutoSync(sources: string[]): Promise { - sources.forEach(source => this.telemetryService.publicLog2<{ source: string }, AutoSyncTriggerClassification>('sync/triggerAutoSync', { source })); - if (this.enabled) { - return this.syncDelayer.trigger(() => { - this.logService.info('Auto Sync: Triggered.'); - return this.sync(false, true); - }, this.successiveFailures - ? 1000 * 1 * Math.min(this.successiveFailures, 60) /* Delay by number of seconds as number of failures up to 1 minute */ - : 1000); - } else { - this.syncDelayer.cancel(); + if (this.autoSync.value === undefined) { + return this.syncTriggerDelayer.cancel(); } + + /* + If sync is not triggered by sync resource (triggered by other sources like window focus etc.,) or by resource enablement + then limit sync to once per 10s + */ + const hasToLimitSync = sources.indexOf(RESOURCE_ENABLEMENT_SOURCE) === -1 && ALL_SYNC_RESOURCES.every(syncResource => sources.indexOf(syncResource) === -1); + if (hasToLimitSync && this.lastSyncTriggerTime + && Math.round((new Date().getTime() - this.lastSyncTriggerTime) / 1000) < 10) { + this.logService.debug('Auto Sync Skipped: Limited to once per 10 seconds.'); + return; + } + + this.sources.push(...sources); + return this.syncTriggerDelayer.trigger(async () => { + this.telemetryService.publicLog2<{ sources: string[] }, AutoSyncClassification>('sync/triggered', { sources: this.sources }); + this.sources = []; + if (this.autoSync.value) { + await this.autoSync.value.sync('Activity'); + } + }, this.successiveFailures + ? 1000 * 1 * Math.min(Math.pow(2, this.successiveFailures), 60) /* Delay exponentially until max 1 minute */ + : 1000); /* Debounce for a second if there are no failures */ + + } + +} + +class AutoSync extends Disposable { + + private static readonly INTERVAL_SYNCING = 'Interval'; + + private readonly intervalHandler = this._register(new MutableDisposable()); + + private readonly _onDidStartSync = this._register(new Emitter()); + readonly onDidStartSync = this._onDidStartSync.event; + + private readonly _onDidFinishSync = this._register(new Emitter()); + readonly onDidFinishSync = this._onDidFinishSync.event; + + constructor( + private readonly interval: number /* in milliseconds */, + private readonly userDataSyncService: IUserDataSyncService, + private readonly logService: IUserDataSyncLogService, + ) { + super(); + } + + start(): void { + this._register(this.onDidFinishSync(() => this.waitUntilNextIntervalAndSync())); + this._register(toDisposable(() => { + this.userDataSyncService.stop(); + this.logService.info('Auto Sync: Stopped'); + })); + this.logService.info('Auto Sync: Started'); + this.sync(AutoSync.INTERVAL_SYNCING); + } + + private waitUntilNextIntervalAndSync(): void { + this.intervalHandler.value = disposableTimeout(() => this.sync(AutoSync.INTERVAL_SYNCING), this.interval); + } + + async sync(reason: string): Promise { + this.logService.info(`Auto Sync: Triggered by ${reason}`); + this._onDidStartSync.fire(); + let error: Error | undefined; + try { + await this.userDataSyncService.sync(); + } catch (e) { + this.logService.error(e); + error = e; + } + this._onDidFinishSync.fire(error); + } + + register(t: T): T { + return super._register(t); } } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 68db6dad6f9..ee1fcbcb602 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -6,7 +6,6 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { Event } from 'vs/base/common/event'; import { IExtensionIdentifier, EXTENSION_IDENTIFIER_PATTERN } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, allSettings } from 'vs/platform/configuration/common/configurationRegistry'; import { localize } from 'vs/nls'; @@ -149,7 +148,7 @@ export const enum SyncResource { export const ALL_SYNC_RESOURCES: SyncResource[] = [SyncResource.Settings, SyncResource.Keybindings, SyncResource.Snippets, SyncResource.Extensions, SyncResource.GlobalState]; export interface IUserDataManifest { - latest?: Record + latest?: Record session: string; } @@ -159,16 +158,17 @@ export interface IResourceRefHandle { } export const IUserDataSyncStoreService = createDecorator('IUserDataSyncStoreService'); +export type ServerResource = SyncResource | 'machines'; export interface IUserDataSyncStoreService { _serviceBrand: undefined; readonly userDataSyncStore: IUserDataSyncStore | undefined; - read(resource: SyncResource, oldValue: IUserData | null): Promise; - write(resource: SyncResource, content: string, ref: string | null): Promise; + read(resource: ServerResource, oldValue: IUserData | null): Promise; + write(resource: ServerResource, content: string, ref: string | null): Promise; manifest(): Promise; clear(): Promise; - getAllRefs(resource: SyncResource): Promise; - resolveContent(resource: SyncResource, ref: string): Promise; - delete(resource: SyncResource): Promise; + getAllRefs(resource: ServerResource): Promise; + resolveContent(resource: ServerResource, ref: string): Promise; + delete(resource: ServerResource): Promise; } export const IUserDataSyncBackupStoreService = createDecorator('IUserDataSyncBackupStoreService'); @@ -184,17 +184,21 @@ export interface IUserDataSyncBackupStoreService { // #region User Data Sync Error export enum UserDataSyncErrorCode { - // Server Errors - Unauthorized = 'Unauthorized', - Forbidden = 'Forbidden', + // Client Errors (>= 400 ) + Unauthorized = 'Unauthorized', /* 401 */ + Gone = 'Gone', /* 410 */ + PreconditionFailed = 'PreconditionFailed', /* 412 */ + TooLarge = 'TooLarge', /* 413 */ + UpgradeRequired = 'UpgradeRequired', /* 426 */ + PreconditionRequired = 'PreconditionRequired', /* 428 */ + TooManyRequests = 'RemoteTooManyRequests', /* 429 */ + + // Local Errors ConnectionRefused = 'ConnectionRefused', - RemotePreconditionFailed = 'RemotePreconditionFailed', - TooLarge = 'TooLarge', NoRef = 'NoRef', TurnedOff = 'TurnedOff', SessionExpired = 'SessionExpired', - - // Local Errors + LocalTooManyRequests = 'LocalTooManyRequests', LocalPreconditionFailed = 'LocalPreconditionFailed', LocalInvalidContent = 'LocalInvalidContent', LocalError = 'LocalError', @@ -223,7 +227,11 @@ export class UserDataSyncError extends Error { } -export class UserDataSyncStoreError extends UserDataSyncError { } +export class UserDataSyncStoreError extends UserDataSyncError { + constructor(message: string, code: UserDataSyncErrorCode) { + super(message, code); + } +} //#endregion @@ -233,6 +241,7 @@ export interface ISyncExtension { identifier: IExtensionIdentifier; version?: string; disabled?: boolean; + installed?: boolean; } export interface IStorageValue { @@ -259,6 +268,7 @@ export interface ISyncResourceHandle { export type Conflict = { remote: URI, local: URI }; export interface ISyncPreviewResult { + readonly isLastSyncFromCurrentMachine: boolean; readonly hasLocalChanged: boolean; readonly hasRemoteChanged: boolean; } @@ -289,6 +299,7 @@ export interface IUserDataSynchroniser { getRemoteSyncResourceHandles(): Promise; getLocalSyncResourceHandles(): Promise; getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; + getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; } //#endregion @@ -335,13 +346,14 @@ export interface IUserDataSyncService { reset(): Promise; resetLocal(): Promise; - isFirstTimeSyncWithMerge(): Promise; + isFirstTimeSyncingWithAnotherMachine(): Promise; resolveContent(resource: URI): Promise; acceptConflict(conflictResource: URI, content: string): Promise; getLocalSyncResourceHandles(resource: SyncResource): Promise; getRemoteSyncResourceHandles(resource: SyncResource): Promise; getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; + getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise; } export const IUserDataAutoSyncService = createDecorator('IUserDataAutoSyncService'); @@ -371,9 +383,6 @@ export interface IConflictSetting { //#endregion export const USER_DATA_SYNC_SCHEME = 'vscode-userdata-sync'; -export const CONTEXT_SYNC_STATE = new RawContextKey('syncStatus', SyncStatus.Uninitialized); -export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey('syncEnabled', false); - export const PREVIEW_DIR_NAME = 'preview'; export function getSyncResourceFromLocalPreview(localPreview: URI, environmentService: IEnvironmentService): SyncResource | undefined { if (localPreview.scheme === USER_DATA_SYNC_SCHEME) { @@ -382,19 +391,3 @@ export function getSyncResourceFromLocalPreview(localPreview: URI, environmentSe localPreview = localPreview.with({ scheme: environmentService.userDataSyncHome.scheme }); return ALL_SYNC_RESOURCES.filter(syncResource => isEqualOrParent(localPreview, joinPath(environmentService.userDataSyncHome, syncResource, PREVIEW_DIR_NAME)))[0]; } - -export function getSyncAreaLabel(source: SyncResource): string { - switch (source) { - case SyncResource.Settings: return localize('settings', "Settings"); - case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts"); - case SyncResource.Snippets: return localize('snippets', "User Snippets"); - case SyncResource.Extensions: return localize('extensions', "Extensions"); - case SyncResource.GlobalState: return localize('ui state label', "UI State"); - } -} - -// Commands -export const TURN_ON_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.turnOn'; -export const TURN_OFF_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.turnOff'; -export const MANAGE_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.configure'; -export const SHOW_SYNC_LOG_COMMAND_ID = 'workbench.userDataSync.actions.showLog'; diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 45643cde05f..9608c10359d 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts @@ -11,10 +11,12 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { FormattingOptions } from 'vs/base/common/jsonFormatter'; import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys'; import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService } from 'vs/platform/log/common/log'; +import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; export class UserDataSyncChannel implements IServerChannel { - constructor(private readonly service: IUserDataSyncService) { } + constructor(private readonly service: IUserDataSyncService, private readonly logService: ILogService) { } listen(_: unknown, event: string): Event { switch (event) { @@ -27,7 +29,17 @@ export class UserDataSyncChannel implements IServerChannel { throw new Error(`Event not found: ${event}`); } - call(context: any, command: string, args?: any): Promise { + async call(context: any, command: string, args?: any): Promise { + try { + const result = await this._call(context, command, args); + return result; + } catch (e) { + this.logService.error(e); + throw e; + } + } + + private _call(context: any, command: string, args?: any): Promise { switch (command) { case '_getInitialData': return Promise.resolve([this.service.status, this.service.conflicts, this.service.lastSyncTime]); case 'pull': return this.service.pull(); @@ -36,12 +48,13 @@ export class UserDataSyncChannel implements IServerChannel { case 'replace': return this.service.replace(URI.revive(args[0])); case 'reset': return this.service.reset(); case 'resetLocal': return this.service.resetLocal(); - case 'isFirstTimeSyncWithMerge': return this.service.isFirstTimeSyncWithMerge(); + case 'isFirstTimeSyncingWithAnotherMachine': return this.service.isFirstTimeSyncingWithAnotherMachine(); case 'acceptConflict': return this.service.acceptConflict(URI.revive(args[0]), args[1]); case 'resolveContent': return this.service.resolveContent(URI.revive(args[0])); case 'getLocalSyncResourceHandles': return this.service.getLocalSyncResourceHandles(args[0]); case 'getRemoteSyncResourceHandles': return this.service.getRemoteSyncResourceHandles(args[0]); case 'getAssociatedResources': return this.service.getAssociatedResources(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) }); + case 'getMachineId': return this.service.getMachineId(args[0], { created: args[1].created, uri: URI.revive(args[1].uri) }); } throw new Error('Invalid call'); } @@ -152,3 +165,24 @@ export class StorageKeysSyncRegistryChannelClient extends Disposable implements } } + +export class UserDataSyncMachinesServiceChannel implements IServerChannel { + + constructor(private readonly service: IUserDataSyncMachinesService) { } + + listen(_: unknown, event: string): Event { + throw new Error(`Event not found: ${event}`); + } + + async call(context: any, command: string, args?: any): Promise { + switch (command) { + case 'getMachines': return this.service.getMachines(); + case 'addCurrentMachine': return this.service.addCurrentMachine(args[0]); + case 'removeCurrentMachine': return this.service.removeCurrentMachine(); + case 'renameMachine': return this.service.renameMachine(args[0], args[1]); + case 'disableMachine': return this.service.disableMachine(args[0]); + } + throw new Error('Invalid call'); + } + +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncMachines.ts b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts new file mode 100644 index 00000000000..e52d8658793 --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync'; +import { localize } from 'vs/nls'; +import { IProductService } from 'vs/platform/product/common/productService'; + +interface IMachineData { + id: string; + name: string; + disabled?: boolean; +} + +interface IMachinesData { + version: number; + machines: IMachineData[]; +} + +export type IUserDataSyncMachine = Readonly & { readonly isCurrent: boolean }; + + +export const IUserDataSyncMachinesService = createDecorator('IUserDataSyncMachinesService'); +export interface IUserDataSyncMachinesService { + _serviceBrand: any; + + getMachines(manifest?: IUserDataManifest): Promise; + + addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise; + removeCurrentMachine(manifest?: IUserDataManifest): Promise; + + renameMachine(machineId: string, name: string): Promise; + disableMachine(machineId: string): Promise +} + +export class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService { + + private static readonly VERSION = 1; + private static readonly RESOURCE = 'machines'; + + _serviceBrand: any; + + private readonly currentMachineIdPromise: Promise; + private userData: IUserData | null = null; + + constructor( + @IEnvironmentService environmentService: IEnvironmentService, + @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, + @IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService, + @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, + @IProductService private readonly productService: IProductService, + ) { + super(); + this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService); + } + + async getMachines(manifest?: IUserDataManifest): Promise { + const currentMachineId = await this.currentMachineIdPromise; + const machineData = await this.readMachinesData(manifest); + return machineData.machines.map(machine => ({ ...machine, ...{ isCurrent: machine.id === currentMachineId } })); + } + + async addCurrentMachine(name: string, manifest?: IUserDataManifest): Promise { + const currentMachineId = await this.currentMachineIdPromise; + const machineData = await this.readMachinesData(manifest); + let currentMachine = machineData.machines.find(({ id }) => id === currentMachineId); + if (currentMachine) { + currentMachine.name = name; + } else { + machineData.machines.push({ id: currentMachineId, name }); + } + await this.writeMachinesData(machineData); + } + + async removeCurrentMachine(manifest?: IUserDataManifest): Promise { + const currentMachineId = await this.currentMachineIdPromise; + const machineData = await this.readMachinesData(manifest); + const updatedMachines = machineData.machines.filter(({ id }) => id !== currentMachineId); + if (updatedMachines.length !== machineData.machines.length) { + machineData.machines = updatedMachines; + await this.writeMachinesData(machineData); + } + } + + async renameMachine(machineId: string, name: string, manifest?: IUserDataManifest): Promise { + const machineData = await this.readMachinesData(manifest); + const currentMachine = machineData.machines.find(({ id }) => id === machineId); + if (currentMachine) { + currentMachine.name = name; + await this.writeMachinesData(machineData); + } + } + + async disableMachine(machineId: string): Promise { + const machineData = await this.readMachinesData(); + const machine = machineData.machines.find(({ id }) => id === machineId); + if (machine) { + machine.disabled = true; + await this.writeMachinesData(machineData); + } + } + + private async readMachinesData(manifest?: IUserDataManifest): Promise { + this.userData = await this.readUserData(manifest); + const machinesData = this.parse(this.userData); + if (machinesData.version !== UserDataSyncMachinesService.VERSION) { + throw new Error(localize('error incompatible', "Cannot read machines data as the current version is incompatible. Please update {0} and try again.", this.productService.nameLong)); + } + return machinesData; + } + + private async writeMachinesData(machinesData: IMachinesData): Promise { + const content = JSON.stringify(machinesData); + const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null); + this.userData = { ref, content }; + } + + private async readUserData(manifest?: IUserDataManifest): Promise { + if (this.userData) { + + const latestRef = manifest && manifest.latest ? manifest.latest[UserDataSyncMachinesService.RESOURCE] : undefined; + + // Last time synced resource and latest resource on server are same + if (this.userData.ref === latestRef) { + return this.userData; + } + + // There is no resource on server and last time it was synced with no resource + if (latestRef === undefined && this.userData.content === null) { + return this.userData; + } + } + + return this.userDataSyncStoreService.read(UserDataSyncMachinesService.RESOURCE, this.userData); + } + + private parse(userData: IUserData): IMachinesData { + if (userData.content !== null) { + try { + return JSON.parse(userData.content); + } catch (e) { + this.logService.error(e); + } + } + return { + version: UserDataSyncMachinesService.VERSION, + machines: [] + }; + } +} diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index ea1397a9667..5dc1999eca0 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncStoreError, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode, UserDataSyncError, SyncResourceConflicts, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { Disposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Emitter, Event } from 'vs/base/common/event'; @@ -19,9 +19,14 @@ import { URI } from 'vs/base/common/uri'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { isEqual } from 'vs/base/common/resources'; import { SnippetsSynchroniser } from 'vs/platform/userDataSync/common/snippetsSync'; +import { Throttler } from 'vs/base/common/async'; +import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { platform, PlatformToString } from 'vs/base/common/platform'; +import { escapeRegExpCharacters } from 'vs/base/common/strings'; -type SyncErrorClassification = { - source: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; +type SyncClassification = { + resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }; }; const SESSION_ID_KEY = 'sync.sessionId'; @@ -31,6 +36,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ _serviceBrand: any; + private readonly syncThrottler: Throttler; private readonly synchronisers: IUserDataSynchroniser[]; private _status: SyncStatus = SyncStatus.Uninitialized; @@ -65,9 +71,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ @IInstantiationService private readonly instantiationService: IInstantiationService, @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IStorageService private readonly storageService: IStorageService + @IStorageService private readonly storageService: IStorageService, + @IUserDataSyncMachinesService private readonly userDataSyncMachinesService: IUserDataSyncMachinesService, + @IProductService private readonly productService: IProductService ) { super(); + this.syncThrottler = new Throttler(); this.settingsSynchroniser = this._register(this.instantiationService.createInstance(SettingsSynchroniser)); this.keybindingsSynchroniser = this._register(this.instantiationService.createInstance(KeybindingsSynchroniser)); this.snippetsSynchroniser = this._register(this.instantiationService.createInstance(SnippetsSynchroniser)); @@ -87,31 +96,55 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ async pull(): Promise { await this.checkEnablement(); - for (const synchroniser of this.synchronisers) { - try { - await synchroniser.pull(); - } catch (e) { - this.handleSyncError(e, synchroniser.resource); + try { + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.pull(); + } catch (e) { + this.handleSynchronizerError(e, synchroniser.resource); + } } + this.updateLastSyncTime(); + } catch (error) { + if (error instanceof UserDataSyncError) { + this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource }); + } + throw error; } - this.updateLastSyncTime(); } async push(): Promise { await this.checkEnablement(); - for (const synchroniser of this.synchronisers) { - try { - await synchroniser.push(); - } catch (e) { - this.handleSyncError(e, synchroniser.resource); + try { + for (const synchroniser of this.synchronisers) { + try { + await synchroniser.push(); + } catch (e) { + this.handleSynchronizerError(e, synchroniser.resource); + } } + this.updateLastSyncTime(); + } catch (error) { + if (error instanceof UserDataSyncError) { + this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource }); + } + throw error; } - this.updateLastSyncTime(); } + private recoveredSettings: boolean = false; async sync(): Promise { await this.checkEnablement(); + if (!this.recoveredSettings) { + await this.settingsSynchroniser.recoverSettings(); + this.recoveredSettings = true; + } + + await this.syncThrottler.queue(() => this.doSync()); + } + + private async doSync(): Promise { const startTime = new Date().getTime(); this._syncErrors = []; try { @@ -120,11 +153,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.setStatus(SyncStatus.Syncing); } + this.telemetryService.publicLog2('sync/getmanifest'); let manifest = await this.userDataSyncStoreService.manifest(); // Server has no data but this machine was synced before if (manifest === null && await this.hasPreviouslySynced()) { - // Sync was turned off from other machine + // Sync was turned off in the cloud throw new UserDataSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff); } @@ -134,11 +168,22 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ throw new UserDataSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired); } + const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined); + const currentMachine = machines.find(machine => machine.isCurrent); + + // Check if sync was turned off from other machine + if (currentMachine?.disabled) { + // Unset the current machine + await this.userDataSyncMachinesService.removeCurrentMachine(manifest || undefined); + // Throw TurnedOff error + throw new UserDataSyncError(localize('turned off machine', "Cannot sync because syncing is turned off on this machine from another machine."), UserDataSyncErrorCode.TurnedOff); + } + for (const synchroniser of this.synchronisers) { try { await synchroniser.sync(manifest); } catch (e) { - this.handleSyncError(e, synchroniser.resource); + this.handleSynchronizerError(e, synchroniser.resource); this._syncErrors.push([synchroniser.resource, UserDataSyncError.toUserDataSyncError(e)]); } } @@ -153,9 +198,19 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.storageService.store(SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); } + if (!currentMachine) { + const name = this.computeDefaultMachineName(machines); + await this.userDataSyncMachinesService.addCurrentMachine(name, manifest || undefined); + } + this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`); this.updateLastSyncTime(); + } catch (error) { + if (error instanceof UserDataSyncError) { + this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${UserDataSyncErrorCode.TooLarge}`, { resource: error.resource }); + } + throw error; } finally { this.updateStatus(); this._onSyncErrors.fire(this._syncErrors); @@ -218,39 +273,58 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle); } - async isFirstTimeSyncWithMerge(): Promise { + getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise { + return this.getSynchroniser(resource).getMachineId(syncResourceHandle); + } + + async isFirstTimeSyncingWithAnotherMachine(): Promise { await this.checkEnablement(); + if (!await this.userDataSyncStoreService.manifest()) { return false; } - if (await this.hasPreviouslySynced()) { + + // skip global state synchronizer + const synchronizers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]; + + let hasLocalData: boolean = false; + for (const synchroniser of synchronizers) { + if (await synchroniser.hasLocalData()) { + hasLocalData = true; + break; + } + } + + if (!hasLocalData) { return false; } - if (!(await this.hasLocalData())) { - return false; - } - for (const synchroniser of [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.extensionsSynchroniser]) { + + for (const synchroniser of synchronizers) { const preview = await synchroniser.getSyncPreview(); - if (preview.hasLocalChanged || preview.hasRemoteChanged) { + if (!preview.isLastSyncFromCurrentMachine && (preview.hasLocalChanged || preview.hasRemoteChanged)) { return true; } } + return false; } async reset(): Promise { await this.checkEnablement(); await this.resetRemote(); - await this.resetLocal(); + await this.resetLocal(true); } - async resetLocal(): Promise { + async resetLocal(donotUnsetMachine?: boolean): Promise { await this.checkEnablement(); this.storageService.remove(SESSION_ID_KEY, StorageScope.GLOBAL); this.storageService.remove(LAST_SYNC_TIME_KEY, StorageScope.GLOBAL); + if (!donotUnsetMachine) { + await this.userDataSyncMachinesService.removeCurrentMachine(); + } for (const synchroniser of this.synchronisers) { try { - synchroniser.resetLocal(); + await synchroniser.resetLocal(); } catch (e) { this.logService.error(`${synchroniser.resource}: ${toErrorMessage(e)}`); this.logService.error(e); @@ -267,15 +341,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return false; } - private async hasLocalData(): Promise { - for (const synchroniser of this.synchronisers) { - if (await synchroniser.hasLocalData()) { - return true; - } - } - return false; - } - private async resetRemote(): Promise { await this.checkEnablement(); try { @@ -331,13 +396,17 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } } - private handleSyncError(e: Error, source: SyncResource): void { - if (e instanceof UserDataSyncStoreError) { + private handleSynchronizerError(e: Error, source: SyncResource): void { + if (e instanceof UserDataSyncError) { switch (e.code) { case UserDataSyncErrorCode.TooLarge: - this.telemetryService.publicLog2<{ source: string }, SyncErrorClassification>('sync/errorTooLarge', { source }); + case UserDataSyncErrorCode.TooManyRequests: + case UserDataSyncErrorCode.LocalTooManyRequests: + case UserDataSyncErrorCode.Gone: + case UserDataSyncErrorCode.UpgradeRequired: + case UserDataSyncErrorCode.Incompatible: + throw e; } - throw e; } this.logService.error(e); this.logService.error(`${source}: ${toErrorMessage(e)}`); @@ -348,6 +417,20 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ .map(s => ({ syncResource: s.resource, conflicts: s.conflicts })); } + private computeDefaultMachineName(machines: IUserDataSyncMachine[]): string { + const namePrefix = `${this.productService.nameLong} (${PlatformToString(platform)})`; + const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`); + + let nameIndex = 0; + for (const machine of machines) { + const matches = nameRegEx.exec(machine.name); + const index = matches ? parseInt(matches[1]) : 0; + nameIndex = index > nameIndex ? index : nameIndex; + } + + return `${namePrefix} #${nameIndex + 1}`; + } + getSynchroniser(source: SyncResource): IUserDataSynchroniser { return this.synchronisers.filter(s => s.resource === source)[0]; } diff --git a/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts b/src/vs/platform/userDataSync/common/userDataSyncStoreService.ts index cc98e7c33e1..d58d6078292 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, getUserDataSyncStore, SyncResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle } from 'vs/platform/userDataSync/common/userDataSync'; import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request'; import { joinPath, relativePath } from 'vs/base/common/resources'; import { CancellationToken } from 'vs/base/common/cancellation'; @@ -22,6 +22,8 @@ import { isWeb } from 'vs/base/common/platform'; const USER_SESSION_ID_KEY = 'sync.user-session-id'; const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id'; +const REQUEST_SESSION_LIMIT = 100; +const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */ export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService { @@ -29,6 +31,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn readonly userDataSyncStore: IUserDataSyncStore | undefined; private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>; + private readonly session: RequestsSession; constructor( @IProductService productService: IProductService, @@ -49,11 +52,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn 'X-Client-Version': productService.version, 'X-Machine-Id': uuid }; + if (productService.commit) { + headers['X-Client-Commit'] = productService.commit; + } return headers; }); + + /* A requests session that limits requests per sessions */ + this.session = new RequestsSession(REQUEST_SESSION_LIMIT, REQUEST_SESSION_INTERVAL, this.requestService); } - async getAllRefs(resource: SyncResource): Promise { + async getAllRefs(resource: ServerResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -61,17 +70,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const uri = joinPath(this.userDataSyncStore.url, 'resource', resource); const headers: IHeaders = {}; - const context = await this.request({ type: 'GET', url: uri.toString(), headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const result = await asJson<{ url: string, created: number }[]>(context) || []; return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ })); } - async resolveContent(resource: SyncResource, ref: string): Promise { + async resolveContent(resource: ServerResource, ref: string): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -80,17 +89,17 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const headers: IHeaders = {}; headers['Cache-Control'] = 'no-cache'; - const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const content = await asText(context); return content; } - async delete(resource: SyncResource): Promise { + async delete(resource: ServerResource): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -98,14 +107,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString(); const headers: IHeaders = {}; - const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, undefined); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } } - async read(resource: SyncResource, oldValue: IUserData | null): Promise { + async read(resource: ServerResource, oldValue: IUserData | null): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -118,7 +127,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn headers['If-None-Match'] = oldValue.ref; } - const context = await this.request({ type: 'GET', url, headers }, resource, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); if (context.res.statusCode === 304) { // There is no new value. Hence return the old value. @@ -126,18 +135,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const ref = context.res.headers['etag']; if (!ref) { - throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource); + throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef); } const content = await asText(context); return { ref, content }; } - async write(resource: SyncResource, data: string, ref: string | null): Promise { + async write(resource: ServerResource, data: string, ref: string | null): Promise { if (!this.userDataSyncStore) { throw new Error('No settings sync store url configured.'); } @@ -148,15 +157,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn headers['If-Match'] = ref; } - const context = await this.request({ type: 'POST', url, data, headers }, resource, CancellationToken.None); + const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None); if (!isSuccess(context)) { - throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, resource); + throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } const newRef = context.res.headers['etag']; if (!newRef) { - throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, resource); + throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef); } return newRef; } @@ -169,7 +178,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'manifest').toString(); const headers: IHeaders = { 'Content-Type': 'application/json' }; - const context = await this.request({ type: 'GET', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None); if (!isSuccess(context)) { throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); } @@ -203,7 +212,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn const url = joinPath(this.userDataSyncStore.url, 'resource').toString(); const headers: IHeaders = { 'Content-Type': 'text/plain' }; - const context = await this.request({ type: 'DELETE', url, headers }, undefined, CancellationToken.None); + const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None); if (!isSuccess(context)) { throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown); @@ -218,10 +227,10 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL); } - private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise { - const authToken = await this.authTokenService.getToken(); + private async request(options: IRequestOptions, token: CancellationToken): Promise { + const authToken = this.authTokenService.token; if (!authToken) { - throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, source); + throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized); } const commonHeaders = await this.commonHeadersPromise; @@ -237,27 +246,38 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn let context; try { - context = await this.requestService.request(options, token); + context = await this.session.request(options, token); this.logService.trace('Request finished', { url: options.url, status: context.res.statusCode }); } catch (e) { - throw new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused, source); + if (!(e instanceof UserDataSyncStoreError)) { + e = new UserDataSyncStoreError(`Connection refused for the request '${options.url?.toString()}'.`, UserDataSyncErrorCode.ConnectionRefused); + } + throw e; } if (context.res.statusCode === 401) { this.authTokenService.sendTokenFailed(); - throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized, source); + throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized); } - if (context.res.statusCode === 403) { - throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, source); + 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); } 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.RemotePreconditionFailed, source); + 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); } if (context.res.statusCode === 413) { - throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge, source); + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too large payload (413).`, UserDataSyncErrorCode.TooLarge); + } + + if (context.res.statusCode === 426) { + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed with status Upgrade Required (426). Please upgrade the client and try again.`, UserDataSyncErrorCode.UpgradeRequired); + } + + if (context.res.statusCode === 429) { + throw new UserDataSyncStoreError(`${options.type} request '${options.url?.toString()}' failed because of too many requests (429).`, UserDataSyncErrorCode.TooManyRequests); } return context; @@ -278,3 +298,40 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn } } + +export class RequestsSession { + + private count: number = 0; + private startTime: Date | undefined = undefined; + + constructor( + private readonly limit: number, + private readonly interval: number, /* in ms */ + private readonly requestService: IRequestService, + ) { } + + request(options: IRequestOptions, token: CancellationToken): Promise { + if (this.isExpired()) { + this.reset(); + } + + if (this.count >= this.limit) { + throw new UserDataSyncStoreError(`Too many requests. Allowed only ${this.limit} requests in ${this.interval / (1000 * 60)} minutes.`, UserDataSyncErrorCode.LocalTooManyRequests); + } + + this.startTime = this.startTime || new Date(); + this.count++; + + return this.requestService.request(options, token); + } + + private isExpired(): boolean { + return this.startTime !== undefined && new Date().getTime() - this.startTime.getTime() > this.interval; + } + + private reset(): void { + this.count = 0; + this.startTime = undefined; + } + +} diff --git a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts index 6fa694e1d27..433968985ff 100644 --- a/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/electron-browser/userDataAutoSyncService.ts @@ -5,10 +5,12 @@ import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; import { Event } from 'vs/base/common/event'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IProductService } from 'vs/platform/product/common/productService'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @@ -19,8 +21,10 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService { @IUserDataSyncLogService logService: IUserDataSyncLogService, @IAuthenticationTokenService authTokenService: IAuthenticationTokenService, @ITelemetryService telemetryService: ITelemetryService, + @IProductService productService: IProductService, + @IConfigurationService configurationService: IConfigurationService, ) { - super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService); + super(userDataSyncEnablementService, userDataSyncService, logService, authTokenService, telemetryService, productService, configurationService); this._register(Event.debounce(Event.any( Event.map(electronService.onWindowFocus, () => 'windowFocus'), diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 3bd7057806f..a66b9d45f8c 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -7,13 +7,13 @@ import * as assert from 'assert'; import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; -suite('ExtensionsMerge - No Conflicts', () => { +suite('ExtensionsMerge', () => { - test('merge returns local extension if remote does not exist', async () => { + test('merge returns local extension if remote does not exist', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, [], []); @@ -24,15 +24,15 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, localExtensions); }); - test('merge returns local extension if remote does not exist with ignored extensions', async () => { + test('merge returns local extension if remote does not exist with ignored extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, [], ['a']); @@ -43,15 +43,15 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', async () => { + test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, [], ['A']); @@ -62,19 +62,19 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge returns local extension if remote does not exist with skipped extensions', async () => { + test('merge returns local extension if remote does not exist with skipped extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const skippedExtension: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, skippedExtension, []); @@ -85,18 +85,18 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge returns local extension if remote does not exist with skipped and ignored extensions', async () => { + test('merge returns local extension if remote does not exist with skipped and ignored extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const skippedExtension: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, skippedExtension, ['a']); @@ -107,180 +107,180 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when there is no base', async () => { + test('merge local and remote extensions when there is no base', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when there is no base and with ignored extensions', async () => { + test('merge local and remote extensions when there is no base and with ignored extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, null, [], ['a']); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when remote is moved forwarded', async () => { + test('merge local and remote extensions when remote is moved forwarded', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote is moved forwarded with disabled extension', async () => { + test('merge local and remote extensions when remote is moved forwarded with disabled extension', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, - { identifier: { id: 'd', uuid: 'd' }, disabled: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]); - assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true }]); + assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true }]); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote moved forwarded with ignored extensions', async () => { + test('merge local and remote extensions when remote moved forwarded with ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote is moved forwarded with skipped extensions', async () => { + test('merge local and remote extensions when remote is moved forwarded with skipped extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', async () => { + test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']); - assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when local is moved forwarded', async () => { + test('merge local and remote extensions when local is moved forwarded', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); @@ -291,19 +291,19 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, localExtensions); }); - test('merge local and remote extensions when local is moved forwarded with disabled extensions', async () => { + test('merge local and remote extensions when local is moved forwarded with disabled extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' }, disabled: true }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, disabled: true, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); @@ -314,18 +314,18 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, localExtensions); }); - test('merge local and remote extensions when local is moved forwarded with ignored settings', async () => { + test('merge local and remote extensions when local is moved forwarded with ignored settings', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['b']); @@ -334,30 +334,30 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, [ - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]); }); - test('merge local and remote extensions when local is moved forwarded with skipped extensions', async () => { + test('merge local and remote extensions when local is moved forwarded with skipped extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); @@ -368,25 +368,25 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', async () => { + test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']); @@ -397,54 +397,54 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded', async () => { + test('merge local and remote extensions when both moved forwarded', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded with ignored extensions', async () => { + test('merge local and remote extensions when both moved forwarded with ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']); @@ -455,58 +455,58 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded with skipped extensions', async () => { + test('merge local and remote extensions when both moved forwarded with skipped extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', async () => { + test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']); @@ -517,30 +517,134 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge when remote extension has no uuid and different extension id case', async () => { + test('merge when remote extension has no uuid and different extension id case', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'A' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'A' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'A', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'A', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); + test('merge when remote extension is not an installed extension', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, null, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when remote extension is not an installed extension but is an installed extension locally', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, null, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, localExtensions); + }); + + test('merge when an extension is not an installed extension remotely and does not exist locally', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'b', uuid: 'b' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when an extension is an installed extension remotely but not locally and updated locally', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, disabled: true }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + ]; + const expected: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true }, + ]; + + const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, expected); + }); + + test('merge when an extension is an installed extension remotely but not locally and updated remotely', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true }, + ]; + + const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, remoteExtensions); + assert.deepEqual(actual.remote, null); + }); + + test('merge not installed extensions', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'b', uuid: 'b' } }, + ]; + const expected: ISyncExtension[] = [ + { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'a', uuid: 'a' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, null, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, expected); + }); }); diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index c620e9ba876..644011b982d 100644 --- a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts @@ -1495,8 +1495,102 @@ suite('SettingsMerge - Add Setting', () => { assert.equal(actual, sourceContent); }); + + test('Insert after a comment with comma separator of previous setting and no next nodes ', () => { + + const sourceContent = ` +{ + "a": 1 + // this is comment for a + , + "b": 2 +}`; + const targetContent = ` +{ + "a": 1 + // this is comment for a + , +}`; + + const expected = ` +{ + "a": 1 + // this is comment for a + , + "b": 2 +}`; + + const actual = addSetting('b', sourceContent, targetContent, formattingOptions); + + assert.equal(actual, expected); + }); + + test('Insert after a comment with comma separator of previous setting and there is a setting after ', () => { + + const sourceContent = ` +{ + "a": 1 + // this is comment for a + , + "b": 2, + "c": 3 +}`; + const targetContent = ` +{ + "a": 1 + // this is comment for a + , + "c": 3 +}`; + + const expected = ` +{ + "a": 1 + // this is comment for a + , + "b": 2, + "c": 3 +}`; + + const actual = addSetting('b', sourceContent, targetContent, formattingOptions); + + assert.equal(actual, expected); + }); + + test('Insert after a comment with comma separator of previous setting and there is a comment after ', () => { + + const sourceContent = ` +{ + "a": 1 + // this is comment for a + , + "b": 2 + // this is a comment +}`; + const targetContent = ` +{ + "a": 1 + // this is comment for a + , + // this is a comment +}`; + + const expected = ` +{ + "a": 1 + // this is comment for a + , + "b": 2 + // this is a comment +}`; + + const actual = addSetting('b', sourceContent, targetContent, formattingOptions); + + assert.equal(actual, expected); + }); }); + function stringify(value: any): string { return JSON.stringify(value, null, '\t'); } diff --git a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts index a9e62326673..4ccf9800534 100644 --- a/src/vs/platform/userDataSync/test/common/settingsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/settingsSync.test.ts @@ -15,6 +15,7 @@ import { VSBuffer } from 'vs/base/common/buffer'; import { ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { Event } from 'vs/base/common/event'; suite('SettingsSync', () => { @@ -267,6 +268,24 @@ suite('SettingsSync', () => { }`); }); + test('local change event is triggered when settings are changed', async () => { + const content = + `{ + "files.autoSave": "afterDelay", + "files.simpleDialog.enable": true, +}`; + + await updateSettings(content); + await testObject.sync(await client.manifest()); + + const promise = Event.toPromise(testObject.onDidChangeLocal); + await updateSettings(`{ + "files.autoSave": "off", + "files.simpleDialog.enable": true, +}`); + await promise; + }); + test('do not sync ignored settings', async () => { const settingsContent = `{ diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 08241ecb441..24c95ffeac4 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -42,8 +42,8 @@ class TestSynchroniser extends AbstractSynchroniser { protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { } async apply(ref: string): Promise { - ref = await this.userDataSyncStoreService.write(this.resource, '', ref); - await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); + const remoteUserData = await this.updateRemoteUserData('', ref); + await this.updateLastSyncUserData(remoteUserData); } async stop(): Promise { @@ -51,8 +51,18 @@ class TestSynchroniser extends AbstractSynchroniser { this.syncBarrier.open(); } + async triggerLocalChange(): Promise { + super.triggerLocalChange(); + } + + onDidTriggerLocalChangeCall: Emitter = this._register(new Emitter()); + protected async doTriggerLocalChange(): Promise { + await super.doTriggerLocalChange(); + this.onDidTriggerLocalChangeCall.fire(); + } + protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { - return { hasLocalChanged: false, hasRemoteChanged: false }; + return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false }; } } @@ -203,5 +213,18 @@ suite('TestSynchronizer', () => { ]); }); + test('no requests are made to server when local change is triggered', async () => { + const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); + testObject.syncBarrier.open(); + await testObject.sync(await client.manifest()); + + server.reset(); + const promise = Event.toPromise(testObject.onDidTriggerLocalChangeCall.event); + await testObject.triggerLocalChange(); + + await promise; + assert.deepEqual(server.requests, []); + }); + }); diff --git a/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts new file mode 100644 index 00000000000..1e1a4ddbf00 --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataAutoSyncService.test.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { UserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService'; +import { IUserDataSyncService, SyncResource, IUserDataSyncEnablementService } from 'vs/platform/userDataSync/common/userDataSync'; + +class TestUserDataAutoSyncService extends UserDataAutoSyncService { + protected startAutoSync(): boolean { return false; } +} + +suite('UserDataAutoSyncService', () => { + + const disposableStore = new DisposableStore(); + + teardown(() => disposableStore.clear()); + + test('test auto sync with sync resource change triggers sync', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + + // Sync once and reset requests + await client.instantiationService.get(IUserDataSyncService).sync(); + target.reset(); + + client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true); + const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + + // Trigger auto sync with settings change + await testObject.triggerAutoSync([SyncResource.Settings]); + + // Make sure only one request is made + assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + }); + + test('test auto sync with sync resource change triggers sync for every change', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + + // Sync once and reset requests + await client.instantiationService.get(IUserDataSyncService).sync(); + target.reset(); + + client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true); + const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + + // Trigger auto sync with settings change multiple times + for (let counter = 0; counter < 2; counter++) { + await testObject.triggerAutoSync([SyncResource.Settings]); + } + + assert.deepEqual(target.requests, [ + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} } + ]); + }); + + test('test auto sync with non sync resource change triggers sync', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + + // Sync once and reset requests + await client.instantiationService.get(IUserDataSyncService).sync(); + target.reset(); + + client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true); + const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + + // Trigger auto sync with window focus once + await testObject.triggerAutoSync(['windowFocus']); + + // Make sure only one request is made + assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + }); + + test('test auto sync with non sync resource change does not trigger continuous syncs', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + + // Sync once and reset requests + await client.instantiationService.get(IUserDataSyncService).sync(); + target.reset(); + + client.instantiationService.get(IUserDataSyncEnablementService).setEnablement(true); + const testObject: UserDataAutoSyncService = client.instantiationService.createInstance(TestUserDataAutoSyncService); + + // Trigger auto sync with window focus multiple times + for (let counter = 0; counter < 2; counter++) { + await testObject.triggerAutoSync(['windowFocus']); + } + + // Make sure only one request is made + assert.deepEqual(target.requests, [{ type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }]); + }); + + +}); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts b/src/vs/platform/userDataSync/test/common/userDataSyncClient.ts index 2810cb1d5a9..c9f9813a7cc 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, IUserDataSyncEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource } 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'; @@ -37,6 +37,7 @@ import product from 'vs/platform/product/common/product'; import { IProductService } from 'vs/platform/product/common/productService'; import { UserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSyncBackupStoreService'; import { IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; +import { IUserDataSyncMachinesService, UserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines'; export class UserDataSyncClient extends Disposable { @@ -83,12 +84,13 @@ export class UserDataSyncClient extends Disposable { this.instantiationService.stub(IRequestService, this.testServer); this.instantiationService.stub(IAuthenticationTokenService, >{ onDidChangeToken: new Emitter().event, - async getToken() { return { authenticationProviderId: 'id', token: 'token' }; } + token: { authenticationProviderId: 'id', token: 'token' } }); this.instantiationService.stub(IUserDataSyncLogService, logService); this.instantiationService.stub(ITelemetryService, NullTelemetryService); this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService)); + this.instantiationService.stub(IUserDataSyncMachinesService, this.instantiationService.createInstance(UserDataSyncMachinesService)); this.instantiationService.stub(IUserDataSyncBackupStoreService, this.instantiationService.createInstance(UserDataSyncBackupStoreService)); this.instantiationService.stub(IUserDataSyncUtilService, new TestUserDataSyncUtilService()); this.instantiationService.stub(IUserDataSyncEnablementService, this.instantiationService.createInstance(UserDataSyncEnablementService)); @@ -130,13 +132,15 @@ export class UserDataSyncClient extends Disposable { } +const ALL_SERVER_RESOURCES: ServerResource[] = [...ALL_SYNC_RESOURCES, 'machines']; + export class UserDataSyncTestServer implements IRequestService { _serviceBrand: any; readonly url: string = 'http://host:3000'; private session: string | null = null; - private readonly data: Map = new Map(); + private readonly data: Map = new Map(); private _requests: { url: string, type: string, headers?: IHeaders }[] = []; get requests(): { url: string, type: string, headers?: IHeaders }[] { return this._requests; } @@ -188,7 +192,7 @@ export class UserDataSyncTestServer implements IRequestService { private async getManifest(headers?: IHeaders): Promise { if (this.session) { - const latest: Record = Object.create({}); + const latest: Record = Object.create({}); const manifest: IUserDataManifest = { session: this.session, latest }; this.data.forEach((value, key) => latest[key] = value.ref); return this.toResponse(200, { 'Content-Type': 'application/json' }, JSON.stringify(manifest)); @@ -197,7 +201,7 @@ export class UserDataSyncTestServer implements IRequestService { } private async getLatestData(resource: string, headers: IHeaders = {}): Promise { - const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); + const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource); if (resourceKey) { const data = this.data.get(resourceKey); if (!data) { @@ -215,7 +219,7 @@ export class UserDataSyncTestServer implements IRequestService { if (!this.session) { this.session = generateUuid(); } - const resourceKey = ALL_SYNC_RESOURCES.find(key => key === resource); + const resourceKey = ALL_SERVER_RESOURCES.find(key => key === resource); if (resourceKey) { const data = this.data.get(resourceKey); if (headers['If-Match'] !== undefined && headers['If-Match'] !== (data ? data.ref : '0')) { diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index b7fbb82ed1c..0493a52be3f 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts @@ -31,6 +31,8 @@ suite('UserDataSyncService', () => { assert.deepEqual(target.requests, [ // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} }, // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } }, @@ -47,6 +49,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } }, ]); }); @@ -64,6 +68,8 @@ suite('UserDataSyncService', () => { assert.deepEqual(target.requests, [ // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} }, // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, // Keybindings @@ -76,6 +82,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } }, ]); }); @@ -95,7 +103,7 @@ suite('UserDataSyncService', () => { // Sync (pull) from the test client target.reset(); - await testObject.isFirstTimeSyncWithMerge(); + await testObject.isFirstTimeSyncingWithAnotherMachine(); await testObject.pull(); assert.deepEqual(target.requests, [ @@ -135,7 +143,7 @@ suite('UserDataSyncService', () => { // Sync (pull) from the test client target.reset(); - await testObject.isFirstTimeSyncWithMerge(); + await testObject.isFirstTimeSyncingWithAnotherMachine(); await testObject.pull(); assert.deepEqual(target.requests, [ @@ -167,7 +175,7 @@ suite('UserDataSyncService', () => { // Sync (merge) from the test client target.reset(); - await testObject.isFirstTimeSyncWithMerge(); + await testObject.isFirstTimeSyncingWithAnotherMachine(); await testObject.sync(); assert.deepEqual(target.requests, [ @@ -179,11 +187,13 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, /* sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '1' } }, ]); }); @@ -209,7 +219,7 @@ suite('UserDataSyncService', () => { // Sync (merge) from the test client target.reset(); - await testObject.isFirstTimeSyncWithMerge(); + await testObject.isFirstTimeSyncingWithAnotherMachine(); await testObject.sync(); assert.deepEqual(target.requests, [ @@ -219,6 +229,7 @@ suite('UserDataSyncService', () => { /* first time sync */ { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '1' } }, { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, @@ -227,6 +238,7 @@ suite('UserDataSyncService', () => { { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '1' } }, { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '1' } }, ]); }); @@ -366,6 +378,8 @@ suite('UserDataSyncService', () => { assert.deepEqual(target.requests, [ // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'GET', url: `${target.url}/v1/resource/machines/latest`, headers: { 'If-None-Match': '1' } }, // Settings { type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} }, { type: 'POST', url: `${target.url}/v1/resource/settings`, headers: { 'If-Match': '0' } }, @@ -382,6 +396,8 @@ suite('UserDataSyncService', () => { { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } }, ]); }); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts index e46525cf2d9..be2912e8f56 100644 --- a/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -4,11 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { IUserDataSyncStoreService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { IProductService } from 'vs/platform/product/common/productService'; import { isWeb } from 'vs/base/common/platform'; +import { RequestsSession } from 'vs/platform/userDataSync/common/userDataSyncStoreService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { IRequestService } from 'vs/platform/request/common/request'; +import { newWriteableBufferStream } from 'vs/base/common/buffer'; +import { timeout } from 'vs/base/common/async'; suite('UserDataSyncStoreService', () => { @@ -316,5 +321,51 @@ suite('UserDataSyncStoreService', () => { assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); }); +}); + +suite('UserDataSyncRequestsSession', () => { + + const requestService: IRequestService = { + _serviceBrand: undefined, + async request() { return { res: { headers: {} }, stream: newWriteableBufferStream() }; }, + async resolveProxy() { return undefined; } + }; + + test('too many requests are thrown when limit exceeded', async () => { + const testObject = new RequestsSession(1, 500, requestService); + await testObject.request({}, CancellationToken.None); + + try { + await testObject.request({}, CancellationToken.None); + } catch (error) { + assert.ok(error instanceof UserDataSyncStoreError); + assert.equal((error).code, UserDataSyncErrorCode.LocalTooManyRequests); + return; + } + assert.fail('Should fail with limit exceeded'); + }); + + test('requests are handled after session is expired', async () => { + const testObject = new RequestsSession(1, 500, requestService); + await testObject.request({}, CancellationToken.None); + await timeout(600); + await testObject.request({}, CancellationToken.None); + }); + + test('too many requests are thrown after session is expired', async () => { + const testObject = new RequestsSession(1, 500, requestService); + await testObject.request({}, CancellationToken.None); + await timeout(600); + await testObject.request({}, CancellationToken.None); + + try { + await testObject.request({}, CancellationToken.None); + } catch (error) { + assert.ok(error instanceof UserDataSyncStoreError); + assert.equal((error).code, UserDataSyncErrorCode.LocalTooManyRequests); + return; + } + assert.fail('Should fail with limit exceeded'); + }); }); diff --git a/src/vs/workbench/contrib/webview/common/mimeTypes.ts b/src/vs/platform/webview/common/mimeTypes.ts similarity index 78% rename from src/vs/workbench/contrib/webview/common/mimeTypes.ts rename to src/vs/platform/webview/common/mimeTypes.ts index 0f1f583d451..f9a54488d19 100644 --- a/src/vs/workbench/contrib/webview/common/mimeTypes.ts +++ b/src/vs/platform/webview/common/mimeTypes.ts @@ -20,7 +20,7 @@ const webviewMimeTypes = new Map([ ['.xml', 'application/xml'], ]); -export function getWebviewContentMimeType(normalizedPath: URI): string { - const ext = extname(normalizedPath.fsPath).toLowerCase(); - return webviewMimeTypes.get(ext) || getMediaMime(normalizedPath.fsPath) || MIME_UNKNOWN; +export function getWebviewContentMimeType(resource: URI): string { + const ext = extname(resource.fsPath).toLowerCase(); + return webviewMimeTypes.get(ext) || getMediaMime(resource.fsPath) || MIME_UNKNOWN; } diff --git a/src/vs/platform/webview/common/resourceLoader.ts b/src/vs/platform/webview/common/resourceLoader.ts new file mode 100644 index 00000000000..18c79e43918 --- /dev/null +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer'; +import { isUNC } from 'vs/base/common/extpath'; +import { Schemas } from 'vs/base/common/network'; +import { sep } from 'vs/base/common/path'; +import { URI } from 'vs/base/common/uri'; +import { IFileService } from 'vs/platform/files/common/files'; +import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; +import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes'; + +export namespace WebviewResourceResponse { + export enum Type { Success, Failed, AccessDenied } + + export class StreamSuccess { + readonly type = Type.Success; + + constructor( + public readonly stream: VSBufferReadableStream, + public readonly mimeType: string + ) { } + } + + export class BufferSuccess { + readonly type = Type.Success; + + constructor( + public readonly buffer: VSBuffer, + public readonly mimeType: string + ) { } + } + + export const Failed = { type: Type.Failed } as const; + export const AccessDenied = { type: Type.AccessDenied } as const; + + export type BufferResponse = BufferSuccess | typeof Failed | typeof AccessDenied; + export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied; +} + +export async function loadLocalResource( + requestUri: URI, + fileService: IFileService, + extensionLocation: URI | undefined, + roots: ReadonlyArray +): Promise { + const resourceToLoad = getResourceToLoad(requestUri, extensionLocation, roots); + if (!resourceToLoad) { + return WebviewResourceResponse.AccessDenied; + } + + try { + const data = await fileService.readFile(resourceToLoad); + const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime + return new WebviewResourceResponse.BufferSuccess(data.value, mime); + } catch (err) { + console.log(err); + return WebviewResourceResponse.Failed; + } +} + +export async function loadLocalResourceStream( + requestUri: URI, + fileService: IFileService, + extensionLocation: URI | undefined, + roots: ReadonlyArray +): Promise { + const resourceToLoad = getResourceToLoad(requestUri, extensionLocation, roots); + if (!resourceToLoad) { + return WebviewResourceResponse.AccessDenied; + } + + try { + const contents = await fileService.readFileStream(resourceToLoad); + const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime + return new WebviewResourceResponse.StreamSuccess(contents.value, mime); + } catch (err) { + console.log(err); + return WebviewResourceResponse.Failed; + } +} + +function getResourceToLoad( + requestUri: URI, + extensionLocation: URI | undefined, + roots: ReadonlyArray +): URI | undefined { + const normalizedPath = normalizeRequestPath(requestUri); + + for (const root of roots) { + if (!containsResource(root, normalizedPath)) { + continue; + } + + if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) { + return URI.from({ + scheme: REMOTE_HOST_SCHEME, + authority: extensionLocation.authority, + path: '/vscode-resource', + query: JSON.stringify({ + requestResourcePath: normalizedPath.path + }) + }); + } else { + return normalizedPath; + } + } + + return undefined; +} + +function normalizeRequestPath(requestUri: URI) { + if (requestUri.scheme !== Schemas.vscodeWebviewResource) { + return requestUri; + } + + // The `vscode-webview-resource` scheme has the following format: + // + // vscode-webview-resource://id/scheme//authority?/path + // + const resourceUri = URI.parse(requestUri.path.replace(/^\/([a-z0-9\-]+)\/{1,2}/i, '$1://')); + + return resourceUri.with({ + query: requestUri.query, + fragment: requestUri.fragment + }); +} + +function containsResource(root: URI, resource: URI): boolean { + let rootPath = root.fsPath + (root.fsPath.endsWith(sep) ? '' : sep); + let resourceFsPath = resource.fsPath; + + if (isUNC(root.fsPath) && isUNC(resource.fsPath)) { + rootPath = rootPath.toLowerCase(); + resourceFsPath = resourceFsPath.toLowerCase(); + } + + return resourceFsPath.startsWith(rootPath); +} diff --git a/src/vs/platform/webview/common/webviewManagerService.ts b/src/vs/platform/webview/common/webviewManagerService.ts new file mode 100644 index 00000000000..5a67ccbc3d4 --- /dev/null +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { UriComponents } from 'vs/base/common/uri'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; + +export const IWebviewManagerService = createDecorator('webviewManagerService'); + +export interface IWebviewManagerService { + _serviceBrand: unknown; + + registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise; + unregisterWebview(id: string): Promise; + updateLocalResourceRoots(id: string, roots: UriComponents[]): Promise; + + setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise; +} + +export interface RegisterWebviewMetadata { + readonly extensionLocation: UriComponents | undefined; + readonly localResourceRoots: readonly UriComponents[]; +} diff --git a/src/vs/platform/webview/electron-main/webviewMainService.ts b/src/vs/platform/webview/electron-main/webviewMainService.ts new file mode 100644 index 00000000000..93990e16fbd --- /dev/null +++ b/src/vs/platform/webview/electron-main/webviewMainService.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 { webContents } from 'electron'; +import { IWebviewManagerService, RegisterWebviewMetadata } from 'vs/platform/webview/common/webviewManagerService'; +import { WebviewProtocolProvider } from 'vs/platform/webview/electron-main/webviewProtocolProvider'; +import { IFileService } from 'vs/platform/files/common/files'; +import { UriComponents, URI } from 'vs/base/common/uri'; + +export class WebviewMainService implements IWebviewManagerService { + + _serviceBrand: undefined; + + private protocolProvider: WebviewProtocolProvider; + + constructor( + @IFileService fileService: IFileService, + ) { + this.protocolProvider = new WebviewProtocolProvider(fileService); + } + + public async registerWebview(id: string, metadata: RegisterWebviewMetadata): Promise { + this.protocolProvider.registerWebview(id, + metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined, + metadata.localResourceRoots.map((x: UriComponents) => URI.from(x)) + ); + } + + public async unregisterWebview(id: string): Promise { + this.protocolProvider.unreigsterWebview(id); + } + + public async updateLocalResourceRoots(id: string, roots: UriComponents[]): Promise { + this.protocolProvider.updateLocalResourceRoots(id, roots.map((x: UriComponents) => URI.from(x))); + } + + public async setIgnoreMenuShortcuts(webContentsId: number, enabled: boolean): Promise { + const contents = webContents.fromId(webContentsId); + if (!contents) { + throw new Error(`Invalid webContentsId: ${webContentsId}`); + } + if (!contents.isDestroyed()) { + contents.setIgnoreMenuShortcuts(enabled); + } + } +} diff --git a/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts new file mode 100644 index 00000000000..171439f0975 --- /dev/null +++ b/src/vs/platform/webview/electron-main/webviewProtocolProvider.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { protocol } from 'electron'; +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { URI } from 'vs/base/common/uri'; +import { streamToNodeReadable } from 'vs/base/node/stream'; +import { IFileService } from 'vs/platform/files/common/files'; +import { loadLocalResourceStream, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader'; + +export class WebviewProtocolProvider extends Disposable { + + private readonly webviewMetadata = new Map(); + + constructor( + @IFileService private readonly fileService: IFileService, + ) { + super(); + + protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise => { + try { + const uri = URI.parse(request.url); + + const id = uri.authority; + const metadata = this.webviewMetadata.get(id); + if (metadata) { + const result = await loadLocalResourceStream(uri, this.fileService, metadata.extensionLocation, metadata.localResourceRoots); + if (result.type === WebviewResourceResponse.Type.Success) { + return callback({ + statusCode: 200, + data: streamToNodeReadable(result.stream), + headers: { + 'Content-Type': result.mimeType, + 'Access-Control-Allow-Origin': '*', + } + }); + } + + if (result.type === WebviewResourceResponse.Type.AccessDenied) { + console.error('Webview: Cannot load resource outside of protocol root'); + return callback({ data: null, statusCode: 401 }); + } + } + } catch { + // noop + } + + return callback({ data: null, statusCode: 404 }); + }); + + this._register(toDisposable(() => protocol.unregisterProtocol(Schemas.vscodeWebviewResource))); + } + + public registerWebview(id: string, extensionLocation: URI | undefined, localResourceRoots: readonly URI[]): void { + this.webviewMetadata.set(id, { extensionLocation, localResourceRoots }); + } + + public unreigsterWebview(id: string): void { + this.webviewMetadata.delete(id); + } + + public updateLocalResourceRoots(id: string, localResourceRoots: readonly URI[]) { + const entry = this.webviewMetadata.get(id); + if (entry) { + this.webviewMetadata.set(id, { + extensionLocation: entry.extensionLocation, + localResourceRoots: localResourceRoots, + }); + } + } +} diff --git a/src/vs/platform/windows/common/windows.ts b/src/vs/platform/windows/common/windows.ts index c8b432f5416..dc42bfc3743 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -8,6 +8,7 @@ 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 { forceReuseWindow?: boolean; @@ -18,6 +19,26 @@ export interface IOpenWindowOptions extends IBaseOpenWindowsOptions { preferNewWindow?: boolean; noRecentEntry?: boolean; + + addMode?: boolean; + + diffMode?: boolean; + gotoLineMode?: boolean; + + waitMarkerFileURI?: URI; +} + +export interface IAddFoldersRequest { + foldersToAdd: UriComponents[]; +} + +export interface IOpenedWindow { + id: number; + workspace?: IWorkspaceIdentifier; + folderUri?: ISingleFolderWorkspaceIdentifier; + title: string; + filename?: string; + dirty: boolean; } export interface IOpenEmptyWindowOptions extends IBaseOpenWindowsOptions { diff --git a/src/vs/code/node/activeWindowTracker.ts b/src/vs/platform/windows/electron-main/windowTracker.ts similarity index 86% rename from src/vs/code/node/activeWindowTracker.ts rename to src/vs/platform/windows/electron-main/windowTracker.ts index a7dbeb98bf2..f9c5c9fe055 100644 --- a/src/vs/code/node/activeWindowTracker.ts +++ b/src/vs/platform/windows/electron-main/windowTracker.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; -import { DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronMainService } from 'vs/platform/electron/electron-main/electronMainService'; export class ActiveWindowManager extends Disposable { @@ -15,7 +15,7 @@ export class ActiveWindowManager extends Disposable { private activeWindowId: number | undefined; - constructor(@IElectronService electronService: IElectronService) { + constructor(@IElectronMainService electronService: IElectronMainService) { super(); // remember last active window id upon events @@ -23,7 +23,7 @@ export class ActiveWindowManager extends Disposable { onActiveWindowChange(this.setActiveWindow, this, this.disposables); // resolve current active window - this.firstActiveWindowIdPromise = createCancelablePromise(() => electronService.getActiveWindowId()); + this.firstActiveWindowIdPromise = createCancelablePromise(() => electronService.getActiveWindowId(-1)); (async () => { try { const windowId = await this.firstActiveWindowIdPromise; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index cdd6731748d..abe0631e996 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -16,12 +16,11 @@ import { INativeEnvironmentService } from 'vs/platform/environment/node/environm import { IStateService } from 'vs/platform/state/node/state'; import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; import { ipcMain as ipc, screen, BrowserWindow, MessageBoxOptions, Display, app, nativeTheme } from 'electron'; -import { parseLineAndColumnAware } from 'vs/code/node/paths'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; -import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; -import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IAddFoldersRequest, IPathsToWaitFor } from 'vs/platform/windows/node/window'; +import { IWindowSettings, IPath, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; +import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri, INativeWindowConfiguration, OpenContext, IPathsToWaitFor } from 'vs/platform/windows/node/window'; import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; @@ -39,7 +38,7 @@ import { once } from 'vs/base/common/functional'; import { Disposable } from 'vs/base/common/lifecycle'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { isWindowsDriveLetter, toSlashes } from 'vs/base/common/extpath'; +import { isWindowsDriveLetter, toSlashes, parseLineAndColumnAware } from 'vs/base/common/extpath'; import { CharCode } from 'vs/base/common/charCode'; export interface IWindowState { diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts index cf737ec23b4..2e84b32f042 100644 --- a/src/vs/platform/windows/node/window.ts +++ b/src/vs/platform/windows/node/window.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IOpenWindowOptions, IWindowConfiguration, IPath, IOpenFileRequest, IPathData } from 'vs/platform/windows/common/windows'; +import { IWindowConfiguration, IPath, IOpenFileRequest, IPathData } from 'vs/platform/windows/common/windows'; import { URI, UriComponents } from 'vs/base/common/uri'; import * as platform from 'vs/base/common/platform'; import * as extpath from 'vs/base/common/extpath'; @@ -13,15 +13,6 @@ import { LogLevel } from 'vs/platform/log/common/log'; import { ExportData } from 'vs/base/common/performance'; import { ParsedArgs } from 'vs/platform/environment/node/argv'; -export interface IOpenedWindow { - id: number; - workspace?: IWorkspaceIdentifier; - folderUri?: ISingleFolderWorkspaceIdentifier; - title: string; - filename?: string; - dirty: boolean; -} - export const enum OpenContext { // opening when running from the command line @@ -53,10 +44,6 @@ export interface IRunKeybindingInWindowRequest { userSettingsLabel: string; } -export interface IAddFoldersRequest { - foldersToAdd: UriComponents[]; -} - export interface INativeWindowConfiguration extends IWindowConfiguration, ParsedArgs { mainPid: number; @@ -100,13 +87,6 @@ export interface IPathsToWaitForData { waitMarkerFileUri: UriComponents; } -export interface INativeOpenWindowOptions extends IOpenWindowOptions { - diffMode?: boolean; - addMode?: boolean; - gotoLineMode?: boolean; - waitMarkerFileURI?: URI; -} - export interface IWindowContext { openedWorkspace?: IWorkspaceIdentifier; openedFolderUri?: URI; diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index a742475e6d4..cb8451feb2d 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -134,7 +134,7 @@ export class WorkspacesMainService extends Disposable implements IWorkspacesMain if (storedWorkspace && Array.isArray(storedWorkspace.folders)) { storedWorkspace.folders = storedWorkspace.folders.filter(folder => isStoredWorkspaceFolder(folder)); } else { - throw new Error(`${path.toString()} looks like an invalid workspace file.`); + throw new Error(`${path.toString(true)} looks like an invalid workspace file.`); } return storedWorkspace; diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 2356a099f18..96f8c6c93ad 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -19,7 +19,7 @@ import { isWindows } from 'vs/base/common/platform'; import { normalizeDriveLetter } from 'vs/base/common/labels'; import { dirname, joinPath } from 'vs/base/common/resources'; import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; -import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs'; +import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IBackupMainService, IWorkspaceBackupInfo } from 'vs/platform/backup/electron-main/backup'; import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index f0fdc10ba2b..65b2021afb1 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2216,7 +2216,7 @@ declare module 'vscode' { } /** - * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) providers. + * Metadata about the type of code actions that a [CodeActionProvider](#CodeActionProvider) provides. */ export interface CodeActionProviderMetadata { /** @@ -2645,7 +2645,6 @@ declare module 'vscode' { TypeParameter = 25 } - /** * Symbol tags are extra annotations that tweak the rendering of a symbol. */ @@ -3055,7 +3054,6 @@ declare module 'vscode' { */ renameFile(oldUri: Uri, newUri: Uri, options?: { overwrite?: boolean, ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void; - /** * Get all text edits grouped by resource. * @@ -3952,9 +3950,13 @@ declare module 'vscode' { * * The editor will only resolve a completion item once. * - * *Note* that accepting a completion item will not wait for it to be resolved. Because of that [`insertText`](#CompletionItem.insertText), - * [`additionalTextEdits`](#CompletionItem.additionalTextEdits), and [`command`](#CompletionItem.command) should not - * be changed when resolving an item. + * *Note* that this function is called when completion items are already showing in the UI or when an item has been + * selected for insertion. Because of that, no property that changes the presentation (label, sorting, filtering etc) + * or the (primary) insert behaviour ([insertText](#CompletionItem.insertText)) can be changed. + * + * This function may fill in [additionalTextEdits](#CompletionItem.additionalTextEdits). However, that means an item might be + * inserted *before* resolving is done and in that case the editor will do a best effort to still apply those additional + * text edits. * * @param item A completion item currently active in the UI. * @param token A cancellation token. @@ -3964,7 +3966,6 @@ declare module 'vscode' { resolveCompletionItem?(item: T, token: CancellationToken): ProviderResult; } - /** * A document link is a range in a text document that links to an internal or external resource, like another * text document or a web site. @@ -5657,7 +5658,6 @@ declare module 'vscode' { private constructor(id: string, label: string); } - /** * A structure that defines a task kind in the system. * The value must be JSON-stringifyable. @@ -6610,7 +6610,7 @@ declare module 'vscode' { readonly enableCommandUris?: boolean; /** - * Root paths from which the webview can load local (filesystem) resources using the `vscode-resource:` scheme. + * Root paths from which the webview can load local (filesystem) resources using uris from `asWebviewUri` * * Default to the root folders of the current workspace plus the extension's install directory. * @@ -6909,6 +6909,278 @@ declare module 'vscode' { resolveCustomTextEditor(document: TextDocument, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void; } + /** + * Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider). + * + * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is + * managed by VS Code. When no more references remain to a `CustomDocument`, it is disposed of. + */ + interface CustomDocument { + /** + * The associated uri for this document. + */ + readonly uri: Uri; + + /** + * Dispose of the custom document. + * + * This is invoked by VS Code when there are no more references to a given `CustomDocument` (for example when + * all editors associated with the document have been closed.) + */ + dispose(): void; + } + + /** + * Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomDocument`](#CustomDocument). + * + * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). + */ + interface CustomDocumentEditEvent { + + /** + * The document that the edit is for. + */ + readonly document: T; + + /** + * Undo the edit operation. + * + * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your + * extension should restore the document and editor to the state they were in just before this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + undo(): Thenable | void; + + /** + * Redo the edit operation. + * + * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your + * extension should restore the document and editor to the state they were in just after this + * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. + */ + redo(): Thenable | void; + + /** + * Display name describing the edit. + * + * This is shown in the UI to users. + */ + readonly label?: string; + } + + /** + * Event triggered by extensions to signal to VS Code that the content of a [`CustomDocument`](#CustomDocument) + * has changed. + * + * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). + */ + interface CustomDocumentContentChangeEvent { + /** + * The document that the change is for. + */ + readonly document: T; + } + + /** + * A backup for an [`CustomDocument`](#CustomDocument). + */ + interface CustomDocumentBackup { + /** + * Unique identifier for the backup. + * + * This id is passed back to your extension in `openCustomDocument` when opening a custom editor from a backup. + */ + readonly id: string; + + /** + * Delete the current backup. + * + * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup + * is made or when the file is saved. + */ + delete(): void; + } + + /** + * Additional information used to implement [`CustomEditableDocument.backup`](#CustomEditableDocument.backup). + */ + interface CustomDocumentBackupContext { + /** + * Suggested file location to write the new backup. + * + * Note that your extension is free to ignore this and use its own strategy for backup. + * + * For editors for workspace resource, this destination will be in the workspace storage. The path may not + */ + readonly destination: Uri; + } + + /** + * Additional information about the opening custom document. + */ + interface CustomDocumentOpenContext { + /** + * The id of the backup to restore the document from or `undefined` if there is no backup. + * + * If this is provided, your extension should restore the editor from the backup instead of reading the file + * the user's workspace. + */ + readonly backupId?: string; + } + + /** + * Provider for readonly custom editors that use a custom document model. + * + * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * + * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple + * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * + * @param T Type of the custom document returned by this provider. + */ + export interface CustomReadonlyEditorProvider { + + /** + * Create a new document for a given resource. + * + * `openCustomDocument` is called when the first editor for a given resource is opened, and the resolve document + * is passed to `resolveCustomEditor`. The resolved `CustomDocument` is re-used for subsequent editor opens. + * If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at + * this point will trigger another call to `openCustomDocument`. + * + * @param uri Uri of the document to open. + * @param openContext Additional information about the opening custom document. + * @param token A cancellation token that indicates the result is no longer needed. + * + * @return The custom document. + */ + openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): Thenable | T; + + /** + * Resolve a custom editor for a given resource. + * + * This is called whenever the user opens a new editor for this `CustomEditorProvider`. + * + * To resolve a custom editor, the provider must fill in its initial html content and hook up all + * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, + * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. + * + * @param document Document for the resource being resolved. + * @param webviewPanel Webview to resolve. + * @param token A cancellation token that indicates the result is no longer needed. + * + * @return Optional thenable indicating that the custom editor has been resolved. + */ + resolveCustomEditor(document: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void; + } + + /** + * Provider for editiable custom editors that use a custom document model. + * + * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). + * This gives extensions full control over actions such as edit, save, and backup. + * + * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple + * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. + * + * @param T Type of the custom document returned by this provider. + */ + export interface CustomEditorProvider extends CustomReadonlyEditorProvider { + /** + * Signal that an edit has occurred inside a custom editor. + * + * This event must be fired by your extension whenever an edit happens in a custom editor. An edit can be + * anything from changing some text, to cropping an image, to reordering a list. Your extension is free to + * define what an edit is and what data is stored on each edit. + * + * Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either + * saves or reverts the file. + * + * Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows + * users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark + * the editor as no longer being dirty if the user undoes all edits to the last saved state. + * + * Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`. + * The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either + * `save` or `revert` the file. + * + * An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events. + */ + readonly onDidChangeCustomDocument: Event> | Event>; + + /** + * Save a custom document. + * + * This method is invoked by VS Code when the user saves a custom editor. This can happen when the user + * triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled. + * + * To implement `save`, the implementer must persist the custom editor. This usually means writing the + * file data for the custom document to disk. After `save` completes, any associated editor instances will + * no longer be marked as dirty. + * + * @param document Document to save. + * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered). + * + * @return Thenable signaling that saving has completed. + */ + saveCustomDocument(document: T, cancellation: CancellationToken): Thenable; + + /** + * Save a custom document to a different location. + * + * This method is invoked by VS Code when the user triggers 'save as' on a custom editor. The implementer must + * persist the custom editor to `destination`. + * + * When the user accepts save as, the current editor is be replaced by an non-dirty editor for the newly saved file. + * + * @param document Document to save. + * @param destination Location to save to. + * @param cancellation Token that signals the save is no longer required. + * + * @return Thenable signaling that saving has completed. + */ + saveCustomDocumentAs(document: T, destination: Uri, cancellation: CancellationToken): Thenable; + + /** + * Revert a custom document to its last saved state. + * + * This method is invoked by VS Code when the user triggers `File: Revert File` in a custom editor. (Note that + * this is only used using VS Code's `File: Revert File` command and not on a `git revert` of the file). + * + * To implement `revert`, the implementer must make sure all editor instances (webviews) for `document` + * are displaying the document in the same state is saved in. This usually means reloading the file from the + * workspace. + * + * @param document Document to revert. + * @param cancellation Token that signals the revert is no longer required. + * + * @return Thenable signaling that the change has completed. + */ + revertCustomDocument(document: T, cancellation: CancellationToken): Thenable; + + /** + * Back up a dirty custom document. + * + * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in + * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in + * the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource, + * your extension should first check to see if any backups exist for the resource. If there is a backup, your + * extension should load the file contents from there instead of from the resource in the workspace. + * + * `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are + * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when + * `auto save` is enabled (since auto save already persists resource ). + * + * @param document Document to backup. + * @param context Information that can be used to backup the document. + * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your + * extension to decided how to respond to cancellation. If for example your extension is backing up a large file + * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather + * than cancelling it to ensure that VS Code has some valid backup. + */ + backupCustomDocument(document: T, context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable; + } + /** * The clipboard provides read and write access to the system's clipboard. */ @@ -7750,7 +8022,8 @@ declare module 'vscode' { * Register a provider for custom editors for the `viewType` contributed by the `customEditors` extension point. * * When a custom editor is opened, VS Code fires an `onCustomEditor:viewType` activation event. Your extension - * must register a [`CustomTextEditorProvider`](#CustomTextEditorProvider) for `viewType` as part of activation. + * must register a [`CustomTextEditorProvider`](#CustomTextEditorProvider), [`CustomReadonlyEditorProvider`](#CustomReadonlyEditorProvider), + * [`CustomEditorProvider`](#CustomEditorProvider)for `viewType` as part of activation. * * @param viewType Unique identifier for the custom editor provider. This should match the `viewType` from the * `customEditors` contribution point. @@ -7759,7 +8032,24 @@ declare module 'vscode' { * * @return Disposable that unregisters the provider. */ - export function registerCustomEditorProvider(viewType: string, provider: CustomTextEditorProvider, options?: { readonly webviewOptions?: WebviewPanelOptions; }): Disposable; + export function registerCustomEditorProvider(viewType: string, provider: CustomTextEditorProvider | CustomReadonlyEditorProvider | CustomEditorProvider, options?: { + readonly webviewOptions?: WebviewPanelOptions; + + /** + * Only applies to `CustomReadonlyEditorProvider | CustomEditorProvider`. + * + * Indicates that the provider allows multiple editor instances to be open at the same time for + * the same resource. + * + * If not set, VS Code only allows one editor instance to be open at a time for each resource. If the + * user tries to open a second editor instance for the resource, the first one is instead moved to where + * the second one was to be opened. + * + * When set, users can split and create copies of the custom editor. The custom editor must make sure it + * can properly synchronize the states of all editor instances for a resource so that they are consistent. + */ + readonly supportsMultipleEditorsPerDocument?: boolean; + }): Disposable; /** * The currently active color theme as configured in the settings. The active @@ -7889,7 +8179,7 @@ declare module 'vscode' { * In order to expand the revealed element, set the option `expand` to `true`. To expand recursively set `expand` to the number of levels to expand. * **NOTE:** You can expand only to 3 levels maximum. * - * **NOTE:** [TreeDataProvider](#TreeDataProvider) is required to implement [getParent](#TreeDataProvider.getParent) method to access this API. + * **NOTE:** The [TreeDataProvider](#TreeDataProvider) that the `TreeView` [is registered with](#window.createTreeView) with must implement [getParent](#TreeDataProvider.getParent) method to access this API. */ reveal(element: T, options?: { select?: boolean, focus?: boolean, expand?: boolean | number }): Thenable; } @@ -8957,7 +9247,6 @@ declare module 'vscode' { readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; } - /** * An event describing a change to the set of [workspace folders](#workspace.workspaceFolders). */ @@ -9594,7 +9883,7 @@ declare module 'vscode' { * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A code action provider. - * @param metadata Metadata about the kind of code actions the provider providers. + * @param metadata Metadata about the kind of code actions the provider provides. * @return A [disposable](#Disposable) that unregisters this provider when being disposed. */ export function registerCodeActionsProvider(selector: DocumentSelector, provider: CodeActionProvider, metadata?: CodeActionProviderMetadata): Disposable; @@ -9745,8 +10034,8 @@ declare module 'vscode' { * Register a rename provider. * * Multiple providers can be registered for a language. In that case providers are sorted - * by their [score](#languages.match) and the best-matching provider is used. Failure - * of the selected provider will cause a failure of the whole operation. + * by their [score](#languages.match) and asked in sequence. The first provider producing a result + * defines the result of the whole operation. * * @param selector A selector that defines the documents this provider is applicable to. * @param provider A rename provider. @@ -10641,7 +10930,6 @@ declare module 'vscode' { */ export let breakpoints: Breakpoint[]; - /** * An [event](#Event) which fires when the [active debug session](#debug.activeDebugSession) * has changed. *Note* that the event also fires when the active debug session changes diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 62609472691..28de594c9c2 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -28,6 +28,41 @@ declare module 'vscode' { scopes: string[]; } + export class AuthenticationSession2 { + /** + * The identifier of the authentication session. + */ + readonly id: string; + + /** + * The access token. + */ + readonly accessToken: string; + + /** + * The account associated with the session. + */ + readonly account: { + /** + * The human-readable name of the account. + */ + readonly displayName: string; + + /** + * The unique identifier of the account. + */ + readonly id: string; + }; + + /** + * The permissions granted by the session's access token. Available scopes + * are defined by the authentication provider. + */ + readonly scopes: string[]; + + constructor(id: string, accessToken: string, account: { displayName: string, id: string }, scopes: string[]); + } + /** * An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed. */ @@ -43,7 +78,6 @@ declare module 'vscode' { readonly removed: string[]; } - /** * Options to be used when getting a session from an [AuthenticationProvider](#AuthenticationProvider). */ @@ -112,12 +146,12 @@ declare module 'vscode' { /** * Returns an array of current sessions. */ - getSessions(): Thenable>; + getSessions(): Thenable>; /** * Prompts a user to login. */ - login(scopes: string[]): Thenable; + login(scopes: string[]): Thenable; /** * Removes the session corresponding to session id. @@ -143,6 +177,12 @@ declare module 'vscode' { */ export const onDidChangeAuthenticationProviders: Event; + /** + * The ids of the currently registered authentication providers. + * @returns An array of the ids of authentication providers that are currently registered. + */ + export function getProviderIds(): Thenable>; + /** * An array of the ids of authentication providers that are currently registered. */ @@ -167,10 +207,21 @@ declare module 'vscode' { * @param providerId The id of the provider to use * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider * @param options The [getSessionOptions](#GetSessionOptions) to use - * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions and - * `createIfNone` was not specified. + * @returns A thenable that resolves to an authentication session */ - export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions): Thenable; + export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable; + + /** + * Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not + * registered, or if the user does not consent to sharing authentication information with + * the extension. If there are multiple sessions with the same scopes, the user will be shown a + * quickpick to select which account they would like to use. + * @param providerId The id of the provider to use + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider + * @param options The [getSessionOptions](#GetSessionOptions) to use + * @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions + */ + export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions): Thenable; /** * @deprecated @@ -966,8 +1017,6 @@ declare module 'vscode' { //#endregion - - //#region Terminal link handlers https://github.com/microsoft/vscode/issues/91606 export namespace window { @@ -1039,6 +1088,11 @@ declare module 'vscode' { */ label?: string | TreeItemLabel | /* for compilation */ any; + /** + * Accessibility information used when screen reader interacts with this tree item. + */ + accessibilityInformation?: AccessibilityInformation; + /** * @param label Label describing this item * @param collapsibleState [TreeItemCollapsibleState](#TreeItemCollapsibleState) of the tree item. Default is [TreeItemCollapsibleState.None](#TreeItemCollapsibleState.None) @@ -1100,6 +1154,11 @@ declare module 'vscode' { */ name: string; + /** + * Accessibility information used when screen reader interacts with this status bar item. + */ + accessibilityInformation?: AccessibilityInformation; + /** * The alignment of the status bar item. */ @@ -1162,317 +1221,12 @@ declare module 'vscode' { //#endregion - //#region Custom editor https://github.com/microsoft/vscode/issues/77131 - - /** - * Represents a custom document used by a [`CustomEditorProvider`](#CustomEditorProvider). - * - * Custom documents are only used within a given `CustomEditorProvider`. The lifecycle of a `CustomDocument` is - * managed by VS Code. When no more references remain to a `CustomDocument`, it is disposed of. - */ - interface CustomDocument { - /** - * The associated uri for this document. - */ - readonly uri: Uri; - - /** - * Dispose of the custom document. - * - * This is invoked by VS Code when there are no more references to a given `CustomDocument` (for example when - * all editors associated with the document have been closed.) - */ - dispose(): void; - } - - /** - * Event triggered by extensions to signal to VS Code that an edit has occurred on an [`CustomDocument`](#CustomDocument). - * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). - */ - interface CustomDocumentEditEvent { - - /** - * The document that the edit is for. - */ - readonly document: T; - - /** - * Undo the edit operation. - * - * This is invoked by VS Code when the user undoes this edit. To implement `undo`, your - * extension should restore the document and editor to the state they were in just before this - * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. - */ - undo(): Thenable | void; - - /** - * Redo the edit operation. - * - * This is invoked by VS Code when the user redoes this edit. To implement `redo`, your - * extension should restore the document and editor to the state they were in just after this - * edit was added to VS Code's internal edit stack by `onDidChangeCustomDocument`. - */ - redo(): Thenable | void; - - /** - * Display name describing the edit. - * - * This is shown in the UI to users. - */ - readonly label?: string; - } - - /** - * Event triggered by extensions to signal to VS Code that the content of a [`CustomDocument`](#CustomDocument) - * has changed. - * - * @see [`CustomDocumentProvider.onDidChangeCustomDocument`](#CustomDocumentProvider.onDidChangeCustomDocument). - */ - interface CustomDocumentContentChangeEvent { - /** - * The document that the change is for. - */ - readonly document: T; - } - - /** - * A backup for an [`CustomDocument`](#CustomDocument). - */ - interface CustomDocumentBackup { - /** - * Unique identifier for the backup. - * - * This id is passed back to your extension in `openCustomDocument` when opening a custom editor from a backup. - */ - readonly id: string; - - /** - * Delete the current backup. - * - * This is called by VS Code when it is clear the current backup is no longer needed, such as when a new backup - * is made or when the file is saved. - */ - delete(): void; - } - - /** - * Additional information used to implement [`CustomEditableDocument.backup`](#CustomEditableDocument.backup). - */ - interface CustomDocumentBackupContext { - /** - * Suggested file location to write the new backup. - * - * Note that your extension is free to ignore this and use its own strategy for backup. - * - * For editors for workspace resource, this destination will be in the workspace storage. The path may not - */ - readonly destination: Uri; - } - - /** - * Additional information about the opening custom document. - */ - interface CustomDocumentOpenContext { - /** - * The id of the backup to restore the document from or `undefined` if there is no backup. - * - * If this is provided, your extension should restore the editor from the backup instead of reading the file - * the user's workspace. - */ - readonly backupId?: string; - } - - /** - * Provider for readonly custom editors that use a custom document model. - * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). - * - * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. - * - * @param T Type of the custom document returned by this provider. - */ - export interface CustomReadonlyEditorProvider { - - /** - * Create a new document for a given resource. - * - * `openCustomDocument` is called when the first editor for a given resource is opened, and the resolve document - * is passed to `resolveCustomEditor`. The resolved `CustomDocument` is re-used for subsequent editor opens. - * If all editors for a given resource are closed, the `CustomDocument` is disposed of. Opening an editor at - * this point will trigger another call to `openCustomDocument`. - * - * @param uri Uri of the document to open. - * @param openContext Additional information about the opening custom document. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return The custom document. - */ - openCustomDocument(uri: Uri, openContext: CustomDocumentOpenContext, token: CancellationToken): Thenable | T; - - /** - * Resolve a custom editor for a given resource. - * - * This is called whenever the user opens a new editor for this `CustomEditorProvider`. - * - * To resolve a custom editor, the provider must fill in its initial html content and hook up all - * the event listeners it is interested it. The provider can also hold onto the `WebviewPanel` to use later, - * for example in a command. See [`WebviewPanel`](#WebviewPanel) for additional details. - * - * @param document Document for the resource being resolved. - * @param webviewPanel Webview to resolve. - * @param token A cancellation token that indicates the result is no longer needed. - * - * @return Optional thenable indicating that the custom editor has been resolved. - */ - resolveCustomEditor(document: T, webviewPanel: WebviewPanel, token: CancellationToken): Thenable | void; - } - - /** - * Provider for editiable custom editors that use a custom document model. - * - * Custom editors use [`CustomDocument`](#CustomDocument) as their document model instead of a [`TextDocument`](#TextDocument). - * This gives extensions full control over actions such as edit, save, and backup. - * - * You should use this type of custom editor when dealing with binary files or more complex scenarios. For simple - * text based documents, use [`CustomTextEditorProvider`](#CustomTextEditorProvider) instead. - * - * @param T Type of the custom document returned by this provider. - */ - export interface CustomEditorProvider extends CustomReadonlyEditorProvider { - /** - * Signal that an edit has occurred inside a custom editor. - * - * This event must be fired by your extension whenever an edit happens in a custom editor. An edit can be - * anything from changing some text, to cropping an image, to reordering a list. Your extension is free to - * define what an edit is and what data is stored on each edit. - * - * Firing `onDidChange` causes VS Code to mark the editors as being dirty. This is cleared when the user either - * saves or reverts the file. - * - * Editors that support undo/redo must fire a `CustomDocumentEditEvent` whenever an edit happens. This allows - * users to undo and redo the edit using VS Code's standard VS Code keyboard shortcuts. VS Code will also mark - * the editor as no longer being dirty if the user undoes all edits to the last saved state. - * - * Editors that support editing but cannot use VS Code's standard undo/redo mechanism must fire a `CustomDocumentContentChangeEvent`. - * The only way for a user to clear the dirty state of an editor that does not support undo/redo is to either - * `save` or `revert` the file. - * - * An editor should only ever fire `CustomDocumentEditEvent` events, or only ever fire `CustomDocumentContentChangeEvent` events. - */ - readonly onDidChangeCustomDocument: Event> | Event>; - - /** - * Save a custom document. - * - * This method is invoked by VS Code when the user saves a custom editor. This can happen when the user - * triggers save while the custom editor is active, by commands such as `save all`, or by auto save if enabled. - * - * To implement `save`, the implementer must persist the custom editor. This usually means writing the - * file data for the custom document to disk. After `save` completes, any associated editor instances will - * no longer be marked as dirty. - * - * @param document Document to save. - * @param cancellation Token that signals the save is no longer required (for example, if another save was triggered). - * - * @return Thenable signaling that saving has completed. - */ - saveCustomDocument(document: T, cancellation: CancellationToken): Thenable; - - /** - * Save a custom document to a different location. - * - * This method is invoked by VS Code when the user triggers 'save as' on a custom editor. The implementer must - * persist the custom editor to `destination`. - * - * When the user accepts save as, the current editor is be replaced by an non-dirty editor for the newly saved file. - * - * @param document Document to save. - * @param destination Location to save to. - * @param cancellation Token that signals the save is no longer required. - * - * @return Thenable signaling that saving has completed. - */ - saveCustomDocumentAs(document: T, destination: Uri, cancellation: CancellationToken): Thenable; - - /** - * Revert a custom document to its last saved state. - * - * This method is invoked by VS Code when the user triggers `File: Revert File` in a custom editor. (Note that - * this is only used using VS Code's `File: Revert File` command and not on a `git revert` of the file). - * - * To implement `revert`, the implementer must make sure all editor instances (webviews) for `document` - * are displaying the document in the same state is saved in. This usually means reloading the file from the - * workspace. - * - * @param document Document to revert. - * @param cancellation Token that signals the revert is no longer required. - * - * @return Thenable signaling that the change has completed. - */ - revertCustomDocument(document: T, cancellation: CancellationToken): Thenable; - - /** - * Back up a dirty custom document. - * - * Backups are used for hot exit and to prevent data loss. Your `backup` method should persist the resource in - * its current state, i.e. with the edits applied. Most commonly this means saving the resource to disk in - * the `ExtensionContext.storagePath`. When VS Code reloads and your custom editor is opened for a resource, - * your extension should first check to see if any backups exist for the resource. If there is a backup, your - * extension should load the file contents from there instead of from the resource in the workspace. - * - * `backup` is triggered whenever an edit it made. Calls to `backup` are debounced so that if multiple edits are - * made in quick succession, `backup` is only triggered after the last one. `backup` is not invoked when - * `auto save` is enabled (since auto save already persists resource ). - * - * @param document Document to backup. - * @param context Information that can be used to backup the document. - * @param cancellation Token that signals the current backup since a new backup is coming in. It is up to your - * extension to decided how to respond to cancellation. If for example your extension is backing up a large file - * in an operation that takes time to complete, your extension may decide to finish the ongoing backup rather - * than cancelling it to ensure that VS Code has some valid backup. - */ - backupCustomDocument(document: T, context: CustomDocumentBackupContext, cancellation: CancellationToken): Thenable; - } - - namespace window { - /** - * Temporary overload for `registerCustomEditorProvider` that takes a `CustomEditorProvider`. - */ - export function registerCustomEditorProvider2( - viewType: string, - provider: CustomReadonlyEditorProvider | CustomEditorProvider, - options?: { - readonly webviewOptions?: WebviewPanelOptions; - - /** - * Only applies to `CustomReadonlyEditorProvider | CustomEditorProvider`. - * - * Indicates that the provider allows multiple editor instances to be open at the same time for - * the same resource. - * - * If not set, VS Code only allows one editor instance to be open at a time for each resource. If the - * user tries to open a second editor instance for the resource, the first one is instead moved to where - * the second one was to be opened. - * - * When set, users can split and create copies of the custom editor. The custom editor must make sure it - * can properly synchronize the states of all editor instances for a resource so that they are consistent. - */ - readonly supportsMultipleEditorsPerDocument?: boolean; - } - ): Disposable; - } - - // #endregion - //#region Custom editor move https://github.com/microsoft/vscode/issues/86146 // TODO: Also for custom editor export interface CustomTextEditorProvider { - /** * Handle when the underlying resource for a custom editor is renamed. * @@ -1490,7 +1244,6 @@ declare module 'vscode' { //#endregion - //#region allow QuickPicks to skip sorting: https://github.com/microsoft/vscode/issues/73904 export interface QuickPick extends QuickInput { @@ -1591,6 +1344,18 @@ declare module 'vscode' { */ runnable?: boolean; + /** + * Controls if the cell has a margin to support the breakpoint UI. + * This metadata is ignored for markdown cell. + */ + breakpointMargin?: boolean; + + /** + * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Defaults to true. + */ + hasExecutionOrder?: boolean; + /** * The order in which this cell was executed. */ @@ -1623,6 +1388,7 @@ declare module 'vscode' { } export interface NotebookCell { + readonly notebook: NotebookDocument; readonly uri: Uri; readonly cellKind: CellKind; readonly document: TextDocument; @@ -1659,10 +1425,10 @@ declare module 'vscode' { cellRunnable?: boolean; /** - * Whether the [execution order](#NotebookCellMetadata.executionOrder) indicator will be displayed. + * Default value for [cell hasExecutionOrder metadata](#NotebookCellMetadata.hasExecutionOrder). * Defaults to true. */ - hasExecutionOrder?: boolean; + cellHasExecutionOrder?: boolean; displayOrder?: GlobPattern[]; @@ -1711,8 +1477,26 @@ declare module 'vscode' { */ readonly selection?: NotebookCell; + /** + * The column in which this editor shows. + */ viewColumn?: ViewColumn; + /** + * Whether the panel is active (focused by the user). + */ + readonly active: boolean; + + /** + * Whether the panel is visible. + */ + readonly visible: boolean; + + /** + * Fired when the panel is disposed. + */ + readonly onDidDispose: Event; + /** * Fired when the output hosting webview posts a message. */ @@ -1726,6 +1510,11 @@ declare module 'vscode' { */ postMessage(message: any): Thenable; + /** + * Convert a uri for the local file system to one that can be used inside outputs webview. + */ + asWebviewUri(localResource: Uri): Uri; + edit(callback: (editBuilder: NotebookEditorCellEdit) => void): Thenable; } @@ -1744,17 +1533,48 @@ declare module 'vscode' { preloads?: Uri[]; } - export interface NotebookDocumentChangeEvent { + export interface NotebookCellsChangeData { + readonly start: number; + readonly deletedCount: number; + readonly items: NotebookCell[]; + } + + export interface NotebookCellsChangeEvent { /** * The affected document. */ readonly document: NotebookDocument; + readonly changes: ReadonlyArray; + } + + export interface NotebookCellMoveEvent { /** - * An array of content changes. + * The affected document. */ - // readonly contentChanges: ReadonlyArray; + readonly document: NotebookDocument; + readonly index: number; + readonly newIndex: number; + } + + export interface NotebookCellOutputsChangeEvent { + + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cells: NotebookCell[]; + } + + export interface NotebookCellLanguageChangeEvent { + + /** + * The affected document. + */ + readonly document: NotebookDocument; + readonly cell: NotebookCell; + readonly language: string; } export interface NotebookCellData { @@ -1788,10 +1608,7 @@ declare module 'vscode' { // revert?(document: NotebookDocument, cancellation: CancellationToken): Thenable; // backup?(document: NotebookDocument, cancellation: CancellationToken): Thenable; - /** - * Responsible for filling in outputs for the cell - */ - executeCell(document: NotebookDocument, cell: NotebookCell | undefined, token: CancellationToken): Promise; + kernel?: NotebookKernel; } export interface NotebookKernel { @@ -1821,15 +1638,17 @@ declare module 'vscode' { export const onDidOpenNotebookDocument: Event; export const onDidCloseNotebookDocument: Event; - // export const onDidChangeVisibleNotebookEditors: Event; + export let visibleNotebookEditors: NotebookEditor[]; + export const onDidChangeVisibleNotebookEditors: Event; // remove activeNotebookDocument, now that there is activeNotebookEditor.document export let activeNotebookDocument: NotebookDocument | undefined; export let activeNotebookEditor: NotebookEditor | undefined; - - export const onDidChangeNotebookDocument: Event; - + export const onDidChangeActiveNotebookEditor: Event; + export const onDidChangeNotebookCells: Event; + export const onDidChangeCellOutputs: Event; + export const onDidChangeCellLanguage: Event; /** * Create a document that is the concatenation of all notebook cells. By default all code-cells are included * but a selector can be provided to narrow to down the set of cells. @@ -1912,7 +1731,6 @@ declare module 'vscode' { //#endregion - //#region @eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 export class TimelineItem { @@ -1973,6 +1791,11 @@ declare module 'vscode' { */ contextValue?: string; + /** + * Accessibility information used when screen reader interacts with this timeline item. + */ + accessibilityInformation?: AccessibilityInformation; + /** * @param label A human-readable string describing the timeline item * @param timestamp A timestamp (in milliseconds since 1 January 1970 00:00:00) for when the timeline item occurred @@ -2120,4 +1943,43 @@ declare module 'vscode' { } //#endregion + + //#region Accessibility information: https://github.com/microsoft/vscode/issues/95360 + + /** + * Accessibility information which controls screen reader behavior. + */ + export interface AccessibilityInformation { + label: string; + role?: string; + } + + export interface StatusBarItem { + /** + * Accessibility information used when screen reader interacts with this StatusBar item + */ + accessibilityInformation?: AccessibilityInformation; + } + + //#endregion + + //#region https://github.com/microsoft/vscode/issues/91555 + + export enum StandardTokenType { + Other = 0, + Comment = 1, + String = 2, + RegEx = 4 + } + + export interface TokenInformation { + type: StandardTokenType; + range: Range; + } + + export namespace languages { + export function getTokenInformationAtPosition(document: TextDocument, position: Position): Promise; + } + + //#endregion } diff --git a/src/vs/workbench/api/browser/mainThreadAuthentication.ts b/src/vs/workbench/api/browser/mainThreadAuthentication.ts index 66d2fed76a0..3c0e9eaa4c6 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -43,6 +43,11 @@ function readAccountUsages(storageService: IStorageService, providerId: string, return usages; } +function removeAccountUsage(storageService: IStorageService, providerId: string, accountName: string): void { + const accountKey = `${providerId}-${accountName}-usages`; + storageService.remove(accountKey, StorageScope.GLOBAL); +} + function addAccountUsage(storageService: IStorageService, providerId: string, accountName: string, extensionId: string, extensionName: string) { const accountKey = `${providerId}-${accountName}-usages`; const usages = readAccountUsages(storageService, providerId, accountName); @@ -74,6 +79,7 @@ export class MainThreadAuthenticationProvider extends Disposable { private readonly _proxy: ExtHostAuthenticationShape, public readonly id: string, public readonly displayName: string, + public readonly supportsMultipleAccounts: boolean, private readonly notificationService: INotificationService, private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, private readonly storageService: IStorageService @@ -193,34 +199,21 @@ export class MainThreadAuthenticationProvider extends Disposable { const accountUsages = readAccountUsages(this.storageService, this.id, session.account.displayName); const sessionsForAccount = this._accounts.get(session.account.displayName); - // Skip dialog if nothing is using the account - if (!accountUsages.length) { - sessionsForAccount?.forEach(sessionId => this.logout(sessionId)); - return; - } - const result = await dialogService.confirm({ title: nls.localize('signOutConfirm', "Sign out of {0}", session.account.displayName), - message: nls.localize('signOutMessage', "The account {0} is has been used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsages.map(usage => usage.extensionName).join('\n')) + message: accountUsages.length + ? nls.localize('signOutMessagve', "The account {0} has been used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsages.map(usage => usage.extensionName).join('\n')) + : nls.localize('signOutMessageSimple', "Sign out of {0}?", session.account.displayName) }); if (result.confirmed) { sessionsForAccount?.forEach(sessionId => this.logout(sessionId)); + removeAccountUsage(this.storageService, this.id, session.account.displayName); } } async getSessions(): Promise> { - return (await this._proxy.$getSessions(this.id)).map(session => { - return { - id: session.id, - account: session.account, - scopes: session.scopes, - getAccessToken: () => { - addAccountUsage(this.storageService, this.id, session.account.displayName, 'preferencessync', nls.localize('sync', "Preferences Sync")); - return this._proxy.$getSessionAccessToken(this.id, session.id); - } - }; - }); + return this._proxy.$getSessions(this.id); } async updateSessionItems(event: modes.AuthenticationSessionsChangeEvent): Promise { @@ -251,14 +244,7 @@ export class MainThreadAuthenticationProvider extends Disposable { } login(scopes: string[]): Promise { - return this._proxy.$login(this.id, scopes).then(session => { - return { - id: session.id, - account: session.account, - scopes: session.scopes, - getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) - }; - }); + return this._proxy.$login(this.id, scopes); } async logout(sessionId: string): Promise { @@ -289,10 +275,26 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu ) { super(); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication); + + this._register(this.authenticationService.onDidChangeSessions(e => { + this._proxy.$onDidChangeAuthenticationSessions(e.providerId, e.event); + })); + + this._register(this.authenticationService.onDidRegisterAuthenticationProvider(providerId => { + this._proxy.$onDidChangeAuthenticationProviders([providerId], []); + })); + + this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(providerId => { + this._proxy.$onDidChangeAuthenticationProviders([], [providerId]); + })); } - async $registerAuthenticationProvider(id: string, displayName: string): Promise { - const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, this.notificationService, this.storageKeysSyncRegistryService, this.storageService); + $getProviderIds(): Promise { + return Promise.resolve(this.authenticationService.getProviderIds()); + } + + async $registerAuthenticationProvider(id: string, displayName: string, supportsMultipleAccounts: boolean): Promise { + const provider = new MainThreadAuthenticationProvider(this._proxy, id, displayName, supportsMultipleAccounts, this.notificationService, this.storageKeysSyncRegistryService, this.storageService); await provider.initialize(); this.authenticationService.registerAuthenticationProvider(id, provider); } @@ -301,15 +303,63 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu this.authenticationService.unregisterAuthenticationProvider(id); } - $onDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { + $sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void { this.authenticationService.sessionsUpdate(id, event); } + $getSessions(id: string): Promise> { + return this.authenticationService.getSessions(id); + } + + $login(providerId: string, scopes: string[]): Promise { + return this.authenticationService.login(providerId, scopes); + } + + $logout(providerId: string, sessionId: string): Promise { + return this.authenticationService.logout(providerId, sessionId); + } + async $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise { return this.authenticationService.requestNewSession(providerId, scopes, extensionId, extensionName); } - async $getSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { + async $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone: boolean, clearSessionPreference: boolean }): Promise { + const orderedScopes = scopes.sort().join(' '); + const sessions = (await this.$getSessions(providerId)).filter(session => session.scopes.sort().join(' ') === orderedScopes); + const displayName = this.authenticationService.getDisplayName(providerId); + + if (sessions.length) { + if (!this.authenticationService.supportsMultipleAccounts(providerId)) { + const session = sessions[0]; + const allowed = await this.$getSessionsPrompt(providerId, session.account.displayName, displayName, extensionId, extensionName); + if (allowed) { + return session; + } else { + throw new Error('User did not consent to login.'); + } + } + + // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid + const selected = await this.$selectSession(providerId, displayName, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + return sessions.find(session => session.id === selected.id); + } else { + if (options.createIfNone) { + const isAllowed = await this.$loginPrompt(displayName, extensionName); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + const session = await this.authenticationService.login(providerId, scopes); + await this.$setTrustedExtension(providerId, session.account.displayName, extensionId, extensionName); + return session; + } else { + await this.$requestNewSession(providerId, scopes, extensionId, extensionName); + return undefined; + } + } + } + + async $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise { if (!potentialSessions.length) { throw new Error('No potential sessions found'); } diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index 8568dedd56b..da0789fab36 100644 --- a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts +++ b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts @@ -94,8 +94,7 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape { }, { allowScripts: options.enableScripts, localResourceRoots: options.localResourceRoots ? options.localResourceRoots.map(uri => URI.revive(uri)) : undefined - }); - webview.extension = { id: extensionId, location: URI.revive(extensionLocation) }; + }, { id: extensionId, location: URI.revive(extensionLocation) }); const webviewZone = new EditorWebviewZone(editor, line, height, webview); @@ -128,7 +127,10 @@ export class MainThreadEditorInsets implements MainThreadEditorInsetsShape { $setOptions(handle: number, options: modes.IWebviewOptions): void { const inset = this.getInset(handle); - inset.webview.contentOptions = options; + inset.webview.contentOptions = { + ...options, + localResourceRoots: options.localResourceRoots?.map(components => URI.from(components)), + }; } async $postMessage(handle: number, value: any): Promise { diff --git a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts index d8a3e129097..f4606720aa1 100644 --- a/src/vs/workbench/api/browser/mainThreadDiagnostics.ts +++ b/src/vs/workbench/api/browser/mainThreadDiagnostics.ts @@ -8,6 +8,7 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { MainThreadDiagnosticsShape, MainContext, IExtHostContext, ExtHostDiagnosticsShape, ExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; @extHostNamedCustomer(MainContext.MainThreadDiagnostics) export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { @@ -15,15 +16,15 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { private readonly _activeOwners = new Set(); private readonly _proxy: ExtHostDiagnosticsShape; - private readonly _markerService: IMarkerService; private readonly _markerListener: IDisposable; constructor( extHostContext: IExtHostContext, - @IMarkerService markerService: IMarkerService + @IMarkerService private readonly _markerService: IMarkerService, + @IUriIdentityService private readonly _uriIdentService: IUriIdentityService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDiagnostics); - this._markerService = markerService; + this._markerListener = this._markerService.onMarkerChanged(this._forwardMarkers, this); } @@ -59,7 +60,7 @@ export class MainThreadDiagnostics implements MainThreadDiagnosticsShape { } } } - this._markerService.changeOne(owner, URI.revive(uri), markers); + this._markerService.changeOne(owner, this._uriIdentService.asCanonicalUri(URI.revive(uri)), markers); } this._activeOwners.add(owner); } diff --git a/src/vs/workbench/api/browser/mainThreadDocuments.ts b/src/vs/workbench/api/browser/mainThreadDocuments.ts index 68454e67aca..025dae93162 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -10,22 +10,25 @@ import { URI, UriComponents } from 'vs/base/common/uri'; import { ITextModel } from 'vs/editor/common/model'; import { IModelService, shouldSynchronizeModel } from 'vs/editor/common/services/modelService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IFileService } from 'vs/platform/files/common/files'; +import { IFileService, FileOperation } from 'vs/platform/files/common/files'; import { MainThreadDocumentsAndEditors } from 'vs/workbench/api/browser/mainThreadDocumentsAndEditors'; import { ExtHostContext, ExtHostDocumentsShape, IExtHostContext, MainThreadDocumentsShape } from 'vs/workbench/api/common/extHost.protocol'; import { ITextEditorModel } from 'vs/workbench/common/editor'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { toLocalResource } from 'vs/base/common/resources'; +import { toLocalResource, extUri, IExtUri } from 'vs/base/common/resources'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; export class BoundModelReferenceCollection { - private _data = new Array<{ length: number, dispose(): void }>(); + private _data = new Array<{ uri: URI, length: number, dispose(): void }>(); private _length = 0; constructor( + private readonly _extUri: IExtUri, private readonly _maxAge: number = 1000 * 60 * 3, - private readonly _maxLength: number = 1024 * 1024 * 80 + private readonly _maxLength: number = 1024 * 1024 * 80, ) { // } @@ -34,10 +37,18 @@ export class BoundModelReferenceCollection { this._data = dispose(this._data); } - add(ref: IReference): void { + remove(uri: URI): void { + for (const entry of [...this._data] /* copy array because dispose will modify it */) { + if (this._extUri.isEqualOrParent(entry.uri, uri)) { + entry.dispose(); + } + } + } + + add(uri: URI, ref: IReference): void { const length = ref.object.textEditorModel.getValueLength(); let handle: any; - let entry: { length: number, dispose(): void }; + let entry: { uri: URI, length: number, dispose(): void }; const dispose = () => { const idx = this._data.indexOf(entry); if (idx >= 0) { @@ -48,7 +59,7 @@ export class BoundModelReferenceCollection { } }; handle = setTimeout(dispose, this._maxAge); - entry = { length, dispose }; + entry = { uri, length, dispose }; this._data.push(entry); this._length += length; @@ -69,12 +80,13 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { private readonly _textFileService: ITextFileService; private readonly _fileService: IFileService; private readonly _environmentService: IWorkbenchEnvironmentService; + private readonly _uriIdentityService: IUriIdentityService; private readonly _toDispose = new DisposableStore(); private _modelToDisposeMap: { [modelUrl: string]: IDisposable; }; private readonly _proxy: ExtHostDocumentsShape; private readonly _modelIsSynced = new Set(); - private _modelReferenceCollection = new BoundModelReferenceCollection(); + private readonly _modelReferenceCollection: BoundModelReferenceCollection; constructor( documentsAndEditors: MainThreadDocumentsAndEditors, @@ -83,19 +95,23 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { @ITextFileService textFileService: ITextFileService, @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IUriIdentityService uriIdentityService: IUriIdentityService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService ) { this._modelService = modelService; this._textModelResolverService = textModelResolverService; this._textFileService = textFileService; this._fileService = fileService; this._environmentService = environmentService; + this._uriIdentityService = uriIdentityService; + + this._modelReferenceCollection = this._toDispose.add(new BoundModelReferenceCollection(uriIdentityService.extUri)); this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); this._toDispose.add(documentsAndEditors.onDocumentAdd(models => models.forEach(this._onModelAdded, this))); this._toDispose.add(documentsAndEditors.onDocumentRemove(urls => urls.forEach(this._onModelRemoved, this))); - this._toDispose.add(this._modelReferenceCollection); this._toDispose.add(modelService.onModelModeChanged(this._onModelModeChanged, this)); this._toDispose.add(textFileService.files.onDidSave(e => { @@ -109,6 +125,12 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { } })); + this._toDispose.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => { + if (e.source && (e.operation === FileOperation.MOVE || e.operation === FileOperation.DELETE)) { + this._modelReferenceCollection.remove(e.source); + } + })); + this._modelToDisposeMap = Object.create(null); } @@ -163,33 +185,37 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { return this._textFileService.save(URI.revive(uri)).then(target => !!target); } - $tryOpenDocument(_uri: UriComponents): Promise { - const uri = URI.revive(_uri); - if (!uri.scheme || !(uri.fsPath || uri.authority)) { + $tryOpenDocument(uriData: UriComponents): Promise { + const inputUri = URI.revive(uriData); + if (!inputUri.scheme || !(inputUri.fsPath || inputUri.authority)) { return Promise.reject(new Error(`Invalid uri. Scheme and authority or path must be set.`)); } - let promise: Promise; - switch (uri.scheme) { + const canonicalUri = this._uriIdentityService.asCanonicalUri(inputUri); + + let promise: Promise; + switch (canonicalUri.scheme) { case Schemas.untitled: - promise = this._handleUntitledScheme(uri); + promise = this._handleUntitledScheme(canonicalUri); break; case Schemas.file: default: - promise = this._handleAsResourceInput(uri); + promise = this._handleAsResourceInput(canonicalUri); break; } - return promise.then(success => { - if (!success) { - return Promise.reject(new Error('cannot open ' + uri.toString())); - } else if (!this._modelIsSynced.has(uri.toString())) { - return Promise.reject(new Error('cannot open ' + uri.toString() + '. Detail: Files above 50MB cannot be synchronized with extensions.')); + return promise.then(documentUri => { + if (!documentUri) { + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}`)); + } else if (!extUri.isEqual(documentUri, canonicalUri)) { + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Actual document opened as ${documentUri.toString()}`)); + } else if (!this._modelIsSynced.has(canonicalUri.toString())) { + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: Files above 50MB cannot be synchronized with extensions.`)); } else { - return undefined; + return canonicalUri; } }, err => { - return Promise.reject(new Error('cannot open ' + uri.toString() + '. Detail: ' + toErrorMessage(err))); + return Promise.reject(new Error(`cannot open ${canonicalUri.toString()}. Detail: ${toErrorMessage(err)}`)); }); } @@ -197,21 +223,20 @@ export class MainThreadDocuments implements MainThreadDocumentsShape { return this._doCreateUntitled(undefined, options ? options.language : undefined, options ? options.content : undefined); } - private _handleAsResourceInput(uri: URI): Promise { + private _handleAsResourceInput(uri: URI): Promise { return this._textModelResolverService.createModelReference(uri).then(ref => { - this._modelReferenceCollection.add(ref); - const result = !!ref.object; - return result; + this._modelReferenceCollection.add(uri, ref); + return ref.object.textEditorModel.uri; }); } - private _handleUntitledScheme(uri: URI): Promise { + private _handleUntitledScheme(uri: URI): Promise { const asLocalUri = toLocalResource(uri, this._environmentService.configuration.remoteAuthority); return this._fileService.resolve(asLocalUri).then(stats => { // don't create a new file ontop of an existing file return Promise.reject(new Error('file already exists')); }, err => { - return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined).then(resource => !!resource); + return this._doCreateUntitled(Boolean(uri.path) ? uri : undefined); }); } diff --git a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts index 78da88e8955..a8cd6ddfee8 100644 --- a/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadDocumentsAndEditors.ts @@ -27,6 +27,8 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; namespace delta { @@ -326,11 +328,13 @@ export class MainThreadDocumentsAndEditors { @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, @IBulkEditService bulkEditService: IBulkEditService, @IPanelService panelService: IPanelService, - @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService + @IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService, + @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService, + @IUriIdentityService uriIdentityService: IUriIdentityService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); - const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService)); + const mainThreadDocuments = this._toDispose.add(new MainThreadDocuments(this, extHostContext, this._modelService, this._textFileService, fileService, textModelResolverService, environmentService, uriIdentityService, workingCopyFileService)); extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); const mainThreadTextEditors = this._toDispose.add(new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._editorService, this._editorGroupService)); diff --git a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts index d88424433fb..aec20a76da9 100644 --- a/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts +++ b/src/vs/workbench/api/browser/mainThreadFileSystemEventService.ts @@ -8,12 +8,9 @@ import { FileChangeType, IFileService, FileOperation } from 'vs/platform/files/c import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IProgressService } from 'vs/platform/progress/common/progress'; import { localize } from 'vs/nls'; import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry'; import { Registry } from 'vs/platform/registry/common/platform'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { ILogService } from 'vs/platform/log/common/log'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; @extHostCustomer @@ -25,9 +22,6 @@ export class MainThreadFileSystemEventService { extHostContext: IExtHostContext, @IFileService fileService: IFileService, @ITextFileService textFileService: ITextFileService, - @IProgressService progressService: IProgressService, - @IConfigurationService configService: IConfigurationService, - @ILogService logService: ILogService, @IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService ) { diff --git a/src/vs/workbench/api/browser/mainThreadLanguages.ts b/src/vs/workbench/api/browser/mainThreadLanguages.ts index 19bdbd3f9e2..b979dad2a2e 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguages.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguages.ts @@ -8,6 +8,9 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { MainThreadLanguagesShape, MainContext, IExtHostContext } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { IPosition } from 'vs/editor/common/core/position'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { StandardTokenType } from 'vs/editor/common/modes'; @extHostNamedCustomer(MainContext.MainThreadLanguages) export class MainThreadLanguages implements MainThreadLanguagesShape { @@ -40,4 +43,19 @@ export class MainThreadLanguages implements MainThreadLanguagesShape { this._modelService.setMode(model, this._modeService.create(languageId)); return Promise.resolve(undefined); } + + async $tokensAtPosition(resource: UriComponents, position: IPosition): Promise { + const uri = URI.revive(resource); + const model = this._modelService.getModel(uri); + if (!model) { + return undefined; + } + model.tokenizeIfCheap(position.lineNumber); + const tokens = model.getLineTokens(position.lineNumber); + const idx = tokens.findTokenIndexAtOffset(position.column - 1); + return { + type: tokens.getStandardTokenType(idx), + range: new Range(position.lineNumber, 1 + tokens.getStartOffset(idx), position.lineNumber, 1 + tokens.getEndOffset(idx)) + }; + } } diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 2ead740a60d..d908d6c0fb0 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext, INotebookDocumentsAndEditorsDelta, INotebookModelAddedData } from '../common/extHost.protocol'; +import { Disposable, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; import { URI, UriComponents } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookMimeTypeSelector, NOTEBOOK_DISPLAY_ORDER, NotebookCellOutputsSplice, NotebookDocumentMetadata, NotebookCellMetadata, ICellEditOperation, ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER, CellEditType, CellKind, INotebookKernelInfo, INotebookKernelInfoDto, INotebookTextModelBackup, IEditor, INotebookRendererInfo, IOutputRenderRequest, IOutputRenderResponse } 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'; @@ -29,12 +29,14 @@ export class MainThreadNotebookDocument extends Disposable { private readonly _proxy: ExtHostNotebookShape, public handle: number, public viewType: string, - public uri: URI + public uri: URI, + readonly notebookService: INotebookService ) { super(); this._textModel = new NotebookTextModel(handle, viewType, uri); - this._register(this._textModel.onDidModelChange(e => { + 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; @@ -42,25 +44,134 @@ export class MainThreadNotebookDocument extends Disposable { })); } - applyEdit(modelVersionId: number, edits: ICellEditOperation[]): boolean { - return this._textModel.applyEdit(modelVersionId, edits); + async applyEdit(modelVersionId: number, edits: ICellEditOperation[]): Promise { + await this.notebookService.transformEditsOutputs(this.textModel, edits); + return this._textModel.$applyEdit(modelVersionId, edits); } - updateRenderers(renderers: number[]) { - this._textModel.updateRenderers(renderers); + async spliceNotebookCellOutputs(cellHandle: number, splices: NotebookCellOutputsSplice[]) { + await this.notebookService.transformSpliceOutputs(this.textModel, splices); + this._textModel.$spliceNotebookCellOutputs(cellHandle, splices); } - dispose() { this._textModel.dispose(); super.dispose(); } } +class DocumentAndEditorState { + static ofSets(before: Set, after: Set): { removed: T[], added: T[] } { + const removed: T[] = []; + const added: T[] = []; + before.forEach(element => { + if (!after.has(element)) { + removed.push(element); + } + }); + after.forEach(element => { + if (!before.has(element)) { + added.push(element); + } + }); + return { removed, added }; + } + + static ofMaps(before: Map, after: Map): { removed: V[], added: V[] } { + const removed: V[] = []; + const added: V[] = []; + before.forEach((value, index) => { + if (!after.has(index)) { + removed.push(value); + } + }); + after.forEach((value, index) => { + if (!before.has(index)) { + added.push(value); + } + }); + return { removed, added }; + } + + static compute(before: DocumentAndEditorState | undefined, after: DocumentAndEditorState): INotebookDocumentsAndEditorsDelta { + if (!before) { + const apiEditors = []; + for (let id in after.textEditors) { + const editor = after.textEditors.get(id)!; + apiEditors.push({ id, documentUri: editor.uri!, selections: editor!.textModel!.selections }); + } + + return { + addedDocuments: [], + addedEditors: apiEditors, + visibleEditors: [...after.visibleEditors].map(editor => editor[0]) + }; + } + const documentDelta = DocumentAndEditorState.ofSets(before.documents, after.documents); + const editorDelta = DocumentAndEditorState.ofMaps(before.textEditors, after.textEditors); + const addedAPIEditors = editorDelta.added.map(add => ({ + id: add.getId(), + documentUri: add.uri!, + selections: add.textModel!.selections || [] + })); + + const removedAPIEditors = editorDelta.removed.map(removed => removed.getId()); + + // const oldActiveEditor = before.activeEditor !== after.activeEditor ? before.activeEditor : undefined; + const newActiveEditor = before.activeEditor !== after.activeEditor ? after.activeEditor : undefined; + + const visibleEditorDelta = DocumentAndEditorState.ofMaps(before.visibleEditors, after.visibleEditors); + + return { + addedDocuments: documentDelta.added.map(e => { + return { + viewType: e.viewType, + handle: e.handle, + uri: e.uri, + metadata: e.metadata, + versionId: e.versionId, + cells: e.cells.map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.textBuffer.getLinesContent(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + })), + // attachedEditor: editorId ? { + // id: editorId, + // selections: document.textModel.selections + // } : undefined + }; + }), + removedDocuments: documentDelta.removed.map(e => e.uri), + addedEditors: addedAPIEditors, + removedEditors: removedAPIEditors, + newActiveEditor: newActiveEditor, + visibleEditors: visibleEditorDelta.added.length === 0 && visibleEditorDelta.removed.length === 0 + ? undefined + : [...after.visibleEditors].map(editor => editor[0]) + }; + } + + constructor( + readonly documents: Set, + readonly textEditors: Map, + readonly activeEditor: string | null | undefined, + readonly visibleEditors: Map + ) { + // + } +} + @extHostNamedCustomer(MainContext.MainThreadNotebook) export class MainThreadNotebooks extends Disposable implements MainThreadNotebookShape { private readonly _notebookProviders = new Map(); private readonly _notebookKernels = new Map(); + private readonly _notebookRenderers = new Map(); private readonly _proxy: ExtHostNotebookShape; + private _toDisposeOnEditorRemove = new Map(); + private _currentState?: DocumentAndEditorState; constructor( extHostContext: IExtHostContext, @@ -85,11 +196,41 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo return false; } + private _emitDelta(delta: INotebookDocumentsAndEditorsDelta) { + this._proxy.$acceptDocumentAndEditorsDelta(delta); + } + registerListeners() { + this._notebookService.listNotebookEditors().forEach((e) => { + this._addNotebookEditor(e); + }); + this._register(this._notebookService.onDidChangeActiveEditor(e => { - this._proxy.$acceptDocumentAndEditorsDelta({ - newActiveEditor: e.uri - }); + this._updateState(); + })); + + this._register(this._notebookService.onDidChangeVisibleEditors(e => { + if (this._notebookProviders.size > 0) { + if (!this._currentState) { + // no current state means we didn't even create editors in ext host yet. + return; + } + + // we can't simply update visibleEditors as we need to check if we should create editors first. + this._updateState(); + } + })); + + this._register(this._notebookService.onNotebookEditorAdd(editor => { + this._addNotebookEditor(editor); + })); + + this._register(this._notebookService.onNotebookEditorsRemove(editors => { + this._removeNotebookEditor(editors); + })); + + this._register(this._notebookService.onNotebookDocumentRemove(() => { + this._updateState(); })); const updateOrder = () => { @@ -111,23 +252,120 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => { updateOrder(); })); + + const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; + this._updateState(notebookEditor); } - async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise { - this._notebookService.registerNotebookRenderer(handle, extension, type, selectors, preloads.map(uri => URI.revive(uri))); + async addNotebookDocument(data: INotebookModelAddedData) { + this._updateState(); } - async $unregisterNotebookRenderer(handle: number): Promise { - this._notebookService.unregisterNotebookRenderer(handle); + private _addNotebookEditor(e: IEditor) { + this._toDisposeOnEditorRemove.set(e.getId(), combinedDisposable( + e.onDidChangeModel(() => this._updateState()), + e.onDidFocusEditorWidget(() => { + this._updateState(e); + }), + )); + + const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; + this._updateState(notebookEditor); } - async $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise { - let controller = new MainThreadNotebookController(this._proxy, this, viewType); + private _removeNotebookEditor(editors: IEditor[]) { + editors.forEach(e => { + const sub = this._toDisposeOnEditorRemove.get(e.getId()); + if (sub) { + this._toDisposeOnEditorRemove.delete(e.getId()); + sub.dispose(); + } + }); + + this._updateState(); + } + + private async _updateState(focusedNotebookEditor?: IEditor) { + let activeEditor: string | null = null; + + const activeEditorPane = this.editorService.activeEditorPane as any | undefined; + if (activeEditorPane?.isNotebookEditor) { + const notebookEditor = (activeEditorPane.getControl() as INotebookEditor); + activeEditor = notebookEditor && notebookEditor.hasModel() ? notebookEditor!.getId() : null; + } + + const documentEditorsMap = new Map(); + + const editors = new Map(); + this._notebookService.listNotebookEditors().forEach(editor => { + if (editor.hasModel()) { + editors.set(editor.getId(), editor); + documentEditorsMap.set(editor.textModel!.uri.toString(), editor); + } + }); + + const visibleEditorsMap = new Map(); + this.editorService.visibleEditorPanes.forEach(editor => { + if ((editor as any).isNotebookEditor) { + const nbEditorWidget = (editor as any).getControl() as INotebookEditor; + if (nbEditorWidget && editors.has(nbEditorWidget.getId())) { + visibleEditorsMap.set(nbEditorWidget.getId(), nbEditorWidget); + } + } + }); + + const documents = new Set(); + this._notebookService.listNotebookDocuments().forEach(document => { + if (documentEditorsMap.has(document.uri.toString())) { + documents.add(document); + } + }); + + if (!activeEditor && focusedNotebookEditor && focusedNotebookEditor.hasModel()) { + activeEditor = focusedNotebookEditor.getId(); + } + + // editors always have view model attached, which means there is already a document in exthost. + const newState = new DocumentAndEditorState(documents, editors, activeEditor, visibleEditorsMap); + const delta = DocumentAndEditorState.compute(this._currentState, newState); + // const isEmptyChange = (!delta.addedDocuments || delta.addedDocuments.length === 0) + // && (!delta.removedDocuments || delta.removedDocuments.length === 0) + // && (!delta.addedEditors || delta.addedEditors.length === 0) + // && (!delta.removedEditors || delta.removedEditors.length === 0) + // && (delta.newActiveEditor === undefined) + + // if (!isEmptyChange) { + this._currentState = newState; + await this._emitDelta(delta); + // } + } + + async $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: UriComponents[]): Promise { + const renderer = new MainThreadNotebookRenderer(this._proxy, type, 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, kernel: INotebookKernelInfoDto | undefined): Promise { + let controller = new MainThreadNotebookController(this._proxy, this, viewType, kernel, this._notebookService); this._notebookProviders.set(viewType, controller); this._notebookService.registerNotebookController(viewType, extension, controller); return; } + async $onNotebookChange(viewType: string, uri: UriComponents): Promise { + let controller = this._notebookProviders.get(viewType); + if (controller) { + controller.handleNotebookChange(uri); + } + } + async $unregisterNotebookProvider(viewType: string): Promise { this._notebookProviders.delete(viewType); this._notebookService.unregisterNotebookProvider(viewType); @@ -173,11 +411,11 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo async $spliceNotebookCellOutputs(viewType: string, resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { let controller = this._notebookProviders.get(viewType); - controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); + await controller?.spliceNotebookCellOutputs(resource, cellHandle, splices, renderers); } - async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise { - return this._proxy.$executeNotebook(viewType, uri, undefined, token); + async executeNotebook(viewType: string, uri: URI, useAttachedKernel: boolean, token: CancellationToken): Promise { + return this._proxy.$executeNotebook(viewType, uri, undefined, useAttachedKernel, token); } async $postMessage(handle: number, value: any): Promise { @@ -203,11 +441,14 @@ export class MainThreadNotebookController implements IMainNotebookController { constructor( private readonly _proxy: ExtHostNotebookShape, private _mainThreadNotebook: MainThreadNotebooks, - private _viewType: string + private _viewType: string, + readonly kernel: INotebookKernelInfoDto | undefined, + readonly notebookService: INotebookService, + ) { } - async createNotebook(viewType: string, uri: URI, forBackup: boolean, forceReload: boolean): Promise { + async createNotebook(viewType: string, uri: URI, backup: INotebookTextModelBackup | undefined, forceReload: boolean, editorId?: string): Promise { let mainthreadNotebook = this._mapping.get(URI.from(uri).toString()); if (mainthreadNotebook) { @@ -219,7 +460,7 @@ export class MainThreadNotebookController implements IMainNotebookController { mainthreadNotebook.textModel.languages = data.languages; mainthreadNotebook.textModel.metadata = data.metadata; - mainthreadNotebook.textModel.applyEdit(mainthreadNotebook.textModel.versionId, [ + mainthreadNotebook.textModel.$applyEdit(mainthreadNotebook.textModel.versionId, [ { editType: CellEditType.Delete, count: mainthreadNotebook.textModel.cells.length, index: 0 }, { editType: CellEditType.Insert, index: 0, cells: data.cells } ]); @@ -227,10 +468,43 @@ export class MainThreadNotebookController implements IMainNotebookController { return mainthreadNotebook.textModel; } - let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri); - await this.createNotebookDocument(document); + let document = new MainThreadNotebookDocument(this._proxy, MainThreadNotebookController.documentHandle++, viewType, uri, this.notebookService); + this._mapping.set(document.uri.toString(), document); + + if (backup) { + // trigger events + document.textModel.metadata = backup.metadata; + document.textModel.languages = backup.languages; + + document.textModel.$applyEdit(document.textModel.versionId, [ + { + editType: CellEditType.Insert, + index: 0, + cells: backup.cells || [] + } + ]); + + await this._mainThreadNotebook.addNotebookDocument({ + viewType: document.viewType, + handle: document.handle, + uri: document.uri, + metadata: document.textModel.metadata, + versionId: document.textModel.versionId, + cells: document.textModel.cells.map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.textBuffer.getLinesContent(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + })), + attachedEditor: editorId ? { + id: editorId, + selections: document.textModel.selections + } : undefined + }); - if (forBackup) { return document.textModel; } @@ -250,6 +524,27 @@ export class MainThreadNotebookController implements IMainNotebookController { document.textModel.insertTemplateCell(mainCell); } + await this._mainThreadNotebook.addNotebookDocument({ + viewType: document.viewType, + handle: document.handle, + uri: document.uri, + metadata: document.textModel.metadata, + versionId: document.textModel.versionId, + cells: document.textModel.cells.map(cell => ({ + handle: cell.handle, + uri: cell.uri, + source: cell.textBuffer.getLinesContent(), + language: cell.language, + cellKind: cell.cellKind, + outputs: cell.outputs, + metadata: cell.metadata + })), + attachedEditor: editorId ? { + id: editorId, + selections: document.textModel.selections + } : undefined + }); + this._proxy.$acceptEditorPropertiesChanged(uri, { selections: null, metadata: document.textModel.metadata }); return document.textModel; @@ -259,38 +554,23 @@ export class MainThreadNotebookController implements IMainNotebookController { let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); if (mainthreadNotebook) { - mainthreadNotebook.updateRenderers(renderers); - return mainthreadNotebook.applyEdit(modelVersionId, edits); + return await mainthreadNotebook.applyEdit(modelVersionId, edits); } return false; } - spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): void { + async spliceNotebookCellOutputs(resource: UriComponents, cellHandle: number, splices: NotebookCellOutputsSplice[], renderers: number[]): Promise { let mainthreadNotebook = this._mapping.get(URI.from(resource).toString()); - mainthreadNotebook?.textModel.updateRenderers(renderers); - mainthreadNotebook?.textModel.$spliceNotebookCellOutputs(cellHandle, splices); + await mainthreadNotebook?.spliceNotebookCellOutputs(cellHandle, splices); } - async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise { - return this._mainThreadNotebook.executeNotebook(viewType, uri, token); + async executeNotebook(viewType: string, uri: URI, useAttachedKernel: boolean, token: CancellationToken): Promise { + return this._mainThreadNotebook.executeNotebook(viewType, uri, useAttachedKernel, token); } - onDidReceiveMessage(uri: UriComponents, message: any): void { - this._proxy.$onDidReceiveMessage(uri, message); - } - - async createNotebookDocument(document: MainThreadNotebookDocument): Promise { - this._mapping.set(document.uri.toString(), document); - - await this._proxy.$acceptDocumentAndEditorsDelta({ - addedDocuments: [{ - viewType: document.viewType, - handle: document.handle, - uri: document.uri, - metadata: document.textModel.metadata - }] - }); + onDidReceiveMessage(editorId: string, message: any): void { + this._proxy.$onDidReceiveMessage(editorId, message); } async removeNotebookDocument(notebook: INotebookTextModel): Promise { @@ -300,6 +580,7 @@ export class MainThreadNotebookController implements IMainNotebookController { return; } + // TODO@rebornix, remove cell should use emitDelta as well to ensure document/editor events are sent together await this._proxy.$acceptDocumentAndEditorsDelta({ removedDocuments: [notebook.uri] }); document.dispose(); this._mapping.delete(URI.from(notebook.uri).toString()); @@ -307,6 +588,11 @@ export class MainThreadNotebookController implements IMainNotebookController { // Methods for ExtHost + handleNotebookChange(resource: UriComponents) { + let document = this._mapping.get(URI.from(resource).toString()); + document?.textModel.handleUnknownChange(); + } + updateLanguages(resource: UriComponents, languages: string[]) { let document = this._mapping.get(URI.from(resource).toString()); document?.textModel.updateLanguages(languages); @@ -322,13 +608,8 @@ export class MainThreadNotebookController implements IMainNotebookController { document?.textModel.updateNotebookCellMetadata(handle, metadata); } - updateNotebookRenderers(resource: UriComponents, renderers: number[]): void { - let document = this._mapping.get(URI.from(resource).toString()); - document?.textModel.updateRenderers(renderers); - } - - async executeNotebookCell(uri: URI, handle: number, token: CancellationToken): Promise { - return this._proxy.$executeNotebook(this._viewType, uri, handle, token); + async executeNotebookCell(uri: URI, handle: number, useAttachedKernel: boolean, token: CancellationToken): Promise { + return this._proxy.$executeNotebook(this._viewType, uri, handle, useAttachedKernel, token); } async save(uri: URI, token: CancellationToken): Promise { @@ -357,3 +638,24 @@ export class MainThreadNotebookKernel implements INotebookKernelInfo { return this._proxy.$executeNotebook2(this.id, viewType, uri, handle, token); } } + +export class MainThreadNotebookRenderer implements INotebookRendererInfo { + constructor( + private readonly _proxy: ExtHostNotebookShape, + readonly id: 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/mainThreadStatusBar.ts b/src/vs/workbench/api/browser/mainThreadStatusBar.ts index d2846ff106e..33e5b948c9f 100644 --- a/src/vs/workbench/api/browser/mainThreadStatusBar.ts +++ b/src/vs/workbench/api/browser/mainThreadStatusBar.ts @@ -9,6 +9,7 @@ import { ThemeColor } from 'vs/platform/theme/common/themeService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { dispose } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/modes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; @extHostNamedCustomer(MainContext.MainThreadStatusBar) export class MainThreadStatusBar implements MainThreadStatusBarShape { @@ -25,9 +26,14 @@ export class MainThreadStatusBar implements MainThreadStatusBarShape { this.entries.clear(); } - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined): void { + $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: Command | undefined, color: string | ThemeColor | undefined, alignment: MainThreadStatusBarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation): void { // if there are icons in the text use the tooltip for the aria label - const ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text; + let ariaLabel: string; + if (accessibilityInformation) { + ariaLabel = accessibilityInformation.label; + } else { + ariaLabel = text.indexOf('$(') === -1 ? text : tooltip || text; + } const entry: IStatusbarEntry = { text, tooltip, command, color, ariaLabel }; if (typeof priority === 'undefined') { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 9e4d58a2cd8..6f8e2f24e7c 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -361,6 +361,17 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma } webviewInput.webview.onDispose(() => { + // If the model is still dirty, make sure we have time to save it + if (modelRef.object.isDirty()) { + const sub = modelRef.object.onDidChangeDirty(() => { + if (!modelRef.object.isDirty()) { + sub.dispose(); + modelRef.dispose(); + } + }); + return; + } + modelRef.dispose(); }); @@ -650,10 +661,11 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod ) { super(); + this._fromBackup = fromBackup; + if (_editable) { this._register(workingCopyService.registerWorkingCopy(this)); } - this._fromBackup = fromBackup; } get editorResource() { @@ -711,7 +723,7 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod //#endregion public isReadonly() { - return this._editable; + return !this._editable; } public get viewType() { diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 17516f5f860..ace383a9f24 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -8,11 +8,12 @@ import * as objects from 'vs/base/common/objects'; import { Registry } from 'vs/platform/registry/common/platform'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { IConfigurationNode, IConfigurationRegistry, Extensions, resourceLanguageSettingsSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; +import { IConfigurationNode, IConfigurationRegistry, Extensions, resourceLanguageSettingsSchemaId, IDefaultConfigurationExtension, validateProperty, ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; import { workspaceSettingsSchemaId, launchSchemaId, tasksSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { isObject } from 'vs/base/common/types'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IStringDictionary } from 'vs/base/common/collections'; const configurationRegistry = Registry.as(Extensions.Configuration); @@ -116,7 +117,13 @@ defaultConfigurationExtPoint.setHandler((extensions, { added, removed }) => { const addedDefaultConfigurations = added.map(extension => { const id = extension.description.identifier; const name = extension.description.name; - const defaults = objects.deepClone(extension.value); + const defaults: IStringDictionary = objects.deepClone(extension.value); + for (const key of Object.keys(defaults)) { + if (!OVERRIDE_PROPERTY_PATTERN.test(key) || typeof defaults[key] !== 'object') { + extension.collector.warn(nls.localize('config.property.defaultConfiguration.warning', "Cannot register configuration defaults for '{0}'. Only defaults for language specific settings are supported.", key)); + delete defaults[key]; + } + } return { id, name, defaults }; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 59926ad7adf..1c1e3efeae1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -134,7 +134,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); 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, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors)); + const extHostNotebook = rpcProtocol.set(ExtHostContext.ExtHostNotebook, new ExtHostNotebookController(rpcProtocol, extHostCommands, extHostDocumentsAndEditors, initData.environment)); 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)); @@ -193,14 +193,17 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get onDidChangeAuthenticationProviders(): Event { return extHostAuthentication.onDidChangeAuthenticationProviders; }, + getProviderIds(): Thenable> { + return extHostAuthentication.getProviderIds(); + }, get providerIds(): string[] { return extHostAuthentication.providerIds; }, hasSessions(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.hasSessions(providerId, scopes); }, - getSession(providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions): Thenable { - return extHostAuthentication.getSession(extension, providerId, scopes, options); + getSession(providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions) { + return extHostAuthentication.getSession(extension, providerId, scopes, options as any); }, getSessions(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.getSessions(extension, providerId, scopes); @@ -431,6 +434,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, setLanguageConfiguration: (language: string, configuration: vscode.LanguageConfiguration): vscode.Disposable => { return extHostLanguageFeatures.setLanguageConfiguration(extension, language, configuration); + }, + getTokenInformationAtPosition(doc: vscode.TextDocument, pos: vscode.Position) { + checkProposedApiEnabled(extension); + return extHostLanguages.tokenAtPosition(doc, pos); } }; @@ -531,12 +538,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I let id: string; let name: string; let alignment: number | undefined; + let accessibilityInformation: vscode.AccessibilityInformation | undefined = undefined; if (alignmentOrOptions && typeof alignmentOrOptions !== 'number') { id = alignmentOrOptions.id; name = alignmentOrOptions.name; alignment = alignmentOrOptions.alignment; priority = alignmentOrOptions.priority; + accessibilityInformation = alignmentOrOptions.accessibilityInformation; } else { id = extension.identifier.value; name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name); @@ -544,7 +553,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I priority = priority; } - return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority); + return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation); }, setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): vscode.Disposable { return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable); @@ -590,11 +599,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerWebviewPanelSerializer: (viewType: string, serializer: vscode.WebviewPanelSerializer) => { return extHostWebviews.registerWebviewPanelSerializer(extension, viewType, serializer); }, - registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions } = {}) => { - return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); - }, - registerCustomEditorProvider2: (viewType: string, provider: vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => { - checkProposedApiEnabled(extension); + registerCustomEditorProvider: (viewType: string, provider: vscode.CustomTextEditorProvider | vscode.CustomReadonlyEditorProvider, options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean } = {}) => { return extHostWebviews.registerCustomEditorProvider(extension, viewType, provider, options); }, registerDecorationProvider(provider: vscode.DecorationProvider) { @@ -706,8 +711,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I } return uriPromise.then(uri => { - return extHostDocuments.ensureDocumentData(uri).then(() => { - return extHostDocuments.getDocument(uri); + return extHostDocuments.ensureDocumentData(uri).then(documentData => { + return documentData.document; }); }); }, @@ -916,6 +921,12 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.onDidCloseNotebookDocument; }, + get visibleNotebookEditors() { + return extHostNotebook.visibleNotebookEditors; + }, + get onDidChangeVisibleNotebookEditors() { + return extHostNotebook.onDidChangeVisibleNotebookEditors; + }, registerNotebookContentProvider: (viewType: string, provider: vscode.NotebookContentProvider) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookContentProvider(extension, viewType, provider); @@ -936,9 +947,21 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension); return extHostNotebook.activeNotebookEditor; }, - onDidChangeNotebookDocument(listener, thisArgs?, disposables?) { + onDidChangeActiveNotebookEditor(listener, thisArgs?, disposables?) { checkProposedApiEnabled(extension); - return extHostNotebook.onDidChangeNotebookDocument(listener, thisArgs, disposables); + return extHostNotebook.onDidChangeActiveNotebookEditor(listener, thisArgs, disposables); + }, + onDidChangeNotebookCells(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeNotebookCells(listener, thisArgs, disposables); + }, + onDidChangeCellOutputs(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeCellOutputs(listener, thisArgs, disposables); + }, + onDidChangeCellLanguage(listener, thisArgs?, disposables?) { + checkProposedApiEnabled(extension); + return extHostNotebook.onDidChangeCellLanguage(listener, thisArgs, disposables); }, createConcatTextDocument(notebook, selector) { checkProposedApiEnabled(extension); @@ -1038,6 +1061,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I SnippetString: extHostTypes.SnippetString, SourceBreakpoint: extHostTypes.SourceBreakpoint, SourceControlInputBoxValidationType: extHostTypes.SourceControlInputBoxValidationType, + StandardTokenType: extHostTypes.StandardTokenType, StatusBarAlignment: extHostTypes.StatusBarAlignment, SymbolInformation: extHostTypes.SymbolInformation, SymbolKind: extHostTypes.SymbolKind, @@ -1074,7 +1098,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TimelineItem: extHostTypes.TimelineItem, CellKind: extHostTypes.CellKind, CellOutputKind: extHostTypes.CellOutputKind, - NotebookCellRunState: extHostTypes.NotebookCellRunState + NotebookCellRunState: extHostTypes.NotebookCellRunState, + AuthenticationSession2: extHostTypes.AuthenticationSession }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index aa5543d5f0a..b662b07ac07 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -51,11 +51,12 @@ 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, IOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookMimeTypeSelector, IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEvent, NotebookDataDto, INotebookKernelInfoDto, IMainCellDto, IOutputRenderRequest, IOutputRenderResponse, IRawOutput } 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'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -157,14 +158,20 @@ export interface MainThreadCommentsShape extends IDisposable { } export interface MainThreadAuthenticationShape extends IDisposable { - $registerAuthenticationProvider(id: string, displayName: string): void; + $registerAuthenticationProvider(id: string, displayName: string, supportsMultipleAccounts: boolean): void; $unregisterAuthenticationProvider(id: string): void; - $onDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; - $getSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; + $getProviderIds(): Promise; + $sendDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void; + $getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise; + $selectSession(providerId: string, providerName: string, extensionId: string, extensionName: string, potentialSessions: modes.AuthenticationSession[], scopes: string[], clearSessionPreference: boolean): Promise; $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise; $loginPrompt(providerName: string, extensionName: string): Promise; $setTrustedExtension(providerId: string, accountName: string, extensionId: string, extensionName: string): Promise; $requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): Promise; + + $getSessions(providerId: string): Promise>; + $login(providerId: string, scopes: string[]): Promise; + $logout(providerId: string, sessionId: string): Promise; } export interface MainThreadConfigurationShape extends IDisposable { @@ -213,7 +220,7 @@ export interface MainThreadDocumentContentProvidersShape extends IDisposable { export interface MainThreadDocumentsShape extends IDisposable { $tryCreateDocument(options?: { language?: string; content?: string; }): Promise; - $tryOpenDocument(uri: UriComponents): Promise; + $tryOpenDocument(uri: UriComponents): Promise; $trySaveDocument(uri: UriComponents): Promise; } @@ -392,6 +399,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { export interface MainThreadLanguagesShape extends IDisposable { $getLanguages(): Promise; $changeLanguage(resource: UriComponents, languageId: string): Promise; + $tokensAtPosition(resource: UriComponents, position: IPosition): Promise; } export interface MainThreadMessageOptions { @@ -545,7 +553,7 @@ export interface MainThreadQuickOpenShape extends IDisposable { } export interface MainThreadStatusBarShape extends IDisposable { - $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, alignment: statusbar.StatusbarAlignment, priority: number | undefined): void; + $setEntry(id: number, statusId: string, statusName: string, text: string, tooltip: string | undefined, command: ICommandDto | undefined, color: string | ThemeColor | undefined, alignment: statusbar.StatusbarAlignment, priority: number | undefined, accessibilityInformation: IAccessibilityInformation | undefined): void; $dispose(id: number): void; } @@ -672,7 +680,7 @@ export interface ICellDto { source: string[]; language: string; cellKind: CellKind; - outputs: IOutput[]; + outputs: IProcessedOutput[]; metadata?: NotebookCellMetadata; } @@ -685,14 +693,15 @@ export type NotebookCellsSplice = [ export type NotebookCellOutputsSplice = [ number /* start */, number /* delete count */, - IOutput[] + IRawOutput[] ]; export interface MainThreadNotebookShape extends IDisposable { - $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string): Promise; + $registerNotebookProvider(extension: NotebookExtensionDescription, viewType: string, kernelInfoDto: INotebookKernelInfoDto | undefined): Promise; + $onNotebookChange(viewType: string, resource: UriComponents): Promise; $unregisterNotebookProvider(viewType: string): Promise; - $registerNotebookRenderer(extension: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, handle: number, preloads: UriComponents[]): Promise; - $unregisterNotebookRenderer(handle: number): 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; $unregisterNotebookKernel(id: string): Promise; $tryApplyEdits(viewType: string, resource: UriComponents, modelVersionId: number, edits: ICellEditOperation[], renderers: number[]): Promise; @@ -1005,6 +1014,8 @@ export interface ExtHostAuthenticationShape { $getSessionAccessToken(id: string, sessionId: string): Promise; $login(id: string, scopes: string[]): Promise; $logout(id: string, sessionId: string): Promise; + $onDidChangeAuthenticationSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): Promise; + $onDidChangeAuthenticationProviders(added: string[], removed: string[]): Promise; } export interface ExtHostSearchShape { @@ -1545,27 +1556,38 @@ export interface INotebookEditorPropertiesChangeData { export interface INotebookModelAddedData { uri: UriComponents; handle: number; - // versionId: number; + versionId: number; + cells: IMainCellDto[], viewType: string; metadata?: NotebookDocumentMetadata; + attachedEditor?: { id: string; selections: number[]; } +} + +export interface INotebookEditorAddData { + id: string; + documentUri: UriComponents; + selections: number[]; } export interface INotebookDocumentsAndEditorsDelta { removedDocuments?: UriComponents[]; addedDocuments?: INotebookModelAddedData[]; - // removedEditors?: string[]; - // addedEditors?: ITextEditorAddData[]; - newActiveEditor?: UriComponents | null; + removedEditors?: string[]; + addedEditors?: INotebookEditorAddData[]; + newActiveEditor?: string | null; + visibleEditors?: string[]; } export interface ExtHostNotebookShape { $resolveNotebookData(viewType: string, uri: UriComponents): Promise; - $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise; + $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, useAttachedKernel: boolean, token: CancellationToken): Promise; $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise; $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise; $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise; $acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void; - $onDidReceiveMessage(uri: UriComponents, message: any): void; + $renderOutputs(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; + $renderOutputs2(uriComponents: UriComponents, id: string, request: IOutputRenderRequest): Promise | undefined>; + $onDidReceiveMessage(editorId: string, message: any): void; $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void; $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void; $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise; diff --git a/src/vs/workbench/api/common/extHostAuthentication.ts b/src/vs/workbench/api/common/extHostAuthentication.ts index 59dfdb260cf..2aaf39aa40c 100644 --- a/src/vs/workbench/api/common/extHostAuthentication.ts +++ b/src/vs/workbench/api/common/extHostAuthentication.ts @@ -24,6 +24,10 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication); } + getProviderIds(): Promise> { + return this._proxy.$getProviderIds(); + } + get providerIds(): string[] { const ids: string[] = []; this._authenticationProviders.forEach(provider => { @@ -33,31 +37,42 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return ids; } - async hasSessions(providerId: string, scopes: string[]): Promise { + private async resolveSessions(providerId: string): Promise> { const provider = this._authenticationProviders.get(providerId); + + let sessions; if (!provider) { - throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + sessions = await this._proxy.$getSessions(providerId); + } else { + sessions = await provider.getSessions(); } - const orderedScopes = scopes.sort().join(' '); - return !!(await provider.getSessions()).filter(session => session.scopes.sort().join(' ') === orderedScopes).length; + return sessions; } - async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions): Promise { + async hasSessions(providerId: string, scopes: string[]): Promise { + const orderedScopes = scopes.sort().join(' '); + const sessions = await this.resolveSessions(providerId); + return !!(sessions.filter(session => session.scopes.sort().join(' ') === orderedScopes).length); + } + + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise; + async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions): Promise { const provider = this._authenticationProviders.get(providerId); + const extensionName = requestingExtension.displayName || requestingExtension.name; + const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); + if (!provider) { - throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options); } const orderedScopes = scopes.sort().join(' '); const sessions = (await provider.getSessions()).filter(session => session.scopes.sort().join(' ') === orderedScopes); - const extensionName = requestingExtension.displayName || requestingExtension.name; - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); - if (sessions.length) { + if (sessions.length) { if (!provider.supportsMultipleAccounts) { const session = sessions[0]; - const allowed = await this._proxy.$getSessionsPrompt(provider.id, session.account.displayName, provider.displayName, extensionId, extensionName); + const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.displayName, provider.displayName, extensionId, extensionName); if (allowed) { return session; } else { @@ -66,7 +81,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } // On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid - const selected = await this._proxy.$getSession(provider.id, provider.displayName, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + const selected = await this._proxy.$selectSession(providerId, provider.displayName, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); return sessions.find(session => session.id === selected.id); } else { if (options.createIfNone) { @@ -76,25 +91,20 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { } const session = await provider.login(scopes); - await this._proxy.$setTrustedExtension(provider.id, session.account.displayName, extensionId, extensionName); + await this._proxy.$setTrustedExtension(providerId, session.account.displayName, extensionId, extensionName); return session; } else { - await this._proxy.$requestNewSession(provider.id, scopes, extensionId, extensionName); + await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); return undefined; } } } async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { - const provider = this._authenticationProviders.get(providerId); - if (!provider) { - throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); - } - const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier); const orderedScopes = scopes.sort().join(' '); - - return (await provider.getSessions()) + const sessions = await this.resolveSessions(providerId); + return sessions .filter(session => session.scopes.sort().join(' ') === orderedScopes) .map(session => { return { @@ -103,9 +113,10 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { scopes: session.scopes, getAccessToken: async () => { const isAllowed = await this._proxy.$getSessionsPrompt( - provider.id, + providerId, session.account.displayName, - provider.displayName, + '', // TODO + // provider.displayName, extensionId, requestingExtension.displayName || requestingExtension.name); @@ -113,7 +124,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error('User did not consent to token access.'); } - return session.getAccessToken(); + return session.accessToken; } }; }); @@ -149,7 +160,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error('User did not consent to token access.'); } - return session.getAccessToken(); + return session.accessToken; } }; } @@ -157,7 +168,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { async logout(providerId: string, sessionId: string): Promise { const provider = this._authenticationProviders.get(providerId); if (!provider) { - throw new Error(`No authentication provider with id '${providerId}' is currently registered.`); + return this._proxy.$logout(providerId, sessionId); } return provider.logout(sessionId); @@ -171,18 +182,15 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { this._authenticationProviders.set(provider.id, provider); const listener = provider.onDidChangeSessions(e => { - this._proxy.$onDidChangeSessions(provider.id, e); - this._onDidChangeSessions.fire({ [provider.id]: e }); + this._proxy.$sendDidChangeSessions(provider.id, e); }); - this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName); - this._onDidChangeAuthenticationProviders.fire({ added: [provider.id], removed: [] }); + this._proxy.$registerAuthenticationProvider(provider.id, provider.displayName, provider.supportsMultipleAccounts); return new Disposable(() => { listener.dispose(); this._authenticationProviders.delete(provider.id); this._proxy.$unregisterAuthenticationProvider(provider.id); - this._onDidChangeAuthenticationProviders.fire({ added: [], removed: [provider.id] }); }); } @@ -219,7 +227,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { const sessions = await authProvider.getSessions(); const session = sessions.find(session => session.id === sessionId); if (session) { - return session.getAccessToken(); + return session.accessToken; } throw new Error(`Unable to find session with id: ${sessionId}`); @@ -227,4 +235,14 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error(`Unable to find authentication provider with handle: ${providerId}`); } + + $onDidChangeAuthenticationSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent) { + this._onDidChangeSessions.fire({ [providerId]: event }); + return Promise.resolve(); + } + + $onDidChangeAuthenticationProviders(added: string[], removed: string[]) { + this._onDidChangeAuthenticationProviders.fire({ added, removed }); + return Promise.resolve(); + } } diff --git a/src/vs/workbench/api/common/extHostDecorations.ts b/src/vs/workbench/api/common/extHostDecorations.ts index cccb18f5fa9..9ed26739e6d 100644 --- a/src/vs/workbench/api/common/extHostDecorations.ts +++ b/src/vs/workbench/api/common/extHostDecorations.ts @@ -9,10 +9,10 @@ import { MainContext, ExtHostDecorationsShape, MainThreadDecorationsShape, Decor import { Disposable, Decoration } from 'vs/workbench/api/common/extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; -import { asArray } from 'vs/base/common/arrays'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ILogService } from 'vs/platform/log/common/log'; +import { asArray } from 'vs/base/common/arrays'; interface ProviderData { provider: vscode.DecorationProvider; @@ -40,7 +40,9 @@ export class ExtHostDecorations implements IExtHostDecorations { this._proxy.$registerDecorationProvider(handle, extensionId.value); const listener = provider.onDidChangeDecorations(e => { - this._proxy.$onDidChange(handle, !e ? null : asArray(e)); + this._proxy.$onDidChange(handle, !e || (Array.isArray(e) && e.length > 250) + ? null + : asArray(e)); }); return new Disposable(() => { diff --git a/src/vs/workbench/api/common/extHostDocuments.ts b/src/vs/workbench/api/common/extHostDocuments.ts index 4e8bb4d1252..af4a3f9de52 100644 --- a/src/vs/workbench/api/common/extHostDocuments.ts +++ b/src/vs/workbench/api/common/extHostDocuments.ts @@ -84,9 +84,10 @@ export class ExtHostDocuments implements ExtHostDocumentsShape { let promise = this._documentLoader.get(uri.toString()); if (!promise) { - promise = this._proxy.$tryOpenDocument(uri).then(() => { + promise = this._proxy.$tryOpenDocument(uri).then(uriData => { this._documentLoader.delete(uri.toString()); - return assertIsDefined(this._documentsAndEditors.getDocument(uri)); + const canonicalUri = URI.revive(uriData); + return assertIsDefined(this._documentsAndEditors.getDocument(canonicalUri)); }, err => { this._documentLoader.delete(uri.toString()); return Promise.reject(err); diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index d4ab9840abf..fb7b82cdda6 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -432,11 +432,37 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio this._logService.error(err); }); + for (const desc of this._registry.getAllExtensionDescriptions()) { + if (desc.activationEvents) { + for (const activationEvent of desc.activationEvents) { + if (/^onStartup:/.test(activationEvent)) { + const strTime = activationEvent.substr('onStartup:'.length); + const time = parseInt(strTime, 10); + if (!isNaN(time)) { + this._activateDelayed(desc, activationEvent, time); + } + } + } + } + } + this._disposables.add(this._extHostWorkspace.onDidChangeWorkspace((e) => this._handleWorkspaceContainsEagerExtensions(e.added))); const folders = this._extHostWorkspace.workspace ? this._extHostWorkspace.workspace.folders : []; return this._handleWorkspaceContainsEagerExtensions(folders); } + private _activateDelayed(desc: IExtensionDescription, activationEvent: string, delayMs: number): void { + setTimeout(() => { + this._activateById(desc.identifier, { + startup: true, + extensionId: desc.identifier, + activationEvent: activationEvent + }).then(undefined, (err) => { + this._logService.error(err); + }); + }, delayMs); + } + private _handleWorkspaceContainsEagerExtensions(folders: ReadonlyArray): Promise { if (folders.length === 0) { return Promise.resolve(undefined); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 13697b1e1ed..a114726c453 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -857,16 +857,11 @@ class SuggestAdapter { private _cache = new Cache('CompletionItem'); private _disposables = new Map(); - private _didWarnMust: boolean = false; - private _didWarnShould: boolean = false; - constructor( private readonly _documents: ExtHostDocuments, private readonly _commands: CommandsConverter, private readonly _provider: vscode.CompletionItemProvider, - private readonly _logService: ILogService, private readonly _apiDeprecation: IExtHostApiDeprecationService, - private readonly _telemetry: extHostProtocol.MainThreadTelemetryShape, private readonly _extension: IExtensionDescription, ) { } @@ -930,41 +925,12 @@ class SuggestAdapter { return undefined; } - const _mustNotChange = SuggestAdapter._mustNotChangeHash(item); - const _mayNotChange = SuggestAdapter._mayNotChangeHash(item); - const resolvedItem = await asPromise(() => this._provider.resolveCompletionItem!(item, token)); if (!resolvedItem) { return undefined; } - type BlameExtension = { - extensionId: string; - kind: string; - index: string; - }; - - type BlameExtensionMeta = { - extensionId: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; - kind: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; - index: { classification: 'SystemMetaData', purpose: 'PerformanceAndHealth' }; - }; - - let _mustNotChangeIndex = !this._didWarnMust && SuggestAdapter._mustNotChangeDiff(_mustNotChange, resolvedItem); - if (typeof _mustNotChangeIndex === 'string') { - this._logService.warn(`[${this._extension.identifier.value}] INVALID result from 'resolveCompletionItem', extension MUST NOT change any of: label, sortText, filterText, insertText, or textEdit`); - this._telemetry.$publicLog2('badresolvecompletion', { extensionId: this._extension.identifier.value, kind: 'must', index: _mustNotChangeIndex }); - this._didWarnMust = true; - } - - let _mayNotChangeIndex = !this._didWarnShould && SuggestAdapter._mayNotChangeDiff(_mayNotChange, resolvedItem); - if (typeof _mayNotChangeIndex === 'string') { - this._logService.info(`[${this._extension.identifier.value}] UNSAVE result from 'resolveCompletionItem', extension SHOULD NOT change any of: additionalTextEdits, or command`); - this._telemetry.$publicLog2('badresolvecompletion', { extensionId: this._extension.identifier.value, kind: 'should', index: _mayNotChangeIndex }); - this._didWarnShould = true; - } - return this._convertCompletionItem(resolvedItem, id); } @@ -1035,45 +1001,6 @@ class SuggestAdapter { return result; } - - private static _mustNotChangeHash(item: vscode.CompletionItem) { - const res = JSON.stringify([item.label, item.sortText, item.filterText, item.insertText, item.range]); - return res; - } - - private static _mustNotChangeDiff(hash: string, item: vscode.CompletionItem): string | void { - const thisArr = [item.label, item.sortText, item.filterText, item.insertText, item.range]; - const thisHash = JSON.stringify(thisArr); - if (hash === thisHash) { - return; - } - const arr = JSON.parse(hash); - for (let i = 0; i < 6; i++) { - if (JSON.stringify(arr[i] !== JSON.stringify(thisArr[i]))) { - return i.toString(); - } - } - return 'unknown'; - } - - private static _mayNotChangeHash(item: vscode.CompletionItem) { - return JSON.stringify([item.additionalTextEdits, item.command]); - } - - private static _mayNotChangeDiff(hash: string, item: vscode.CompletionItem): string | void { - const thisArr = [item.additionalTextEdits, item.command]; - const thisHash = JSON.stringify(thisArr); - if (hash === thisHash) { - return; - } - const arr = JSON.parse(hash); - for (let i = 0; i < 6; i++) { - if (JSON.stringify(arr[i] !== JSON.stringify(thisArr[i]))) { - return i.toString(); - } - } - return 'unknown'; - } } class SignatureHelpAdapter { @@ -1392,7 +1319,6 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF private readonly _uriTransformer: IURITransformer | null; private readonly _proxy: extHostProtocol.MainThreadLanguageFeaturesShape; - private readonly _telemetryShape: extHostProtocol.MainThreadTelemetryShape; private _documents: ExtHostDocuments; private _commands: ExtHostCommands; private _diagnostics: ExtHostDiagnostics; @@ -1411,7 +1337,6 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF ) { this._uriTransformer = uriTransformer; this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadLanguageFeatures); - this._telemetryShape = mainContext.getProxy(extHostProtocol.MainContext.MainThreadTelemetry); this._documents = documents; this._commands = commands; this._diagnostics = diagnostics; @@ -1780,7 +1705,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF // --- suggestion registerCompletionItemProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { - const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._logService, this._apiDeprecation, this._telemetryShape, extension), extension); + const handle = this._addNewAdapter(new SuggestAdapter(this._documents, this._commands.converter, provider, this._apiDeprecation, extension), extension); this._proxy.$registerSuggestSupport(handle, this._transformDocumentSelector(selector), triggerCharacters, SuggestAdapter.supportsResolving(provider), extension.identifier); return this._createDisposable(handle); } diff --git a/src/vs/workbench/api/common/extHostLanguages.ts b/src/vs/workbench/api/common/extHostLanguages.ts index 2d9e1af6550..035e22148dd 100644 --- a/src/vs/workbench/api/common/extHostLanguages.ts +++ b/src/vs/workbench/api/common/extHostLanguages.ts @@ -6,6 +6,8 @@ import { MainContext, MainThreadLanguagesShape, IMainContext } from './extHost.protocol'; import type * as vscode from 'vscode'; import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments'; +import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; +import { StandardTokenType, Range, Position } from 'vs/workbench/api/common/extHostTypes'; export class ExtHostLanguages { @@ -32,4 +34,31 @@ export class ExtHostLanguages { } return data.document; } + + async tokenAtPosition(document: vscode.TextDocument, position: vscode.Position): Promise { + const versionNow = document.version; + const pos = typeConvert.Position.from(position); + const info = await this._proxy.$tokensAtPosition(document.uri, pos); + const defaultRange = { + type: StandardTokenType.Other, + range: document.getWordRangeAtPosition(position) ?? new Range(position.line, position.character, position.line, position.character) + }; + if (!info) { + // no result + return defaultRange; + } + const result = { + range: typeConvert.Range.to(info.range), + type: typeConvert.TokenType.to(info.type) + }; + if (!result.range.contains(position)) { + // bogous result + return defaultRange; + } + if (versionNow !== document.version) { + // concurrent change + return defaultRange; + } + return result; + } } diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index 4403c2a169c..0d6daa6086a 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -10,15 +10,16 @@ import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecyc import { ISplice } from 'vs/base/common/sequence'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CellKind, CellOutputKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; +import { CellKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol'; import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors'; -import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, IErrorOutput, INotebookDisplayOrder, INotebookEditData, IOrderedMimeType, IStreamOutput, ITransformedDisplayOutputDto, mimeTypeSupportedByCore, NotebookCellsChangedEvent, NotebookCellsSplice2, sortMimeTypes, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Disposable as VSCodeDisposable } from './extHostTypes'; +import { CellEditType, CellUri, diff, ICellEditOperation, ICellInsertEdit, INotebookDisplayOrder, INotebookEditData, NotebookCellsChangedEvent, NotebookCellsSplice2, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseOutputInfo, IOutputRenderResponseCellInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import * as extHostTypes from 'vs/workbench/api/common/extHostTypes'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData'; import { NotImplementedProxy } from 'vs/base/common/types'; import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters'; +import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview'; interface IObservable { proxy: T; @@ -41,6 +42,12 @@ function getObservable(obj: T): IObservable { }; } +interface INotebookEventEmitter { + emitModelChange(events: vscode.NotebookCellsChangeEvent): void; + emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void; + emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void; +} + export class ExtHostCell extends Disposable implements vscode.NotebookCell { // private originalSource: string[]; @@ -60,14 +67,17 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { return this._documentData.document; } + get notebook(): vscode.NotebookDocument { + return this._notebook; + } + get source() { // todo@jrieken remove this return this._documentData.getText(); } constructor( - private readonly viewType: string, - private readonly documentUri: URI, + private readonly _notebook: ExtHostNotebookDocument, readonly handle: number, readonly uri: URI, content: string, @@ -134,7 +144,7 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell { } private updateMetadata(): Promise { - return this._proxy.$updateNotebookCellMetadata(this.viewType, this.documentUri, this.handle, this._metadata); + return this._proxy.$updateNotebookCellMetadata(this._notebook.viewType, this._notebook.uri, this.handle, this._metadata); } attachTextDocument(document: ExtHostDocumentData) { @@ -218,9 +228,12 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo return this._versionId; } + private _disposed = false; + constructor( private readonly _proxy: MainThreadNotebookShape, private _documentsAndEditors: ExtHostDocumentsAndEditors, + private _emitter: INotebookEventEmitter, public viewType: string, public uri: URI, public renderingHandler: ExtHostNotebookOutputRenderingHandler @@ -239,6 +252,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } dispose() { + this._disposed = true; super.dispose(); this._cellDisposableMapping.forEach(cell => cell.dispose()); } @@ -247,7 +261,8 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo get isDirty() { return false; } - accpetModelChanged(event: NotebookCellsChangedEvent) { + accpetModelChanged(event: NotebookCellsChangedEvent): void { + this._versionId = event.versionId; if (event.kind === NotebookCellsChangeType.ModelChange) { this.$spliceNotebookCells(event.changes); } else if (event.kind === NotebookCellsChangeType.Move) { @@ -259,19 +274,19 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } else if (event.kind === NotebookCellsChangeType.ChangeLanguage) { this.$changeCellLanguage(event.index, event.language); } - - this._versionId = event.versionId; } private $spliceNotebookCells(splices: NotebookCellsSplice2[]): void { - if (!splices.length) { + if (this._disposed) { return; } + let contentChangeEvents: vscode.NotebookCellsChangeData[] = []; + splices.reverse().forEach(splice => { let cellDtos = splice[2]; let newCells = cellDtos.map(cell => { - const extCell = new ExtHostCell(this.viewType, this.uri, cell.handle, URI.revive(cell.uri), cell.source.join('\n'), cell.cellKind, cell.language, cell.outputs, cell.metadata, this._proxy); + const extCell = new ExtHostCell(this, cell.handle, URI.revive(cell.uri), cell.source.join('\n'), cell.cellKind, cell.language, cell.outputs, cell.metadata, this._proxy); const documentData = this._documentsAndEditors.getDocument(URI.revive(cell.uri)); if (documentData) { @@ -298,107 +313,88 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } this.cells.splice(splice[0], splice[1], ...newCells); + + const event: vscode.NotebookCellsChangeData = { + start: splice[0], + deletedCount: splice[1], + items: newCells + }; + + contentChangeEvents.push(event); + }); + + this._emitter.emitModelChange({ + document: this, + changes: contentChangeEvents }); } - private $moveCell(index: number, newIdx: number) { + 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, + items: [] + }, { + start: newIdx, + deletedCount: 0, + items: cells + }]; + this._emitter.emitModelChange({ + document: this, + changes + }); } - private $clearCellOutputs(index: number) { + 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() { - this.cells.forEach(cell => cell.outputs = []); + 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) { + private $changeCellLanguage(index: number, language: string): void { const cell = this.cells[index]; cell.language = language; + const event: vscode.NotebookCellLanguageChangeEvent = { document: this, cell, language }; + this._emitter.emitCellLanguageChange(event); } - eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { + async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice[]) { let renderers = new Set(); let outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => { let outputs = diff.toInsert; - - let transformedOutputs = outputs.map(output => { - if (output.outputKind === CellOutputKind.Rich) { - const ret = this.transformMimeTypes(output); - - if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { - renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); - } - return ret; - } else { - return output as IStreamOutput | IErrorOutput; - } - }); - - return [diff.start, diff.deleteCount, transformedOutputs]; + return [diff.start, diff.deleteCount, outputs]; }); - this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers)); - } - - transformMimeTypes(output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { - let mimeTypes = Object.keys(output.data); - let coreDisplayOrder = this.renderingHandler.outputDisplayOrder; - const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], this._displayOrder, coreDisplayOrder?.defaultOrder || []); - - let orderMimeTypes: IOrderedMimeType[] = []; - - sorted.forEach(mimeType => { - let handlers = this.renderingHandler.findBestMatchedRenderer(mimeType); - - if (handlers.length) { - let renderedOutput = handlers[0].render(this, output, mimeType); - - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: true, - rendererId: handlers[0].handle, - output: renderedOutput - }); - - for (let i = 1; i < handlers.length; i++) { - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: false, - rendererId: handlers[i].handle - }); - } - - if (mimeTypeSupportedByCore(mimeType)) { - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: false, - rendererId: -1 - }); - } - } else { - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: false - }); - } + await this._proxy.$spliceNotebookCellOutputs(this.viewType, this.uri, cell.handle, outputDtos, Array.from(renderers)); + this._emitter.emitCellOutputsChange({ + document: this, + cells: [cell] }); - - return { - outputKind: output.outputKind, - data: output.data, - orderedMimeTypes: orderMimeTypes, - pickedMimeTypeIndex: 0 - }; } getCell(cellHandle: number) { return this.cells.find(cell => cell.handle === cellHandle); } + getCell2(cellUri: UriComponents) { + return this.cells.find(cell => cell.uri.fragment === cellUri.fragment); + } + attachCellTextDocument(textDocument: ExtHostDocumentData) { let cell = this.cells.find(cell => cell.uri.toString() === textDocument.document.uri.toString()); if (cell) { @@ -449,25 +445,10 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE source: sourceArr, language, cellKind: type, - outputs: (outputs as any[]), // TODO@rebornix + outputs: outputs, metadata }; - const transformedOutputs = outputs.map(output => { - if (output.outputKind === CellOutputKind.Rich) { - const ret = this.editor.document.transformMimeTypes(output); - - if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { - this._renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); - } - return ret; - } else { - return output as IStreamOutput | IErrorOutput; - } - }); - - cell.outputs = transformedOutputs; - this._collectedEdits.push({ editType: CellEditType.Insert, index, @@ -490,6 +471,35 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook private _viewColumn: vscode.ViewColumn | undefined; selection?: ExtHostCell = undefined; + + private _active: boolean = false; + get active(): boolean { + return this._active; + } + + set active(_state: boolean) { + throw readonly('active'); + } + + private _visible: boolean = false; + get visible(): boolean { + return this._visible; + } + + set visible(_state: boolean) { + throw readonly('visible'); + } + + _acceptVisibility(value: boolean) { + this._visible = value; + } + + _acceptActive(value: boolean) { + this._active = value; + } + + private _onDidDispose = new Emitter(); + readonly onDidDispose: Event = this._onDidDispose.event; onDidReceiveMessage: vscode.Event = this._onDidReceiveMessage.event; constructor( @@ -498,6 +508,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook public uri: URI, private _proxy: MainThreadNotebookShape, private _onDidReceiveMessage: Emitter, + private _webviewInitData: WebviewInitData, public document: ExtHostNotebookDocument, private _documentsAndEditors: ExtHostDocumentsAndEditors ) { @@ -506,7 +517,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook for (const documentData of documents) { let data = CellUri.parse(documentData.document.uri); if (data) { - if (this.document.uri.toString() === data.notebook.toString()) { + if (this.document.uri.fsPath === data.notebook.fsPath) { document.attachCellTextDocument(documentData); } } @@ -517,7 +528,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook for (const documentData of documents) { let data = CellUri.parse(documentData.document.uri); if (data) { - if (this.document.uri.toString() === data.notebook.toString()) { + if (this.document.uri.fsPath === data.notebook.fsPath) { document.detachCellTextDocument(documentData); } } @@ -585,6 +596,13 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook return this._proxy.$postMessage(this.document.handle, message); } + asWebviewUri(localResource: vscode.Uri): vscode.Uri { + return asWebviewUri(this._webviewInitData, this.id, localResource); + } + dispose() { + this._onDidDispose.fire(); + super.dispose(); + } } export class ExtHostNotebookOutputRenderer { @@ -621,17 +639,21 @@ export interface ExtHostNotebookOutputRenderingHandler { } export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostNotebookOutputRenderingHandler { - private static _handlePool: number = 0; - private readonly _proxy: MainThreadNotebookShape; private readonly _notebookContentProviders = new Map(); private readonly _notebookKernels = new Map(); private readonly _documents = new Map(); + private readonly _unInitializedDocuments = new Map(); private readonly _editors = new Map; }>(); - private readonly _notebookOutputRenderers = new Map(); - - private readonly _onDidChangeNotebookDocument = new Emitter<{ document: ExtHostNotebookDocument, changes: NotebookCellsChangedEvent[]; }>(); - readonly onDidChangeNotebookDocument: Event<{ document: ExtHostNotebookDocument, changes: NotebookCellsChangedEvent[]; }> = this._onDidChangeNotebookDocument.event; + private readonly _notebookOutputRenderers = new Map(); + private readonly _onDidChangeNotebookCells = new Emitter(); + readonly onDidChangeNotebookCells = this._onDidChangeNotebookCells.event; + private readonly _onDidChangeCellOutputs = new Emitter(); + readonly onDidChangeCellOutputs = this._onDidChangeCellOutputs.event; + private readonly _onDidChangeCellLanguage = new Emitter(); + readonly onDidChangeCellLanguage = this._onDidChangeCellLanguage.event; + private readonly _onDidChangeActiveNotebookEditor = new Emitter(); + readonly onDidChangeActiveNotebookEditor = this._onDidChangeActiveNotebookEditor.event; private _outputDisplayOrder: INotebookDisplayOrder | undefined; @@ -655,8 +677,11 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN onDidOpenNotebookDocument: Event = this._onDidOpenNotebookDocument.event; private _onDidCloseNotebookDocument = new Emitter(); onDidCloseNotebookDocument: Event = this._onDidCloseNotebookDocument.event; + visibleNotebookEditors: ExtHostNotebookEditor[] = []; + private _onDidChangeVisibleNotebookEditors = new Emitter(); + onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event; - constructor(mainContext: IMainContext, commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors) { + constructor(mainContext: IMainContext, commands: ExtHostCommands, private _documentsAndEditors: ExtHostDocumentsAndEditors, private readonly _webviewInitData: WebviewInitData) { this._proxy = mainContext.getProxy(MainContext.MainThreadNotebook); commands.registerArgumentProcessor({ @@ -685,15 +710,85 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN filter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer ): vscode.Disposable { + if (this._notebookKernels.has(type)) { + throw new Error(`Notebook renderer for '${type}' already registered`); + } + let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer); - this._notebookOutputRenderers.set(extHostRenderer.handle, extHostRenderer); - this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, extHostRenderer.handle, renderer.preloads || []); - return new VSCodeDisposable(() => { - this._notebookOutputRenderers.delete(extHostRenderer.handle); - this._proxy.$unregisterNotebookRenderer(extHostRenderer.handle); + this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer); + this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation }, type, filter, renderer.preloads || []); + return new extHostTypes.Disposable(() => { + this._notebookOutputRenderers.delete(extHostRenderer.type); + this._proxy.$unregisterNotebookRenderer(extHostRenderer.type); }); } + 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).toString()); + + if (!document) { + return; + } + + const renderer = this._notebookOutputRenderers.get(id)!; + const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { + const cell = document.getCell2(cellInfo.key); + const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { + return { + index: output.index, + mimeType: output.mimeType, + handlerId: id, + transformedOutput: renderer.render(document, cell!.outputs[output.index] as vscode.CellDisplayOutput, 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).toString()); + + if (!document) { + return; + } + + const renderer = this._notebookOutputRenderers.get(id)!; + const cellsResponse: IOutputRenderResponseCellInfo[] = request.items.map(cellInfo => { + const outputResponse: IOutputRenderResponseOutputInfo[] = cellInfo.outputs.map(output => { + return { + index: output.index, + mimeType: output.mimeType, + handlerId: id, + transformedOutput: renderer.render(document, output.output! as vscode.CellDisplayOutput, output.mimeType) + }; + }); + + return { + key: cellInfo.key, + outputs: outputResponse + }; + }); + + return { items: cellsResponse }; + } + findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] { let matches: ExtHostNotebookOutputRenderer[] = []; for (let renderer of this._notebookOutputRenderers) { @@ -715,9 +810,19 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN throw new Error(`Notebook provider for '${viewType}' already registered`); } + // if ((provider).executeCell) { + // throw new Error('NotebookContentKernel.executeCell is removed, please use vscode.notebook.registerNotebookKernel instead.'); + // } + this._notebookContentProviders.set(viewType, { extension, provider }); - this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType); - return new VSCodeDisposable(() => { + + const listener = provider.onDidChangeNotebook + ? provider.onDidChangeNotebook(e => this._proxy.$onNotebookChange(viewType, e.document.uri)) + : Disposable.None; + + this._proxy.$registerNotebookProvider({ id: extension.identifier, location: extension.extensionLocation }, viewType, provider.kernel ? { id: viewType, label: provider.kernel.label, extensionLocation: extension.extensionLocation, preloads: provider.kernel.preloads } : undefined); + return new extHostTypes.Disposable(() => { + listener.dispose(); this._notebookContentProviders.delete(viewType); this._proxy.$unregisterNotebookProvider(viewType); }); @@ -732,48 +837,43 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN const transformedSelectors = selectors.map(selector => typeConverters.GlobPattern.from(selector)); this._proxy.$registerNotebookKernel({ id: extension.identifier, location: extension.extensionLocation }, id, kernel.label, transformedSelectors, kernel.preloads || []); - return new VSCodeDisposable(() => { + return new extHostTypes.Disposable(() => { this._notebookKernels.delete(id); this._proxy.$unregisterNotebookKernel(id); }); } async $resolveNotebookData(viewType: string, uri: UriComponents): Promise { - let provider = this._notebookContentProviders.get(viewType); - let document = this._documents.get(URI.revive(uri).toString()); + const provider = this._notebookContentProviders.get(viewType); + const revivedUri = URI.revive(uri); + + if (provider) { + let document = this._documents.get(URI.revive(uri).toString()); + + if (!document) { + const that = this; + document = this._unInitializedDocuments.get(revivedUri.toString()) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { + emitModelChange(event: vscode.NotebookCellsChangeEvent): void { + that._onDidChangeNotebookCells.fire(event); + }, + emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { + that._onDidChangeCellOutputs.fire(event); + }, + emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { + that._onDidChangeCellLanguage.fire(event); + } + }, viewType, revivedUri, this); + this._unInitializedDocuments.set(revivedUri.toString(), document); + } - if (provider && document) { const rawCells = await provider.provider.openNotebook(URI.revive(uri)); - const renderers = new Set(); const dto = { metadata: { ...notebookDocumentMetadataDefaults, ...rawCells.metadata }, languages: rawCells.languages, - cells: rawCells.cells.map(cell => { - let transformedOutputs = cell.outputs.map(output => { - if (output.outputKind === CellOutputKind.Rich) { - // TODO display string[] - const ret = this._transformMimeTypes(document!, (rawCells.metadata.displayOrder as string[]) || [], output); - - if (ret.orderedMimeTypes[ret.pickedMimeTypeIndex].isResolved) { - renderers.add(ret.orderedMimeTypes[ret.pickedMimeTypeIndex].rendererId!); - } - return ret; - } else { - return output as IStreamOutput | IErrorOutput; - } - }); - - return { - language: cell.language, - cellKind: cell.cellKind, - metadata: cell.metadata, - source: cell.source, - outputs: transformedOutputs - }; - }) + cells: rawCells.cells, }; return dto; @@ -782,58 +882,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return; } - private _transformMimeTypes(document: ExtHostNotebookDocument, displayOrder: string[], output: vscode.CellDisplayOutput): ITransformedDisplayOutputDto { - let mimeTypes = Object.keys(output.data); - let coreDisplayOrder = this.outputDisplayOrder; - const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], displayOrder, coreDisplayOrder?.defaultOrder || []); - - let orderMimeTypes: IOrderedMimeType[] = []; - - sorted.forEach(mimeType => { - let handlers = this.findBestMatchedRenderer(mimeType); - - if (handlers.length) { - let renderedOutput = handlers[0].render(document, output, mimeType); - - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: true, - rendererId: handlers[0].handle, - output: renderedOutput - }); - - for (let i = 1; i < handlers.length; i++) { - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: false, - rendererId: handlers[i].handle - }); - } - - if (mimeTypeSupportedByCore(mimeType)) { - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: false, - rendererId: -1 - }); - } - } else { - orderMimeTypes.push({ - mimeType: mimeType, - isResolved: false - }); - } - }); - - return { - outputKind: output.outputKind, - data: output.data, - orderedMimeTypes: orderMimeTypes, - pickedMimeTypeIndex: 0 - }; - } - - async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { + async $executeNotebook(viewType: string, uri: UriComponents, cellHandle: number | undefined, useAttachedKernel: boolean, token: CancellationToken): Promise { let document = this._documents.get(URI.revive(uri).toString()); if (!document) { @@ -841,9 +890,16 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } if (this._notebookContentProviders.has(viewType)) { - let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + const provider = this._notebookContentProviders.get(viewType)!.provider; - return this._notebookContentProviders.get(viewType)!.provider.executeCell(document, cell, token); + if (provider.kernel && useAttachedKernel) { + if (cell) { + return provider.kernel.executeCell(document, cell, token); + } else { + return provider.kernel.executeAllCells(document, token); + } + } } } @@ -911,8 +967,21 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN this._outputDisplayOrder = displayOrder; } - $onDidReceiveMessage(uri: UriComponents, message: any): void { - let editor = this._editors.get(URI.revive(uri).toString()); + // TODO: remove document - editor one on one mapping + private _getEditorFromURI(uriComponents: UriComponents) { + const uriStr = URI.revive(uriComponents).toString(); + let editor: { editor: ExtHostNotebookEditor, onDidReceiveMessage: Emitter; } | undefined; + this._editors.forEach(e => { + if (e.editor.uri.toString() === uriStr) { + editor = e; + } + }); + + return editor; + } + + $onDidReceiveMessage(editorId: string, message: any): void { + let editor = this._editors.get(editorId); if (editor) { editor.onDidReceiveMessage.fire(message); @@ -920,20 +989,15 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } $acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void { - let editor = this._editors.get(URI.revive(uriComponents).toString()); + const document = this._documents.get(URI.revive(uriComponents).toString()); - if (editor) { - editor.editor.document.accpetModelChanged(event); - this._onDidChangeNotebookDocument.fire({ - document: editor.editor.document, - changes: [event] - }); + if (document) { + document.accpetModelChanged(event); } - } $acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void { - let editor = this._editors.get(URI.revive(uriComponents).toString()); + let editor = this._getEditorFromURI(uriComponents); if (!editor) { return; @@ -958,33 +1022,81 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN } } + private _createExtHostEditor(document: ExtHostNotebookDocument, editorId: string, selections: number[]) { + const onDidReceiveMessage = new Emitter(); + const revivedUri = document.uri; + + let editor = new ExtHostNotebookEditor( + document.viewType, + editorId, + revivedUri, + this._proxy, + onDidReceiveMessage, + this._webviewInitData, + document, + this._documentsAndEditors + ); + + const cells = editor.document.cells; + + if (selections.length) { + const firstCell = selections[0]; + editor.selection = cells.find(cell => cell.handle === firstCell); + } else { + editor.selection = undefined; + } + + this._editors.get(editorId)?.editor.dispose(); + + this._editors.set(editorId, { editor, onDidReceiveMessage }); + } + async $acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta) { + let editorChanged = false; + if (delta.removedDocuments) { delta.removedDocuments.forEach((uri) => { - let document = this._documents.get(URI.revive(uri).toString()); + const revivedUri = URI.revive(uri); + const revivedUriStr = revivedUri.toString(); + let document = this._documents.get(revivedUriStr); if (document) { document.dispose(); - this._documents.delete(URI.revive(uri).toString()); + this._documents.delete(revivedUriStr); this._onDidCloseNotebookDocument.fire(document); } - let editor = this._editors.get(URI.revive(uri).toString()); - - if (editor) { - editor.editor.dispose(); - editor.onDidReceiveMessage.dispose(); - this._editors.delete(URI.revive(uri).toString()); - } + [...this._editors.values()].forEach((e) => { + if (e.editor.uri.toString() === revivedUriStr) { + e.editor.dispose(); + e.onDidReceiveMessage.dispose(); + this._editors.delete(e.editor.id); + editorChanged = true; + } + }); }); } if (delta.addedDocuments) { delta.addedDocuments.forEach(modelData => { const revivedUri = URI.revive(modelData.uri); + const revivedUriStr = revivedUri.toString(); const viewType = modelData.viewType; - if (!this._documents.has(revivedUri.toString())) { - let document = new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, viewType, revivedUri, this); + if (!this._documents.has(revivedUriStr)) { + const that = this; + let document = this._unInitializedDocuments.get(revivedUriStr) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, { + emitModelChange(event: vscode.NotebookCellsChangeEvent): void { + that._onDidChangeNotebookCells.fire(event); + }, + emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void { + that._onDidChangeCellOutputs.fire(event); + }, + emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void { + that._onDidChangeCellLanguage.fire(event); + } + }, viewType, revivedUri, this); + + this._unInitializedDocuments.delete(revivedUriStr); if (modelData.metadata) { document.metadata = { ...notebookDocumentMetadataDefaults, @@ -992,32 +1104,110 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN }; } - this._documents.set(revivedUri.toString(), document); + document.accpetModelChanged({ + kind: NotebookCellsChangeType.ModelChange, + versionId: modelData.versionId, + changes: [[ + 0, + 0, + modelData.cells + ]] + }); + + this._documents.get(revivedUriStr)?.dispose(); + this._documents.set(revivedUriStr, document); + + // create editor if populated + if (modelData.attachedEditor) { + this._createExtHostEditor(document, modelData.attachedEditor.id, modelData.attachedEditor.selections); + editorChanged = true; + } } - const onDidReceiveMessage = new Emitter(); - const document = this._documents.get(revivedUri.toString())!; - - let editor = new ExtHostNotebookEditor( - viewType, - `${ExtHostNotebookController._handlePool++}`, - revivedUri, - this._proxy, - onDidReceiveMessage, - document, - this._documentsAndEditors - ); - + const document = this._documents.get(revivedUriStr)!; this._onDidOpenNotebookDocument.fire(document); - - // TODO, does it already exist? - this._editors.set(revivedUri.toString(), { editor, onDidReceiveMessage }); }); } - if (delta.newActiveEditor) { - this._activeNotebookDocument = this._documents.get(URI.revive(delta.newActiveEditor).toString()); - this._activeNotebookEditor = this._editors.get(URI.revive(delta.newActiveEditor).toString())?.editor; + if (delta.addedEditors) { + delta.addedEditors.forEach(editorModelData => { + if (this._editors.has(editorModelData.id)) { + return; + } + + const revivedUri = URI.revive(editorModelData.documentUri); + const document = this._documents.get(revivedUri.toString()); + + if (document) { + this._createExtHostEditor(document, editorModelData.id, editorModelData.selections); + editorChanged = true; + } + }); + } + + const removedEditors: { editor: ExtHostNotebookEditor, onDidReceiveMessage: Emitter; }[] = []; + + if (delta.removedEditors) { + delta.removedEditors.forEach(editorid => { + const editor = this._editors.get(editorid); + + if (editor) { + editorChanged = true; + this._editors.delete(editorid); + + if (this.activeNotebookEditor?.id === editor.editor.id) { + this._activeNotebookEditor = undefined; + this._activeNotebookDocument = undefined; + } + + removedEditors.push(editor); + } + }); + } + + if (editorChanged) { + removedEditors.forEach(e => { + e.editor.dispose(); + e.onDidReceiveMessage.dispose(); + }); + } + + if (delta.visibleEditors) { + this.visibleNotebookEditors = delta.visibleEditors.map(id => this._editors.get(id)!.editor).filter(editor => !!editor) as ExtHostNotebookEditor[]; + const visibleEditorsSet = new Set(); + this.visibleNotebookEditors.forEach(editor => visibleEditorsSet.add(editor.id)); + + [...this._editors.values()].forEach((e) => { + const newValue = visibleEditorsSet.has(e.editor.id); + e.editor._acceptVisibility(newValue); + }); + + this.visibleNotebookEditors = [...this._editors.values()].map(e => e.editor).filter(e => e.visible); + this._onDidChangeVisibleNotebookEditors.fire(this.visibleNotebookEditors); + } + + if (delta.newActiveEditor !== undefined) { + if (delta.newActiveEditor) { + this._activeNotebookEditor = this._editors.get(delta.newActiveEditor)?.editor; + this._activeNotebookEditor?._acceptActive(true); + this._activeNotebookDocument = this._activeNotebookEditor ? this._documents.get(this._activeNotebookEditor!.uri.toString()) : undefined; + [...this._editors.values()].forEach((e) => { + if (e.editor !== this.activeNotebookEditor) { + e.editor._acceptActive(false); + } + }); + } else { + // clear active notebook as current active editor is non-notebook editor + this._activeNotebookEditor = undefined; + this._activeNotebookDocument = undefined; + + [...this._editors.values()].forEach((e) => { + e.editor._acceptActive(false); + }); + + } + + this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor); } } } diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index 887ac671ccf..c23541ca7d2 100644 --- a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts +++ b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts @@ -44,13 +44,17 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD this._onDidChange.fire(undefined); } })); - this._disposables.add(extHostNotebooks.onDidChangeNotebookDocument(e => { - if (e.document === this._notebook) { + const documentChange = (document: vscode.NotebookDocument) => { + if (document === this._notebook) { this._init(); this._versionId += 1; this._onDidChange.fire(undefined); } - })); + }; + + this._disposables.add(extHostNotebooks.onDidChangeCellLanguage(e => documentChange(e.document))); + this._disposables.add(extHostNotebooks.onDidChangeCellOutputs(e => documentChange(e.document))); + this._disposables.add(extHostNotebooks.onDidChangeNotebookCells(e => documentChange(e.document))); } dispose(): void { diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index d26ee3d7c86..bc2c6fa2d0b 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -35,8 +35,9 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { private _timeoutHandle: any; private _proxy: MainThreadStatusBarShape; private _commands: CommandsConverter; + private _accessibilityInformation?: vscode.AccessibilityInformation; - constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number) { + constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) { this._id = ExtHostStatusBarEntry.ID_GEN++; this._proxy = proxy; this._commands = commands; @@ -44,6 +45,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { this._statusName = name; this._alignment = alignment; this._priority = priority; + this._accessibilityInformation = accessibilityInformation; } public get id(): number { @@ -74,6 +76,10 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { return this._command?.fromApi; } + public get accessibilityInformation(): vscode.AccessibilityInformation | undefined { + return this._accessibilityInformation; + } + public set text(text: string) { this._text = text; this.update(); @@ -136,7 +142,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { // Set to status bar this._proxy.$setEntry(this.id, this._statusId, this._statusName, this.text, this.tooltip, this._command?.internal, this.color, this._alignment === ExtHostStatusBarAlignment.Left ? MainThreadStatusBarAlignment.LEFT : MainThreadStatusBarAlignment.RIGHT, - this._priority); + this._priority, this._accessibilityInformation); }, 0); } @@ -196,8 +202,8 @@ export class ExtHostStatusBar { this._statusMessage = new StatusBarMessage(this); } - createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number): vscode.StatusBarItem { - return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority); + createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem { + return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation); } setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable): Disposable { diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 71430153a3d..27759c2666f 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -384,6 +384,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { protected _handleCounter: number; protected _handlers: Map; protected _taskExecutions: Map; + protected _taskExecutionPromises: Map>; protected _providedCustomExecutions2: Map; private _notProvidedCustomExecutions: Set; // Used for custom executions tasks that are created and run through executeTask. protected _activeCustomExecutions2: Map; @@ -412,6 +413,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { this._handleCounter = 0; this._handlers = new Map(); this._taskExecutions = new Map(); + this._taskExecutionPromises = new Map>(); this._providedCustomExecutions2 = new Map(); this._notProvidedCustomExecutions = new Set(); this._activeCustomExecutions2 = new Map(); @@ -496,6 +498,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { public async $OnDidEndTask(execution: tasks.TaskExecutionDTO): Promise { const _execution = await this.getTaskExecution(execution); + this._taskExecutionPromises.delete(execution.id); this._taskExecutions.delete(execution.id); this.customExecutionComplete(execution); this._onDidTerminateTask.fire({ @@ -626,17 +629,24 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { return taskExecution; } - let result: TaskExecutionImpl | undefined = this._taskExecutions.get(execution.id); + let result: Promise | undefined = this._taskExecutionPromises.get(execution.id); if (result) { return result; } - const taskToCreate = task ? task : await TaskDTO.to(execution.task, this._workspaceProvider); - if (!taskToCreate) { - throw new Error('Unexpected: Task does not exist.'); - } - const createdResult: TaskExecutionImpl = new TaskExecutionImpl(this, execution.id, taskToCreate); - this._taskExecutions.set(execution.id, createdResult); - return createdResult; + const createdResult: Promise = new Promise(async (resolve, reject) => { + const taskToCreate = task ? task : await TaskDTO.to(execution.task, this._workspaceProvider); + if (!taskToCreate) { + reject('Unexpected: Task does not exist.'); + } else { + resolve(new TaskExecutionImpl(this, execution.id, taskToCreate)); + } + }); + + this._taskExecutionPromises.set(execution.id, createdResult); + return createdResult.then(result => { + this._taskExecutions.set(execution.id, result); + return result; + }); } protected checkDeprecation(task: vscode.Task, handler: HandlerData) { diff --git a/src/vs/workbench/api/common/extHostTimeline.ts b/src/vs/workbench/api/common/extHostTimeline.ts index c15a9e73617..24a5092542e 100644 --- a/src/vs/workbench/api/common/extHostTimeline.ts +++ b/src/vs/workbench/api/common/extHostTimeline.ts @@ -152,7 +152,8 @@ export class ExtHostTimeline implements IExtHostTimeline { command: item.command ? commandConverter.toInternal(item.command, disposables) : undefined, icon: icon, iconDark: iconDark, - themeIcon: themeIcon + themeIcon: themeIcon, + accessibilityInformation: item.accessibilityInformation }; }; }; @@ -188,4 +189,3 @@ export class ExtHostTimeline implements IExtHostTimeline { function getUriKey(uri: URI | undefined): string | undefined { return uri?.toString(); } - diff --git a/src/vs/workbench/api/common/extHostTreeViews.ts b/src/vs/workbench/api/common/extHostTreeViews.ts index a2e117ad7e5..67a97da2d52 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -486,7 +486,7 @@ class ExtHostTreeView extends Disposable { return node; } - private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode | Root): TreeNode { + private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem2, parent: TreeNode | Root): TreeNode { const disposable = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); @@ -502,7 +502,8 @@ class ExtHostTreeView extends Disposable { icon, iconDark: this.getDarkIconPath(extensionTreeItem) || icon, themeIcon: extensionTreeItem.iconPath instanceof ThemeIcon ? { id: extensionTreeItem.iconPath.id } : undefined, - collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState + collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState, + accessibilityInformation: extensionTreeItem.accessibilityInformation }; return { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index b24be8d20a0..df4c3725115 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -95,11 +95,22 @@ export namespace Range { } } +export namespace TokenType { + export function to(type: modes.StandardTokenType): types.StandardTokenType { + switch (type) { + case modes.StandardTokenType.Comment: return types.StandardTokenType.Comment; + case modes.StandardTokenType.Other: return types.StandardTokenType.Other; + case modes.StandardTokenType.RegEx: return types.StandardTokenType.RegEx; + case modes.StandardTokenType.String: return types.StandardTokenType.String; + } + } +} + export namespace Position { export function to(position: IPosition): types.Position { return new types.Position(position.lineNumber - 1, position.column - 1); } - export function from(position: types.Position): IPosition { + export function from(position: types.Position | vscode.Position): IPosition { return { lineNumber: position.line + 1, column: position.character + 1 }; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 4894d276e46..1cae03bf240 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2766,3 +2766,17 @@ export enum ExtensionMode { } //#endregion ExtensionContext + + +//#region Authentication +export class AuthenticationSession implements vscode.AuthenticationSession2 { + constructor(public id: string, public accessToken: string, public account: { displayName: string, id: string }, public scopes: string[]) { } +} + +//#endregion Authentication +export enum StandardTokenType { + Other = 0, + Comment = 1, + String = 2, + RegEx = 4 +} diff --git a/src/vs/workbench/api/common/extHostWebview.ts b/src/vs/workbench/api/common/extHostWebview.ts index ee06aec4d66..896d591826f 100644 --- a/src/vs/workbench/api/common/extHostWebview.ts +++ b/src/vs/workbench/api/common/extHostWebview.ts @@ -573,7 +573,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { } const { serializer, extension } = entry; - const webview = new ExtHostWebview(webviewHandle, this._proxy, options, this.initData, this.workspace, extension, this._deprecationService); + 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); @@ -628,7 +628,7 @@ export class ExtHostWebviews implements extHostProtocol.ExtHostWebviewsShape { throw new Error(`No provider found for '${viewType}'`); } - const webview = new ExtHostWebview(handle, this._proxy, options, this.initData, this.workspace, entry.extension, this._deprecationService); + 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); @@ -761,6 +761,15 @@ function convertWebviewOptions( }; } +function reviveOptions( + options: modes.IWebviewOptions & modes.IWebviewPanelOptions +): vscode.WebviewOptions { + return { + ...options, + localResourceRoots: options.localResourceRoots?.map(components => URI.from(components)), + }; +} + function getDefaultLocalResourceRoots( extension: IExtensionDescription, workspace: IExtHostWorkspace | undefined, diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index 4342d65c24b..b1ac5aa3af8 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -64,6 +64,7 @@ namespace schema { switch (menuId) { case MenuId.StatusBarWindowIndicatorMenu: case MenuId.MenubarWebNavigationMenu: + case MenuId.NotebookCellTitle: return true; } return false; diff --git a/src/vs/workbench/api/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index ad28cc495c0..7cae126cc0f 100644 --- a/src/vs/workbench/api/node/extHostCLIServer.ts +++ b/src/vs/workbench/api/node/extHostCLIServer.ts @@ -7,10 +7,9 @@ import { generateRandomPipeName } from 'vs/base/parts/ipc/node/ipc.net'; import * as http from 'http'; import * as fs from 'fs'; import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands'; -import { IWindowOpenable } from 'vs/platform/windows/common/windows'; +import { IWindowOpenable, IOpenWindowOptions } from 'vs/platform/windows/common/windows'; import { URI } from 'vs/base/common/uri'; import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; -import { INativeOpenWindowOptions } from 'vs/platform/windows/node/window'; import { ILogService } from 'vs/platform/log/common/log'; export interface OpenCommandPipeArgs { @@ -123,7 +122,7 @@ export class CLIServer { if (urisToOpen.length) { const waitMarkerFileURI = waitMarkerFilePath ? URI.file(waitMarkerFilePath) : undefined; const preferNewWindow = !forceReuseWindow && !waitMarkerFileURI && !addMode; - const windowOpenArgs: INativeOpenWindowOptions = { forceNewWindow, diffMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI }; + const windowOpenArgs: IOpenWindowOptions = { forceNewWindow, diffMode, addMode, gotoLineMode, forceReuseWindow, preferNewWindow, waitMarkerFileURI }; this._commands.executeCommand('_files.windowOpen', urisToOpen, windowOpenArgs); } res.writeHead(200); diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 6312c90b200..88371fe8b4c 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -687,7 +687,7 @@ export class MoveFocusedViewAction extends Action { const quickPick = this.quickInputService.createQuickPick(); quickPick.placeholder = nls.localize('moveFocusedView.selectDestination', "Select a Destination for the View"); - quickPick.title = nls.localize('moveFocusedView.title', "View: Move {0}", viewDescriptor.name); + quickPick.title = nls.localize({ key: 'moveFocusedView.title', comment: ['{0} indicates the title of the view the user has selected to move.'] }, "View: Move {0}", viewDescriptor.name); const items: Array = []; const currentContainer = this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!; diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index 2f369a77572..0295a2580ff 100644 --- a/src/vs/workbench/browser/actions/quickAccessActions.ts +++ b/src/vs/workbench/browser/actions/quickAccessActions.ts @@ -151,7 +151,7 @@ CommandsRegistry.registerCommand({ } }); -CommandsRegistry.registerCommand('workbench.action.quickOpenPreviousEditor', async function (accessor: ServicesAccessor, prefix: string | null = null) { +CommandsRegistry.registerCommand('workbench.action.quickOpenPreviousEditor', async accessor => { const quickInputService = accessor.get(IQuickInputService); quickInputService.quickAccess.show('', { itemActivation: ItemActivation.SECOND }); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 544ac2c0492..707294b9e45 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -47,11 +47,7 @@ export class DraggedEditorIdentifier { export class DraggedEditorGroupIdentifier { - constructor(private _identifier: GroupIdentifier) { } - - get identifier(): GroupIdentifier { - return this._identifier; - } + constructor(public readonly identifier: GroupIdentifier) { } } export interface IDraggedEditor extends IDraggedResource { @@ -675,6 +671,11 @@ export class CompositeDragAndDropObserver extends Disposable { disposableStore.add(addDisposableListener(element, EventType.DRAG_START, e => { const { id, type } = draggedItemProvider(); this.writeDragData(id, type); + + if (e.dataTransfer) { + e.dataTransfer.setDragImage(element, 0, 0); + } + this._onDragStart.fire({ eventData: e, dragAndDropData: this.readDragData(type)! }); })); disposableStore.add(new DragAndDropObserver(element, { diff --git a/src/vs/workbench/browser/labels.ts b/src/vs/workbench/browser/labels.ts index 6c79377f286..570118ed8b7 100644 --- a/src/vs/workbench/browser/labels.ts +++ b/src/vs/workbench/browser/labels.ts @@ -505,6 +505,7 @@ class ResourceLabelWidget extends IconLabel { italic: this.options?.italic, strikethrough: this.options?.strikethrough, matches: this.options?.matches, + descriptionMatches: this.options?.descriptionMatches, extraClasses: [], separator: this.options?.separator, domId: this.options?.domId diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index ac32addcfee..d278d065a06 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -1394,9 +1394,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } // If panel part becomes hidden, also hide the current active panel if any + let focusEditor = false; if (hidden && this.panelService.getActivePanel()) { this.panelService.hideActivePanel(); - this.editorGroupService.activeGroup.focus(); // Pass focus to editor group if panel part is now hidden + focusEditor = true; } // If panel part becomes visible, show last active panel or default panel @@ -1427,6 +1428,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (hidden && this.state.editor.hidden) { this.setEditorHidden(false, true); } + + if (focusEditor) { + this.editorGroupService.activeGroup.focus(); // Pass focus to editor group if panel part is now hidden + } } toggleMaximizedPanel(): void { @@ -1446,7 +1451,6 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } else { this.setEditorHidden(false); this.workbenchGrid.resizeView(this.panelPartView, { width: this.state.panel.position === Position.BOTTOM ? size.width : this.state.panel.lastNonMaximizedWidth, height: this.state.panel.position === Position.BOTTOM ? this.state.panel.lastNonMaximizedHeight : size.height }); - this.editorGroupService.activeGroup.focus(); } } diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 34de2b1a02c..279916417ec 100644 --- a/src/vs/workbench/browser/panecomposite.ts +++ b/src/vs/workbench/browser/panecomposite.ts @@ -16,29 +16,28 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { ViewPaneContainer } from './parts/views/viewPaneContainer'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IAction, IActionViewItem } from 'vs/base/common/actions'; +import { ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; +import { MenuId } from 'vs/platform/actions/common/actions'; +import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; export class PaneComposite extends Composite implements IPaneComposite { + private menuActions: ViewContainerMenuActions; + constructor( id: string, protected readonly viewPaneContainer: ViewPaneContainer, - @ITelemetryService - telemetryService: ITelemetryService, - @IStorageService - protected storageService: IStorageService, - @IInstantiationService - protected instantiationService: IInstantiationService, - @IThemeService - themeService: IThemeService, - @IContextMenuService - protected contextMenuService: IContextMenuService, - @IExtensionService - protected extensionService: IExtensionService, - @IWorkspaceContextService - protected contextService: IWorkspaceContextService + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService protected storageService: IStorageService, + @IInstantiationService protected instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService protected contextMenuService: IContextMenuService, + @IExtensionService protected extensionService: IExtensionService, + @IWorkspaceContextService protected contextService: IWorkspaceContextService ) { super(id, telemetryService, themeService, storageService); + this.menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.getId(), MenuId.ViewContainerTitleContext)); this._register(this.viewPaneContainer.onTitleAreaUpdate(() => this.updateTitleArea())); } @@ -68,7 +67,15 @@ export class PaneComposite extends Composite implements IPaneComposite { } getContextMenuActions(): ReadonlyArray { - return this.viewPaneContainer.getContextMenuActions(); + const result = []; + result.push(...this.menuActions.getContextMenuActions()); + + if (result.length) { + result.push(new Separator()); + } + + result.push(...this.viewPaneContainer.getContextMenuActions()); + return result; } getActions(): ReadonlyArray { diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index 45add441fe7..191d51a4d50 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -17,7 +17,7 @@ export abstract class Panel extends PaneComposite implements IPanel { } */ export class PanelDescriptor extends CompositeDescriptor { - public static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, _commandId?: string): PanelDescriptor { + static create(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, _commandId?: string): PanelDescriptor { return new PanelDescriptor(ctor as IConstructorSignature0, id, name, cssClass, order, _commandId); } diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index 431b9a0d69d..ca6e7504280 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -15,7 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction } from 'vs/workbench/browser/actions/layoutActions'; import { IThemeService, IColorTheme } from 'vs/platform/theme/common/themeService'; -import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND } from 'vs/workbench/common/theme'; +import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { Dimension, addClass, removeNode, createCSSRule, asCSSUrl } from 'vs/base/browser/dom'; @@ -74,6 +74,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { private static readonly ACTION_HEIGHT = 48; static readonly PINNED_VIEW_CONTAINERS = 'workbench.activity.pinnedViewlets2'; private static readonly PLACEHOLDER_VIEW_CONTAINERS = 'workbench.activity.placeholderViewlets'; + private static readonly HOME_BAR_VISIBILITY_PREFERENCE = 'workbench.activity.showHomeIndicator'; //#region IView @@ -122,6 +123,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService); storageKeysSyncRegistryService.registerStorageKey({ key: ActivitybarPart.PINNED_VIEW_CONTAINERS, version: 1 }); + storageKeysSyncRegistryService.registerStorageKey({ key: ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE, version: 1 }); this.migrateFromOldCachedViewContainersValue(); for (const cachedViewContainer of this.cachedViewContainers) { @@ -144,6 +146,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { getContextMenuActions: () => { const menuBarVisibility = getMenuBarVisibility(this.configurationService, this.environmentService); const actions = []; + if (this.homeBarContainer) { + actions.push(new Action('toggleHomeBarAction', + this.homeBarVisibilityPreference ? nls.localize('hideHomeBar', "Hide Home Button") : nls.localize('showHomeBar', "Show Home Button"), + undefined, + true, + async () => { this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference; })); + } if (menuBarVisibility === 'compact' || (menuBarVisibility === 'hidden' && isWeb)) { actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu"))); @@ -157,7 +166,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { hidePart: () => this.layoutService.setSideBarHidden(true), dndHandler: new CompositeDragAndDrop(this.viewDescriptorService, ViewContainerLocation.Sidebar, (id: string, focus?: boolean) => this.viewsService.openViewContainer(id, focus), - (from: string, to: string, before?: Before2D) => this.compositeBar.move(from, to, before?.verticallyBefore) + (from: string, to: string, before?: Before2D) => this.compositeBar.move(from, to, before?.verticallyBefore), + () => this.compositeBar.getCompositeBarItems(), ), compositeSize: 52, colors: (theme: IColorTheme) => this.getActivitybarItemColors(theme), @@ -226,6 +236,12 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } + private onDidChangeHomeBarVisibility(): void { + if (this.homeBarContainer) { + this.homeBarContainer.style.display = this.homeBarVisibilityPreference ? '' : 'none'; + } + } + private onDidRegisterExtensions(): void { this.removeNotExistingComposites(); this.saveCachedViewContainers(); @@ -363,6 +379,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { codicon = Codicon.code; } this.createHomeBar(homeIndicator.href, homeIndicator.command, homeIndicator.title, codicon); + this.onDidChangeHomeBarVisibility(); } // Install menubar if compact @@ -393,7 +410,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { orientation: ActionsOrientation.VERTICAL, animated: false, ariaLabel: nls.localize('home', "Home"), - actionViewItemProvider: command ? undefined : action => new HomeActionViewItem(action) + actionViewItemProvider: command ? undefined : action => new HomeActionViewItem(action), + allowContextMenu: true })); const homeBarIconBadge = document.createElement('div'); @@ -436,7 +454,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { activeBackground: theme.getColor(ACTIVITY_BAR_ACTIVE_BACKGROUND), badgeBackground: theme.getColor(ACTIVITY_BAR_BADGE_BACKGROUND), badgeForeground: theme.getColor(ACTIVITY_BAR_BADGE_FOREGROUND), - dragAndDropBackground: theme.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND), + dragAndDropBorder: theme.getColor(ACTIVITY_BAR_DRAG_AND_DROP_BORDER), activeBackgroundColor: undefined, inactiveBackgroundColor: undefined, activeBorderBottomColor: undefined, }; } @@ -542,7 +560,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { } this.viewContainerDisposables.delete(viewContainer.id); - this.hideComposite(viewContainer.id); + this.removeComposite(viewContainer.id); } private updateActivity(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { @@ -619,6 +637,17 @@ export class ActivitybarPart extends Part implements IActivityBarService { } } + private removeComposite(compositeId: string): void { + this.compositeBar.removeComposite(compositeId); + + const compositeActions = this.compositeActions.get(compositeId); + if (compositeActions) { + compositeActions.activityAction.dispose(); + compositeActions.pinnedAction.dispose(); + this.compositeActions.delete(compositeId); + } + } + getPinnedViewContainerIds(): string[] { const pinnedCompositeIds = this.compositeBar.getPinnedComposites().map(v => v.id); return this.getViewContainers() @@ -692,6 +721,10 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.compositeBar.setCompositeBarItems(newCompositeItems); } + + if (e.key === ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE && e.scope === StorageScope.GLOBAL) { + this.onDidChangeHomeBarVisibility(); + } } private saveCachedViewContainers(): void { @@ -821,6 +854,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.storageService.store(ActivitybarPart.PLACEHOLDER_VIEW_CONTAINERS, value, StorageScope.GLOBAL); } + private get homeBarVisibilityPreference(): boolean { + return this.storageService.getBoolean(ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE, StorageScope.GLOBAL, true); + } + + private set homeBarVisibilityPreference(value: boolean) { + this.storageService.store(ActivitybarPart.HOME_BAR_VISIBILITY_PREFERENCE, value, StorageScope.GLOBAL); + } + private migrateFromOldCachedViewContainersValue(): void { const value = this.storageService.get('workbench.activity.pinnedViewlets', StorageScope.GLOBAL); if (value !== undefined) { diff --git a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css index 0ae4319d8f6..7182e817ad4 100644 --- a/src/vs/workbench/browser/parts/activitybar/media/activityaction.css +++ b/src/vs/workbench/browser/parts/activitybar/media/activityaction.css @@ -20,9 +20,8 @@ width: 48px; height: 2px; display: block; - background-color: var(--insert-border-color); - opacity: 0; - transition-property: opacity; + background-color: transparent; + transition-property: background-color; transition-duration: 0ms; transition-delay: 100ms; } @@ -53,7 +52,7 @@ .monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.top::before, .monaco-workbench .activitybar > .content > .composite-bar > .monaco-action-bar .action-item.bottom::after, .monaco-workbench .activitybar > .content.dragged-over > .composite-bar > .monaco-action-bar .action-item:last-of-type::after { - opacity: 1; + background-color: var(--insert-border-color); } .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-label { diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index 53673ac4e90..1142be0f778 100644 --- a/src/vs/workbench/browser/parts/compositeBar.ts +++ b/src/vs/workbench/browser/parts/compositeBar.ts @@ -39,6 +39,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { private targetContainerLocation: ViewContainerLocation, private openComposite: (id: string, focus?: boolean) => Promise, private moveComposite: (from: string, to: string, before?: Before2D) => void, + private getItems: () => ICompositeBarItem[], ) { } drop(data: CompositeDragAndDropData, targetCompositeId: string | undefined, originalEvent: DragEvent, before?: Before2D): void { @@ -61,11 +62,7 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { return; } - this.viewDescriptorService.moveViewContainerToLocation(currentContainer, this.targetContainerLocation); - - if (targetCompositeId) { - this.moveComposite(currentContainer.id, targetCompositeId, before); - } + this.viewDescriptorService.moveViewContainerToLocation(currentContainer, this.targetContainerLocation, this.getTargetIndex(targetCompositeId, before)); } } @@ -98,6 +95,16 @@ export class CompositeDragAndDrop implements ICompositeDragAndDrop { return this.canDrop(data, targetCompositeId); } + private getTargetIndex(targetId: string | undefined, before2d: Before2D | undefined): number | undefined { + if (!targetId) { + return undefined; + } + + const items = this.getItems(); + const before = this.targetContainerLocation === ViewContainerLocation.Panel ? before2d?.horizontallyBefore : before2d?.verticallyBefore; + return items.findIndex(o => o.id === targetId) + (before ? 0 : 1); + } + private canDrop(data: CompositeDragAndDropData, targetCompositeId: string | undefined): boolean { const dragData = data.getData(); @@ -666,9 +673,18 @@ class CompositeBarModel { } this._items = result; } + + this.updateItemsOrder(); return hasChanges; } + + private updateItemsOrder(): void { + if (this._items) { + this.items.forEach((item, index) => { if (item.order !== undefined) { item.order = index; } }); + } + } + get visibleItems(): ICompositeBarModelItem[] { return this.items.filter(item => item.visible); } @@ -704,6 +720,8 @@ class CompositeBarModel { item.visible = true; changed = true; } + + this.updateItemsOrder(); return changed; } else { const item = this.createCompositeBarItem(id, name, order, true, true); @@ -716,6 +734,8 @@ class CompositeBarModel { } this.items.splice(index, 0, item); } + + this.updateItemsOrder(); return true; } } @@ -724,6 +744,7 @@ class CompositeBarModel { for (let index = 0; index < this.items.length; index++) { if (this.items[index].id === id) { this.items.splice(index, 1); + this.updateItemsOrder(); return true; } } @@ -759,6 +780,8 @@ class CompositeBarModel { // Make sure a moved composite gets pinned sourceItem.pinned = true; + this.updateItemsOrder(); + return true; } diff --git a/src/vs/workbench/browser/parts/compositeBarActions.ts b/src/vs/workbench/browser/parts/compositeBarActions.ts index 98acbac56f6..11198285ec1 100644 --- a/src/vs/workbench/browser/parts/compositeBarActions.ts +++ b/src/vs/workbench/browser/parts/compositeBarActions.ts @@ -119,7 +119,7 @@ export interface ICompositeBarColors { inactiveForegroundColor?: Color; badgeBackground?: Color; badgeForeground?: Color; - dragAndDropBackground?: Color; + dragAndDropBorder?: Color; } export interface IActivityActionViewItemOptions extends IBaseActionViewItemOptions { @@ -169,16 +169,14 @@ export class ActivityActionViewItem extends BaseActionViewItem { this.label.style.color = foreground ? foreground.toString() : ''; this.label.style.backgroundColor = ''; } - - const dragColor = colors.activeBackgroundColor || colors.activeForegroundColor; - this.container.style.setProperty('--insert-border-color', dragColor ? dragColor.toString() : ''); } else { const foreground = this._action.checked ? colors.activeForegroundColor : colors.inactiveForegroundColor; const borderBottomColor = this._action.checked ? colors.activeBorderBottomColor : null; this.label.style.color = foreground ? foreground.toString() : ''; this.label.style.borderBottomColor = borderBottomColor ? borderBottomColor.toString() : ''; - this.container.style.setProperty('--insert-border-color', colors.activeForegroundColor ? colors.activeForegroundColor.toString() : ''); } + + this.container.style.setProperty('--insert-border-color', colors.dragAndDropBorder ? colors.dragAndDropBorder.toString() : ''); } // Badge @@ -203,7 +201,7 @@ export class ActivityActionViewItem extends BaseActionViewItem { // Make the container tab-able for keyboard navigation this.container.tabIndex = 0; - this.container.setAttribute('role', this.options.icon ? 'button' : 'tab'); + this.container.setAttribute('role', 'tab'); // Try hard to prevent keyboard only focus feedback when using mouse this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_DOWN, () => { @@ -649,9 +647,11 @@ export class CompositeActionViewItem extends ActivityActionViewItem { if (this.getAction().checked) { dom.addClass(this.container, 'checked'); this.container.setAttribute('aria-label', nls.localize('compositeActive', "{0} active", this.container.title)); + this.container.setAttribute('aria-expanded', 'true'); } else { dom.removeClass(this.container, 'checked'); this.container.setAttribute('aria-label', this.container.title); + this.container.setAttribute('aria-expanded', 'false'); } this.updateStyles(); } diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index fa05222640f..1c07cd2b501 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -106,10 +106,6 @@ export abstract class BaseEditor extends Composite implements IEditorPane { this.createEditor(parent); } - onHide() { } - - onWillHide() { } - /** * Called to create the editor in the parent HTMLElement. */ @@ -133,6 +129,16 @@ export abstract class BaseEditor extends Composite implements IEditorPane { this._group = group; } + /** + * Called before the editor is being removed from the DOM. + */ + onWillHide() { } + + /** + * Called after the editor has been removed from the DOM. + */ + onDidHide() { } + protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { const mementoKey = `${this.getId()}${key}`; diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 722efdf0105..4fc47fa281b 100644 --- a/src/vs/workbench/browser/parts/editor/binaryEditor.ts +++ b/src/vs/workbench/browser/parts/editor/binaryEditor.ts @@ -160,6 +160,7 @@ export abstract class BaseBinaryResourceEditor extends BaseEditor { super.dispose(); } } + export interface IResourceDescriptor { readonly resource: URI; readonly name: string; diff --git a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts index 368de57eda5..7863a8c2c12 100644 --- a/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts +++ b/src/vs/workbench/browser/parts/editor/breadcrumbsControl.ts @@ -11,7 +11,7 @@ import { tail } from 'vs/base/common/arrays'; import { timeout } from 'vs/base/common/async'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { isEqual } from 'vs/base/common/resources'; +import { extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import 'vs/css!./media/breadcrumbscontrol'; import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser'; @@ -71,7 +71,7 @@ class Item extends BreadcrumbsItem { return false; } if (this.element instanceof FileElement && other.element instanceof FileElement) { - return (isEqual(this.element.uri, other.element.uri, false) && + return (extUri.isEqual(this.element.uri, other.element.uri) && this.options.showFileIcons === other.options.showFileIcons && this.options.showSymbolIcons === other.options.showSymbolIcons); } diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 482abde4189..77554b697b7 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -5,7 +5,7 @@ import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditorPane, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorInput } from 'vs/workbench/common/editor'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, GroupsArrangement, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Dimension } from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; @@ -14,6 +14,7 @@ import { ISerializableView } from 'vs/base/browser/ui/grid/grid'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; +import { localize } from 'vs/nls'; export const EDITOR_TITLE_HEIGHT = 35; @@ -41,6 +42,26 @@ export const DEFAULT_EDITOR_PART_OPTIONS: IEditorPartOptions = { splitSizing: 'distribute' }; +export function computeEditorAriaLabel(input: IEditorInput, index: number | undefined, group: IEditorGroup | undefined, groupCount: number): string { + let ariaLabel = input.getAriaLabel(); + if (group && !group.isPinned(input)) { + ariaLabel = localize('preview', "{0}, preview", ariaLabel); + } + + if (group && group.isSticky(index ?? input)) { + ariaLabel = localize('pinned', "{0}, pinned", ariaLabel); + } + + // Apply group information to help identify in + // which group we are (only if more than one group + // is actually opened) + if (group && groupCount > 1) { + ariaLabel = `${ariaLabel}, ${group.ariaLabel}`; + } + + return ariaLabel; +} + export function impactsEditorPartOptions(event: IConfigurationChangeEvent): boolean { return event.affectsConfiguration('workbench.editor') || event.affectsConfiguration('workbench.iconTheme'); } @@ -68,6 +89,11 @@ export interface IEditorOpeningEvent extends IEditorIdentifier { */ options?: IEditorOptions; + /** + * Context indicates how the editor open event is initialized. + */ + context?: OpenEditorContext; + /** * Allows to prevent the opening of an editor by providing a callback * that will be executed instead. By returning another editor promise @@ -144,11 +170,6 @@ export function getActiveTextEditorOptions(group: IEditorGroup, expectedActiveEd */ export interface EditorServiceImpl extends IEditorService { - /** - * Emitted when an editor is closed. - */ - readonly onDidCloseEditor: Event; - /** * Emitted when an editor failed to open. */ diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index f80f1ff03be..5e03ab84808 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -1339,7 +1339,8 @@ export class QuickAccessPreviousEditorFromHistoryAction extends Action { id: string, label: string, @IQuickInputService private readonly quickInputService: IQuickInputService, - @IKeybindingService private readonly keybindingService: IKeybindingService + @IKeybindingService private readonly keybindingService: IKeybindingService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { super(id, label); } @@ -1347,7 +1348,14 @@ export class QuickAccessPreviousEditorFromHistoryAction extends Action { async run(): Promise { const keybindings = this.keybindingService.lookupKeybindings(this.id); - this.quickInputService.quickAccess.show('', { quickNavigateConfiguration: { keybindings } }); + // Enforce to activate the first item in quick access if + // the currently active editor group has n editor opened + let itemActivation: ItemActivation | undefined = undefined; + if (this.editorGroupService.activeGroup.count === 0) { + itemActivation = ItemActivation.FIRST; + } + + this.quickInputService.quickAccess.show('', { quickNavigateConfiguration: { keybindings }, itemActivation }); } } diff --git a/src/vs/workbench/browser/parts/editor/editorControl.ts b/src/vs/workbench/browser/parts/editor/editorControl.ts index b0e2f9d4a37..d7ccd8c71c8 100644 --- a/src/vs/workbench/browser/parts/editor/editorControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorControl.ts @@ -35,6 +35,8 @@ export class EditorControl extends Disposable { readonly onDidSizeConstraintsChange = this._onDidSizeConstraintsChange.event; private _activeEditorPane: BaseEditor | null = null; + get activeEditorPane(): IVisibleEditorPane | null { return this._activeEditorPane as IVisibleEditorPane | null; } + private readonly editorPanes: BaseEditor[] = []; private readonly activeEditorPaneDisposables = this._register(new DisposableStore()); @@ -53,10 +55,6 @@ export class EditorControl extends Disposable { this.editorOperation = this._register(new LongRunningOperation(editorProgressService)); } - get activeEditorPane(): IVisibleEditorPane | null { - return this._activeEditorPane as IVisibleEditorPane | null; - } - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { // Editor pane @@ -208,7 +206,7 @@ export class EditorControl extends Disposable { this._activeEditorPane.onWillHide(); this.parent.removeChild(editorPaneContainer); hide(editorPaneContainer); - this._activeEditorPane.onHide(); + this._activeEditorPane.onDidHide(); } // Indicate to editor pane diff --git a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts index 90a75c3c0a0..1f7016f89ba 100644 --- a/src/vs/workbench/browser/parts/editor/editorDropTarget.ts +++ b/src/vs/workbench/browser/parts/editor/editorDropTarget.ts @@ -22,6 +22,9 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { URI } from 'vs/base/common/uri'; import { joinPath } from 'vs/base/common/resources'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import { localize } from 'vs/nls'; interface IDropOperation { splitDirection?: GroupDirection; @@ -31,8 +34,10 @@ class DropOverlay extends Themable { private static readonly OVERLAY_ID = 'monaco-workbench-editor-drop-overlay'; - private container!: HTMLElement; - private overlay!: HTMLElement; + private static readonly MAX_FILE_UPLOAD_SIZE = 100 * 1024 * 1024; // 100mb + + private container: HTMLElement | undefined; + private overlay: HTMLElement | undefined; private currentDropOperation: IDropOperation | undefined; private _disposed: boolean | undefined; @@ -48,7 +53,8 @@ class DropOverlay extends Themable { @IThemeService themeService: IThemeService, @IInstantiationService private instantiationService: IInstantiationService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @IEditorService private readonly editorService: IEditorService + @IEditorService private readonly editorService: IEditorService, + @INotificationService private readonly notificationService: INotificationService ) { super(themeService); @@ -65,45 +71,46 @@ class DropOverlay extends Themable { const overlayOffsetHeight = this.getOverlayOffsetHeight(); // Container - this.container = document.createElement('div'); - this.container.id = DropOverlay.OVERLAY_ID; - this.container.style.top = `${overlayOffsetHeight}px`; + const container = this.container = document.createElement('div'); + container.id = DropOverlay.OVERLAY_ID; + container.style.top = `${overlayOffsetHeight}px`; // Parent - this.groupView.element.appendChild(this.container); + this.groupView.element.appendChild(container); addClass(this.groupView.element, 'dragged-over'); this._register(toDisposable(() => { - this.groupView.element.removeChild(this.container); + this.groupView.element.removeChild(container); removeClass(this.groupView.element, 'dragged-over'); })); // Overlay this.overlay = document.createElement('div'); addClass(this.overlay, 'editor-group-overlay-indicator'); - this.container.appendChild(this.overlay); + container.appendChild(this.overlay); // Overlay Event Handling - this.registerListeners(); + this.registerListeners(container); // Styles this.updateStyles(); } protected updateStyles(): void { + const overlay = assertIsDefined(this.overlay); // Overlay drop background - this.overlay.style.backgroundColor = this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) || ''; + overlay.style.backgroundColor = this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) || ''; // Overlay contrast border (if any) const activeContrastBorderColor = this.getColor(activeContrastBorder); - this.overlay.style.outlineColor = activeContrastBorderColor || ''; - this.overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : ''; - this.overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : ''; - this.overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : ''; + overlay.style.outlineColor = activeContrastBorderColor || ''; + overlay.style.outlineOffset = activeContrastBorderColor ? '-2px' : ''; + overlay.style.outlineStyle = activeContrastBorderColor ? 'dashed' : ''; + overlay.style.outlineWidth = activeContrastBorderColor ? '2px' : ''; } - private registerListeners(): void { - this._register(new DragAndDropObserver(this.container, { + private registerListeners(container: HTMLElement): void { + this._register(new DragAndDropObserver(container, { onDragEnter: e => undefined, onDragOver: e => { const isDraggingGroup = this.groupTransfer.hasData(DraggedEditorGroupIdentifier.prototype); @@ -161,7 +168,7 @@ class DropOverlay extends Themable { } })); - this._register(addDisposableListener(this.container, EventType.MOUSE_OVER, () => { + this._register(addDisposableListener(container, EventType.MOUSE_OVER, () => { // Under some circumstances we have seen reports where the drop overlay is not being // cleaned up and as such the editor area remains under the overlay so that you cannot // type into the editor anymore. This seems related to using VMs and DND via host and @@ -295,6 +302,14 @@ class DropOverlay extends Themable { for (let i = 0; i < files.length; i++) { const file = files.item(i); if (file) { + + // Skip for very large files because this operation is unbuffered + if (file.size > DropOverlay.MAX_FILE_UPLOAD_SIZE) { + this.notificationService.warn(localize('fileTooLarge', "File is too large to open as untitled editor. Please upload it first into the file explorer and then try again.")); + continue; + } + + // Read file fully and open as untitled editor const reader = new FileReader(); reader.readAsArrayBuffer(file); reader.onload = async event => { @@ -456,30 +471,32 @@ class DropOverlay extends Themable { } // Make sure the overlay is visible now - this.overlay.style.opacity = '1'; + const overlay = assertIsDefined(this.overlay); + overlay.style.opacity = '1'; // Enable transition after a timeout to prevent initial animation - setTimeout(() => addClass(this.overlay, 'overlay-move-transition'), 0); + setTimeout(() => addClass(overlay, 'overlay-move-transition'), 0); // Remember as current split direction this.currentDropOperation = { splitDirection }; } private doPositionOverlay(options: { top: string, left: string, width: string, height: string }): void { + const [container, overlay] = assertAllDefined(this.container, this.overlay); // Container const offsetHeight = this.getOverlayOffsetHeight(); if (offsetHeight) { - this.container.style.height = `calc(100% - ${offsetHeight}px)`; + container.style.height = `calc(100% - ${offsetHeight}px)`; } else { - this.container.style.height = '100%'; + container.style.height = '100%'; } // Overlay - this.overlay.style.top = options.top; - this.overlay.style.left = options.left; - this.overlay.style.width = options.width; - this.overlay.style.height = options.height; + overlay.style.top = options.top; + overlay.style.left = options.left; + overlay.style.width = options.width; + overlay.style.height = options.height; } private getOverlayOffsetHeight(): number { @@ -491,11 +508,12 @@ class DropOverlay extends Themable { } private hideOverlay(): void { + const overlay = assertIsDefined(this.overlay); // Reset overlay this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' }); - this.overlay.style.opacity = '0'; - removeClass(this.overlay, 'overlay-move-transition'); + overlay.style.opacity = '0'; + removeClass(overlay, 'overlay-move-transition'); // Reset current operation this.currentDropOperation = undefined; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 7e26de12c5d..3f54f961044 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -16,7 +16,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant, Themable } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER, EDITOR_GROUP_HEADER_BORDER } from 'vs/workbench/common/theme'; -import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions, ICloseAllEditorsOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions, ICloseAllEditorsOptions, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; @@ -863,7 +863,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#region openEditor() - async openEditor(editor: EditorInput, options?: EditorOptions): Promise { + async openEditor(editor: EditorInput, options?: EditorOptions, context?: OpenEditorContext): Promise { // Guard against invalid inputs if (!editor) { @@ -871,7 +871,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Editor opening event allows for prevention - const event = new EditorOpeningEvent(this._group.id, editor, options); + const event = new EditorOpeningEvent(this._group.id, editor, options, context); this._onWillOpenEditor.fire(event); const prevented = event.isPrevented(); if (prevented) { @@ -1125,7 +1125,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Move across groups else { - this.doMoveOrCopyEditorAcrossGroups(editor, target, options); + this.doMoveOrCopyEditorAcrossGroups(editor, target, options, false); } } @@ -1171,7 +1171,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { })); // A move to another group is an open first... - target.openEditor(editor, options); + target.openEditor(editor, options, keepCopy ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR); // ...and a close afterwards (unless we copy) if (!keepCopy) { @@ -1717,7 +1717,8 @@ class EditorOpeningEvent implements IEditorOpeningEvent { constructor( private _group: GroupIdentifier, private _editor: EditorInput, - private _options: EditorOptions | undefined + private _options: EditorOptions | undefined, + private _context: OpenEditorContext | undefined ) { } @@ -1733,6 +1734,10 @@ class EditorOpeningEvent implements IEditorOpeningEvent { return this._options; } + get context(): OpenEditorContext | undefined { + return this._context; + } + prevent(callback: () => Promise): void { this.override = callback; } diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index ac6dc47d50f..90b596924ec 100644 --- a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css @@ -29,7 +29,7 @@ /* Title Actions */ .monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label, -.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-label { +.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-label:not(span) { display: flex; height: 35px; min-width: 28px; diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 98304f2366b..5f3a4cf23c5 100644 --- a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts +++ b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts @@ -5,7 +5,7 @@ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IRange } from 'vs/editor/common/core/range'; import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -26,9 +26,11 @@ export class RangeHighlightDecorations extends Disposable { private readonly editorDisposables = this._register(new DisposableStore()); private readonly _onHighlightRemoved: Emitter = this._register(new Emitter()); - readonly onHighlightRemoved: Event = this._onHighlightRemoved.event; + readonly onHighlightRemoved = this._onHighlightRemoved.event; - constructor(@IEditorService private readonly editorService: IEditorService) { + constructor( + @IEditorService private readonly editorService: IEditorService + ) { super(); } diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 459c0852ebf..fe9ec55c2d4 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -31,10 +31,10 @@ import { ResourcesDropHandler, DraggedEditorIdentifier, DraggedEditorGroupIdenti import { Color } from 'vs/base/common/color'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { MergeGroupMode, IMergeGroupOptions, GroupsArrangement, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { addClass, addDisposableListener, hasClass, EventType, EventHelper, removeClass, Dimension, scheduleAtNextAnimationFrame, findParentWithClass, clearNode } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl, EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, EditorServiceImpl, EDITOR_TITLE_HEIGHT, computeEditorAriaLabel } from 'vs/workbench/browser/parts/editor/editor'; import { CloseOneEditorAction } from 'vs/workbench/browser/parts/editor/editorActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { BreadcrumbsControl } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; @@ -102,7 +102,8 @@ export class TabsTitleControl extends TitleControl { @IConfigurationService configurationService: IConfigurationService, @IFileService fileService: IFileService, @IEditorService private readonly editorService: EditorServiceImpl, - @IPathService private readonly pathService: IPathService + @IPathService private readonly pathService: IPathService, + @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService ) { super(parent, accessor, group, contextMenuService, instantiationService, contextKeyService, keybindingService, telemetryService, notificationService, menuService, quickInputService, themeService, extensionService, configurationService, fileService); @@ -883,12 +884,12 @@ export class TabsTitleControl extends TitleControl { const { verbosity, shortenDuplicates } = this.getLabelConfigFlags(labelFormat); // Build labels and descriptions for each editor - const labels = this.group.editors.map(editor => ({ + const labels = this.group.editors.map((editor, index) => ({ editor, name: editor.getName(), description: editor.getDescription(verbosity), title: withNullAsUndefined(editor.getTitle(Verbosity.LONG)), - ariaLabel: editor.isReadonly() ? localize('readonlyEditor', "{0} readonly", editor.getTitle(Verbosity.SHORT)) : editor.getTitle(Verbosity.SHORT) + ariaLabel: computeEditorAriaLabel(editor, index, this.group, this.editorGroupService.count) })); // Shorten labels as needed diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index c74280811de..6ecda2f20ed 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -215,19 +215,6 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan return options; } - protected getAriaLabel(): string { - let ariaLabel: string; - - const inputName = this.input?.getName(); - if (this.input?.isReadonly()) { - ariaLabel = inputName ? nls.localize('readonlyEditorWithInputAriaLabel', "{0} readonly compare", inputName) : nls.localize('readonlyEditorAriaLabel', "Readonly compare"); - } else { - ariaLabel = inputName ? nls.localize('editableEditorWithInputAriaLabel', "{0} compare", inputName) : nls.localize('editableEditorAriaLabel', "Compare"); - } - - return ariaLabel; - } - private isFileBinaryError(error: Error[]): boolean; private isFileBinaryError(error: Error): boolean; private isFileBinaryError(error: Error | Error[]): boolean { diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index e41206a40d9..ab124afdf52 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -23,6 +23,7 @@ import { isCodeEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { computeEditorAriaLabel } from 'vs/workbench/browser/parts/editor/editor'; export interface IEditorConfiguration { editor: object; @@ -102,16 +103,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa } private computeAriaLabel(): string { - let ariaLabel = this.getAriaLabel(); - - // Apply group information to help identify in - // which group we are (only if more than one group - // is actually opened) - if (ariaLabel && this.group && this.editorGroupService.count > 1) { - ariaLabel = localize('editorLabelWithGroup', "{0}, {1}", ariaLabel, this.group.ariaLabel); - } - - return ariaLabel; + return this._input ? computeEditorAriaLabel(this._input, undefined, this.group, this.editorGroupService.count) : localize('editor', "Editor"); } protected getConfigurationOverrides(): IEditorOptions { @@ -303,8 +295,6 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditorPa return undefined; } - protected abstract getAriaLabel(): string; - dispose(): void { this.lastAppliedEditorOptions = undefined; diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 43320493a52..bf60f8d5e3e 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -25,7 +25,6 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { EditorOption, IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { basenameOrAuthority } from 'vs/base/common/resources'; import { ModelConstants } from 'vs/editor/common/model'; /** @@ -108,11 +107,6 @@ export class AbstractTextResourceEditor extends BaseTextEditor { } } - protected getAriaLabel(): string { - const inputName = this.input instanceof UntitledTextEditorInput ? basenameOrAuthority(this.input.resource) : this.input?.getName() || nls.localize('writeableEditorAriaLabel', "Editor"); - return this.input?.isReadonly() ? nls.localize('readonlyEditor', "{0} readonly", inputName) : inputName; - } - /** * Reveals the last line of this editor if it has a model set. */ diff --git a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css index d5646d18f76..9d01ee4526b 100644 --- a/src/vs/workbench/browser/parts/notifications/media/notificationsList.css +++ b/src/vs/workbench/browser/parts/notifications/media/notificationsList.css @@ -121,3 +121,15 @@ height: 2px; bottom: 0; } + +.monaco-workbench.mac:not(.web) .notifications-list-container .monaco-progress-container.infinite .progress-bit { + /** macOS native: reduce animation steps for reduced CPU load (https://github.com/microsoft/vscode/issues/97900) */ + animation-timing-function: steps(100); +} + +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + /** macOS native: do not change the animation-timing-function on highDPI screens to reduce stutter */ + .monaco-workbench.mac:not(.web) .notifications-list-container .monaco-progress-container.infinite .progress-bit { + animation-timing-function: linear; + } +} diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index d1dec4f90fd..faff5be9118 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts @@ -305,7 +305,7 @@ export class NotificationsCenter extends Themable implements INotificationsCente this.hide(); // Close all - for (const notification of this.model.notifications) { + for (const notification of [...this.model.notifications] /* copy array since we modify it from closing */) { if (!notification.hasProgress) { notification.close(); } diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index d815edc0f9a..cb0d283b52f 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -282,7 +282,6 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(TogglePanelActi actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(FocusPanelAction), 'View: Focus into Panel', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMaximizedPanelAction), 'View: Toggle Maximized Panel', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ClosePanelAction), 'View: Close Panel', nls.localize('view', "View")); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleMaximizedPanelAction), 'View: Toggle Panel Position', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(PreviousPanelViewAction), 'View: Previous Panel View', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.from(NextPanelViewAction), 'View: Next Panel View', nls.localize('view', "View")); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index d3d952877c3..d44fcc689e3 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -20,7 +20,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; -import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND } from 'vs/workbench/common/theme'; +import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_INPUT_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; import { CompositeBar, ICompositeBarItem, CompositeDragAndDrop } from 'vs/workbench/browser/parts/compositeBar'; import { ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions'; @@ -35,7 +35,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { ViewContainer, IViewDescriptorService, IViewContainerModel, ViewContainerLocation } from 'vs/workbench/common/views'; import { MenuId } from 'vs/platform/actions/common/actions'; -import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; +import { ViewMenuActions, ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { Before2D, CompositeDragAndDropObserver, ICompositeDragAndDrop } from 'vs/workbench/browser/dnd'; @@ -144,7 +144,8 @@ export class PanelPart extends CompositePart implements IPanelService { this.dndHandler = new CompositeDragAndDrop(this.viewDescriptorService, ViewContainerLocation.Panel, (id: string, focus?: boolean) => (this.openPanel(id, focus) as Promise).then(panel => panel || null), - (from: string, to: string, before?: Before2D) => this.compositeBar.move(from, to, before?.horizontallyBefore) + (from: string, to: string, before?: Before2D) => this.compositeBar.move(from, to, before?.horizontallyBefore), + () => this.compositeBar.getCompositeBarItems() ); this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { @@ -175,7 +176,7 @@ export class PanelPart extends CompositePart implements IPanelService { inactiveForegroundColor: theme.getColor(PANEL_INACTIVE_TITLE_FOREGROUND), badgeBackground: theme.getColor(badgeBackground), badgeForeground: theme.getColor(badgeForeground), - dragAndDropBackground: theme.getColor(PANEL_DRAG_AND_DROP_BACKGROUND) + dragAndDropBorder: theme.getColor(PANEL_DRAG_AND_DROP_BORDER) }) })); @@ -196,6 +197,10 @@ export class PanelPart extends CompositePart implements IPanelService { result.push(...viewMenuActions.getContextMenuActions()); viewMenuActions.dispose(); } + + const viewContainerMenuActions = this.instantiationService.createInstance(ViewContainerMenuActions, container.id, MenuId.ViewContainerTitleContext); + result.push(...viewContainerMenuActions.getContextMenuActions()); + viewContainerMenuActions.dispose(); } return result; } @@ -204,10 +209,21 @@ export class PanelPart extends CompositePart implements IPanelService { for (const panel of panels) { const cachedPanel = this.getCachedPanels().filter(({ id }) => id === panel.id)[0]; const activePanel = this.getActivePanel(); - const isActive = activePanel?.getId() === panel.id || (!activePanel && this.getLastActivePanelId() === panel.id); + const isActive = + activePanel?.getId() === panel.id || + (!activePanel && this.getLastActivePanelId() === panel.id) || + (this.extensionsRegistered && this.compositeBar.getVisibleComposites().length === 0); if (isActive || !this.shouldBeHidden(panel.id, cachedPanel)) { - this.compositeBar.addComposite(panel); + + // Override order + const newPanel = { + id: panel.id, + name: panel.name, + order: cachedPanel?.order === undefined ? panel.order : cachedPanel.order + }; + + this.compositeBar.addComposite(newPanel); // Pin it by default if it is new if (!cachedPanel) { @@ -304,6 +320,7 @@ export class PanelPart extends CompositePart implements IPanelService { } private registerListeners(): void { + // Panel registration this._register(this.registry.onDidRegister(panel => this.onDidRegisterPanels([panel]))); this._register(this.registry.onDidDeregister(panel => this.onDidDeregisterPanel(panel.id))); diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index dde922f9908..6f17fd71773 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts @@ -9,7 +9,6 @@ import { toErrorMessage } from 'vs/base/common/errorMessage'; import { dispose, IDisposable, Disposable, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { CodiconLabel } from 'vs/base/browser/ui/codicons/codiconLabel'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Part } from 'vs/workbench/browser/part'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -726,7 +725,6 @@ class StatusbarEntryItem extends Disposable { @ICommandService private readonly commandService: ICommandService, @INotificationService private readonly notificationService: INotificationService, @ITelemetryService private readonly telemetryService: ITelemetryService, - @IEditorService private readonly editorService: IEditorService, @IThemeService private readonly themeService: IThemeService ) { super(); @@ -829,12 +827,6 @@ class StatusbarEntryItem extends Disposable { const id = typeof command === 'string' ? command : command.id; const args = typeof command === 'string' ? [] : command.arguments ?? []; - // Maintain old behaviour of always focusing the editor here - const activeTextEditorControl = this.editorService.activeTextEditorControl; - if (activeTextEditorControl) { - activeTextEditorControl.focus(); - } - this.telemetryService.publicLog2('workbenchActionExecuted', { id, from: 'status bar' }); try { await this.commandService.executeCommand(id, ...args); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index bd4cb681639..dfe56ef6508 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -333,7 +333,6 @@ export class TitlebarPart extends Part implements ITitleService { this.customMenubar = this._register(this.instantiationService.createInstance(CustomMenubarControl)); this.menubar = this.element.insertBefore($('div.menubar'), this.title); - this.menubar.setAttribute('role', 'menubar'); this.customMenubar.create(this.menubar); diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9483f3539e1..be38ca6c005 100644 --- a/src/vs/workbench/browser/parts/views/treeView.ts +++ b/src/vs/workbench/browser/parts/views/treeView.ts @@ -68,6 +68,9 @@ export class TreeViewPane extends ViewPane { this._register(toDisposable(() => this.treeView.setVisibility(false))); this._register(this.onDidChangeBodyVisibility(() => this.updateTreeVisibility())); this._register(this.treeView.onDidChangeWelcomeState(() => this._onDidChangeViewWelcomeState.fire())); + if (options.title !== this.treeView.title) { + this.updateTitle(this.treeView.title); + } this.updateTreeVisibility(); } @@ -161,7 +164,7 @@ export class TreeView extends Disposable implements ITreeView { private readonly _onDidCompleteRefresh: Emitter = this._register(new Emitter()); constructor( - protected readonly id: string, + readonly id: string, private _title: string, @IThemeService private readonly themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -425,8 +428,15 @@ export class TreeView extends Disposable implements ITreeView { identityProvider: new TreeViewIdentityProvider(), accessibilityProvider: { getAriaLabel(element: ITreeItem): string { + if (element.accessibilityInformation) { + return element.accessibilityInformation.label; + } + return element.tooltip ? element.tooltip : element.label ? element.label.label : ''; }, + getRole(element: ITreeItem): string | undefined { + return element.accessibilityInformation?.role; + }, getWidgetAriaLabel(): string { return widgetAriaLabel; } @@ -783,15 +793,15 @@ class TreeRenderer extends Disposable implements ITreeRenderer { - if ((Math.abs(start) > label.length) || (Math.abs(end) >= label.length)) { - return ({ start: 0, end: 0 }); - } if (start < 0) { start = label.length + start; } if (end < 0) { end = label.length + end; } + if ((start >= label.length) || (end > label.length)) { + return ({ start: 0, end: 0 }); + } if (start > end) { const swap = start; start = end; diff --git a/src/vs/workbench/browser/parts/views/viewMenuActions.ts b/src/vs/workbench/browser/parts/views/viewMenuActions.ts index 3c0ad25bc70..6d3f84da8d5 100644 --- a/src/vs/workbench/browser/parts/views/viewMenuActions.ts +++ b/src/vs/workbench/browser/parts/views/viewMenuActions.ts @@ -69,3 +69,37 @@ export class ViewMenuActions extends Disposable { return this.contextMenuActions; } } + +export class ViewContainerMenuActions extends Disposable { + + private readonly titleActionsDisposable = this._register(new MutableDisposable()); + private contextMenuActions: IAction[] = []; + + constructor( + containerId: string, + contextMenuId: MenuId, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, + ) { + super(); + + const scopedContextKeyService = this._register(this.contextKeyService.createScoped()); + scopedContextKeyService.createKey('container', containerId); + + const contextMenu = this._register(this.menuService.createMenu(contextMenuId, scopedContextKeyService)); + const updateContextMenuActions = () => { + this.contextMenuActions = []; + this.titleActionsDisposable.value = createAndFillInActionBarActions(contextMenu, { shouldForwardArgs: true }, { primary: [], secondary: this.contextMenuActions }); + }; + this._register(contextMenu.onDidChange(updateContextMenuActions)); + updateContextMenuActions(); + + this._register(toDisposable(() => { + this.contextMenuActions = []; + })); + } + + getContextMenuActions(): IAction[] { + return this.contextMenuActions; + } +} diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 0425cbe6a5c..77247227d33 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -8,7 +8,7 @@ import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { ColorIdentifier, activeContrastBorder, foreground } from 'vs/platform/theme/common/colorRegistry'; import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler'; -import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, EDITOR_DRAG_AND_DROP_BACKGROUND, PANEL_BORDER } from 'vs/workbench/common/theme'; +import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND, PANEL_SECTION_HEADER_FOREGROUND, PANEL_SECTION_HEADER_BACKGROUND, PANEL_SECTION_HEADER_BORDER, PANEL_SECTION_DRAG_AND_DROP_BACKGROUND, PANEL_SECTION_BORDER } from 'vs/workbench/common/theme'; import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass, createCSSRule, asCSSUrl, addClasses } from 'vs/base/browser/dom'; import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { firstIndex } from 'vs/base/common/arrays'; @@ -28,12 +28,12 @@ import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IView import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined, isString } from 'vs/base/common/types'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { Component } from 'vs/workbench/common/component'; -import { MenuId, MenuItemAction } from 'vs/platform/actions/common/actions'; +import { MenuId, MenuItemAction, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions'; import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ViewMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions'; import { parseLinkedText } from 'vs/base/common/linkedText'; @@ -49,6 +49,8 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; +import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -375,10 +377,10 @@ export abstract class ViewPane extends Pane implements IView { private calculateTitle(title: string): string { const viewContainer = this.viewDescriptorService.getViewContainerByViewId(this.id)!; const model = this.viewDescriptorService.getViewContainerModel(viewContainer); - const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id)!; + const viewDescriptor = this.viewDescriptorService.getViewDescriptorById(this.id); const isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; - if (!isDefault && viewDescriptor.containerTitle && model.title !== viewDescriptor.containerTitle) { + if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { return `${viewDescriptor.containerTitle}: ${title}`; } @@ -619,7 +621,8 @@ class ViewPaneDropOverlay extends Themable { constructor( private paneElement: HTMLElement, private orientation: Orientation | undefined, - protected themeService: IThemeService + protected location: ViewContainerLocation, + protected themeService: IThemeService, ) { super(themeService); this.cleanupOverlayScheduler = this._register(new RunOnceScheduler(() => this.dispose(), 300)); @@ -660,7 +663,7 @@ class ViewPaneDropOverlay extends Themable { protected updateStyles(): void { // Overlay drop background - this.overlay.style.backgroundColor = this.getColor(EDITOR_DRAG_AND_DROP_BACKGROUND) || ''; + this.overlay.style.backgroundColor = this.getColor(this.location === ViewContainerLocation.Panel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND) || ''; // Overlay contrast border (if any) const activeContrastBorderColor = this.getColor(activeContrastBorder); @@ -903,7 +906,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return; } - overlay = new ViewPaneDropOverlay(parent, undefined, this.themeService); + overlay = new ViewPaneDropOverlay(parent, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id) { @@ -911,7 +914,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!viewsToMove.some(v => !v.canMoveView)) { - overlay = new ViewPaneDropOverlay(parent, undefined, this.themeService); + overlay = new ViewPaneDropOverlay(parent, undefined, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } } @@ -1308,13 +1311,13 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } }); - // TODO@sbatten Styling is viewlet specific, must fix + const isPanel = this.viewDescriptorService.getViewLocationById(this.viewContainer.id) === ViewContainerLocation.Panel; const paneStyler = attachStyler(this.themeService, { - headerForeground: SIDE_BAR_SECTION_HEADER_FOREGROUND, - headerBackground: SIDE_BAR_SECTION_HEADER_BACKGROUND, - headerBorder: SIDE_BAR_SECTION_HEADER_BORDER, - leftBorder: PANEL_BORDER, - dropBackground: SIDE_BAR_DRAG_AND_DROP_BACKGROUND + headerForeground: isPanel ? PANEL_SECTION_HEADER_FOREGROUND : SIDE_BAR_SECTION_HEADER_FOREGROUND, + headerBackground: isPanel ? PANEL_SECTION_HEADER_BACKGROUND : SIDE_BAR_SECTION_HEADER_BACKGROUND, + headerBorder: isPanel ? PANEL_SECTION_HEADER_BORDER : SIDE_BAR_SECTION_HEADER_BORDER, + dropBackground: isPanel ? PANEL_SECTION_DRAG_AND_DROP_BACKGROUND : SIDE_BAR_DRAG_AND_DROP_BACKGROUND, + leftBorder: isPanel ? PANEL_SECTION_BORDER : undefined }, pane); const disposable = combinedDisposable(pane, onDidFocus, onDidChangeTitleArea, paneStyler, onDidChange, onDidChangeVisibility); const paneItem: IViewPaneItem = { pane, disposable }; @@ -1339,7 +1342,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return; } - overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.themeService); + overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } if (dropData.type === 'composite' && dropData.id !== this.viewContainer.id && !this.viewContainer.rejectAddedViews) { @@ -1347,7 +1350,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { const viewsToMove = this.viewDescriptorService.getViewContainerModel(container).allViewDescriptors; if (!viewsToMove.some(v => !v.canMoveView)) { - overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.themeService); + overlay = new ViewPaneDropOverlay(pane.dropTargetElement, this.orientation ?? Orientation.VERTICAL, this.viewDescriptorService.getViewContainerLocation(this.viewContainer)!, this.themeService); } } } @@ -1540,3 +1543,96 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } } + +class MoveViewPosition extends Action2 { + constructor(desc: Readonly, private readonly offset: number) { + super(desc); + } + + async run(accessor: ServicesAccessor): Promise { + const viewDescriptorService = accessor.get(IViewDescriptorService); + const contextKeyService = accessor.get(IContextKeyService); + + const viewId = FocusedViewContext.getValue(contextKeyService); + if (viewId === undefined) { + return; + } + + const viewContainer = viewDescriptorService.getViewContainerByViewId(viewId)!; + const model = viewDescriptorService.getViewContainerModel(viewContainer); + + const viewDescriptor = model.visibleViewDescriptors.find(vd => vd.id === viewId)!; + const currentIndex = model.visibleViewDescriptors.indexOf(viewDescriptor); + if (currentIndex + this.offset < 0 || currentIndex + this.offset >= model.visibleViewDescriptors.length) { + return; + } + + const newPosition = model.visibleViewDescriptors[currentIndex + this.offset]; + + model.move(viewDescriptor.id, newPosition.id); + } +} + +registerAction2( + class MoveViewUp extends MoveViewPosition { + constructor() { + super({ + id: 'views.moveViewUp', + title: nls.localize('viewMoveUp', "Move View Up"), + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KEY_K, KeyCode.UpArrow), + weight: KeybindingWeight.WorkbenchContrib + 1, + when: FocusedViewContext.notEqualsTo('') + } + }, -1); + } + } +); + +registerAction2( + class MoveViewLeft extends MoveViewPosition { + constructor() { + super({ + id: 'views.moveViewLeft', + title: nls.localize('viewMoveLeft', "Move View Left"), + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KEY_K, KeyCode.LeftArrow), + weight: KeybindingWeight.WorkbenchContrib + 1, + when: FocusedViewContext.notEqualsTo('') + } + }, -1); + } + } +); + +registerAction2( + class MoveViewDown extends MoveViewPosition { + constructor() { + super({ + id: 'views.moveViewDown', + title: nls.localize('viewMoveDown', "Move View Down"), + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KEY_K, KeyCode.DownArrow), + weight: KeybindingWeight.WorkbenchContrib + 1, + when: FocusedViewContext.notEqualsTo('') + } + }, 1); + } + } +); + +registerAction2( + class MoveViewRight extends MoveViewPosition { + constructor() { + super({ + id: 'views.moveViewRight', + title: nls.localize('viewMoveRight', "Move View Right"), + keybinding: { + primary: KeyChord(KeyMod.CtrlCmd + KeyCode.KEY_K, KeyCode.RightArrow), + weight: KeybindingWeight.WorkbenchContrib + 1, + when: FocusedViewContext.notEqualsTo('') + } + }, 1); + } + } +); diff --git a/src/vs/workbench/browser/parts/views/viewsService.ts b/src/vs/workbench/browser/parts/views/viewsService.ts index a1373aa9de8..fbce01f7e0a 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -186,8 +186,8 @@ export class ViewsService extends Disposable implements IViewsService { super({ id: `${viewDescriptor.id}.resetViewLocation`, title: { - original: 'Reset View Location', - value: localize('resetViewLocation', "Reset View Location") + original: 'Reset Location', + value: localize('resetViewLocation', "Reset Location") }, menu: [{ id: MenuId.ViewTitleContext, @@ -202,6 +202,15 @@ export class ViewsService extends Disposable implements IViewsService { } run(accessor: ServicesAccessor): void { const viewDescriptorService = accessor.get(IViewDescriptorService); + const defaultContainer = viewDescriptorService.getDefaultContainerById(viewDescriptor.id)!; + const containerModel = viewDescriptorService.getViewContainerModel(defaultContainer)!; + + // The default container is hidden so we should try to reset its location first + if (defaultContainer.hideIfEmpty && containerModel.visibleViewDescriptors.length === 0) { + const defaultLocation = viewDescriptorService.getDefaultViewContainerLocation(defaultContainer)!; + viewDescriptorService.moveViewContainerToLocation(defaultContainer, defaultLocation); + } + viewDescriptorService.moveViewsToContainer([viewDescriptor], viewDescriptorService.getDefaultContainerById(viewDescriptor.id)!); accessor.get(IViewsService).openView(viewDescriptor.id, true); } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index ea6fcaa4edf..13fe1340361 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -14,20 +14,14 @@ import { IInstantiationService, IConstructorSignature0, ServicesAccessor, Brande import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Registry } from 'vs/platform/registry/common/platform'; import { ITextModel } from 'vs/editor/common/model'; -import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ICompositeControl, IComposite } from 'vs/workbench/common/composite'; import { ActionRunner, IAction } from 'vs/base/common/actions'; -import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { IPathData } from 'vs/platform/windows/common/windows'; import { coalesce, firstOrDefault } from 'vs/base/common/arrays'; -import { ITextFileSaveOptions, ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IEditorService, IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; -import { isEqual, dirname } from 'vs/base/common/resources'; +import { IResourceEditorInputType } from 'vs/workbench/services/editor/common/editorService'; import { IRange } from 'vs/editor/common/core/range'; -import { createMemoizer } from 'vs/base/common/decorators'; -import { ILabelService } from 'vs/platform/label/common/label'; -import { Schemas } from 'vs/base/common/network'; -import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; export const DirtyWorkingCopiesContext = new RawContextKey('dirtyWorkingCopies', false); export const ActiveEditorContext = new RawContextKey('activeEditor', null); @@ -169,8 +163,14 @@ export interface IEditorControl extends ICompositeControl { } export interface IFileEditorInputFactory { + /** + * Creates new new editor input capable of showing files. + */ createFileEditorInput(resource: URI, encoding: string | undefined, mode: string | undefined, instantiationService: IInstantiationService): IFileEditorInput; + /** + * Check if the provided object is a file editor input. + */ isFileEditorInput(obj: unknown): obj is IFileEditorInput; } @@ -403,6 +403,11 @@ export interface IEditorInput extends IDisposable { */ getTitle(verbosity?: Verbosity): string | undefined; + /** + * Returns the aria label to be read out by a screen reader. + */ + getAriaLabel(): string; + /** * Resolves the input. */ @@ -512,6 +517,10 @@ export abstract class EditorInput extends Disposable implements IEditorInput { return this.getName(); } + getAriaLabel(): string { + return this.getTitle(Verbosity.SHORT); + } + /** * Returns the preferred editor for this input. A list of candidate editors is passed in that whee registered * for the input. This allows subclasses to decide late which editor to use for the input on a case by case basis. @@ -595,164 +604,6 @@ export abstract class EditorInput extends Disposable implements IEditorInput { } } -export abstract class TextResourceEditorInput extends EditorInput { - - private static readonly MEMOIZER = createMemoizer(); - - constructor( - public readonly resource: URI, - @IEditorService protected readonly editorService: IEditorService, - @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, - @ITextFileService protected readonly textFileService: ITextFileService, - @ILabelService protected readonly labelService: ILabelService, - @IFileService protected readonly fileService: IFileService, - @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService - ) { - super(); - - this.registerListeners(); - } - - protected registerListeners(): void { - - // Clear label memoizer on certain events that have impact - this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); - } - - private onLabelEvent(scheme: string): void { - if (scheme === this.resource.scheme) { - - // Clear any cached labels from before - TextResourceEditorInput.MEMOIZER.clear(); - - // Trigger recompute of label - this._onDidChangeLabel.fire(); - } - } - - getName(): string { - return this.basename; - } - - @TextResourceEditorInput.MEMOIZER - private get basename(): string { - return this.labelService.getUriBasenameLabel(this.resource); - } - - getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { - switch (verbosity) { - case Verbosity.SHORT: - return this.shortDescription; - case Verbosity.LONG: - return this.longDescription; - case Verbosity.MEDIUM: - default: - return this.mediumDescription; - } - } - - @TextResourceEditorInput.MEMOIZER - private get shortDescription(): string { - return this.labelService.getUriBasenameLabel(dirname(this.resource)); - } - - @TextResourceEditorInput.MEMOIZER - private get mediumDescription(): string { - return this.labelService.getUriLabel(dirname(this.resource), { relative: true }); - } - - @TextResourceEditorInput.MEMOIZER - private get longDescription(): string { - return this.labelService.getUriLabel(dirname(this.resource)); - } - - @TextResourceEditorInput.MEMOIZER - private get shortTitle(): string { - return this.getName(); - } - - @TextResourceEditorInput.MEMOIZER - private get mediumTitle(): string { - return this.labelService.getUriLabel(this.resource, { relative: true }); - } - - @TextResourceEditorInput.MEMOIZER - private get longTitle(): string { - return this.labelService.getUriLabel(this.resource); - } - - getTitle(verbosity: Verbosity): string { - switch (verbosity) { - case Verbosity.SHORT: - return this.shortTitle; - case Verbosity.LONG: - return this.longTitle; - default: - case Verbosity.MEDIUM: - return this.mediumTitle; - } - } - - isUntitled(): boolean { - return this.resource.scheme === Schemas.untitled; - } - - isReadonly(): boolean { - if (this.isUntitled()) { - return false; // untitled is never readonly - } - - return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); - } - - isSaving(): boolean { - if (this.isUntitled()) { - return false; // untitled is never saving automatically - } - - 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?: ITextFileSaveOptions): Promise { - return this.doSave(group, options, false); - } - - saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { - return this.doSave(group, options, true); - } - - private async doSave(group: GroupIdentifier, options: ISaveOptions | undefined, saveAs: boolean): Promise { - - // Save / Save As - let target: URI | undefined; - if (saveAs) { - target = await this.textFileService.saveAs(this.resource, undefined, options); - } else { - target = await this.textFileService.save(this.resource, options); - } - - if (!target) { - return undefined; // save cancelled - } - - if (!isEqual(target, this.resource)) { - return this.editorService.createEditorInput({ resource: target }); - } - - return this; - } - - async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { - await this.textFileService.revert(this.resource, options); - } -} - export const enum EncodingMode { /** diff --git a/src/vs/workbench/common/editor/diffEditorModel.ts b/src/vs/workbench/common/editor/diffEditorModel.ts index 93f9d7e7fe8..dfa51d62d4d 100644 --- a/src/vs/workbench/common/editor/diffEditorModel.ts +++ b/src/vs/workbench/common/editor/diffEditorModel.ts @@ -13,7 +13,10 @@ import { IEditorModel } from 'vs/platform/editor/common/editor'; export class DiffEditorModel extends EditorModel { protected readonly _originalModel: IEditorModel | null; + get originalModel(): IEditorModel | null { return this._originalModel; } + protected readonly _modifiedModel: IEditorModel | null; + get modifiedModel(): IEditorModel | null { return this._modifiedModel; } constructor(originalModel: IEditorModel | null, modifiedModel: IEditorModel | null) { super(); @@ -22,14 +25,6 @@ export class DiffEditorModel extends EditorModel { this._modifiedModel = modifiedModel; } - get originalModel(): IEditorModel | null { - return this._originalModel; - } - - get modifiedModel(): IEditorModel | null { - return this._modifiedModel; - } - async load(): Promise { await Promise.all([ this._originalModel?.load(), diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 3ea14b511f8..df60cae876e 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITextEditorModel, IModeSupport, TextResourceEditorInput } from 'vs/workbench/common/editor'; +import { ITextEditorModel, IModeSupport } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { IReference } from 'vs/base/common/lifecycle'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; @@ -14,12 +14,13 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; /** * A read-only text editor input whos contents are made of the provided resource that points to an existing * code editor model. */ -export class ResourceEditorInput extends TextResourceEditorInput implements IModeSupport { +export class ResourceEditorInput extends AbstractTextResourceEditorInput implements IModeSupport { static readonly ID: string = 'workbench.editors.resourceEditorInput'; @@ -64,6 +65,7 @@ export class ResourceEditorInput extends TextResourceEditorInput implements IMod setDescription(description: string): void { if (this.description !== description) { this.description = description; + this._onDidChangeLabel.fire(); } } @@ -87,9 +89,8 @@ export class ResourceEditorInput extends TextResourceEditorInput implements IMod const ref = await this.modelReference; - const model = ref.object; - // Ensure the resolved model is of expected type + const model = ref.object; if (!(model instanceof ResourceEditorModel)) { ref.dispose(); this.modelReference = undefined; diff --git a/src/vs/workbench/common/editor/textDiffEditorModel.ts b/src/vs/workbench/common/editor/textDiffEditorModel.ts index 579499c30f6..fcb97b428b2 100644 --- a/src/vs/workbench/common/editor/textDiffEditorModel.ts +++ b/src/vs/workbench/common/editor/textDiffEditorModel.ts @@ -15,9 +15,13 @@ import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel'; export class TextDiffEditorModel extends DiffEditorModel { protected readonly _originalModel: BaseTextEditorModel | null; + get originalModel(): BaseTextEditorModel | null { return this._originalModel; } + protected readonly _modifiedModel: BaseTextEditorModel | null; + get modifiedModel(): BaseTextEditorModel | null { return this._modifiedModel; } private _textDiffEditorModel: IDiffEditorModel | null = null; + get textDiffEditorModel(): IDiffEditorModel | null { return this._textDiffEditorModel; } constructor(originalModel: BaseTextEditorModel, modifiedModel: BaseTextEditorModel) { super(originalModel, modifiedModel); @@ -28,14 +32,6 @@ export class TextDiffEditorModel extends DiffEditorModel { this.updateTextDiffEditorModel(); } - get originalModel(): BaseTextEditorModel | null { - return this._originalModel; - } - - get modifiedModel(): BaseTextEditorModel | null { - return this._modifiedModel; - } - async load(): Promise { await super.load(); @@ -63,10 +59,6 @@ export class TextDiffEditorModel extends DiffEditorModel { } } - get textDiffEditorModel(): IDiffEditorModel | null { - return this._textDiffEditorModel; - } - isResolved(): boolean { return !!this._textDiffEditorModel; } diff --git a/src/vs/workbench/common/editor/textEditorModel.ts b/src/vs/workbench/common/editor/textEditorModel.ts index 2627ffc5890..f730d008e9a 100644 --- a/src/vs/workbench/common/editor/textEditorModel.ts +++ b/src/vs/workbench/common/editor/textEditorModel.ts @@ -41,7 +41,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel // We need the resource to point to an existing model const model = this.modelService.getModel(textEditorModelHandle); if (!model) { - throw new Error(`Document with resource ${textEditorModelHandle.toString()} does not exist`); + throw new Error(`Document with resource ${textEditorModelHandle.toString(true)} does not exist`); } this.textEditorModelHandle = textEditorModelHandle; diff --git a/src/vs/workbench/common/editor/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts new file mode 100644 index 00000000000..73c4152fee7 --- /dev/null +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { EditorInput, Verbosity, GroupIdentifier, IEditorInput, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { URI } from 'vs/base/common/uri'; +import { ITextFileService, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { createMemoizer } from 'vs/base/common/decorators'; +import { Schemas } from 'vs/base/common/network'; +import { dirname } from 'vs/base/common/resources'; + +/** + * The base class for all editor inputs that open in text editors. + */ +export abstract class AbstractTextResourceEditorInput extends EditorInput { + + private static readonly MEMOIZER = createMemoizer(); + + constructor( + public readonly resource: URI, + @IEditorService protected readonly editorService: IEditorService, + @IEditorGroupsService protected readonly editorGroupService: IEditorGroupsService, + @ITextFileService protected readonly textFileService: ITextFileService, + @ILabelService protected readonly labelService: ILabelService, + @IFileService protected readonly fileService: IFileService, + @IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService + ) { + super(); + + this.registerListeners(); + } + + protected registerListeners(): void { + + // Clear label memoizer on certain events that have impact + this._register(this.labelService.onDidChangeFormatters(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onLabelEvent(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onLabelEvent(e.scheme))); + } + + private onLabelEvent(scheme: string): void { + if (scheme === this.resource.scheme) { + + // Clear any cached labels from before + AbstractTextResourceEditorInput.MEMOIZER.clear(); + + // Trigger recompute of label + this._onDidChangeLabel.fire(); + } + } + + getName(): string { + return this.basename; + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get basename(): string { + return this.labelService.getUriBasenameLabel(this.resource); + } + + getDescription(verbosity: Verbosity = Verbosity.MEDIUM): string | undefined { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortDescription; + case Verbosity.LONG: + return this.longDescription; + case Verbosity.MEDIUM: + default: + return this.mediumDescription; + } + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get shortDescription(): string { + return this.labelService.getUriBasenameLabel(dirname(this.resource)); + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get mediumDescription(): string { + return this.labelService.getUriLabel(dirname(this.resource), { relative: true }); + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get longDescription(): string { + return this.labelService.getUriLabel(dirname(this.resource)); + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get shortTitle(): string { + return this.getName(); + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get mediumTitle(): string { + return this.labelService.getUriLabel(this.resource, { relative: true }); + } + + @AbstractTextResourceEditorInput.MEMOIZER + private get longTitle(): string { + return this.labelService.getUriLabel(this.resource); + } + + getTitle(verbosity: Verbosity): string { + switch (verbosity) { + case Verbosity.SHORT: + return this.shortTitle; + case Verbosity.LONG: + return this.longTitle; + default: + case Verbosity.MEDIUM: + return this.mediumTitle; + } + } + + isUntitled(): boolean { + return this.resource.scheme === Schemas.untitled; + } + + isReadonly(): boolean { + if (this.isUntitled()) { + return false; // untitled is never readonly + } + + return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); + } + + isSaving(): boolean { + if (this.isUntitled()) { + return false; // untitled is never saving automatically + } + + 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?: ITextFileSaveOptions): Promise { + return this.doSave(group, options, false); + } + + saveAs(group: GroupIdentifier, options?: ITextFileSaveOptions): Promise { + return this.doSave(group, options, true); + } + + private async doSave(group: GroupIdentifier, options: ISaveOptions | undefined, saveAs: boolean): Promise { + + // Save / Save As + let target: URI | undefined; + if (saveAs) { + target = await this.textFileService.saveAs(this.resource, undefined, options); + } else { + target = await this.textFileService.save(this.resource, options); + } + + if (!target) { + return undefined; // save cancelled + } + + return this.editorService.createEditorInput({ resource: target }); + } + + async revert(group: GroupIdentifier, options?: IRevertOptions): Promise { + await this.textFileService.revert(this.resource, options); + } +} diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index 2f7bca6e769..ef9b068f426 100644 --- a/src/vs/workbench/common/theme.ts +++ b/src/vs/workbench/common/theme.ts @@ -283,18 +283,50 @@ export const PANEL_ACTIVE_TITLE_BORDER = registerColor('panelTitle.activeBorder' hc: contrastBorder }, nls.localize('panelActiveTitleBorder', "Border color for the active panel title. Panels are shown below the editor area and contain views like output and integrated terminal.")); -export const PANEL_DRAG_AND_DROP_BACKGROUND = registerColor('panel.dropBackground', { - dark: Color.white.transparent(0.12), - light: Color.fromHex('#2677CB').transparent(0.18), - hc: Color.white.transparent(0.12) -}, nls.localize('panelDragAndDropBackground', "Drag and drop feedback color for the panel title items. The color should have transparency so that the panel entries can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal.")); - export const PANEL_INPUT_BORDER = registerColor('panelInput.border', { dark: null, light: Color.fromHex('#ddd'), hc: null }, nls.localize('panelInputBorder', "Input box border for inputs in the panel.")); +export const PANEL_DRAG_AND_DROP_BORDER = registerColor('panel.dropBorder', { + dark: PANEL_ACTIVE_TITLE_FOREGROUND, + light: PANEL_ACTIVE_TITLE_FOREGROUND, + hc: PANEL_ACTIVE_TITLE_FOREGROUND, +}, nls.localize('panelDragAndDropBorder', "Drag and drop feedback color for the panel titles. Panels are shown below the editor area and contain views like output and integrated terminal.")); + + +export const PANEL_SECTION_DRAG_AND_DROP_BACKGROUND = registerColor('panelSection.dropBackground', { + dark: EDITOR_DRAG_AND_DROP_BACKGROUND, + light: EDITOR_DRAG_AND_DROP_BACKGROUND, + hc: EDITOR_DRAG_AND_DROP_BACKGROUND, +}, nls.localize('panelSectionDragAndDropBackground', "Drag and drop feedback color for the panel sections. The color should have transparency so that the panel sections can still shine through. Panels are shown below the editor area and contain views like output and integrated terminal.")); + +export const PANEL_SECTION_HEADER_BACKGROUND = registerColor('panelSectionHeader.background', { + dark: Color.fromHex('#808080').transparent(0.2), + light: Color.fromHex('#808080').transparent(0.2), + hc: null +}, nls.localize('panelSectionHeaderBackground', "Panel section header background color. Panels are shown below the editor area and contain views like output and integrated terminal.")); + +export const PANEL_SECTION_HEADER_FOREGROUND = registerColor('panelSectionHeader.foreground', { + dark: null, + light: null, + hc: null +}, nls.localize('panelSectionHeaderForeground', "Panel section header foreground color. Panels are shown below the editor area and contain views like output and integrated terminal.")); + +export const PANEL_SECTION_HEADER_BORDER = registerColor('panelSectionHeader.border', { + dark: contrastBorder, + light: contrastBorder, + hc: contrastBorder +}, nls.localize('panelSectionHeaderBorder', "Panel section header border color. Panels are shown below the editor area and contain views like output and integrated terminal.")); + +export const PANEL_SECTION_BORDER = registerColor('panelSection.border', { + dark: PANEL_BORDER, + light: PANEL_BORDER, + hc: PANEL_BORDER +}, nls.localize('panelSectionBorder', "Panel section border color. Panels are shown below the editor area and contain views like output and integrated terminal.")); + + // < --- Status --- > export const STATUS_BAR_FOREGROUND = registerColor('statusBar.foreground', { @@ -407,11 +439,11 @@ export const ACTIVITY_BAR_ACTIVE_BACKGROUND = registerColor('activityBar.activeB hc: null }, nls.localize('activityBarActiveBackground', "Activity bar background color for the active item. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); -export const ACTIVITY_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('activityBar.dropBackground', { - dark: Color.white.transparent(0.12), - light: Color.white.transparent(0.12), - hc: Color.white.transparent(0.12), -}, nls.localize('activityBarDragAndDropBackground', "Drag and drop feedback color for the activity bar items. The color should have transparency so that the activity bar entries can still shine through. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); +export const ACTIVITY_BAR_DRAG_AND_DROP_BORDER = registerColor('activityBar.dropBorder', { + dark: ACTIVITY_BAR_FOREGROUND, + light: ACTIVITY_BAR_FOREGROUND, + hc: ACTIVITY_BAR_FOREGROUND, +}, nls.localize('activityBarDragAndDropBorder', "Drag and drop feedback color for the activity bar items. The activity bar is showing on the far left or right and allows to switch between views of the side bar.")); export const ACTIVITY_BAR_BADGE_BACKGROUND = registerColor('activityBarBadge.background', { dark: '#007ACC', @@ -480,9 +512,9 @@ export const SIDE_BAR_TITLE_FOREGROUND = registerColor('sideBarTitle.foreground' }, nls.localize('sideBarTitleForeground', "Side bar title foreground color. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_DRAG_AND_DROP_BACKGROUND = registerColor('sideBar.dropBackground', { - dark: Color.white.transparent(0.12), - light: Color.black.transparent(0.1), - hc: Color.white.transparent(0.3), + dark: EDITOR_DRAG_AND_DROP_BACKGROUND, + light: EDITOR_DRAG_AND_DROP_BACKGROUND, + hc: EDITOR_DRAG_AND_DROP_BACKGROUND, }, nls.localize('sideBarDragAndDropBackground', "Drag and drop feedback color for the side bar sections. The color should have transparency so that the side bar sections can still shine through. The side bar is the container for views like explorer and search.")); export const SIDE_BAR_SECTION_HEADER_BACKGROUND = registerColor('sideBarSectionHeader.background', { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 6b7a1506c99..6a8dd33202f 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -22,6 +22,7 @@ import { SetMap } from 'vs/base/common/collections'; import { IProgressIndicator } from 'vs/platform/progress/common/progress'; import Severity from 'vs/base/common/severity'; import { IPaneComposite } from 'vs/workbench/common/panecomposite'; +import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; @@ -49,8 +50,6 @@ export interface IViewContainerDescriptor { readonly alwaysUseContainerInfo?: boolean; - readonly order?: number; - readonly focusCommand?: { id: string, keybindings?: IKeybindings }; readonly viewOrderDelegate?: ViewOrderDelegate; @@ -60,6 +59,8 @@ export interface IViewContainerDescriptor { readonly extensionId?: ExtensionIdentifier; readonly rejectAddedViews?: boolean; + + order?: number; } export interface IViewContainersRegistry { @@ -236,6 +237,11 @@ export interface IAddedViewDescriptorRef extends IViewDescriptorRef { size?: number; } +export interface IAddedViewDescriptorState { + viewDescriptor: IViewDescriptor, + collapsed?: boolean; +} + export interface IViewContainerModel { readonly title: string; @@ -509,7 +515,7 @@ export interface IViewDescriptorService { getViewContainerModel(viewContainer: ViewContainer): IViewContainerModel; readonly onDidChangeContainerLocation: Event<{ viewContainer: ViewContainer, from: ViewContainerLocation, to: ViewContainerLocation }>; - moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation): void; + moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, order?: number): void; // Views getViewDescriptorById(id: string): IViewDescriptor | null; @@ -634,6 +640,8 @@ export interface ITreeItem { command?: Command; children?: ITreeItem[]; + + accessibilityInformation?: IAccessibilityInformation; } export interface ITreeViewDataProvider { diff --git a/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts similarity index 96% rename from src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts rename to src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts index 9868c6285fe..43a574d8bf4 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backup.contribution.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backup.contribution.ts @@ -6,7 +6,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; -import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; +import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; // Register Backup Tracker Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NativeBackupTracker, LifecyclePhase.Starting); diff --git a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts similarity index 99% rename from src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts rename to src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts index 3048c945dc6..8585de7a240 100644 --- a/src/vs/workbench/contrib/backup/electron-browser/backupTracker.ts +++ b/src/vs/workbench/contrib/backup/electron-sandbox/backupTracker.ts @@ -14,7 +14,7 @@ import Severity from 'vs/base/common/severity'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { isMacintosh } from 'vs/base/common/platform'; import { HotExitConfiguration } from 'vs/platform/files/common/files'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { ILogService } from 'vs/platform/log/common/log'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts index cab5dd56e4b..68c8751e028 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts @@ -13,7 +13,7 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { DefaultEndOfLine } from 'vs/editor/common/model'; import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; -import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; +import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; import { workbenchInstantiationService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts index ea8c85ea656..485390ddab3 100644 --- a/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupTracker.test.ts @@ -11,7 +11,7 @@ import * as pfs from 'vs/base/node/pfs'; import { URI } from 'vs/base/common/uri'; import { getRandomTestPath } from 'vs/base/test/node/testUtils'; import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; -import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-browser/backupTracker'; +import { NativeBackupTracker } from 'vs/workbench/contrib/backup/electron-sandbox/backupTracker'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; @@ -34,7 +34,7 @@ import { HotExitConfiguration } from 'vs/platform/files/common/files'; import { ShutdownReason, ILifecycleService, BeforeShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; import { IFileDialogService, ConfirmResult, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, Workspace } from 'vs/platform/workspace/common/workspace'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { BackupTracker } from 'vs/workbench/contrib/backup/common/backupTracker'; import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts index 4a9b99b6971..ce447754ab1 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts @@ -20,6 +20,7 @@ import { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; import { ConflictDetector } from 'vs/workbench/services/bulkEdit/browser/conflicts'; import { ResourceMap } from 'vs/base/common/map'; import { localize } from 'vs/nls'; +import { extUri } from 'vs/base/common/resources'; export class CheckedStates { @@ -209,13 +210,13 @@ export class BulkFileOperations { } const insert = (uri: URI, map: Map) => { - let key = uri.toString(); + let key = extUri.getComparisonKey(uri, true); let operation = map.get(key); // rename if (!operation && newToOldUri.has(uri)) { uri = newToOldUri.get(uri)!; - key = uri.toString(); + key = extUri.getComparisonKey(uri, true); operation = map.get(key); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts index 1d61fbdff5a..433c5d7f663 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts @@ -352,6 +352,10 @@ export abstract class SimpleFindReplaceWidget extends Widget { }, 0); } + public focus(): void { + this._findInput.focus(); + } + public show(initialInput?: string): void { if (initialInput && !this._isVisible) { this._findInput.setValue(initialInput); @@ -363,6 +367,8 @@ export abstract class SimpleFindReplaceWidget extends Widget { dom.addClass(this._domNode, 'visible'); dom.addClass(this._domNode, 'visible-transition'); this._domNode.setAttribute('aria-hidden', 'false'); + + this.focus(); }, 0); } diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts index f4c36cd032c..ef2018bcd53 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts @@ -47,31 +47,31 @@ export class ToggleColumnSelectionAction extends Action { if (!codeEditor || codeEditor !== this._getCodeEditor() || oldValue === newValue || !codeEditor.hasModel()) { return; } - const cursors = codeEditor._getCursors(); + const viewModel = codeEditor._getViewModel(); if (codeEditor.getOption(EditorOption.columnSelection)) { const selection = codeEditor.getSelection(); const modelSelectionStart = new Position(selection.selectionStartLineNumber, selection.selectionStartColumn); - const viewSelectionStart = cursors.context.coordinatesConverter.convertModelPositionToViewPosition(modelSelectionStart); + const viewSelectionStart = viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelSelectionStart); const modelPosition = new Position(selection.positionLineNumber, selection.positionColumn); - const viewPosition = cursors.context.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); + const viewPosition = viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); - CoreNavigationCommands.MoveTo.runCoreEditorCommand(cursors, { + CoreNavigationCommands.MoveTo.runCoreEditorCommand(viewModel, { position: modelSelectionStart, viewPosition: viewSelectionStart }); - const visibleColumn = CursorColumns.visibleColumnFromColumn2(cursors.context.config, cursors.context.viewModel, viewPosition); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursors, { + const visibleColumn = CursorColumns.visibleColumnFromColumn2(viewModel.cursorConfig, viewModel, viewPosition); + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { position: modelPosition, viewPosition: viewPosition, doColumnSelect: true, mouseColumn: visibleColumn + 1 }); } else { - const columnSelectData = cursors.getColumnSelectData(); - const fromViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.fromViewLineNumber, columnSelectData.fromViewVisualColumn); - const fromPosition = cursors.context.coordinatesConverter.convertViewPositionToModelPosition(new Position(columnSelectData.fromViewLineNumber, fromViewColumn)); - const toViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.toViewLineNumber, columnSelectData.toViewVisualColumn); - const toPosition = cursors.context.coordinatesConverter.convertViewPositionToModelPosition(new Position(columnSelectData.toViewLineNumber, toViewColumn)); + const columnSelectData = viewModel.getCursorColumnSelectData(); + const fromViewColumn = CursorColumns.columnFromVisibleColumn2(viewModel.cursorConfig, viewModel, columnSelectData.fromViewLineNumber, columnSelectData.fromViewVisualColumn); + const fromPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(columnSelectData.fromViewLineNumber, fromViewColumn)); + const toViewColumn = CursorColumns.columnFromVisibleColumn2(viewModel.cursorConfig, viewModel, columnSelectData.toViewLineNumber, columnSelectData.toViewVisualColumn); + const toPosition = viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(columnSelectData.toViewLineNumber, toViewColumn)); codeEditor.setSelection(new Selection(fromPosition.lineNumber, fromPosition.column, toPosition.lineNumber, toPosition.column)); } diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/codeEditor.contribution.ts index 21de3dba004..22513f6ef6c 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/codeEditor.contribution.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/codeEditor.contribution.ts @@ -3,7 +3,4 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './inputClipboardActions'; -import './sleepResumeRepaintMinimap'; -import './selectionClipboard'; import './startDebugTextMate'; diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/startDebugTextMate.ts b/src/vs/workbench/contrib/codeEditor/electron-browser/startDebugTextMate.ts index b6cc9a28663..b2fb6d834d2 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/startDebugTextMate.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-browser/startDebugTextMate.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as os from 'os'; -import * as path from 'path'; import * as nls from 'vs/nls'; import { Range } from 'vs/editor/common/core/range'; @@ -22,6 +21,7 @@ import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService import { ITextModel } from 'vs/editor/common/model'; import { Constants } from 'vs/base/common/uint'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { join } from 'vs/base/common/path'; class StartDebugTextMate extends Action { @@ -59,7 +59,7 @@ class StartDebugTextMate extends Action { } public async run(): Promise { - const pathInTemp = path.join(os.tmpdir(), `vcode-tm-log-${generateUuid()}.txt`); + const pathInTemp = join(os.tmpdir(), `vcode-tm-log-${generateUuid()}.txt`); const logger = createRotatingLogger(`tm-log`, pathInTemp, 1024 * 1024 * 30, 1); const model = this._getOrCreateModel(); const append = (str: string) => { diff --git a/src/vs/workbench/contrib/codeEditor/electron-sandbox/codeEditor.contribution.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/codeEditor.contribution.ts new file mode 100644 index 00000000000..08763754187 --- /dev/null +++ b/src/vs/workbench/contrib/codeEditor/electron-sandbox/codeEditor.contribution.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './inputClipboardActions'; +import './selectionClipboard'; +import './sleepResumeRepaintMinimap'; diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/inputClipboardActions.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/inputClipboardActions.ts similarity index 100% rename from src/vs/workbench/contrib/codeEditor/electron-browser/inputClipboardActions.ts rename to src/vs/workbench/contrib/codeEditor/electron-sandbox/inputClipboardActions.ts diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts similarity index 100% rename from src/vs/workbench/contrib/codeEditor/electron-browser/selectionClipboard.ts rename to src/vs/workbench/contrib/codeEditor/electron-sandbox/selectionClipboard.ts diff --git a/src/vs/workbench/contrib/codeEditor/electron-browser/sleepResumeRepaintMinimap.ts b/src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts similarity index 90% rename from src/vs/workbench/contrib/codeEditor/electron-browser/sleepResumeRepaintMinimap.ts rename to src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts index 7ade76dcb8d..36e1dba73ac 100644 --- a/src/vs/workbench/contrib/codeEditor/electron-browser/sleepResumeRepaintMinimap.ts +++ b/src/vs/workbench/contrib/codeEditor/electron-sandbox/sleepResumeRepaintMinimap.ts @@ -6,7 +6,7 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { ipcRenderer as ipc } from 'electron'; +import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; class SleepResumeRepaintMinimap implements IWorkbenchContribution { @@ -14,7 +14,7 @@ class SleepResumeRepaintMinimap implements IWorkbenchContribution { constructor( @ICodeEditorService codeEditorService: ICodeEditorService ) { - ipc.on('vscode:osResume', () => { + ipcRenderer.on('vscode:osResume', () => { codeEditorService.listCodeEditors().forEach(editor => editor.render(true)); }); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index 2bfbb0590ef..b33a1aa5618 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts @@ -97,9 +97,8 @@ export class CustomEditorInputFactory extends WebviewEditorInputFactory { const webview = webviewService.createWebviewOverlay(data.id, { enableFindWidget: data.options.enableFindWidget, retainContextWhenHidden: data.options.retainContextWhenHidden - }, data.options); + }, data.options, data.extension); webview.state = data.state; - webview.extension = data.extension; return webview; }); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index 95fecef9b18..fd4a1c01ab4 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -17,9 +17,9 @@ import { EditorActivation, IEditorOptions, ITextEditorOptions } from 'vs/platfor import { FileOperation, IFileService } from 'vs/platform/files/common/files'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IStorageService } from 'vs/platform/storage/common/storage'; import * as colorRegistry from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput, EditorOptions, GroupIdentifier, IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -37,7 +37,7 @@ import { CustomEditorInput } from './customEditorInput'; export class CustomEditorService extends Disposable implements ICustomEditorService, ICustomEditorViewTypesHandler { _serviceBrand: any; - private readonly _contributedEditors = this._register(new ContributedCustomEditors()); + private readonly _contributedEditors: ContributedCustomEditors; private readonly _editorCapabilities = new Map(); private readonly _models = new CustomEditorModelManager(); @@ -51,6 +51,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ constructor( @IContextKeyService contextKeyService: IContextKeyService, @IFileService fileService: IFileService, + @IStorageService storageService: IStorageService, @IConfigurationService private readonly configurationService: IConfigurationService, @IEditorService private readonly editorService: IEditorService, @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @@ -64,11 +65,13 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ this._focusedCustomEditorIsEditable = CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE.bindTo(contextKeyService); this._webviewHasOwnEditFunctions = webviewHasOwnEditFunctionsContext.bindTo(contextKeyService); - this._register(this.editorService.registerCustomEditorViewTypesHandler('Custom Editor', this)); + + this._contributedEditors = this._register(new ContributedCustomEditors(storageService)); this._register(this._contributedEditors.onChange(() => { this.updateContexts(); this._onDidChangeViewTypes.fire(); })); + this._register(this.editorService.registerCustomEditorViewTypesHandler('Custom Editor', this)); this._register(this.editorService.onDidActiveEditorChange(() => this.updateContexts())); this._register(fileService.onDidRunOperation(e => { @@ -231,7 +234,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ const id = generateUuid(); const webview = new Lazy(() => { - return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {}); + return this.webviewService.createWebviewOverlay(id, { customClasses: options?.customClasses }, {}, undefined); }); const input = this.instantiationService.createInstance(CustomEditorInput, resource, viewType, id, webview, {}); if (typeof group !== 'undefined') { @@ -418,13 +421,13 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ export class CustomEditorContribution extends Disposable implements IWorkbenchContribution { constructor( - @IEditorService private readonly editorService: EditorServiceImpl, + @IEditorService private readonly editorService: IEditorService, @ICustomEditorService private readonly customEditorService: ICustomEditorService, ) { super(); this._register(this.editorService.overrideOpenEditor({ - open: (editor, options, group, id) => { + open: (editor, options, group, context, id) => { return this.onEditorOpening(editor, options, group, id); }, getEditorOverrides: (resource: URI, _options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[] => { diff --git a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts index b00fa6c5f63..7633e95e174 100644 --- a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts +++ b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts @@ -8,9 +8,12 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { CustomEditorInfo, CustomEditorPriority } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { Memento } from 'vs/workbench/common/memento'; +import { CustomEditorDescriptor, CustomEditorInfo, CustomEditorPriority } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { customEditorsExtensionPoint, ICustomEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/common/extensionPoint'; import { DEFAULT_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; +import { IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in"); @@ -26,32 +29,52 @@ export const defaultCustomEditor = new CustomEditorInfo({ export class ContributedCustomEditors extends Disposable { - private readonly _editors = new Map(); + private static readonly CUSTOM_EDITORS_STORAGE_ID = 'customEditors'; + private static readonly CUSTOM_EDITORS_ENTRY_ID = 'editors'; - constructor() { + private readonly _editors = new Map(); + private readonly _memento: Memento; + + constructor(storageService: IStorageService) { super(); - customEditorsExtensionPoint.setHandler(extensions => { - this._editors.clear(); + this._memento = new Memento(ContributedCustomEditors.CUSTOM_EDITORS_STORAGE_ID, storageService); - for (const extension of extensions) { - for (const webviewEditorContribution of extension.value) { - this.add(new CustomEditorInfo({ - id: webviewEditorContribution.viewType, - displayName: webviewEditorContribution.displayName, - providerDisplayName: extension.description.isBuiltin ? builtinProviderDisplayName : extension.description.displayName || extension.description.identifier.value, - selector: webviewEditorContribution.selector || [], - priority: getPriorityFromContribution(webviewEditorContribution, extension.description), - })); - } - } - this._onChange.fire(); + const mementoObject = this._memento.getMemento(StorageScope.GLOBAL); + for (const info of (mementoObject[ContributedCustomEditors.CUSTOM_EDITORS_ENTRY_ID] || []) as CustomEditorDescriptor[]) { + this.add(new CustomEditorInfo(info)); + } + + customEditorsExtensionPoint.setHandler(extensions => { + this.update(extensions); }); } private readonly _onChange = this._register(new Emitter()); public readonly onChange = this._onChange.event; + private update(extensions: readonly IExtensionPointUser[]) { + this._editors.clear(); + + for (const extension of extensions) { + for (const webviewEditorContribution of extension.value) { + this.add(new CustomEditorInfo({ + id: webviewEditorContribution.viewType, + displayName: webviewEditorContribution.displayName, + providerDisplayName: extension.description.isBuiltin ? builtinProviderDisplayName : extension.description.displayName || extension.description.identifier.value, + selector: webviewEditorContribution.selector || [], + priority: getPriorityFromContribution(webviewEditorContribution, extension.description), + })); + } + } + + const mementoObject = this._memento.getMemento(StorageScope.GLOBAL); + mementoObject[ContributedCustomEditors.CUSTOM_EDITORS_ENTRY_ID] = Array.from(this._editors.values()); + this._memento.saveMemento(); + + this._onChange.fire(); + } + public [Symbol.iterator](): Iterator { return this._editors.values(); } diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index e914afd609f..cbe489306b9 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -79,7 +79,15 @@ export interface CustomEditorSelector { readonly filenamePattern?: string; } -export class CustomEditorInfo { +export interface CustomEditorDescriptor { + readonly id: string; + readonly displayName: string; + readonly providerDisplayName: string; + readonly priority: CustomEditorPriority; + readonly selector: readonly CustomEditorSelector[]; +} + +export class CustomEditorInfo implements CustomEditorDescriptor { public readonly id: string; public readonly displayName: string; @@ -87,13 +95,7 @@ export class CustomEditorInfo { public readonly priority: CustomEditorPriority; public readonly selector: readonly CustomEditorSelector[]; - constructor(descriptor: { - readonly id: string; - readonly displayName: string; - readonly providerDisplayName: string; - readonly priority: CustomEditorPriority; - readonly selector: readonly CustomEditorSelector[]; - }) { + constructor(descriptor: CustomEditorDescriptor) { this.id = descriptor.id; this.displayName = descriptor.displayName; this.providerDisplayName = descriptor.providerDisplayName; diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index f89ac5a0857..fc20b6d386c 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -6,6 +6,7 @@ import 'vs/css!./media/debug.contribution'; import 'vs/css!./media/debugHover'; import * as nls from 'vs/nls'; +import { Color } from 'vs/base/common/color'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -37,14 +38,14 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { LoadedScriptsView } from 'vs/workbench/contrib/debug/browser/loadedScriptsView'; -import { TOGGLE_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; +import { ADD_LOG_POINT_ID, TOGGLE_CONDITIONAL_BREAKPOINT_ID, TOGGLE_BREAKPOINT_ID, RunToCursorAction } from 'vs/workbench/contrib/debug/browser/debugEditorActions'; import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchExpressionsView'; import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView'; import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; import { WelcomeView } from 'vs/workbench/contrib/debug/browser/welcomeView'; import { ThemeIcon, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { registerColor, foreground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; +import { registerColor, foreground, badgeBackground, badgeForeground, listDeemphasizedForeground, contrastBorder, inputBorder, editorWarningForeground, errorForeground, editorInfoForeground } from 'vs/platform/theme/common/colorRegistry'; import { DebugViewPaneContainer, OpenDebugConsoleAction } from 'vs/workbench/contrib/debug/browser/debugViewlet'; import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { CallStackEditorContribution } from 'vs/workbench/contrib/debug/browser/callStackEditorContribution'; @@ -55,6 +56,7 @@ import { IQuickAccessRegistry, Extensions as QuickAccessExtensions } from 'vs/pl import { StartDebugQuickAccessProvider } from 'vs/workbench/contrib/debug/browser/debugQuickAccess'; import { DebugProgressContribution } from 'vs/workbench/contrib/debug/browser/debugProgress'; import { DebugTitleContribution } from 'vs/workbench/contrib/debug/browser/debugTitle'; +import { Codicon } from 'vs/base/common/codicons'; class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; @@ -75,7 +77,7 @@ const viewContainer = Registry.as(ViewExtensions.ViewCo id: VIEWLET_ID, name: nls.localize('run', "Run"), ctorDescriptor: new SyncDescriptor(DebugViewPaneContainer), - icon: 'codicon-debug-alt-2', + icon: Codicon.debugAlt.classNames, alwaysUseContainerInfo: true, order: 2 }, ViewContainerLocation.Sidebar); @@ -92,21 +94,21 @@ const openPanelKb: IKeybindings = { const VIEW_CONTAINER: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ id: DEBUG_PANEL_ID, name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), - icon: 'codicon-debug-console', + icon: Codicon.debugConsole.classNames, ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [DEBUG_PANEL_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]), storageId: DEBUG_PANEL_ID, focusCommand: { id: OpenDebugConsoleAction.ID, keybindings: openPanelKb }, - order: 3, + order: 2, hideIfEmpty: true }, ViewContainerLocation.Panel); Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ id: REPL_VIEW_ID, name: nls.localize({ comment: ['Debug is a noun in this context, not a verb.'], key: 'debugPanel' }, 'Debug Console'), - containerIcon: 'codicon-debug-console', + containerIcon: Codicon.debugConsole.classNames, canToggleVisibility: false, canMoveView: true, ctorDescriptor: new SyncDescriptor(Repl), @@ -114,12 +116,12 @@ Registry.as(ViewExtensions.ViewsRegistry).registerViews([{ // Register default debug views const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); -viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), containerIcon: 'codicon-debug-alt-2', ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), containerIcon: 'codicon-debug-alt-2', ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), containerIcon: 'codicon-debug-alt-2', ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); -viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), containerIcon: 'codicon-debug-alt-2', ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); -viewsRegistry.registerViews([{ id: WelcomeView.ID, name: WelcomeView.LABEL, containerIcon: 'codicon-debug-alt-2', ctorDescriptor: new SyncDescriptor(WelcomeView), order: 1, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); -viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), containerIcon: 'codicon-debug-alt-2', ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); +viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(VariablesView), order: 10, weight: 40, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(WatchExpressionsView), order: 20, weight: 10, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(CallStackView), order: 30, weight: 30, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(BreakpointsView), order: 40, weight: 20, canToggleVisibility: true, canMoveView: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: ContextKeyExpr.or(CONTEXT_BREAKPOINTS_EXIST, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); +viewsRegistry.registerViews([{ id: WelcomeView.ID, name: WelcomeView.LABEL, containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(WelcomeView), order: 1, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); +viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), containerIcon: Codicon.debugAlt.classNames, ctorDescriptor: new SyncDescriptor(LoadedScriptsView), order: 35, weight: 5, canToggleVisibility: true, canMoveView: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); registerCommands(); @@ -512,7 +514,7 @@ MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { MenuRegistry.appendMenuItem(MenuId.MenubarNewBreakpointMenu, { group: '1_breakpoints', command: { - id: TOGGLE_LOG_POINT_ID, + id: ADD_LOG_POINT_ID, title: nls.localize({ key: 'miLogPoint', comment: ['&& denotes a mnemonic'] }, "&&Logpoint...") }, order: 4 @@ -599,12 +601,18 @@ const debugTokenExpressionBoolean = registerColor('debugTokenExpression.boolean' const debugTokenExpressionNumber = registerColor('debugTokenExpression.number', { dark: '#b5cea8', light: '#098658', hc: '#89d185' }, 'Foreground color for numbers in the debug views (ie. the Variables or Watch view).'); const debugTokenExpressionError = registerColor('debugTokenExpression.error', { dark: '#f48771', light: '#e51400', hc: '#f48771' }, 'Foreground color for expression errors in the debug views (ie. the Variables or Watch view) and for error logs shown in the debug console.'); -const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); +const debugViewExceptionLabelForeground = registerColor('debugView.exceptionLabelForeground', { dark: foreground, light: '#FFF', hc: foreground }, 'Foreground color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); const debugViewExceptionLabelBackground = registerColor('debugView.exceptionLabelBackground', { dark: '#6C2022', light: '#A31515', hc: '#6C2022' }, 'Background color for a label shown in the CALL STACK view when the debugger breaks on an exception.'); const debugViewStateLabelForeground = registerColor('debugView.stateLabelForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); const debugViewStateLabelBackground = registerColor('debugView.stateLabelBackground', { dark: '#88888844', light: '#88888844', hc: '#88888844' }, 'Background color for a label in the CALL STACK view showing the current session\'s or thread\'s state.'); const debugViewValueChangedHighlight = registerColor('debugView.valueChangedHighlight', { dark: '#569CD6', light: '#569CD6', hc: '#569CD6' }, 'Color used to highlight value changes in the debug views (ie. in the Variables view).'); +const debugConsoleInfoForeground = registerColor('debugConsole.infoForeground', { dark: editorInfoForeground, light: editorInfoForeground, hc: foreground }, 'Foreground color for info messages in debug REPL console.'); +const debugConsoleWarningForeground = registerColor('debugConsole.warningForeground', { dark: editorWarningForeground, light: editorWarningForeground, hc: '#008000' }, 'Foreground color for warning messages in debug REPL console.'); +const debugConsoleErrorForeground = registerColor('debugConsole.errorForeground', { dark: errorForeground, light: errorForeground, hc: errorForeground }, 'Foreground color for error messages in debug REPL console.'); +const debugConsoleSourceForeground = registerColor('debugConsole.sourceForeground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for source filenames in debug REPL console.'); +const debugConsoleInputIconForeground = registerColor('debugConsoleInputIcon.foreground', { dark: foreground, light: foreground, hc: foreground }, 'Foreground color for debug console input marker icon.'); + registerThemingParticipant((theme, collector) => { // All these colours provide a default value so they will never be undefined, hence the `!` const badgeBackgroundColor = theme.getColor(badgeBackground)!; @@ -714,4 +722,53 @@ registerThemingParticipant((theme, collector) => { color: ${tokenNumberColor}; } `); + + const debugConsoleInputBorderColor = theme.getColor(inputBorder) || Color.fromHex('#80808060'); + const debugConsoleInfoForegroundColor = theme.getColor(debugConsoleInfoForeground)!; + const debugConsoleWarningForegroundColor = theme.getColor(debugConsoleWarningForeground)!; + const debugConsoleErrorForegroundColor = theme.getColor(debugConsoleErrorForeground)!; + const debugConsoleSourceForegroundColor = theme.getColor(debugConsoleSourceForeground)!; + const debugConsoleInputIconForegroundColor = theme.getColor(debugConsoleInputIconForeground)!; + + collector.addRule(` + .repl .repl-input-wrapper { + border-top: 1px solid ${debugConsoleInputBorderColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .value.info { + color: ${debugConsoleInfoForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .value.warn { + color: ${debugConsoleWarningForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .value.error { + color: ${debugConsoleErrorForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .output .expression .source { + color: ${debugConsoleSourceForegroundColor}; + } + + .monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { + color: ${debugConsoleInputIconForegroundColor}; + } + `); + + if (!theme.defines(debugConsoleInputIconForeground)) { + collector.addRule(` + .monaco-workbench.vs .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 0.25; + } + + .monaco-workbench.vs-dark .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 0.4; + } + + .monaco-workbench.hc-black .repl .repl-tree .monaco-tl-contents .arrow { + opacity: 1; + } + `); + } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index c660175a3de..3a58d54b58c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -125,7 +125,6 @@ export class StartDebugActionViewItem implements IActionViewItem { selectBoxContainer.style.borderLeft = colors.selectBorder ? `1px solid ${colors.selectBorder}` : ''; const selectBackgroundColor = colors.selectBackground ? `${colors.selectBackground}` : ''; this.container.style.backgroundColor = selectBackgroundColor; - this.start.style.backgroundColor = selectBackgroundColor; })); this.debugService.getConfigurationManager().getDynamicProviders().then(providers => { this.providers = providers; diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts index 9aeef0effac..98be9ee05bb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActions.ts @@ -8,7 +8,7 @@ import { Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Breakpoint, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { Variable, Breakpoint, FunctionBreakpoint, Expression } from 'vs/workbench/contrib/debug/common/debugModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; @@ -142,7 +142,10 @@ export class StartAction extends AbstractDebugAction { if (debugService.state === State.Initializing) { return false; } - if ((sessions.length > 0) && !debugService.getConfigurationManager().selectedConfiguration.name) { + let { name, config } = debugService.getConfigurationManager().selectedConfiguration; + let nameToStart = name || config?.name; + + if (sessions.some(s => s.configuration.name === nameToStart)) { // There is already a debug session running and we do not have any launch configuration selected return false; } @@ -380,12 +383,12 @@ export class CopyValueAction extends Action { static readonly LABEL = nls.localize('copyValue', "Copy Value"); constructor( - id: string, label: string, private value: Variable | string, private context: string, + id: string, label: string, private value: Variable | Expression, private context: string, @IDebugService private readonly debugService: IDebugService, @IClipboardService private readonly clipboardService: IClipboardService ) { - super(id, label, 'debug-action copy-value'); - this._enabled = typeof this.value === 'string' || (this.value instanceof Variable && !!this.value.evaluateName); + super(id, label); + this._enabled = (this.value instanceof Expression) || (this.value instanceof Variable && !!this.value.evaluateName); } async run(): Promise { @@ -396,7 +399,7 @@ export class CopyValueAction extends Action { } const context = session.capabilities.supportsClipboardContext ? 'clipboard' : this.context; - const toEvaluate = typeof this.value === 'string' ? this.value : this.value.evaluateName || this.value.value; + const toEvaluate = this.value instanceof Variable ? (this.value.evaluateName || this.value.value) : this.value.name; try { const evaluation = await session.evaluate(toEvaluate, stackFrame.frameId, context); diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index 59a632fc9d5..11420b45968 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -81,12 +81,12 @@ class ConditionalBreakpointAction extends EditorAction { } } -export const TOGGLE_LOG_POINT_ID = 'editor.debug.action.toggleLogPoint'; +export const ADD_LOG_POINT_ID = 'editor.debug.action.addLogPoint'; class LogPointAction extends EditorAction { constructor() { super({ - id: TOGGLE_LOG_POINT_ID, + id: ADD_LOG_POINT_ID, label: nls.localize('logPointEditorAction', "Debug: Add Logpoint..."), alias: 'Debug: Add Logpoint...', precondition: undefined diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 754fc963e34..72a879b493e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugHover.ts +++ b/src/vs/workbench/contrib/debug/browser/debugHover.ts @@ -341,7 +341,7 @@ class DebugHoverAccessibilityProvider implements IListAccessibilityProvider 0) { - picks.push({ type: 'separator', label: localize('contributed', "contributed") }); + picks.push({ + type: 'separator', label: localize({ + key: 'contributed', + comment: ['contributed is lower case because it looks better like that in UI. Nothing preceeds it. It is a name of the grouping of debug configurations.'] + }, "contributed") + }); } dynamicProviders.forEach(provider => { picks.push({ label: `$(folder) ${provider.label}...`, - ariaLabel: localize('providerAriaLabel', "{0} contributed configurations", provider.label), + ariaLabel: localize({ key: 'providerAriaLabel', comment: ['Placeholder stands for the provider label. For example "NodeJS".'] }, "{0} contributed configurations", provider.label), accept: async () => { const pick = await provider.pick(); if (pick) { diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 203c7f85a30..d041fc8869e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -46,6 +46,7 @@ import { IViewsService } from 'vs/workbench/common/views'; import { generateUuid } from 'vs/base/common/uuid'; 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'; export class DebugService implements IDebugService { _serviceBrand: undefined; @@ -305,6 +306,9 @@ export class DebugService implements IDebugService { return false; } } + if (compound.stopAll) { + options = { ...options, compoundRoot: new DebugCompoundRoot() }; + } const values = await Promise.all(compound.configurations.map(configData => { const name = typeof configData === 'string' ? configData : configData.name; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index d3318677eff..8794d1eb7d4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -98,6 +98,11 @@ export class DebugSession implements IDebugSession { dispose(toDispose); })); } + + const compoundRoot = this._options.compoundRoot; + if (compoundRoot) { + toDispose.push(compoundRoot.onDidSessionStop(() => this.terminate())); + } } getId(): string { @@ -280,6 +285,10 @@ export class DebugSession implements IDebugSession { } else { await this.raw.disconnect(restart); } + + if (!restart) { + this._options.compoundRoot?.sessionStopped(); + } } /** @@ -292,6 +301,10 @@ export class DebugSession implements IDebugSession { this.cancelAllRequests(); await this.raw.disconnect(restart); + + if (!restart) { + this._options.compoundRoot?.sessionStopped(); + } } /** diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index ef17521522d..1970134e9c3 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -41,11 +41,6 @@ .monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { position:absolute; left: 2px; - opacity: 0.25; -} - -.monaco-workbench.vs-dark .repl .repl-tree .monaco-tl-contents .arrow { - opacity: 0.4; } .monaco-workbench .repl .repl-tree .output.expression.value-and-source .source { @@ -88,18 +83,6 @@ font-style: italic; } -.monaco-workbench.vs .repl .repl-tree .output.expression > .warn { - color: #cd9731; -} - -.monaco-workbench.vs-dark .repl .repl-tree .output.expression > .warn { - color: #cd9731; -} - -.monaco-workbench.hc-black .repl .repl-tree .output.expression > .warn { - color: #008000; -} - /* ANSI Codes */ .monaco-workbench .repl .repl-tree .output.expression .code-bold { font-weight: bold; } .monaco-workbench .repl .repl-tree .output.expression .code-italic { font-style: italic; } diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index 2bed66ee1d4..abc3e70d141 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -5,7 +5,6 @@ import 'vs/css!./media/repl'; import { URI as uri } from 'vs/base/common/uri'; -import { Color } from 'vs/base/common/color'; import { IAction, IActionViewItem, Action } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; @@ -21,7 +20,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { memoize } from 'vs/base/common/decorators'; import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; @@ -34,7 +33,7 @@ import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/ import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { getSimpleEditorOptions, getSimpleCodeEditorWidgetOptions } from 'vs/workbench/contrib/codeEditor/browser/simpleEditorOptions'; import { IDecorationOptions } from 'vs/editor/common/editorCommon'; -import { transparent, editorForeground, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { transparent, editorForeground } from 'vs/platform/theme/common/colorRegistry'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/debugActionViewItems'; import { CompletionContext, CompletionList, CompletionProviderRegistry, CompletionItem, completionKindFromString, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; @@ -815,13 +814,3 @@ export class ClearReplAction extends Action { function getReplView(viewsService: IViewsService): Repl | undefined { return viewsService.getActiveViewWithId(REPL_VIEW_ID) as Repl ?? undefined; } - -registerThemingParticipant((theme, collector) => { - const inputBorderColor = theme.getColor(inputBorder) || Color.fromHex('#80808060'); - - collector.addRule(` - .repl .repl-input-wrapper { - border-top: 1px solid ${inputBorderColor}; - } - `); -}); diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 25337d5249a..3284b8caa70 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -23,7 +23,6 @@ import { IReplElementSource, IDebugService, IExpression, IReplElement, IDebugCon import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { localize } from 'vs/nls'; -import { Codicon } from 'vs/base/common/codicons'; const $ = dom.$; @@ -37,7 +36,6 @@ interface IReplGroupTemplateData { interface IReplEvaluationResultTemplateData { value: HTMLElement; - annotation: HTMLElement; } interface ISimpleReplElementTemplateData { @@ -53,7 +51,6 @@ interface IRawObjectReplTemplateData { expression: HTMLElement; name: HTMLElement; value: HTMLElement; - annotation: HTMLElement; label: HighlightedLabel; } @@ -116,9 +113,8 @@ export class ReplEvaluationResultsRenderer implements ITreeRenderer, index: number, templateData: IReplEvaluationResultTemplateData): void { @@ -128,10 +124,6 @@ export class ReplEvaluationResultsRenderer implements ITreeRenderer, index: number, templateData: IRawObjectReplTemplateData): void { @@ -260,15 +251,6 @@ export class ReplRawObjectsRenderer implements ITreeRenderer { diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 31129e574cd..997e8f4df84 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,6 +24,7 @@ import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService' import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes'; +import { DebugCompoundRoot } from 'vs/workbench/contrib/debug/common/debugCompoundRoot'; export const VIEWLET_ID = 'workbench.view.debug'; @@ -155,6 +156,7 @@ export interface IDebugSessionOptions { noDebug?: boolean; parentSession?: IDebugSession; repl?: IDebugSessionReplMode; + compoundRoot?: DebugCompoundRoot; } export interface IDebugSession extends ITreeElement { @@ -518,6 +520,7 @@ export interface IConfig extends IEnvConfig { export interface ICompound { name: string; + stopAll?: boolean; preLaunchTask?: string | TaskIdentifier; configurations: (string | { name: string, folder: string })[]; presentation?: IConfigPresentation; diff --git a/src/vs/workbench/contrib/debug/common/debugCompoundRoot.ts b/src/vs/workbench/contrib/debug/common/debugCompoundRoot.ts new file mode 100644 index 00000000000..adbf292db5c --- /dev/null +++ b/src/vs/workbench/contrib/debug/common/debugCompoundRoot.ts @@ -0,0 +1,20 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +export class DebugCompoundRoot { + private stopped = false; + private stopEmitter = new Emitter(); + + onDidSessionStop = this.stopEmitter.event; + + sessionStopped(): void { + if (!this.stopped) { // avoid sending extranous terminate events + this.stopped = true; + this.stopEmitter.fire(); + } + } +} diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index 77f2180954a..d2e735701be 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -215,6 +215,11 @@ export const launchSchema: IJSONSchema = { }, description: nls.localize('app.launch.json.compounds.configurations', "Names of configurations that will be started as part of this compound.") }, + stopAll: { + type: 'boolean', + default: false, + description: nls.localize('app.launch.json.compound.stopAll', "Controls whether manually terminating one session will stop all of the compound sessions.") + }, preLaunchTask: { type: 'string', default: '', diff --git a/src/vs/workbench/contrib/debug/electron-browser/extensionHostDebugService.ts b/src/vs/workbench/contrib/debug/electron-sandbox/extensionHostDebugService.ts similarity index 91% rename from src/vs/workbench/contrib/debug/electron-browser/extensionHostDebugService.ts rename to src/vs/workbench/contrib/debug/electron-sandbox/extensionHostDebugService.ts index ce00abedcab..5b504f89cbf 100644 --- a/src/vs/workbench/contrib/debug/electron-browser/extensionHostDebugService.ts +++ b/src/vs/workbench/contrib/debug/electron-sandbox/extensionHostDebugService.ts @@ -5,7 +5,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug'; -import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService'; +import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService'; import { ExtensionHostDebugChannelClient, ExtensionHostDebugBroadcastChannel } from 'vs/platform/debug/common/extensionHostDebugIpc'; export class ExtensionHostDebugService extends ExtensionHostDebugChannelClient { diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 9abe1a5cb22..719360dd924 100644 --- a/src/vs/workbench/contrib/debug/node/terminals.ts +++ b/src/vs/workbench/contrib/debug/node/terminals.ts @@ -164,7 +164,7 @@ export function prepareCommand(args: DebugProtocol.RunInTerminalRequestArguments case ShellType.bash: quote = (s: string) => { - s = s.replace(/(["'\\])/g, '\\$1'); + s = s.replace(/(["'\\\$])/g, '\\$1'); return (s.indexOf(' ') >= 0 || s.indexOf(';') >= 0 || s.length === 0) ? `"${s}"` : s; }; diff --git a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts index 54d67cfe69c..a69f5495dc4 100644 --- a/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts +++ b/src/vs/workbench/contrib/experiments/test/electron-browser/experimentService.test.ts @@ -15,7 +15,7 @@ import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/exte import { ExtensionManagementService } from 'vs/platform/extensionManagement/node/extensionManagementService'; import { Emitter } from 'vs/base/common/event'; import { TestExtensionEnablementService } from 'vs/workbench/services/extensionManagement/test/browser/extensionEnablementService.test'; -import { URLService } from 'vs/platform/url/node/urlService'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { IURLService } from 'vs/platform/url/common/url'; import { ITelemetryService, lastSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; @@ -89,7 +89,7 @@ suite('Experiment Service', () => { instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.stub(ITelemetryService, NullTelemetryService); - instantiationService.stub(IURLService, URLService); + instantiationService.stub(IURLService, NativeURLService); instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', [local]); testConfigurationService = new TestConfigurationService(); instantiationService.stub(IConfigurationService, testConfigurationService); diff --git a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts index c092c7c81ea..9bb5657c0cc 100644 --- a/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/configBasedRecommendations.ts @@ -7,7 +7,6 @@ import { IExtensionTipsService, IExtensionManagementService, ILocalExtension, IC import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/contrib/extensions/browser/extensionRecommendations'; import { localize } from 'vs/nls'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { ExtensionRecommendationReason } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -75,7 +74,7 @@ export class ConfigBasedRecommendations extends ExtensionRecommendations { return; } - const local = await this.extensionManagementService.getInstalled(ExtensionType.User); + const local = await this.extensionManagementService.getInstalled(); const { uninstalled } = this.groupByInstalled(distinct(this.importantTips.map(({ extensionId }) => extensionId)), local); if (uninstalled.length === 0) { return; diff --git a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts index 92d7e07c4fe..f153ecd7a3a 100644 --- a/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/exeBasedRecommendations.ts @@ -9,7 +9,6 @@ import { ExtensionRecommendations, ExtensionRecommendation } from 'vs/workbench/ import { timeout } from 'vs/base/common/async'; import { localize } from 'vs/nls'; import { IStringDictionary } from 'vs/base/common/collections'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { basename } from 'vs/base/common/path'; @@ -61,7 +60,7 @@ export class ExeBasedRecommendations extends ExtensionRecommendations { importantExeBasedRecommendations[tip.extensionId.toLowerCase()] = tip; }); - const local = await this.extensionManagementService.getInstalled(ExtensionType.User); + const local = await this.extensionManagementService.getInstalled(); const { installed, uninstalled } = this.groupByInstalled(Object.keys(importantExeBasedRecommendations), local); /* Log installed and uninstalled exe based recommendations */ diff --git a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts index 5ad28727bc8..b750c1df2f5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -588,7 +588,7 @@ export class ExtensionEditor extends BaseEditor { const webview = this.contentDisposables.add(this.webviewService.createWebviewOverlay('extensionEditor', { enableFindWidget: true, - }, {})); + }, {}, undefined)); webview.claim(this); webview.layoutWebviewOverElement(template.content); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index dfbef720e2a..e152bd1bc9c 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -45,11 +45,10 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; -import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { CONTEXT_SYNC_ENABLEMENT } from 'vs/platform/userDataSync/common/userDataSync'; import { IQuickAccessRegistry, Extensions } from 'vs/platform/quickinput/common/quickAccess'; import { InstallExtensionQuickAccessProvider, ManageExtensionsQuickAccessProvider } from 'vs/workbench/contrib/extensions/browser/extensionsQuickAccess'; import { ExtensionRecommendationsService } from 'vs/workbench/contrib/extensions/browser/extensionRecommendationsService'; +import { CONTEXT_SYNC_ENABLEMENT } from 'vs/workbench/services/userDataSync/common/userDataSync'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -450,15 +449,11 @@ registerAction2(class extends Action2 { } async run(accessor: ServicesAccessor, id: string) { - const configurationService = accessor.get(IConfigurationService); - const ignoredExtensions = [...configurationService.getValue('sync.ignoredExtensions')]; - const index = ignoredExtensions.findIndex(ignoredExtension => areSameExtensions({ id: ignoredExtension }, { id })); - if (index !== -1) { - ignoredExtensions.splice(index, 1); - } else { - ignoredExtensions.push(id); + const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService); + const extension = extensionsWorkbenchService.local.find(e => areSameExtensions({ id }, e.identifier)); + if (extension) { + return extensionsWorkbenchService.toggleExtensionIgnoredToSync(extension); } - return configurationService.updateValue('sync.ignoredExtensions', ignoredExtensions.length ? ignoredExtensions : undefined, ConfigurationTarget.USER); } }); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index a08e08da5cd..71f209daca1 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -808,7 +808,7 @@ export class MenuItemExtensionAction extends ExtensionAction { constructor( private readonly action: IAction, - @IConfigurationService private readonly configurationService: IConfigurationService + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super(action.id, action.label); } @@ -818,7 +818,7 @@ export class MenuItemExtensionAction extends ExtensionAction { return; } if (this.action.id === TOGGLE_IGNORE_EXTENSION_ACTION_ID) { - this.checked = !this.configurationService.getValue('sync.ignoredExtensions').some(id => areSameExtensions({ id }, this.extension!.identifier)); + this.checked = !this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension); } } @@ -2660,7 +2660,8 @@ export class SyncIgnoredIconAction extends ExtensionAction { private static readonly DISABLE_CLASS = `${SyncIgnoredIconAction.ENABLE_CLASS} hide`; constructor( - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IExtensionsWorkbenchService private readonly extensionsWorkbenchService: IExtensionsWorkbenchService, ) { super('extensions.syncignore', '', SyncIgnoredIconAction.DISABLE_CLASS, false); this._register(Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectedKeys.includes('sync.ignoredExtensions'))(() => this.update())); @@ -2670,11 +2671,8 @@ export class SyncIgnoredIconAction extends ExtensionAction { update(): void { this.class = SyncIgnoredIconAction.DISABLE_CLASS; - if (this.extension) { - const ignoredExtensions = this.configurationService.getValue('sync.ignoredExtensions') || []; - if (ignoredExtensions.some(id => areSameExtensions({ id }, this.extension!.identifier))) { - this.class = SyncIgnoredIconAction.ENABLE_CLASS; - } + if (this.extension && this.extensionsWorkbenchService.isExtensionIgnoredToSync(this.extension)) { + this.class = SyncIgnoredIconAction.ENABLE_CLASS; } } diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index ea288746ac5..0b6d0a5f6c8 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -19,7 +19,7 @@ import { import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, areSameExtensions, getMaliciousExtensionsSet, groupByExtension, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { URI } from 'vs/base/common/uri'; import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey } from 'vs/workbench/contrib/extensions/common/extensions'; @@ -37,6 +37,7 @@ import { IExtensionManifest, ExtensionType, IExtension as IPlatformExtension, is import { IModeService } from 'vs/editor/common/services/modeService'; import { IProductService } from 'vs/platform/product/common/productService'; import { asDomUri } from 'vs/base/browser/dom'; +import { getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; interface IExtensionStateProvider { (extension: Extension): T; @@ -94,8 +95,8 @@ class Extension implements IExtension { return this.gallery.publisherDisplayName || this.gallery.publisher; } - if (this.local!.metadata && this.local!.metadata.publisherDisplayName) { - return this.local!.metadata.publisherDisplayName; + if (this.local?.publisherDisplayName) { + return this.local.publisherDisplayName; } return this.local!.manifest.publisher; @@ -367,7 +368,7 @@ class Extensions extends Disposable { } // Sync the local extension with gallery extension if local extension doesnot has metadata if (extension.local) { - const local = extension.local.metadata ? extension.local : await this.server.extensionManagementService.updateMetadata(extension.local, { id: compatible.identifier.uuid, publisherDisplayName: compatible.publisherDisplayName, publisherId: compatible.publisherId }); + const local = extension.local.identifier.uuid ? extension.local : await this.server.extensionManagementService.updateMetadata(extension.local, { id: compatible.identifier.uuid, publisherDisplayName: compatible.publisherDisplayName, publisherId: compatible.publisherId }); extension.local = local; extension.gallery = compatible; this._onChange.fire({ extension }); @@ -863,6 +864,39 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension }, () => this.extensionService.reinstallFromGallery(toReinstall).then(() => this.local.filter(local => areSameExtensions(local.identifier, extension.identifier))[0])); } + isExtensionIgnoredToSync(extension: IExtension): boolean { + const localExtensions = (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer + ? this.local.filter(i => i.server === this.extensionManagementServerService.localExtensionManagementServer) + : this.local) + .filter(l => !!l.local) + .map(l => l.local!); + + const ignoredExtensions = getIgnoredExtensions(localExtensions, this.configurationService); + return ignoredExtensions.includes(extension.identifier.id.toLowerCase()); + } + + toggleExtensionIgnoredToSync(extension: IExtension): Promise { + const isIgnored = this.isExtensionIgnoredToSync(extension); + const isDefaultIgnored = extension.local?.isMachineScoped; + const id = extension.identifier.id.toLowerCase(); + + // first remove the extension completely from ignored extensions + let currentValue = [...this.configurationService.getValue('sync.ignoredExtensions')].map(id => id.toLowerCase()); + currentValue = currentValue.filter(v => v !== id && v !== `-${id}`); + + // If ignored, then add only if it is ignored by default + if (isIgnored && isDefaultIgnored) { + currentValue.push(`-${id}`); + } + + // If asked not to sync, then add only if it is not ignored by default + if (!isIgnored && !isDefaultIgnored) { + currentValue.push(id); + } + + return this.configurationService.updateValue('sync.ignoredExtensions', currentValue.length ? currentValue : undefined, ConfigurationTarget.USER); + } + private installWithProgress(installTask: () => Promise, extensionName?: string): Promise { const title = extensionName ? nls.localize('installing named extension', "Installing '{0}' extension....", extensionName) : nls.localize('installing extension', 'Installing extension....'); return this.progressService.withProgress({ diff --git a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts index af198f44531..527d8180ca3 100644 --- a/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/fileBasedRecommendations.ts @@ -12,7 +12,6 @@ import { ExtensionRecommendationSource, ExtensionRecommendationReason } from 'vs import { IExtensionsViewPaneContainer, IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/common/extensions'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IProductService } from 'vs/platform/product/common/productService'; @@ -181,7 +180,7 @@ export class FileBasedRecommendations extends ExtensionRecommendations { return; } - const installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + const installed = await this.extensionManagementService.getInstalled(); if (await this.promptRecommendedExtensionForFileType(recommendationsToPrompt, installed)) { return; } diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts index 84c47be3064..3ce9d79e586 100644 --- a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts +++ b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts @@ -30,7 +30,7 @@ export class RemoteExtensionsInstaller extends Disposable implements IWorkbenchC disposable = MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command: { id: 'workbench.extensions.installLocalExtensions', - category: localize('remote', "Remote"), + category: localize({ key: 'remote', comment: ['Remote as in remote machine'] }, "Remote"), title: installLocalExtensionsInRemoteAction.label } }); diff --git a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts index 4d05b1d420e..b17fd1dec71 100644 --- a/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts +++ b/src/vs/workbench/contrib/extensions/browser/workspaceRecommendations.ts @@ -17,7 +17,6 @@ import { EXTENSIONS_CONFIG } from 'vs/workbench/contrib/extensions/common/extens import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken } from 'vs/base/common/cancellation'; import { localize } from 'vs/nls'; -import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { InstallWorkspaceRecommendedExtensionsAction, ShowRecommendedExtensionsAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { StorageScope, IStorageService } from 'vs/platform/storage/common/storage'; @@ -105,7 +104,7 @@ export class WorkspaceRecommendations extends ExtensionRecommendations { return; } - let installed = await this.extensionManagementService.getInstalled(ExtensionType.User); + 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))); diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index fae5943aaf6..6119b8de80c 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -88,6 +88,10 @@ export interface IExtensionsWorkbenchService { setEnablement(extensions: IExtension | IExtension[], enablementState: EnablementState): Promise; open(extension: IExtension, options?: { sideByside?: boolean, preserveFocus?: boolean, pinned?: boolean }): Promise; checkForUpdates(): Promise; + + // Sync APIs + isExtensionIgnoredToSync(extension: IExtension): boolean; + toggleExtensionIgnoredToSync(extension: IExtension): Promise; } export const ConfigurationKey = 'extensions'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts index 7f988b5c93c..f95e31cd436 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionProfileService.ts @@ -12,7 +12,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { StatusbarAlignment, IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { IExtensionHostProfileService, ProfileSessionState } from 'vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { randomPort } from 'vs/base/node/ports'; import product from 'vs/platform/product/common/product'; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts index 94a29c5e229..4289877aade 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsActions.ts @@ -9,7 +9,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { Schemas } from 'vs/base/common/network'; export class OpenExtensionsFolderAction extends Action { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index cc851c4bc4d..edc85904b4a 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts @@ -24,7 +24,7 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { EnablementState } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { writeFile } from 'vs/base/node/pfs'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { memoize } from 'vs/base/common/decorators'; @@ -372,6 +372,9 @@ export class RuntimeExtensionsEditor extends BaseEditor { '{0} will be a glob pattern' ] }, "Activated by {1} because searching for {0} took too long", glob, activationId); + } else if (/^onStartup:/.test(activationEvent)) { + const time = activationEvent.substr('onStartup:'.length); + title = nls.localize('startupActivation', "Activated by {0} with a delay of {1} ms on start-up", activationId, time); } else if (/^onLanguage:/.test(activationEvent)) { let language = activationEvent.substr('onLanguage:'.length); title = nls.localize('languageActivation', "Activated by {1} because you opened a {0} file", language, activationId); 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 ef9b59e46c6..b20db7d9a94 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 @@ -41,7 +41,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { IModelService } from 'vs/editor/common/services/modelService'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { INotificationService, Severity, IPromptChoice, IPromptOptions } from 'vs/platform/notification/common/notification'; -import { URLService } from 'vs/platform/url/node/urlService'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { IExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { TestExperimentService } from 'vs/workbench/contrib/experiments/test/electron-browser/experimentService.test'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -206,7 +206,7 @@ suite('ExtensionRecommendationsService Test', () => { instantiationService.stub(IExtensionManagementService, 'onDidUninstallExtension', didUninstallEvent.event); instantiationService.stub(IWorkbenchExtensionEnablementService, new TestExtensionEnablementService(instantiationService)); instantiationService.stub(ITelemetryService, NullTelemetryService); - instantiationService.stub(IURLService, URLService); + instantiationService.stub(IURLService, NativeURLService); instantiationService.stub(IWorkspaceTagsService, new NoOpWorkspaceTagsService()); instantiationService.stub(IStorageService, new TestStorageService()); instantiationService.stub(ILogService, new NullLogService()); 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 dfe557cec9d..0c554f06717 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 @@ -11,11 +11,10 @@ import * as ExtensionsActions from 'vs/workbench/contrib/extensions/browser/exte import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation, IExtensionTipsService + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer, IExtensionRecommendationsService } 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'; @@ -30,7 +29,7 @@ import { TestContextService } from 'vs/workbench/test/common/workbenchTestServic import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { URLService } from 'vs/platform/url/node/urlService'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; @@ -82,11 +81,21 @@ async function setupTest() { instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); - 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 getExtensionsReport() { return []; }, + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata) { + local.identifier.uuid = metadata.id; + local.publisherDisplayName = metadata.publisherDisplayName; + local.publisherId = metadata.publisherId; + return local; + } + }); + instantiationService.stub(IRemoteAgentService, RemoteAgentService); instantiationService.stub(IExtensionManagementServerService, new class extends ExtensionManagementServerService { @@ -105,10 +114,8 @@ async function setupTest() { instantiationService.stub(IExperimentService, instantiationService.createInstance(TestExperimentService)); instantiationService.stub(IExtensionTipsService, instantiationService.createInstance(ExtensionTipsService)); instantiationService.stub(IExtensionRecommendationsService, {}); - instantiationService.stub(IURLService, URLService); + instantiationService.stub(IURLService, NativeURLService); - instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); - instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stub(IExtensionService, >{ getExtensions: () => Promise.resolve([]), onDidChangeExtensions: new Emitter().event, canAddExtension: (extension: IExtensionDescription) => false, canRemoveExtension: (extension: IExtensionDescription) => false }); (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); @@ -117,7 +124,7 @@ async function setupTest() { } -suite('ExtensionsActions Test', () => { +suite('ExtensionsActions', () => { setup(setupTest); teardown(() => disposables.dispose()); @@ -2491,8 +2498,7 @@ function aLocalExtension(name: string = 'someext', manifest: any = {}, propertie properties = assign({ type: ExtensionType.User, location: URI.file(`pub.${name}`), - identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: undefined }, - metadata: { id: getGalleryExtensionId(manifest.publisher, manifest.name), publisherId: manifest.publisher, publisherDisplayName: 'somename' } + identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) } }, properties); return Object.create({ manifest, ...properties }); } @@ -2563,7 +2569,13 @@ function createExtensionManagementService(installed: ILocalExtension[] = []): IE onUninstallExtension: Event.None, onDidUninstallExtension: Event.None, getInstalled: () => Promise.resolve(installed), - installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')) + installFromGallery: (extension: IGalleryExtension) => Promise.reject(new Error('not supported')), + updateMetadata: async (local: ILocalExtension, metadata: IGalleryMetadata) => { + local.identifier.uuid = metadata.id; + local.publisherDisplayName = metadata.publisherDisplayName; + local.publisherId = metadata.publisherId; + return local; + } }; } 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 a9d3ab44f81..82fc05ca3f9 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 @@ -30,7 +30,7 @@ import { TestMenuService } from 'vs/workbench/test/browser/workbenchTestServices import { TestSharedProcessService } from 'vs/workbench/test/electron-browser/workbenchTestServices'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; -import { URLService } from 'vs/platform/url/node/urlService'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { SinonStub } from 'sinon'; @@ -142,7 +142,7 @@ suite('ExtensionsListView Tests', () => { return reasons; } }); - instantiationService.stub(IURLService, URLService); + instantiationService.stub(IURLService, NativeURLService); }); setup(async () => { diff --git a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts index 48af67b6e03..82da373c436 100644 --- a/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts +++ b/src/vs/workbench/contrib/extensions/test/electron-browser/extensionsWorkbenchService.test.ts @@ -12,11 +12,10 @@ import { IExtensionsWorkbenchService, ExtensionState, AutoCheckUpdatesConfigurat import { ExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/browser/extensionsWorkbenchService'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, - DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService + DidInstallExtensionEvent, DidUninstallExtensionEvent, InstallExtensionEvent, IGalleryExtensionAssets, IExtensionIdentifier, InstallOperation, IExtensionTipsService, IGalleryMetadata } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionRecommendationsService } 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'; @@ -32,7 +31,7 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { ProgressService } from 'vs/workbench/services/progress/browser/progressService'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { URLService } from 'vs/platform/url/node/urlService'; +import { NativeURLService } from 'vs/platform/url/common/urlService'; import { URI } from 'vs/base/common/uri'; import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; @@ -72,7 +71,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IProductService, {}); instantiationService.stub(IExtensionGalleryService, ExtensionGalleryService); - instantiationService.stub(IURLService, URLService); + instantiationService.stub(IURLService, NativeURLService); instantiationService.stub(ISharedProcessService, TestSharedProcessService); instantiationService.stub(IWorkspaceContextService, new TestContextService()); @@ -85,11 +84,20 @@ suite('ExtensionsWorkbenchServiceTest', () => { instantiationService.stub(IRemoteAgentService, RemoteAgentService); - 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 getExtensionsReport() { return []; }, + async updateMetadata(local: ILocalExtension, metadata: IGalleryMetadata) { + local.identifier.uuid = metadata.id; + local.publisherDisplayName = metadata.publisherDisplayName; + local.publisherId = metadata.publisherId; + return local; + } + }); instantiationService.stub(IExtensionManagementServerService, { localExtensionManagementServer: { @@ -109,7 +117,6 @@ suite('ExtensionsWorkbenchServiceTest', () => { setup(async () => { instantiationService.stubPromise(IExtensionManagementService, 'getInstalled', []); - instantiationService.stubPromise(IExtensionManagementService, 'getExtensionsReport', []); instantiationService.stubPromise(IExtensionGalleryService, 'query', aPage()); instantiationService.stubPromise(INotificationService, 'prompt', 0); await (instantiationService.get(IWorkbenchExtensionEnablementService)).reset(); @@ -985,8 +992,7 @@ suite('ExtensionsWorkbenchServiceTest', () => { properties = assign({ type: ExtensionType.User, location: URI.file(`pub.${name}`), - identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name), uuid: undefined }, - metadata: { id: getGalleryExtensionId(manifest.publisher, manifest.name), publisherId: manifest.publisher, publisherDisplayName: 'somename' } + identifier: { id: getGalleryExtensionId(manifest.publisher, manifest.name) } }, properties); return Object.create({ manifest, ...properties }); } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 7924a4dde95..91a1fc23a1d 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, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { EditorOptions, TextEditorOptions, IEditorCloseEvent, Verbosity } from 'vs/workbench/common/editor'; +import { EditorOptions, TextEditorOptions, IEditorCloseEvent } 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'; @@ -245,11 +245,6 @@ export class TextFileEditor extends BaseTextEditor { } } - protected getAriaLabel(): string { - const title = this.input?.getTitle(Verbosity.SHORT) || nls.localize('fileEditorAriaLabel', "editor"); - return this.input?.isReadonly() ? nls.localize('readonlyEditor', "{0} readonly", title) : title; - } - clearInput(): void { // Update/clear editor view state in settings diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts index 6a2baff6544..869a4900104 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -17,7 +17,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ResourceMap } from 'vs/base/common/map'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; -import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { TextFileContentProvider } from 'vs/workbench/contrib/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { SAVE_FILE_COMMAND_ID, REVERT_FILE_COMMAND_ID, SAVE_FILE_AS_COMMAND_ID, SAVE_FILE_AS_LABEL } from 'vs/workbench/contrib/files/browser/fileCommands'; @@ -43,14 +43,15 @@ const conflictEditorHelp = nls.localize('userGuide', "Use the actions in the edi // A handler for text file save error happening with conflict resolution actions export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHandler, IWorkbenchContribution { - private messages: ResourceMap; - private conflictResolutionContext: IContextKey; - private activeConflictResolutionResource?: URI; + + private readonly messages = new ResourceMap(); + private readonly conflictResolutionContext = new RawContextKey(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(this.contextKeyService); + private activeConflictResolutionResource: URI | undefined = undefined; constructor( @INotificationService private readonly notificationService: INotificationService, @ITextFileService private readonly textFileService: ITextFileService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService private contextKeyService: IContextKeyService, @IEditorService private readonly editorService: IEditorService, @ITextModelService textModelService: ITextModelService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -62,9 +63,6 @@ export class TextFileSaveErrorHandler extends Disposable implements ISaveErrorHa // opt-in to syncing storageKeysSyncRegistryService.registerStorageKey({ key: LEARN_MORE_DIRTY_WRITE_IGNORE_KEY, version: 1 }); - this.messages = new ResourceMap(); - this.conflictResolutionContext = new RawContextKey(CONFLICT_RESOLUTION_CONTEXT, false).bindTo(contextKeyService); - const provider = this._register(instantiationService.createInstance(TextFileContentProvider)); this._register(textModelService.registerTextModelContentProvider(CONFLICT_RESOLUTION_SCHEME, provider)); @@ -350,18 +348,23 @@ export const acceptLocalChangesCommand = async (accessor: ServicesAccessor, reso const reference = await resolverService.createModelReference(resource); const model = reference.object as IResolvedTextFileEditorModel; - clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions + try { - // Trigger save - await model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); + // hide any previously shown message about how to use these actions + clearPendingResolveSaveConflictMessages(); - // Reopen file input - await editorService.openEditor({ resource: model.resource }, group); + // Trigger save + await model.save({ ignoreModifiedSince: true, reason: SaveReason.EXPLICIT }); - // Clean up - group.closeEditor(editor); - editor.dispose(); - reference.dispose(); + // Reopen file input + await editorService.openEditor({ resource: model.resource }, group); + + // Clean up + group.closeEditor(editor); + editor.dispose(); + } finally { + reference.dispose(); + } }; export const revertLocalChangesCommand = async (accessor: ServicesAccessor, resource: URI) => { @@ -379,16 +382,21 @@ export const revertLocalChangesCommand = async (accessor: ServicesAccessor, reso const reference = await resolverService.createModelReference(resource); const model = reference.object as ITextFileEditorModel; - clearPendingResolveSaveConflictMessages(); // hide any previously shown message about how to use these actions + try { - // Revert on model - await model.revert(); + // hide any previously shown message about how to use these actions + clearPendingResolveSaveConflictMessages(); - // Reopen file input - await editorService.openEditor({ resource: model.resource }, group); + // Revert on model + await model.revert(); - // Clean up - group.closeEditor(editor); - editor.dispose(); - reference.dispose(); + // Reopen file input + await editorService.openEditor({ resource: model.resource }, group); + + // Clean up + group.closeEditor(editor); + editor.dispose(); + } finally { + reference.dispose(); + } }; diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 4ef2a0dd098..b30f36916e5 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -144,9 +144,9 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ weight: KeybindingWeight.WorkbenchContrib + explorerCommandsWeightBonus, when: ContextKeyExpr.and(FilesExplorerFocusCondition, ExplorerResourceCut), primary: KeyCode.Escape, - handler: (accessor: ServicesAccessor) => { + handler: async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); - explorerService.setToCopy([], true); + await explorerService.setToCopy([], true); } }); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 2b4911f6fde..d9f3f24ab87 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -48,7 +48,7 @@ import { sequence, timeout } from 'vs/base/common/async'; import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService'; import { once } from 'vs/base/common/functional'; import { IEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { Codicon } from 'vs/base/common/codicons'; import { IViewsService } from 'vs/workbench/common/views'; import { openEditorWith, getAllAvailableEditors } from 'vs/workbench/contrib/files/common/openWith'; @@ -576,7 +576,7 @@ export class ToggleEditorTypeCommand extends Action { return; } - await firstNonActiveOverride[0].open(input, options, group, firstNonActiveOverride[1].id)?.override; + await firstNonActiveOverride[0].open(input, options, group, OpenEditorContext.NEW_EDITOR, firstNonActiveOverride[1].id)?.override; } } @@ -1060,20 +1060,20 @@ export const deleteFileHandler = async (accessor: ServicesAccessor) => { }; let pasteShouldMove = false; -export const copyFileHandler = (accessor: ServicesAccessor) => { +export const copyFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true); if (stats.length > 0) { - explorerService.setToCopy(stats, false); + await explorerService.setToCopy(stats, false); pasteShouldMove = false; } }; -export const cutFileHandler = (accessor: ServicesAccessor) => { +export const cutFileHandler = async (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const stats = explorerService.getContext(true); if (stats.length > 0) { - explorerService.setToCopy(stats, true); + await explorerService.setToCopy(stats, true); pasteShouldMove = true; } }; @@ -1140,7 +1140,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { const configurationService = accessor.get(IConfigurationService); const context = explorerService.getContext(true); - const toPaste = resources.distinctParents(clipboardService.readResources(), r => r); + const toPaste = resources.distinctParents(await clipboardService.readResources(), r => r); const element = context.length ? context[0] : explorerService.roots[0]; // Check if target is ancestor of pasted folder @@ -1178,7 +1178,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { if (pasteShouldMove) { // Cut is done. Make sure to clear cut state. - explorerService.setToCopy([], false); + await explorerService.setToCopy([], false); pasteShouldMove = false; } if (stats.length >= 1) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 01e0db2c640..97ee108e43c 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -43,7 +43,6 @@ import { IStorageService, StorageScope } from 'vs/platform/storage/common/storag import { IAsyncDataTreeViewState } from 'vs/base/browser/ui/tree/asyncDataTree'; import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { isEqualOrParent } from 'vs/base/common/resources'; import { values } from 'vs/base/common/map'; import { first } from 'vs/base/common/arrays'; import { withNullAsUndefined } from 'vs/base/common/types'; @@ -56,6 +55,7 @@ import { Color } from 'vs/base/common/color'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IViewDescriptorService } from 'vs/workbench/common/views'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; interface IExplorerViewColors extends IColorMapping { listDropBackground?: ColorValue | undefined; @@ -171,6 +171,7 @@ export class ExplorerView extends ViewPane { @IStorageService private readonly storageService: IStorageService, @IClipboardService private clipboardService: IClipboardService, @IFileService private readonly fileService: IFileService, + @IUriIdentityService private readonly uriIdentityService: IUriIdentityService, @IOpenerService openerService: IOpenerService, ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); @@ -471,7 +472,7 @@ export class ExplorerView extends ViewPane { } } - private onContextMenu(e: ITreeContextMenuEvent): void { + private async onContextMenu(e: ITreeContextMenuEvent): Promise { const disposables = new DisposableStore(); let stat = e.element; let anchor = e.anchor; @@ -490,7 +491,7 @@ export class ExplorerView extends ViewPane { } // update dynamic contexts - this.fileCopiedContextKey.set(this.clipboardService.hasResources()); + this.fileCopiedContextKey.set(await this.clipboardService.hasResources()); this.setContextKeys(stat); const selection = this.tree.getSelection(); @@ -652,8 +653,7 @@ export class ExplorerView extends ViewPane { } // Expand all stats in the parent chain. - const ignoreCase = !this.fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive); - let item: ExplorerItem | undefined = this.explorerService.roots.filter(i => isEqualOrParent(resource, i.resource, ignoreCase)) + let item: ExplorerItem | undefined = this.explorerService.roots.filter(i => this.uriIdentityService.extUri.isEqualOrParent(resource, i.resource)) // Take the root that is the closest to the stat #72299 .sort((first, second) => second.resource.path.length - first.resource.path.length)[0]; @@ -663,7 +663,7 @@ export class ExplorerView extends ViewPane { } catch (e) { return this.selectResource(resource, reveal, retry + 1); } - item = first(values(item.children), i => isEqualOrParent(resource, i.resource, ignoreCase)); + item = first(values(item.children), i => this.uriIdentityService.extUri.isEqualOrParent(resource, i.resource)); } if (item) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 48d7f80bb71..e1b45a1e01b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -7,7 +7,7 @@ import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; import * as DOM from 'vs/base/browser/dom'; import * as glob from 'vs/base/common/glob'; import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; -import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; +import { IProgressService, ProgressLocation, IProgressStep, IProgress } from 'vs/platform/progress/common/progress'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; @@ -19,7 +19,7 @@ import { ITreeNode, ITreeFilter, TreeVisibility, TreeFilterResult, IAsyncDataSou import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; -import { IFilesConfiguration, IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { IFilesConfiguration, IExplorerService, VIEW_ID } from 'vs/workbench/contrib/files/common/files'; import { dirname, joinPath, isEqualOrParent, basename, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; @@ -50,12 +50,13 @@ import { Emitter, Event, EventMultiplexer } from 'vs/base/common/event'; import { ITreeCompressionDelegate } from 'vs/base/browser/ui/tree/asyncDataTree'; import { ICompressibleTreeRenderer } from 'vs/base/browser/ui/tree/objectTree'; import { ICompressedTreeNode } from 'vs/base/browser/ui/tree/compressedObjectTreeModel'; -import { VSBuffer } from 'vs/base/common/buffer'; +import { VSBuffer, newWriteableBufferStream } from 'vs/base/common/buffer'; import { ILabelService } from 'vs/platform/label/common/label'; import { isNumber } from 'vs/base/common/types'; import { domEvent } from 'vs/base/browser/event'; import { IEditableData } from 'vs/workbench/common/views'; import { IEditorInput } from 'vs/workbench/common/editor'; +import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; export class ExplorerDelegate implements IListVirtualDelegate { @@ -721,7 +722,7 @@ interface IWebkitDataTransferItem { } interface IWebkitDataTransferItemEntry { - name: string; + name: string | undefined; isFile: boolean; isDirectory: boolean; @@ -753,7 +754,8 @@ export class FileDragAndDrop implements ITreeDragAndDrop { @IInstantiationService private instantiationService: IInstantiationService, @IWorkingCopyFileService private workingCopyFileService: IWorkingCopyFileService, @IHostService private hostService: IHostService, - @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService + @IWorkspaceEditingService private workspaceEditingService: IWorkspaceEditingService, + @IProgressService private readonly progressService: IProgressService ) { this.toDispose = []; @@ -971,22 +973,37 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } const results: { isFile: boolean, resource: URI }[] = []; - for (let entry of entries) { - const result = await this.doUploadWebFileEntry(entry, target.resource, target); + const cts = new CancellationTokenSource(); - if (result) { - results.push(result); + // Start upload and report progress globally + const uploadPromise = this.progressService.withProgress({ + location: ProgressLocation.Window, + delay: 800, + cancellable: true, + title: localize('uploadingFiles', "Uploading") + }, async progress => { + for (let entry of entries) { + const result = await this.doUploadWebFileEntry(entry, target.resource, target, progress, cts.token); + if (result) { + results.push(result); + } } - } + }, () => cts.dispose(true)); + + // Also indicate progress in the files view + this.progressService.withProgress({ location: VIEW_ID, delay: 800 }, () => uploadPromise); + + // Wait until upload is done + await uploadPromise; // Open uploaded file in editor only if we upload just one - if (results.length === 1 && results[0].isFile) { + if (!cts.token.isCancellationRequested && results.length === 1 && results[0].isFile) { await this.editorService.openEditor({ resource: results[0].resource, options: { pinned: true } }); } } - private async doUploadWebFileEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined): Promise<{ isFile: boolean, resource: URI } | undefined> { - if (!entry.isFile && !entry.isDirectory) { + private async doUploadWebFileEntry(entry: IWebkitDataTransferItemEntry, parentResource: URI, target: ExplorerItem | undefined, progress: IProgress, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> { + if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) { return undefined; } @@ -1000,25 +1017,44 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } } + if (token.isCancellationRequested) { + return undefined; + } + + // Report progress + progress.report({ message: entry.name }); + // Handle file upload if (entry.isFile) { const file = await new Promise((resolve, reject) => entry.file(resolve, reject)); - const reader = new FileReader(); - reader.readAsArrayBuffer(file); - reader.onload = async (event) => { - const name = file.name; - if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) { - await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target.result))); - } - }; + + if (token.isCancellationRequested) { + return undefined; + } + + // Chrome/Edge/Firefox support stream method + if (typeof file.stream === 'function') { + await this.doUploadWebFileEntryBuffered(resource, file); + } + + // Fallback to unbuffered upload for other browsers + else { + await this.doUploadWebFileEntryUnbuffered(resource, file); + } return { isFile: true, resource }; } // Handle folder upload else { + + // Create target folder await this.fileService.createFolder(resource); + if (token.isCancellationRequested) { + return undefined; + } + // Recursive upload files in this directory const folderTarget = target && target.getChild(entry.name) || undefined; const dirReader = entry.createReader(); @@ -1027,13 +1063,58 @@ export class FileDragAndDrop implements ITreeDragAndDrop { }); for (let childEntry of childEntries) { - await this.doUploadWebFileEntry(childEntry, resource, folderTarget); + await this.doUploadWebFileEntry(childEntry, resource, folderTarget, progress, token); } return { isFile: false, resource }; } } + private async doUploadWebFileEntryBuffered(resource: URI, file: File): Promise { + const writeableStream = newWriteableBufferStream(); + + // Read the file in chunks using File.stream() web APIs + (async () => { + try { + const reader: ReadableStreamDefaultReader = file.stream().getReader(); + + let res = await reader.read(); + while (!res.done) { + writeableStream.write(VSBuffer.wrap(res.value)); + + res = await reader.read(); + } + writeableStream.end(res.value instanceof Uint8Array ? VSBuffer.wrap(res.value) : undefined); + } catch (error) { + writeableStream.end(error); + } + })(); + + await this.fileService.writeFile(resource, writeableStream); + } + + private doUploadWebFileEntryUnbuffered(resource: URI, file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = async event => { + try { + if (event.target?.result instanceof ArrayBuffer) { + await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target.result))); + } else { + throw new Error('Could not read from dropped file.'); + } + + resolve(); + } catch (error) { + reject(error); + } + }; + + // Start reading the file to trigger `onload` + reader.readAsArrayBuffer(file); + }); + } + private async handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { // Check for dropped external files to be folders diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index e4ae372e9d9..d06919c8880 100644 --- a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts @@ -5,7 +5,8 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { EncodingMode, IFileEditorInput, Verbosity, TextResourceEditorInput, GroupIdentifier, IMoveResult, isTextEditorPane } from 'vs/workbench/common/editor'; +import { EncodingMode, IFileEditorInput, Verbosity, GroupIdentifier, IMoveResult, isTextEditorPane } from 'vs/workbench/common/editor'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; import { ITextFileService, TextFileEditorModelState, TextFileLoadReason, TextFileOperationError, TextFileOperationResult, ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; @@ -17,7 +18,7 @@ import { ILabelService } from 'vs/platform/label/common/label'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { isEqual } from 'vs/base/common/resources'; +import { extUri } from 'vs/base/common/resources'; import { Event } from 'vs/base/common/event'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; @@ -30,7 +31,7 @@ const enum ForceOpenAs { /** * A file editor input is the input type for the file editor of file system resources. */ -export class FileEditorInput extends TextResourceEditorInput implements IFileEditorInput { +export class FileEditorInput extends AbstractTextResourceEditorInput implements IFileEditorInput { private preferredEncoding: string | undefined; private preferredMode: string | undefined; @@ -84,7 +85,13 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi // Once the text file model is created, we keep it inside // the input to be able to implement some methods properly - if (isEqual(model.resource, this.resource)) { + // TODO@ben once we are certain that models will only be + // created with canonical URIs, this should use the URI + // identity service. But as long as there is a chance + // that a model is created with same path but different + // case, we can only accept that model here if the URIs + // are 100% identical. + if (extUri.isEqual(model.resource, this.resource)) { this.model = model; this.registerModelListeners(model); @@ -291,7 +298,7 @@ export class FileEditorInput extends TextResourceEditorInput implements IFileEdi private getViewStateFor(group: GroupIdentifier): IEditorViewState | undefined { for (const editorPane of this.editorService.visibleEditorPanes) { - if (editorPane.group.id === group && isEqual(editorPane.input.resource, this.resource)) { + if (editorPane.group.id === group && extUri.isEqual(editorPane.input.resource, this.resource)) { if (isTextEditorPane(editorPane)) { return editorPane.getViewState(); } diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index a411f52ba7b..b028ae30524 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -126,10 +126,10 @@ export class ExplorerService implements IExplorerService { await this.view.setEditable(stat, isEditing); } - setToCopy(items: ExplorerItem[], cut: boolean): void { + async setToCopy(items: ExplorerItem[], cut: boolean): Promise { const previouslyCutItems = this.cutItems; this.cutItems = cut ? items : undefined; - this.clipboardService.writeResources(items.map(s => s.resource)); + await this.clipboardService.writeResources(items.map(s => s.resource)); this.view?.itemsCopied(items, cut, previouslyCutItems); } @@ -362,7 +362,7 @@ export class ExplorerService implements IExplorerService { } private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent { - return new FileChangesEvent(e.changes.filter(change => { + return e.filter(change => { if (change.type === FileChangeType.UPDATED && this._sortOrder !== SortOrder.Modified) { return false; // we only are about updated if we sort by modified time } @@ -376,7 +376,7 @@ export class ExplorerService implements IExplorerService { } return true; - })); + }); } private async onConfigurationUpdated(configuration: IFilesConfiguration, event?: IConfigurationChangeEvent): Promise { diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index d9e592b2779..c970d8a04b7 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -51,7 +51,7 @@ export interface IExplorerService { isEditable(stat: ExplorerItem | undefined): boolean; findClosest(resource: URI): ExplorerItem | null; refresh(): Promise; - setToCopy(stats: ExplorerItem[], cut: boolean): void; + setToCopy(stats: ExplorerItem[], cut: boolean): Promise; isCut(stat: ExplorerItem): boolean; /** diff --git a/src/vs/workbench/contrib/files/common/openWith.ts b/src/vs/workbench/contrib/files/common/openWith.ts index 265e49d5832..1d8b7180bab 100644 --- a/src/vs/workbench/contrib/files/common/openWith.ts +++ b/src/vs/workbench/contrib/files/common/openWith.ts @@ -13,7 +13,7 @@ import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { DEFAULT_EDITOR_ID } from 'vs/workbench/contrib/files/common/files'; import { CustomEditorAssociation, CustomEditorsAssociations, customEditorsAssociationsSettingId } from 'vs/workbench/services/editor/common/editorAssociationsSetting'; -import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroup, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, IOpenEditorOverrideEntry, IOpenEditorOverrideHandler } from 'vs/workbench/services/editor/common/editorService'; const builtinProviderDisplayName = nls.localize('builtinProviderDisplayName', "Built-in"); @@ -45,7 +45,7 @@ export async function openEditorWith( const overrideToUse = typeof id === 'string' && allEditorOverrides.find(([_, entry]) => entry.id === id); if (overrideToUse) { - return overrideToUse[0].open(input, options, group, id)?.override; + return overrideToUse[0].open(input, options, group, OpenEditorContext.NEW_EDITOR, id)?.override; } // Prompt @@ -108,7 +108,7 @@ export async function openEditorWith( picker.show(); }); - return pickedItem?.handler.open(input!, options, group, pickedItem.id)?.override; + return pickedItem?.handler.open(input!, options, group, OpenEditorContext.NEW_EDITOR, pickedItem.id)?.override; } export const defaultEditorOverrideEntry = Object.freeze({ diff --git a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts similarity index 97% rename from src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts rename to src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts index 52b1cf0897f..a335946a0d5 100644 --- a/src/vs/workbench/contrib/files/electron-browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileActions.contribution.ts @@ -9,7 +9,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { isWindows, isMacintosh } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; @@ -17,7 +17,7 @@ import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; import { IListService } from 'vs/platform/list/browser/listService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { revealResourcesInOS } from 'vs/workbench/contrib/files/electron-browser/fileCommands'; +import { revealResourcesInOS } from 'vs/workbench/contrib/files/electron-sandbox/fileCommands'; import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { appendToCommandPalette, appendEditorTitleContextMenuItem } from 'vs/workbench/contrib/files/browser/fileActions.contribution'; diff --git a/src/vs/workbench/contrib/files/electron-browser/fileCommands.ts b/src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts similarity index 94% rename from src/vs/workbench/contrib/files/electron-browser/fileCommands.ts rename to src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts index 274ee632713..e941d1ece27 100644 --- a/src/vs/workbench/contrib/files/electron-browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/fileCommands.ts @@ -9,7 +9,7 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { sequence } from 'vs/base/common/async'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; // Commands diff --git a/src/vs/workbench/contrib/files/electron-browser/files.contribution.ts b/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts similarity index 76% rename from src/vs/workbench/contrib/files/electron-browser/files.contribution.ts rename to src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts index 737eeea2d5c..3f6524d0f64 100644 --- a/src/vs/workbench/contrib/files/electron-browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/files.contribution.ts @@ -3,17 +3,13 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as os from 'os'; -import * as fs from 'fs'; import * as nls from 'vs/nls'; -import { join } from 'vs/base/common/path'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorInput } from 'vs/workbench/common/editor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; -import { NativeTextFileEditor } from 'vs/workbench/contrib/files/electron-browser/textFileEditor'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { NativeTextFileEditor } from 'vs/workbench/contrib/files/electron-sandbox/textFileEditor'; // Register file editor Registry.as(EditorExtensions.Editors).registerEditor( @@ -26,8 +22,3 @@ Registry.as(EditorExtensions.Editors).registerEditor( new SyncDescriptor(FileEditorInput) ] ); - -// Register mkdtemp command -CommandsRegistry.registerCommand('mkdtemp', function () { - return fs.promises.mkdtemp(join(os.tmpdir(), 'vscodetmp-')); -}); diff --git a/src/vs/workbench/contrib/files/electron-browser/textFileEditor.ts b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts similarity index 94% rename from src/vs/workbench/contrib/files/electron-browser/textFileEditor.ts rename to src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts index 4ea06a47056..b5e50bd3c54 100644 --- a/src/vs/workbench/contrib/files/electron-browser/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/electron-sandbox/textFileEditor.ts @@ -7,8 +7,7 @@ import * as nls from 'vs/nls'; import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { EditorOptions } from 'vs/workbench/common/editor'; -import { FileOperationError, FileOperationResult, IFileService } from 'vs/platform/files/common/files'; -import { MIN_MAX_MEMORY_SIZE_MB, FALLBACK_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/node/files'; +import { FileOperationError, FileOperationResult, IFileService, MIN_MAX_MEMORY_SIZE_MB, FALLBACK_MAX_MEMORY_SIZE_MB } from 'vs/platform/files/common/files'; import { createErrorWithActions } from 'vs/base/common/errorsWithActions'; import { Action } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -23,7 +22,7 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; /** diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 5b78f85f045..172665231a1 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { URI } from 'vs/base/common/uri'; import { toResource } from 'vs/base/test/common/utils'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { workbenchInstantiationService, TestServiceAccessor } from 'vs/workbench/test/browser/workbenchTestServices'; +import { workbenchInstantiationService, TestServiceAccessor, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { EncodingMode, Verbosity } from 'vs/workbench/common/editor'; import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; @@ -17,13 +17,23 @@ import { timeout } from 'vs/base/common/async'; import { ModesRegistry, PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; suite('Files - FileEditorInput', () => { let instantiationService: IInstantiationService; let accessor: TestServiceAccessor; setup(() => { - instantiationService = workbenchInstantiationService(); + instantiationService = workbenchInstantiationService({ + editorService: () => { + return new class extends TestEditorService { + createEditorInput(input: IResourceEditorInput) { + return instantiationService.createInstance(FileEditorInput, input.resource, undefined, undefined); + } + }; + } + }); + accessor = instantiationService.createInstance(TestServiceAccessor); }); diff --git a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts index 7f8da4afd56..d97c0447e45 100644 --- a/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/textFileEditorTracker.test.ts @@ -25,7 +25,7 @@ import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { isEqual } from 'vs/base/common/resources'; +import { isEqual, extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -99,7 +99,7 @@ suite('Files - TextFileEditorTracker', () => { await model.save(); // change event (watcher) - accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }])); + accessor.fileService.fireFileChanges(new FileChangesEvent([{ resource, type: FileChangeType.UPDATED }], extUri)); await timeout(0); // due to event updating model async diff --git a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts similarity index 93% rename from src/vs/workbench/contrib/issue/browser/issue.contribution.ts rename to src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts index 65f8ffb8245..9285c7067a4 100644 --- a/src/vs/workbench/contrib/issue/browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/browser/issue.web.contribution.ts @@ -47,4 +47,8 @@ class RegisterIssueContribution implements IWorkbenchContribution { Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(RegisterIssueContribution, LifecyclePhase.Starting); +CommandsRegistry.registerCommand('_issues.getSystemStatus', (accessor) => { + return nls.localize('statusUnsupported', "The --status argument is not yet supported in browsers."); +}); + registerSingleton(IWebIssueService, WebIssueService, true); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts index 542cfb5d5dc..8a206ec3501 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.contribution.ts @@ -13,7 +13,8 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { WorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issueService'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; -import { IIssueService, IssueReporterData } from 'vs/platform/issue/node/issue'; +import { IssueReporterData } from 'vs/platform/issue/common/issue'; +import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; import { OpenIssueReporterArgs, OpenIssueReporterActionId } from 'vs/workbench/contrib/issue/common/commands'; const helpCategory = { value: nls.localize('help', "Help"), original: 'Help' }; diff --git a/src/vs/workbench/contrib/issue/electron-browser/issue.ts b/src/vs/workbench/contrib/issue/electron-browser/issue.ts index da0a15a4e64..fc6d4b70552 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issue.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issue.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IssueReporterData } from 'vs/platform/issue/node/issue'; +import { IssueReporterData } from 'vs/platform/issue/common/issue'; export const IWorkbenchIssueService = createDecorator('workbenchIssueService'); diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts b/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts index 4b6b25c31c0..4eaaf7c3029 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueActions.ts @@ -5,7 +5,7 @@ import { Action } from 'vs/base/common/actions'; import * as nls from 'vs/nls'; -import { IssueType } from 'vs/platform/issue/node/issue'; +import { IssueType } from 'vs/platform/issue/common/issue'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; export class OpenProcessExplorer extends Action { diff --git a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts index 3efe946f1ee..2aa3029feef 100644 --- a/src/vs/workbench/contrib/issue/electron-browser/issueService.ts +++ b/src/vs/workbench/contrib/issue/electron-browser/issueService.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IssueReporterStyles, IIssueService, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/node/issue'; +import { IssueReporterStyles, IssueReporterData, ProcessExplorerData, IssueReporterExtensionData } from 'vs/platform/issue/common/issue'; +import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue'; import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService'; import { textLinkForeground, inputBackground, inputBorder, inputForeground, buttonBackground, buttonHoverBackground, buttonForeground, inputValidationErrorBorder, foreground, inputActiveOptionBorder, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, editorBackground, editorForeground, listHoverBackground, listHoverForeground, listHighlightForeground, textLinkActiveForeground, inputValidationErrorBackground, inputValidationErrorForeground } from 'vs/platform/theme/common/colorRegistry'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IWorkbenchExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; -import { webFrame } from 'electron'; +import { webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals'; import { assign } from 'vs/base/common/objects'; import { IWorkbenchIssueService } from 'vs/workbench/contrib/issue/electron-browser/issue'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts b/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts index 97c16d4d4af..3109475f950 100644 --- a/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts +++ b/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts @@ -7,7 +7,7 @@ import { Action } from 'vs/base/common/actions'; import { join } from 'vs/base/common/path'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { IFileService } from 'vs/platform/files/common/files'; diff --git a/src/vs/workbench/contrib/markers/browser/markersModel.ts b/src/vs/workbench/contrib/markers/browser/markersModel.ts index 773a02c319a..1d93da1a803 100644 --- a/src/vs/workbench/contrib/markers/browser/markersModel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersModel.ts @@ -3,25 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { basename } from 'vs/base/common/resources'; +import { basename, extUri } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { Range, IRange } from 'vs/editor/common/core/range'; import { IMarker, MarkerSeverity, IRelatedInformation, IMarkerData } from 'vs/platform/markers/common/markers'; -import { isFalsyOrEmpty, mergeSort } from 'vs/base/common/arrays'; -import { values } from 'vs/base/common/map'; -import { memoize } from 'vs/base/common/decorators'; +import { mergeSort, isNonEmptyArray, flatten } from 'vs/base/common/arrays'; +import { ResourceMap } from 'vs/base/common/map'; import { Emitter, Event } from 'vs/base/common/event'; import { Hasher } from 'vs/base/common/hash'; import { withUndefinedAsNull } from 'vs/base/common/types'; -function compareUris(a: URI, b: URI) { - const astr = a.toString(); - const bstr = b.toString(); - return astr === bstr ? 0 : (astr < bstr ? -1 : 1); -} export function compareMarkersByUri(a: IMarker, b: IMarker) { - return compareUris(a.resource, b.resource); + return extUri.compare(a.resource, b.resource); } function compareResourceMarkers(a: ResourceMarkers, b: ResourceMarkers): number { @@ -44,13 +38,45 @@ function compareMarkers(a: Marker, b: Marker): number { export class ResourceMarkers { - @memoize - get path(): string { return this.resource.fsPath; } + readonly path: string; - @memoize - get name(): string { return basename(this.resource); } + readonly name: string; - constructor(readonly id: string, readonly resource: URI, public markers: Marker[]) { } + private markersMap = new ResourceMap(); + private _total: number = 0; + + constructor(readonly id: string, readonly resource: URI) { + this.path = this.resource.fsPath; + this.name = basename(this.resource); + } + + get markers(): readonly Marker[] { + return flatten([...this.markersMap.values()]); + } + + has(uri: URI) { + return this.markersMap.has(uri); + } + + set(uri: URI, marker: Marker[]) { + this.delete(uri); + if (isNonEmptyArray(marker)) { + this.markersMap.set(uri, marker); + this._total += marker.length; + } + } + + delete(uri: URI) { + let array = this.markersMap.get(uri); + if (array) { + this._total -= array.length; + this.markersMap.delete(uri); + } + } + + get total() { + return this._total; + } } export class Marker { @@ -91,9 +117,9 @@ export class RelatedInformation { } export interface MarkerChangesEvent { - readonly added: ResourceMarkers[]; - readonly removed: ResourceMarkers[]; - readonly updated: ResourceMarkers[]; + readonly added: Set; + readonly removed: Set; + readonly updated: Set; } export class MarkersModel { @@ -105,9 +131,8 @@ export class MarkersModel { get resourceMarkers(): ResourceMarkers[] { if (!this.cachedSortedResources) { - this.cachedSortedResources = values(this.resourcesByUri).sort(compareResourceMarkers); + this.cachedSortedResources = [...this.resourcesByUri.values()].sort(compareResourceMarkers); } - return this.cachedSortedResources; } @@ -123,28 +148,33 @@ export class MarkersModel { } getResourceMarkers(resource: URI): ResourceMarkers | null { - return withUndefinedAsNull(this.resourcesByUri.get(resource.toString())); + return withUndefinedAsNull(this.resourcesByUri.get(extUri.getComparisonKey(resource, true))); } setResourceMarkers(resourcesMarkers: [URI, IMarker[]][]): void { - const change: MarkerChangesEvent = { added: [], removed: [], updated: [] }; + const change: MarkerChangesEvent = { added: new Set(), removed: new Set(), updated: new Set() }; for (const [resource, rawMarkers] of resourcesMarkers) { - let resourceMarkers = this.resourcesByUri.get(resource.toString()); - if (isFalsyOrEmpty(rawMarkers)) { - if (resourceMarkers) { - this.resourcesByUri.delete(resource.toString()); - change.removed.push(resourceMarkers); - this._total -= resourceMarkers.markers.length; + + const key = extUri.getComparisonKey(resource, true); + let resourceMarkers = this.resourcesByUri.get(key); + + if (isNonEmptyArray(rawMarkers)) { + // update, add + if (!resourceMarkers) { + const resourceMarkersId = this.id(resource.toString()); + resourceMarkers = new ResourceMarkers(resourceMarkersId, resource.with({ fragment: null })); + this.resourcesByUri.set(key, resourceMarkers); + change.added.add(resourceMarkers); + } else { + change.updated.add(resourceMarkers); } - } else { - const resourceMarkersId = this.id(resource.toString()); const markersCountByKey = new Map(); const markers = mergeSort(rawMarkers.map((rawMarker) => { const key = IMarkerData.makeKey(rawMarker); const index = markersCountByKey.get(key) || 0; markersCountByKey.set(key, index + 1); - const markerId = this.id(resourceMarkersId, key, index); + const markerId = this.id(resourceMarkers!.id, key, index); let relatedInformation: RelatedInformation[] | undefined = undefined; if (rawMarker.relatedInformation) { @@ -154,21 +184,26 @@ export class MarkersModel { return new Marker(markerId, rawMarker, relatedInformation); }), compareMarkers); - if (resourceMarkers) { - this._total -= resourceMarkers.markers.length; - resourceMarkers.markers = markers; - change.updated.push(resourceMarkers); + this._total -= resourceMarkers.total; + resourceMarkers.set(resource, markers); + this._total += resourceMarkers.total; + + } else if (resourceMarkers) { + // clear + this._total -= resourceMarkers.total; + resourceMarkers.delete(resource); + this._total += resourceMarkers.total; + if (resourceMarkers.total === 0) { + this.resourcesByUri.delete(key); + change.removed.add(resourceMarkers); } else { - resourceMarkers = new ResourceMarkers(resourceMarkersId, resource, markers); - change.added.push(resourceMarkers); + change.updated.add(resourceMarkers); } - this._total += resourceMarkers.markers.length; - this.resourcesByUri.set(resource.toString(), resourceMarkers); } } this.cachedSortedResources = undefined; - if (change.added.length || change.removed.length || change.updated.length) { + if (change.added.size || change.removed.size || change.updated.size) { this._onDidChange.fire(change); } } diff --git a/src/vs/workbench/contrib/markers/browser/markersView.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts index 397cd7f274d..c6f9232e833 100644 --- a/src/vs/workbench/contrib/markers/browser/markersView.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -301,7 +301,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { if (markerOrChange instanceof Marker) { this.tree.rerender(markerOrChange); } else { - if (markerOrChange.added.length || markerOrChange.removed.length) { + if (markerOrChange.added.size || markerOrChange.removed.size) { // Reset complete tree this.resetTree(); } else { @@ -705,7 +705,7 @@ export class MarkersView extends ViewPane implements IMarkerFilterController { let selectedElement = this.tree.getSelection(); if (selectedElement && selectedElement.length > 0) { if (selectedElement[0] instanceof Marker) { - if (resource.resource.toString() === (selectedElement[0]).marker.resource.toString()) { + if (resource.has((selectedElement[0]).marker.resource)) { return true; } } diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index 6ccf3e40f3a..b5570494b91 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -22,7 +22,6 @@ .monaco-workbench.vs .monaco-action-bar .markers-panel-action-filter .monaco-inputbox { height: 25px; - border: 1px solid transparent; } .markers-panel-action-filter > .markers-panel-filter-controls { diff --git a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts index 1cff079b0ae..987e9d38583 100644 --- a/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts +++ b/src/vs/workbench/contrib/markers/test/browser/markersModel.test.ts @@ -141,6 +141,28 @@ suite('MarkersModel Test', () => { assert.equal(JSON.stringify({ ...marker, resource: marker.resource.path, relatedInformation: marker.relatedInformation!.map(r => ({ ...r, resource: r.resource.path })) }, null, '\t'), testObject.toString()); }); + test('Markers for same-document but different fragment', function () { + const model = new TestMarkersModel([anErrorWithRange(1)]); + + assert.equal(model.total, 1); + + const document = URI.parse('foo://test/path/file'); + const frag1 = URI.parse('foo://test/path/file#1'); + const frag2 = URI.parse('foo://test/path/file#two'); + + model.setResourceMarkers([[document, [{ ...aMarker(), resource: frag1 }, { ...aMarker(), resource: frag2 }]]]); + + assert.equal(model.total, 3); + let a = model.getResourceMarkers(document); + let b = model.getResourceMarkers(frag1); + let c = model.getResourceMarkers(frag2); + assert.ok(a === b); + assert.ok(a === c); + + model.setResourceMarkers([[document, [{ ...aMarker(), resource: frag2 }]]]); + assert.equal(model.total, 2); + }); + function compareResource(a: ResourceMarkers, b: string): boolean { return a.resource.toString() === URI.file(b).toString(); } diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index e65cd871273..2cdd663f6d8 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -5,7 +5,7 @@ // Scrollable Element -export const SCROLLABLE_ELEMENT_PADDING_TOP = 16; +export const SCROLLABLE_ELEMENT_PADDING_TOP = 20; // Cell sizing related export const CELL_MARGIN = 20; diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index 763dbc7199d..9033a50877f 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -18,8 +18,8 @@ import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/context import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { BaseCellRenderTemplate, CellEditState, CellRunState, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { BaseCellRenderTemplate, CellEditState, ICellViewModel, INotebookEditor, NOTEBOOK_CELL_EDITABLE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_TYPE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -33,6 +33,7 @@ const NOTEBOOK_UNDO = 'notebook.undo'; const NOTEBOOK_CURSOR_UP = 'notebook.cursorUp'; const NOTEBOOK_CURSOR_DOWN = 'notebook.cursorDown'; const CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID = 'notebook.clearAllCellsOutputs'; +const RENDER_ALL_MARKDOWN_CELLS = 'notebook.renderAllMarkdownCells'; // Cell Commands const INSERT_CODE_CELL_ABOVE_COMMAND_ID = 'notebook.cell.insertCodeCellAbove'; @@ -65,6 +66,7 @@ const EXECUTE_CELL_SELECT_BELOW = 'notebook.cell.executeAndSelectBelow'; const EXECUTE_CELL_INSERT_BELOW = 'notebook.cell.executeAndInsertBelow'; const CLEAR_CELL_OUTPUTS_COMMAND_ID = 'notebook.cell.clearOutputs'; const CHANGE_CELL_LANGUAGE = 'notebook.cell.changeLanguage'; +const CENTER_ACTIVE_CELL = 'notebook.centerActiveCell'; const FOCUS_IN_OUTPUT_COMMAND_ID = 'notebook.cell.focusInOutput'; const FOCUS_OUT_OUTPUT_COMMAND_ID = 'notebook.cell.focusOutOutput'; @@ -74,17 +76,52 @@ export const NOTEBOOK_ACTIONS_CATEGORY = localize('notebookActions.category', "N const EDITOR_WIDGET_ACTION_WEIGHT = KeybindingWeight.EditorContrib; // smaller than Suggest Widget, etc const enum CellToolbarOrder { - MoveCellUp, - MoveCellDown, EditCell, SplitCell, SaveCell, ClearCellOutput, - InsertCell, DeleteCell } -registerAction2(class extends Action2 { +abstract class NotebookAction extends Action2 { + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + if (!this.isCellActionContext(context)) { + context = this.getActiveCellContext(accessor); + if (!context) { + return; + } + } + + this.runWithContext(accessor, context); + } + + abstract async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise; + + private isCellActionContext(context: any): context is INotebookCellActionContext { + return context && !!context.cell && !!context.notebookEditor; + } + + private getActiveCellContext(accessor: ServicesAccessor): INotebookCellActionContext | undefined { + const editorService = accessor.get(IEditorService); + + const editor = getActiveNotebookEditor(editorService); + if (!editor) { + return; + } + + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return; + } + + return { + cell: activeCell, + notebookEditor: editor + }; + } +} + +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_CELL_COMMAND_ID, @@ -98,25 +135,18 @@ registerAction2(class extends Action2 { }, weight: EDITOR_WIDGET_ACTION_WEIGHT }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, icon: { id: 'codicon/play' }, f1: true }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { return runCell(context); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: CANCEL_CELL_COMMAND_ID, @@ -128,14 +158,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { return context.notebookEditor.cancelNotebookCellExecution(context.cell); } }); @@ -177,7 +200,7 @@ export class CancelCellAction extends MenuItemAction { } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_CELL_SELECT_BELOW, @@ -188,42 +211,33 @@ registerAction2(class extends Action2 { primary: KeyMod.Shift | KeyCode.Enter, weight: EDITOR_WIDGET_ACTION_WEIGHT }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const activeCell = await runActiveCell(accessor); - if (!activeCell) { - return; - } + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + await runCell(context); - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const idx = editor.viewModel?.getCellIndex(activeCell); + const idx = context.notebookEditor.viewModel?.getCellIndex(context.cell); if (typeof idx !== 'number') { return; } // Try to select below, fall back on inserting - const nextCell = editor.viewModel?.viewCells[idx + 1]; + const nextCell = context.notebookEditor.viewModel?.viewCells[idx + 1]; if (nextCell) { - editor.focusNotebookCell(nextCell, activeCell.editState === CellEditState.Editing ? 'editor' : 'container'); + await context.notebookEditor.focusNotebookCell(nextCell, context.cell.editState === CellEditState.Editing ? 'editor' : 'container'); } else { - const newCell = editor.insertNotebookCell(activeCell, CellKind.Code, 'below'); + const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); if (newCell) { - editor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_CELL_INSERT_BELOW, @@ -234,31 +248,37 @@ registerAction2(class extends Action2 { primary: KeyMod.Alt | KeyCode.Enter, weight: EDITOR_WIDGET_ACTION_WEIGHT }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const activeCell = await runActiveCell(accessor); - if (!activeCell) { - return; - } - - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const newCell = editor.insertNotebookCell(activeCell, CellKind.Code, 'below'); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + await runCell(context); + const newCell = context.notebookEditor.insertNotebookCell(context.cell, CellKind.Code, 'below'); if (newCell) { - editor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { + constructor() { + super({ + id: RENDER_ALL_MARKDOWN_CELLS, + title: localize('notebookActions.renderMarkdown', "Render All Markdown Cells"), + category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, + f1: true + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + renderAllMarkdownCells(context); + } +}); + +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_NOTEBOOK_COMMAND_ID, @@ -269,18 +289,21 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - return editor.executeNotebook(); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + renderAllMarkdownCells(context); + return context.notebookEditor.executeNotebook(); } }); -registerAction2(class extends Action2 { +function renderAllMarkdownCells(context: INotebookCellActionContext): void { + context.notebookEditor.viewModel!.viewCells.forEach(cell => { + if (cell.cellKind === CellKind.Markdown) { + cell.editState = CellEditState.Preview; + } + }); +} + +registerAction2(class extends NotebookAction { constructor() { super({ id: CANCEL_NOTEBOOK_COMMAND_ID, @@ -291,48 +314,32 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - return editor.cancelNotebookExecution(); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + return context.notebookEditor.cancelNotebookExecution(); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: QUIT_EDIT_CELL_COMMAND_ID, title: localize('notebookActions.quitEditing', "Quit Notebook Cell Editing"), category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext, EditorContextKeys.hoverVisible.toNegated()), primary: KeyCode.Escape, weight: EDITOR_WIDGET_ACTION_WEIGHT - 5 }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, }); } - async run(accessor: ServicesAccessor): Promise { - let editorService = accessor.get(IEditorService); - let editor = getActiveNotebookEditor(editorService); - - if (!editor) { - return; + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + if (context.cell.cellKind === CellKind.Markdown) { + context.cell.editState = CellEditState.Preview; } - let activeCell = editor.getActiveCell(); - if (activeCell) { - if (activeCell.cellKind === CellKind.Markdown) { - activeCell.editState = CellEditState.Preview; - } - - editor.focusNotebookCell(activeCell, 'container'); - } + await context.notebookEditor.focusNotebookCell(context.cell, 'container'); } }); @@ -373,7 +380,7 @@ MenuRegistry.appendMenuItem(MenuId.EditorTitle, { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_CELL_RUNNABLE) }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: CHANGE_CELL_TO_CODE_COMMAND_ID, @@ -384,17 +391,17 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR), f1: true }); } - async run(accessor: ServicesAccessor): Promise { - return changeActiveCellToKind(CellKind.Code, accessor); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + await changeCellToKind(CellKind.Code, context); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: CHANGE_CELL_TO_MARKDOWN_COMMAND_ID, @@ -405,13 +412,13 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - return changeActiveCellToKind(CellKind.Markdown, accessor); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + await changeCellToKind(CellKind.Markdown, context); } }); @@ -421,45 +428,14 @@ export function getActiveNotebookEditor(editorService: IEditorService): INoteboo return activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; } -async function runActiveCell(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; - } - - editor.executeNotebookCell(activeCell); - return activeCell; -} - async function runCell(context: INotebookCellActionContext): Promise { - if (context.cell.runState === CellRunState.Running) { + if (context.cell.metadata?.runState === NotebookCellRunState.Running) { return; } return context.notebookEditor.executeNotebookCell(context.cell); } -async function changeActiveCellToKind(kind: CellKind, accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; - } - - changeCellToKind(kind, { cell: activeCell, notebookEditor: editor }); -} - export async function changeCellToKind(kind: CellKind, context: INotebookCellActionContext, language?: string): Promise { const { cell, notebookEditor } = context; @@ -486,43 +462,20 @@ export async function changeCellToKind(kind: CellKind, context: INotebookCellAct newCell.model.language = language; } - notebookEditor.focusNotebookCell(newCell, cell.editState === CellEditState.Editing ? 'editor' : 'container'); + await notebookEditor.focusNotebookCell(newCell, cell.editState === CellEditState.Editing ? 'editor' : 'container'); notebookEditor.deleteNotebookCell(cell); return newCell; } export interface INotebookCellActionContext { - cellTemplate?: BaseCellRenderTemplate; - cell: ICellViewModel; - notebookEditor: INotebookEditor; - ui?: boolean; + readonly cellTemplate?: BaseCellRenderTemplate; + readonly cell: ICellViewModel; + readonly notebookEditor: INotebookEditor; + readonly ui?: boolean; } -function isCellActionContext(context: any): context is INotebookCellActionContext { - return context && !!context.cell && !!context.notebookEditor; -} - -function getActiveCellContext(accessor: ServicesAccessor): INotebookCellActionContext | undefined { - const editorService = accessor.get(IEditorService); - - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return; - } - - return { - cell: activeCell, - notebookEditor: editor - }; -} - -abstract class InsertCellCommand extends Action2 { +abstract class InsertCellCommand extends NotebookAction { constructor( desc: Readonly, private kind: CellKind, @@ -531,17 +484,10 @@ abstract class InsertCellCommand extends Action2 { super(desc); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const newCell = context.notebookEditor.insertNotebookCell(context.cell, this.kind, this.direction, undefined, context.ui); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } @@ -558,7 +504,7 @@ registerAction2(class extends InsertCellCommand { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }, CellKind.Code, @@ -596,7 +542,7 @@ registerAction2(class extends InsertCellCommand { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, }, CellKind.Code, @@ -611,7 +557,7 @@ registerAction2(class extends InsertCellCommand { id: INSERT_MARKDOWN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellAbove', "Insert Markdown Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }, CellKind.Markdown, @@ -643,7 +589,7 @@ registerAction2(class extends InsertCellCommand { id: INSERT_MARKDOWN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.insertMarkdownCellBelow', "Insert Markdown Cell Below"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }, CellKind.Markdown, @@ -651,7 +597,7 @@ registerAction2(class extends InsertCellCommand { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -674,19 +620,12 @@ registerAction2(class extends Action2 { }); } - run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - - return context.notebookEditor.editNotebookCell(context.cell); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + context.notebookEditor.editNotebookCell(context.cell); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -704,19 +643,12 @@ registerAction2(class extends Action2 { }); } - run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return context.notebookEditor.saveNotebookCell(context.cell); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -737,19 +669,12 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, icon: { id: 'codicon/trash' }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { const index = context.notebookEditor.viewModel!.getCellIndex(context.cell); const result = await context.notebookEditor.deleteNotebookCell(context.cell); @@ -757,12 +682,12 @@ registerAction2(class extends Action2 { // deletion succeeds, move focus to the next cell const nextCellIdx = index < context.notebookEditor.viewModel!.length ? index : context.notebookEditor.viewModel!.length - 1; if (nextCellIdx >= 0) { - context.notebookEditor.focusNotebookCell(context.notebookEditor.viewModel!.viewCells[nextCellIdx], 'container'); + await context.notebookEditor.focusNotebookCell(context.notebookEditor.viewModel!.viewCells[nextCellIdx], 'container'); } else { // No cells left, insert a new empty one const newCell = context.notebookEditor.insertNotebookCell(undefined, context.cell.cellKind); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } @@ -776,7 +701,7 @@ async function moveCell(context: INotebookCellActionContext, direction: 'up' | ' if (result) { // move cell command only works when the cell container has focus - context.notebookEditor.focusNotebookCell(context.cell, 'container'); + await context.notebookEditor.focusNotebookCell(context.cell, 'container'); } } @@ -785,11 +710,11 @@ async function copyCell(context: INotebookCellActionContext, direction: 'up' | ' const newCellDirection = direction === 'up' ? 'above' : 'below'; const newCell = context.notebookEditor.insertNotebookCell(context.cell, context.cell.cellKind, newCellDirection, text); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'container'); + await context.notebookEditor.focusNotebookCell(newCell, 'container'); } } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -797,7 +722,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellUp', "Move Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-up' }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyCode.UpArrow, @@ -807,19 +732,12 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return moveCell(context, 'up'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -827,7 +745,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellDown', "Move Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-down' }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyCode.DownArrow, @@ -837,26 +755,19 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return moveCell(context, 'down'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: COPY_CELL_COMMAND_ID, title: localize('notebookActions.copy', "Copy Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -866,14 +777,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { const clipboardService = accessor.get(IClipboardService); const notebookService = accessor.get(INotebookService); clipboardService.writeText(context.cell.getText()); @@ -881,14 +785,14 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: CUT_CELL_COMMAND_ID, title: localize('notebookActions.cut', "Cut Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -898,14 +802,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { const clipboardService = accessor.get(IClipboardService); const notebookService = accessor.get(INotebookService); clipboardService.writeText(context.cell.getText()); @@ -920,14 +817,14 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: PASTE_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.pasteAbove', "Paste Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -937,14 +834,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { const notebookService = accessor.get(INotebookService); const pasteCells = notebookService.getToCopy() || []; @@ -962,14 +852,14 @@ registerAction2(class extends Action2 { }); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: PASTE_CELL_COMMAND_ID, title: localize('notebookActions.paste', "Paste Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -979,14 +869,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { const notebookService = accessor.get(INotebookService); const pasteCells = notebookService.getToCopy() || []; @@ -1005,14 +888,14 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: COPY_CELL_UP_COMMAND_ID, title: localize('notebookActions.copyCellUp', "Copy Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, @@ -1022,26 +905,19 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return copyCell(context, 'up'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: COPY_CELL_DOWN_COMMAND_ID, title: localize('notebookActions.copyCellDown', "Copy Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, @@ -1051,19 +927,12 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return copyCell(context, 'down'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_CURSOR_DOWN, @@ -1077,14 +946,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; const activeCell = context.cell; @@ -1099,11 +961,11 @@ registerAction2(class extends Action2 { return; } - editor.focusNotebookCell(newCell, 'editor'); + await editor.focusNotebookCell(newCell, 'editor'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_CURSOR_UP, @@ -1117,14 +979,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; const activeCell = context.cell; @@ -1144,74 +999,60 @@ registerAction2(class extends Action2 { return; } - editor.focusNotebookCell(newCell, 'editor'); + await editor.focusNotebookCell(newCell, 'editor'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: FOCUS_IN_OUTPUT_COMMAND_ID, title: localize('focusOutput', 'Focus In Active Cell Output'), category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED), + when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - mac: { primary: KeyMod.WinCtrl | KeyCode.DownArrow, }, + mac: { primary: KeyMod.WinCtrl | KeyMod.CtrlCmd | KeyCode.DownArrow, }, weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; const activeCell = context.cell; - editor.focusNotebookCell(activeCell, 'output'); + await editor.focusNotebookCell(activeCell, 'output'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: FOCUS_OUT_OUTPUT_COMMAND_ID, title: localize('focusOutputOut', 'Focus Out Active Cell Output'), category: NOTEBOOK_ACTIONS_CATEGORY, keybinding: { - when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED), + when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - mac: { primary: KeyMod.WinCtrl | KeyCode.UpArrow, }, + mac: { primary: KeyMod.WinCtrl | KeyMod.CtrlCmd | KeyCode.UpArrow, }, weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; const activeCell = context.cell; - editor.focusNotebookCell(activeCell, 'editor'); + await editor.focusNotebookCell(activeCell, 'editor'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_UNDO, @@ -1225,15 +1066,8 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const viewModel = editor.viewModel; + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + const viewModel = context.notebookEditor.viewModel; if (!viewModel) { return; @@ -1243,7 +1077,7 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_REDO, @@ -1257,25 +1091,12 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return; - } - - const viewModel = editor.viewModel; - - if (!viewModel) { - return; - } - - viewModel.redo(); + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + context.notebookEditor.viewModel?.redo(); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_FOCUS_TOP, @@ -1287,30 +1108,23 @@ registerAction2(class extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }, weight: KeybindingWeight.WorkbenchContrib }, - precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; if (!editor.viewModel || !editor.viewModel.length) { return; } const firstCell = editor.viewModel.viewCells[0]; - editor.focusNotebookCell(firstCell, 'container'); + await editor.focusNotebookCell(firstCell, 'container'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_FOCUS_BOTTOM, @@ -1327,25 +1141,18 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; if (!editor.viewModel || !editor.viewModel.length) { return; } const firstCell = editor.viewModel.viewCells[editor.viewModel.length - 1]; - editor.focusNotebookCell(firstCell, 'container'); + await editor.focusNotebookCell(firstCell, 'container'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, @@ -1362,14 +1169,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; if (!editor.viewModel || !editor.viewModel.length) { return; @@ -1384,7 +1184,7 @@ interface ILanguagePickInput extends IQuickPickItem { description: string; } -export class ChangeCellLanguageAction extends Action2 { +export class ChangeCellLanguageAction extends NotebookAction { constructor() { super({ id: CHANGE_CELL_LANGUAGE, @@ -1395,14 +1195,7 @@ export class ChangeCellLanguageAction extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { this.showLanguagePicker(accessor, context); } @@ -1458,7 +1251,7 @@ export class ChangeCellLanguageAction extends Action2 { if (selection.languageId === 'markdown' && context.cell?.language !== 'markdown') { const newCell = await changeCellToKind(CellKind.Markdown, { cell: context.cell, notebookEditor: context.notebookEditor }); if (newCell) { - context.notebookEditor.focusNotebookCell(newCell, 'editor'); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } else if (selection.languageId !== 'markdown' && context.cell?.language === 'markdown') { await changeCellToKind(CellKind.Code, { cell: context.cell, notebookEditor: context.notebookEditor }, selection.languageId); @@ -1489,7 +1282,7 @@ export class ChangeCellLanguageAction extends Action2 { } registerAction2(ChangeCellLanguageAction); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: CLEAR_ALL_CELLS_OUTPUTS_COMMAND_ID, @@ -1507,14 +1300,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { const editor = context.notebookEditor; if (!editor.viewModel || !editor.viewModel.length) { return; @@ -1528,12 +1314,12 @@ async function splitCell(context: INotebookCellActionContext): Promise { if (context.cell.cellKind === CellKind.Code) { const newCells = await context.notebookEditor.splitNotebookCell(context.cell); if (newCells) { - context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], 'editor'); + await context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], 'editor'); } } } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -1551,14 +1337,7 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return splitCell(context); } }); @@ -1567,11 +1346,11 @@ registerAction2(class extends Action2 { async function joinCells(context: INotebookCellActionContext, direction: 'above' | 'below'): Promise { const cell = await context.notebookEditor.joinNotebookCells(context.cell, direction, CellKind.Code); if (cell) { - context.notebookEditor.focusNotebookCell(cell, 'editor'); + await context.notebookEditor.focusNotebookCell(cell, 'editor'); } } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -1583,19 +1362,12 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return joinCells(context, 'above'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -1607,15 +1379,31 @@ registerAction2(class extends Action2 { }); } - async run(accessor: ServicesAccessor, context?: INotebookCellActionContext) { - if (!isCellActionContext(context)) { - context = getActiveCellContext(accessor); - if (!context) { - return; - } - } - + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext) { return joinCells(context, 'below'); } }); +registerAction2(class extends NotebookAction { + constructor() { + super({ + id: CENTER_ACTIVE_CELL, + title: localize('notebookActions.centerActiveCell', "Center Active Cell"), + keybinding: { + when: NOTEBOOK_EDITOR_FOCUSED, + primary: KeyMod.CtrlCmd | KeyCode.KEY_L, + mac: { + primary: KeyMod.WinCtrl | KeyCode.KEY_L, + }, + weight: KeybindingWeight.WorkbenchContrib + }, + category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, + f1: true + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + return context.notebookEditor.revealInCenter(context.cell); + } +}); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts index 4b76b3ff71c..4fed3476101 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -3,81 +3,15 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import { INotebookEditor, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; +import { INotebookEditor, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_HAS_MULTIPLE_KERNELS } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IStatusbarService, IStatusbarEntryAccessor, IStatusbarEntry, StatusbarAlignment } from 'vs/workbench/services/statusbar/common/statusbar'; import { IQuickInputService, QuickPickInput, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import * as nls from 'vs/nls'; -import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; +import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions'; import { NOTEBOOK_ACTIONS_CATEGORY, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; - -export class NotebookEditorStatus extends Disposable implements IWorkbenchContribution { - private _localStore: DisposableStore = new DisposableStore(); - private kernelInfoElement = this._register(new MutableDisposable()); - - constructor( - @IEditorService private readonly editorService: IEditorService, - @IStatusbarService private readonly statusbarService: IStatusbarService, - ) { - super(); - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.editorService.onDidActiveEditorChange(() => this.updateStatusBar())); - this.updateStatusBar(); - } - - private async updateStatusBar(): Promise { - this._localStore.clear(); - - const activeEditorPane = this.editorService.activeEditorPane as any | undefined; - if (!activeEditorPane?.isNotebookEditor) { - this.kernelInfoElement.clear(); - return; - } - const editor = activeEditorPane.getControl() as INotebookEditor; - this._localStore.add(editor.onDidChangeKernel(() => { - this.updateKernelInfo(editor.activeKernel); - })); - - this.updateKernelInfo(editor.activeKernel); - } - - private updateKernelInfo(kernelInfo: INotebookKernelInfo | undefined) { - if (!kernelInfo) { - this.kernelInfoElement.clear(); - return; - } - - const props: IStatusbarEntry = { - text: kernelInfo.label, - ariaLabel: kernelInfo.label, - tooltip: nls.localize('selectKernel', "Select Notebook Kernel"), - command: 'notebook.selectKernel' - }; - - this.updateElement(this.kernelInfoElement, props, 'status.notebook.kernel', nls.localize('selectKernel', "Select Notebook Kernel"), StatusbarAlignment.RIGHT, 50); - } - - private updateElement(element: MutableDisposable, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) { - if (!element.value) { - element.value = this.statusbarService.addEntry(props, id, name, alignment, priority); - } else { - element.value.update(props); - } - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NotebookEditorStatus, LifecyclePhase.Eventually); registerAction2(class extends Action2 { @@ -87,6 +21,13 @@ registerAction2(class extends Action2 { category: NOTEBOOK_ACTIONS_CATEGORY, title: nls.localize('notebookActions.selectKernel', "Select Notebook Kernel"), precondition: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, NOTEBOOK_EDITOR_FOCUSED), + icon: { id: 'codicon/server-environment' }, + menu: { + id: MenuId.EditorTitle, + when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_HAS_MULTIPLE_KERNELS), + group: 'navigation', + order: -2, + }, f1: true }); } @@ -118,6 +59,22 @@ registerAction2(class extends Action2 { }; }); + const provider = notebookService.getContributedNotebookProviders(editor.viewModel!.uri)[0]; + + if (provider.kernel) { + picks.unshift({ + id: provider.id, + label: provider.displayName, + picked: !activeKernel, // no active kernel, the builtin kernel of the provider is used + description: activeKernel === undefined + ? nls.localize('currentActiveBuiltinKernel', " (Currently Active)") + : '', + run: () => { + editor.activeKernel = undefined; + } + }); + } + const action = await quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true }); return action?.run(); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts index 6166b067465..fda732869c5 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts @@ -29,13 +29,13 @@ TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements icon: cell.cellKind === CellKind.Markdown ? Codicon.markdown : Codicon.code, label: matches[j].replace(/^[ \t]*(\#+)/, ''), pick() { - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); - notebookWidget.focusNotebookCell(cell, cell.cellKind === CellKind.Markdown ? 'container' : 'editor'); + notebookWidget?.revealInCenterIfOutsideViewport(cell); + notebookWidget?.selectElement(cell); + notebookWidget?.focusNotebookCell(cell, cell.cellKind === CellKind.Markdown ? 'container' : 'editor'); }, preview() { - notebookWidget.revealInCenterIfOutsideViewport(cell); - notebookWidget.selectElement(cell); + notebookWidget?.revealInCenterIfOutsideViewport(cell); + notebookWidget?.selectElement(cell); } }); } diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index 606d713d369..f83999175b2 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .notebookOverlay.notebook-editor { +.monaco-workbench .monaco-workbench .notebookOverlay.notebook-editor { box-sizing: border-box; line-height: 22px; user-select: initial; @@ -17,40 +17,40 @@ white-space: initial; } -.notebookOverlay .simple-fr-find-part-wrapper.visible { +.monaco-workbench .notebookOverlay .simple-fr-find-part-wrapper.visible { z-index: 100; } -.notebookOverlay .cell-list-container .overflowingContentWidgets > div { +.monaco-workbench .notebookOverlay .cell-list-container .overflowingContentWidgets > div { z-index: 600 !important; /* @rebornix: larger than the editor title bar */ } -.notebookOverlay .cell-list-container .monaco-list-rows { +.monaco-workbench .notebookOverlay .cell-list-container .monaco-list-rows { min-height: 100%; overflow: visible !important; } -.notebookOverlay .cell-list-container { +.monaco-workbench .notebookOverlay .cell-list-container { position: relative; } -.notebookOverlay.global-drag-active .webview { +.monaco-workbench .notebookOverlay.global-drag-active .webview { pointer-events: none; } -.notebookOverlay .cell-list-container .webview-cover { +.monaco-workbench .notebookOverlay .cell-list-container .webview-cover { position: absolute; top: 0; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { cursor: default; overflow: visible !important; width: 100%; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { +.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; @@ -58,55 +58,55 @@ padding-top: 8px; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { top: 8px !important; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { bottom: 8px; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { display: none !important; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { display: none; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { display: none; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { width: calc(100% - 32px); /* minus left gutter */ } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { /* Rendered code content - show a single unwrapped line */ height: 20px; overflow: hidden; white-space: pre-wrap; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { white-space: nowrap; overflow: hidden; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { display: flex; } -.notebookOverlay .notebook-content-widgets { +.monaco-workbench .notebookOverlay .notebook-content-widgets { position: absolute; top: 0; left: 0; width: 100%; } -.notebookOverlay .output { +.monaco-workbench .notebookOverlay .output { padding-left: 8px; padding-right: 8px; user-select: text; @@ -115,22 +115,28 @@ box-sizing: border-box; } -.notebookOverlay .output p { +.monaco-workbench .notebookOverlay .output p { white-space: initial; overflow-x: auto; margin: 0px; } -.notebookOverlay .output > div.foreground { +.monaco-workbench .notebookOverlay .output > div.foreground { padding: 8px; box-sizing: border-box; } -.notebookOverlay .cell-drag-image .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .output > div.foreground .output-stream, +.monaco-workbench .notebookOverlay .output > div.foreground .output-plaintext { + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + white-space: pre-wrap; +} + +.monaco-workbench .notebookOverlay .cell-drag-image .output .multi-mimetype-output { display: none; } -.notebookOverlay .output .multi-mimetype-output { +.monaco-workbench .notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; left: -32px; @@ -139,27 +145,27 @@ cursor: pointer; } -.notebookOverlay .output .error_message { +.monaco-workbench .notebookOverlay .output .error_message { color: red; } -.notebookOverlay .output .error > div { +.monaco-workbench .notebookOverlay .output .error > div { white-space: normal; } -.notebookOverlay .output .error pre.traceback { +.monaco-workbench .notebookOverlay .output .error pre.traceback { margin: 8px 0; } -.notebookOverlay .output .error .traceback > span { +.monaco-workbench .notebookOverlay .output .error .traceback > span { display: block; } -.notebookOverlay .output .display img { +.monaco-workbench .notebookOverlay .output .display img { max-width: 100%; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { position: absolute; left: 0; top: 28px; @@ -170,40 +176,40 @@ } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { visibility: visible; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { outline: none !important; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { cursor: pointer; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { visibility: hidden; display: inline-block; position: absolute; height: 26px; - right: 36px; - top: -14px; + right: 32px; + top: -20px; /* this lines up the bottom toolbar border with the current line when on line 01 */ z-index: 30; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { width: 24px; height: 24px; display: flex; @@ -211,59 +217,43 @@ margin: 1px 2px; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { display: flex; align-items: center; margin: auto; } -.notebookOverlay .cell-statusbar-container { +.monaco-workbench .notebookOverlay .cell-statusbar-container { height: 21px; font-size: 12px; display: flex; position: relative; } -.notebookOverlay .cell-statusbar-container .cell-status-left { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; } -.notebookOverlay .cell-statusbar-container .cell-status-right { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; } -.notebookOverlay .cell-statusbar-container .cell-language-picker { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker { padding: 0px 6px; cursor: pointer; } -.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { - background-color: rgba(255, 255, 255, 0.6); -} - -.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { - background-color: rgba(255, 255, 255, 0.9); -} - -.monaco-workbench.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { - background-color: rgba(255, 255, 255, 0.15); -} - -.monaco-workbench.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:active { - background-color: rgba(255, 255, 255, 0.2); -} - -.notebookOverlay .cell-statusbar-container .cell-run-duration { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-duration { margin-right: 8px; } -.notebookOverlay .cell-statusbar-container .cell-status-message { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-status-message { display: flex; align-items: center; } -.notebookOverlay .cell-statusbar-container .cell-run-status { +.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; align-items: center; @@ -272,54 +262,40 @@ margin-right: 2px; } -.notebookOverlay .cell-statusbar-container .codicon { +.monaco-workbench .notebookOverlay .cell-statusbar-container .codicon { font-size: 14px; } -.notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { - color: #89D185; -} - -.monaco-workbench.vs .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { - color: #388A34; -} - -.notebookOverlay .cell-status-placeholder { +.monaco-workbench .notebookOverlay .cell-status-placeholder { position: absolute; left: 18px; - color: #ccc9; display: flex; align-items: center; bottom: 0px; top: 0px; } - -.monaco-workbench.vs .notebookOverlay .cell-status-placeholder { - color: #616161e6; -} - -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { position: relative; height: 22px; flex-shrink: 0; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { margin-top: 8px; visibility: hidden; } -.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 .cell .run-button-container .monaco-toolbar .codicon { margin-right: 8px; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.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 .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 { visibility: visible; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .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 .run-button-container .execution-count-label { position: absolute; top: 2px; font-size: 10px; @@ -333,17 +309,17 @@ opacity: .6; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, -.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, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { +.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; } -.notebookOverlay .cell .cell-editor-part { +.monaco-workbench .notebookOverlay .cell .cell-editor-part { position: relative; } -.notebookOverlay .cell .monaco-progress-container { +.monaco-workbench .notebookOverlay .cell .monaco-progress-container { top: -5px; position: absolute; @@ -352,23 +328,23 @@ height: 2px; } -.notebookOverlay .cell .monaco-progress-container .progress-bit { +.monaco-workbench .notebookOverlay .cell .monaco-progress-container .progress-bit { height: 2px; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-toolbar, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.cell-output-hover > .monaco-toolbar, -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-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 > .monaco-toolbar, +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { visibility: visible; } -.notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { outline: none !important; } -.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { display: block; content: ' '; position: absolute; @@ -379,16 +355,29 @@ top: 22px; bottom: 36px; visibility: hidden; + opacity: 0.6; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator:hover { + cursor: grab; +} + +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator .codicon:hover { cursor: pointer; } -.notebookOverlay .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, -.notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, -.notebookOverlay .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { visibility: visible; } -.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { +.monaco-workbench .notebookOverlay .monaco-list:focus .monaco-list-row .notebook-cell-focus-indicator, +.monaco-workbench .notebookOverlay .monaco-list:focus-within .monaco-list-row .notebook-cell-focus-indicator { + opacity: 1; +} + +.monaco-workbench .notebookOverlay .monaco-list-row .cell-editor-part:before { z-index: 20; content: ""; right: 0px; @@ -401,11 +390,11 @@ pointer-events: none; } -.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { +.monaco-workbench .notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { top: -15px; } -.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { +.monaco-workbench .notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { position: absolute; height: 2px; left: 0px; @@ -415,11 +404,11 @@ z-index: 10; } -.notebookOverlay > .cell-list-container > .monaco-list .monaco-list-row.cell-dragging { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list .monaco-list-row.cell-dragging { opacity: 0.5 !important; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { +.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; opacity: 0; @@ -428,28 +417,28 @@ padding: 0; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { display: none; } -.notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, -.notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, +.monaco-workbench .notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { opacity: 1; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { height: 1px; flex-grow: 1; align-self: center; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { height: 1px; width: 16px; align-self: center; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { display: flex; margin: 0 8px; padding: 0 8px; @@ -460,7 +449,7 @@ font-size: 12px; } -.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { +.monaco-workbench .notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { text-align: center; font-size: 14px; color: inherit; @@ -469,34 +458,34 @@ /* markdown */ -.notebookOverlay .cell.markdown img { +.monaco-workbench .notebookOverlay .cell.markdown img { max-width: 100%; max-height: 100%; } -.notebookOverlay .cell.markdown a { +.monaco-workbench .notebookOverlay .cell.markdown a { text-decoration: none; } -.notebookOverlay .cell.markdown a:hover { +.monaco-workbench .notebookOverlay .cell.markdown a:hover { text-decoration: underline; } -.notebookOverlay .cell.markdown a:focus, -.notebookOverlay .cell.markdown input:focus, -.notebookOverlay .cell.markdown select:focus, -.notebookOverlay .cell.markdown textarea:focus { +.monaco-workbench .notebookOverlay .cell.markdown a:focus, +.monaco-workbench .notebookOverlay .cell.markdown input:focus, +.monaco-workbench .notebookOverlay .cell.markdown select:focus, +.monaco-workbench .notebookOverlay .cell.markdown textarea:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; } -.notebookOverlay .cell.markdown hr { +.monaco-workbench .notebookOverlay .cell.markdown hr { border: 0; height: 2px; border-bottom: 2px solid; } -.notebookOverlay .cell.markdown h1 { +.monaco-workbench .notebookOverlay .cell.markdown h1 { padding-bottom: 0.3em; line-height: 1.2; border-bottom-width: 1px; @@ -504,107 +493,107 @@ border-color: rgba(255, 255, 255, 0.18); } -.monaco-workbench.vs .notebookOverlay .cell.markdown h1 { +.monaco-workbench.vs .monaco-workbench .notebookOverlay .cell.markdown h1 { border-color: rgba(0, 0, 0, 0.18); } -.notebookOverlay .cell.markdown h1, -.notebookOverlay .cell.markdown h2, -.notebookOverlay .cell.markdown h3 { +.monaco-workbench .notebookOverlay .cell.markdown h1, +.monaco-workbench .notebookOverlay .cell.markdown h2, +.monaco-workbench .notebookOverlay .cell.markdown h3 { font-weight: normal; } -.notebookOverlay .cell.markdown div { +.monaco-workbench .notebookOverlay .cell.markdown div { width: 100%; } /* Adjust margin of first item in markdown cell */ -.notebookOverlay .cell.markdown div *:first-child { +.monaco-workbench .notebookOverlay .cell.markdown div *:first-child { margin-top: 4px; } /* h1 tags don't need top margin */ -.notebookOverlay .cell.markdown div h1:first-child { +.monaco-workbench .notebookOverlay .cell.markdown div h1:first-child { margin-top: 0; } /* Removes bottom margin when only one item exists in markdown cell */ -.notebookOverlay .cell.markdown div *:only-child, -.notebookOverlay .cell.markdown div *:last-child { +.monaco-workbench .notebookOverlay .cell.markdown div *:only-child, +.monaco-workbench .notebookOverlay .cell.markdown div *:last-child { margin-bottom: 0; } /* makes all markdown cells consistent */ -.notebookOverlay .cell.markdown div { +.monaco-workbench .notebookOverlay .cell.markdown div { min-height: 32px; } -.notebookOverlay .cell.markdown table { +.monaco-workbench .notebookOverlay .cell.markdown table { border-collapse: collapse; border-spacing: 0; } -.notebookOverlay .cell.markdown table th, -.notebookOverlay .cell.markdown table td { +.monaco-workbench .notebookOverlay .cell.markdown table th, +.monaco-workbench .notebookOverlay .cell.markdown table td { border: 1px solid; } -.notebookOverlay .cell.markdown table > thead > tr > th { +.monaco-workbench .notebookOverlay .cell.markdown table > thead > tr > th { text-align: left; border-bottom: 1px solid; } -.notebookOverlay .cell.markdown table > thead > tr > th, -.notebookOverlay .cell.markdown table > thead > tr > td, -.notebookOverlay .cell.markdown table > tbody > tr > th, -.notebookOverlay .cell.markdown table > tbody > tr > td { +.monaco-workbench .notebookOverlay .cell.markdown table > thead > tr > th, +.monaco-workbench .notebookOverlay .cell.markdown table > thead > tr > td, +.monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr > th, +.monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr > td { padding: 5px 10px; } -.notebookOverlay .cell.markdown table > tbody > tr + tr > td { +.monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr + tr > td { border-top: 1px solid; } -.notebookOverlay .cell.markdown blockquote { +.monaco-workbench .notebookOverlay .cell.markdown blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left-width: 5px; border-left-style: solid; } -.notebookOverlay .cell.markdown code { +.monaco-workbench .notebookOverlay .cell.markdown code { font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; font-size: 1em; line-height: 1.357em; } -.notebookOverlay .cell.markdown body.wordWrap pre { +.monaco-workbench .notebookOverlay .cell.markdown body.wordWrap pre { white-space: pre-wrap; } -.notebookOverlay .cell.markdown pre:not(.hljs), -.notebookOverlay .cell.markdown pre.hljs code > div { +.monaco-workbench .notebookOverlay .cell.markdown pre:not(.hljs), +.monaco-workbench .notebookOverlay .cell.markdown pre.hljs code > div { padding: 16px; border-radius: 3px; overflow: auto; } -.notebookOverlay .cell.markdown pre code { +.monaco-workbench .notebookOverlay .cell.markdown pre code { color: var(--vscode-editor-foreground); tab-size: 4; } -.notebookOverlay .cell.markdown .latex-block { +.monaco-workbench .notebookOverlay .cell.markdown .latex-block { display: block; } -.notebookOverlay .cell.markdown .latex { +.monaco-workbench .notebookOverlay .cell.markdown .latex { vertical-align: middle; display: inline-block; } -.notebookOverlay .cell.markdown .latex img, -.notebookOverlay .cell.markdown .latex-block img { +.monaco-workbench .notebookOverlay .cell.markdown .latex img, +.monaco-workbench .notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(0) } @@ -613,47 +602,50 @@ filter: brightness(0) invert(1) } -.notebookOverlay > .cell-list-container .notebook-folding-indicator { +.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator { position: absolute; top: 8px; left: 6px; - cursor: pointer; +} + +.monaco-workbench .notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { + visibility: visible; } /** Theming */ -/* .notebookOverlay .cell.markdown pre { +/* .monaco-workbench .notebookOverlay .cell.markdown pre { background-color: rgba(220, 220, 220, 0.4); } -.monaco-workbench.vs-dark .notebookOverlay .cell.markdown pre { +.monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown pre { background-color: rgba(10, 10, 10, 0.4); } -.monaco-workbench.hc-black .notebookOverlay .cell.markdown pre { +.monaco-workbench.hc-black .monaco-workbench .notebookOverlay .cell.markdown pre { background-color: rgb(0, 0, 0); } -.monaco-workbench.hc-black .notebookOverlay .cell.markdown h1 { +.monaco-workbench.hc-black .monaco-workbench .notebookOverlay .cell.markdown h1 { border-color: rgb(0, 0, 0); } -.notebookOverlay .cell.markdown table > thead > tr > th { +.monaco-workbench .notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(0, 0, 0, 0.18); } -.monaco-workbench.vs-dark .notebookOverlay .cell.markdown table > thead > tr > th { +.monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(255, 255, 255, 0.18); } -.notebookOverlay .cell.markdown h1, -.notebookOverlay .cell.markdown hr, -.notebookOverlay .cell.markdown table > tbody > tr > td { +.monaco-workbench .notebookOverlay .cell.markdown h1, +.monaco-workbench .notebookOverlay .cell.markdown hr, +.monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(0, 0, 0, 0.18); } -.monaco-workbench.vs-dark .notebookOverlay .cell.markdown h1, -.monaco-workbench.vs-dark .notebookOverlay .cell.markdown hr, -.monaco-workbench.vs-dark .notebookOverlay .cell.markdown table > tbody > tr > td { +.monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown h1, +.monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown hr, +.monaco-workbench.vs-dark .monaco-workbench .notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(255, 255, 255, 0.18); } */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index f447218cf5d..8c716f21d4e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -30,12 +30,16 @@ import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookS import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; import { CellKind, CellUri } 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 { IEditorGroup, OpenEditorContext } 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/editorAssociationsSetting'; import { coalesce, distinct } from 'vs/base/common/arrays'; 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 { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; +import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; // Editor Contribution @@ -52,8 +56,6 @@ import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; 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 { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; /*--------------------------------------------------------------------------------------------- */ @@ -94,7 +96,9 @@ Registry.as(EditorInputExtensions.EditorInputFactor return undefined; } - const input = NotebookEditorInput.getOrCreate(instantiationService, resource, name, viewType); + // if we have two editors open with the same resource (in different editor groups), we should then create two different + // editor inputs, instead of `getOrCreate`. + const input = NotebookEditorInput.create(instantiationService, resource, name, viewType); if (typeof data.group === 'number') { input.updateGroup(data.group); } @@ -112,14 +116,29 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri private _resourceMapping = new ResourceMap(); constructor( - @IEditorService private readonly editorService: EditorServiceImpl, + @IEditorService private readonly editorService: IEditorService, @INotebookService private readonly notebookService: INotebookService, @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configurationService: IConfigurationService - + @IConfigurationService private readonly configurationService: IConfigurationService, + @IUndoRedoService undoRedoService: IUndoRedoService ) { super(); + this._register(undoRedoService.registerUriComparisonKeyComputer({ + getComparisonKey: (uri: URI): string | null => { + if (uri.scheme !== CellUri.scheme) { + return null; + } + + const data = CellUri.parse(uri); + if (!data) { + return null; + } + + return data.notebook.toString(); + } + })); + this._register(this.editorService.overrideOpenEditor({ getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => { const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource)); @@ -138,13 +157,25 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri }; }); }, - open: (editor, options, group, id) => this.onEditorOpening(editor, options, group, id) + open: (editor, options, group, context, id) => this.onEditorOpening(editor, options, group, context, id) + })); + + this._register(this.editorService.onDidVisibleEditorsChange(() => { + const visibleNotebookEditors = editorService.visibleEditorPanes + .filter(pane => (pane as any).isNotebookEditor) + .map(pane => pane.getControl() as INotebookEditor) + .map(editor => editor.getId()); + + this.notebookService.updateVisibleNotebookEditor(visibleNotebookEditors); })); this._register(this.editorService.onDidActiveEditorChange(() => { - if (this.editorService.activeEditor && this.editorService.activeEditor! instanceof NotebookEditorInput) { - let editorInput = this.editorService.activeEditor! as NotebookEditorInput; - this.notebookService.updateActiveNotebookDocument(editorInput.viewType!, editorInput.resource!); + const activeEditorPane = editorService.activeEditorPane as any | undefined; + const notebookEditor = activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; + if (notebookEditor) { + this.notebookService.updateActiveNotebookEditor(notebookEditor); + } else { + this.notebookService.updateActiveNotebookEditor(null); } })); @@ -153,9 +184,15 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return; } - if (!this.editorService.editors.some(other => other === editor)) { - editor.dispose(); + if (!this.editorService.editors.some(other => ( + other.resource === editor.resource + && other instanceof NotebookEditorInput + && other.viewType === editor.viewType + ))) { + editor.clearTextModel(); } + + editor.dispose(); })); } @@ -178,18 +215,30 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return this.notebookService.getContributedNotebookProviders(resource); } - private onEditorOpening(originalInput: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, id: string | undefined): IOpenEditorOverride | undefined { + private onEditorOpening(originalInput: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, context: OpenEditorContext, id: string | undefined): IOpenEditorOverride | undefined { if (originalInput instanceof NotebookEditorInput) { if ((originalInput.group === group.id || originalInput.group === undefined) && (originalInput.viewType === id || typeof id !== 'string')) { // No need to do anything originalInput.updateGroup(group.id); - return undefined; + return { + override: this.editorService.openEditor(originalInput, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) + }; } else { // Create a copy of the input. // Unlike normal editor inputs, we do not want to share custom editor inputs // between multiple editors / groups. const copiedInput = this.instantiationService.createInstance(NotebookEditorInput, originalInput.resource, originalInput.name, originalInput.viewType); copiedInput.updateGroup(group.id); + + if (context === OpenEditorContext.MOVE_EDITOR) { + // transfer ownership of editor widget + const widgetRef = NotebookRegistry.getNotebookEditorWidget(originalInput); + if (widgetRef) { + NotebookRegistry.releaseNotebookEditorWidget(originalInput); + NotebookRegistry.claimNotebookEditorWidget(copiedInput, widgetRef); + } + } + return { override: this.editorService.openEditor(copiedInput, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) }; @@ -215,6 +264,13 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri // user pick a non-notebook editor for this resource return undefined; } + } else { + const existingEditors = group.editors.filter(editor => editor.resource && isEqual(editor.resource, resource) && (editor instanceof NotebookEditorInput) && editor.viewType === id); + + if (existingEditors.length) { + // switch to this cell + return { override: this.editorService.openEditor(existingEditors[0], new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) }; + } } if (this._resourceMapping.has(resource)) { @@ -223,6 +279,8 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri if (!input!.isDisposed()) { input?.updateGroup(group.id); return { override: this.editorService.openEditor(input!, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true }), group) }; + } else { + this._resourceMapping.delete(resource); } } @@ -237,7 +295,7 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri const name = basename(data.notebook); let input = this._resourceMapping.get(data.notebook); if (!input || input.isDisposed()) { - input = NotebookEditorInput.getOrCreate(this.instantiationService, data.notebook, name, info.id); + input = NotebookEditorInput.create(this.instantiationService, data.notebook, name, info.id); this._resourceMapping.set(data.notebook, input); } @@ -253,11 +311,20 @@ export class NotebookContribution extends Disposable implements IWorkbenchContri return undefined; } - const input = NotebookEditorInput.getOrCreate(this.instantiationService, resource, originalInput.getName(), info.id); + const input = NotebookEditorInput.create(this.instantiationService, resource, originalInput.getName(), info.id); input.updateGroup(group.id); this._resourceMapping.set(resource, input); - return { override: this.editorService.openEditor(input, options, group) }; + /** + * Scenario: we are reopening a file editor input which is pinned, we should open in a new editor tab. + */ + let index = undefined; + if (group.activeEditor === originalInput && isEqual(originalInput.resource, resource)) { + const originalEditorIndex = group.getIndexOfEditor(originalInput); + index = group.isPinned(originalInput) ? originalEditorIndex + 1 : originalEditorIndex; + } + + return { override: this.editorService.openEditor(input, new NotebookEditorOptions(options || {}).with({ ignoreOverrides: true, index }), group) }; } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 7f5062358ee..0a6cb18fab2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -18,14 +18,14 @@ import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; import { IPosition } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; import { CellLanguageStatusBarItem, 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, IOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, IRenderOutput, NotebookCellMetadata, NotebookDocumentMetadata, INotebookKernelInfo, IEditor } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { ICompositeCodeEditor } from 'vs/editor/common/editorCommon'; +import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -47,6 +47,10 @@ export const NOTEBOOK_CELL_MARKDOWN_EDIT_MODE = new RawContextKey('note export const NOTEBOOK_CELL_RUN_STATE = new RawContextKey('notebookCellRunState', undefined); // idle, running export const NOTEBOOK_CELL_HAS_OUTPUTS = new RawContextKey('notebookCellHasOutputs', false); // bool +// Kernels + +export const NOTEBOOK_HAS_MULTIPLE_KERNELS = new RawContextKey('notebookHasMultipleKernels', false); + export interface NotebookLayoutInfo { width: number; height: number; @@ -81,6 +85,7 @@ export interface CodeCellLayoutChangeEvent { export interface MarkdownCellLayoutInfo { readonly fontInfo: BareFontInfo | null; readonly editorWidth: number; + readonly editorHeight: number; readonly bottomToolbarOffset: number; readonly totalHeight: number; } @@ -89,6 +94,7 @@ export interface MarkdownCellLayoutChangeEvent { font?: BareFontInfo; outerWidth?: number; totalHeight?: number; + editorHeight?: boolean; } export interface ICellViewModel { @@ -101,7 +107,6 @@ export interface ICellViewModel { language: string; cellKind: CellKind; editState: CellEditState; - readonly runState: CellRunState; currentTokenSource: CancellationTokenSource | undefined; focusMode: CellFocusMode; getText(): string; @@ -137,7 +142,7 @@ export interface INotebookEditorContribution { restoreViewState?(state: any): void; } -export interface INotebookEditor extends ICompositeCodeEditor { +export interface INotebookEditor extends IEditor { /** * Notebook view model attached to the current editor @@ -148,11 +153,13 @@ export interface INotebookEditor extends ICompositeCodeEditor { * An event emitted when the model of this editor has changed. * @event */ - readonly onDidChangeModel: Event; + readonly onDidChangeModel: Event; + readonly onDidFocusEditorWidget: Event; isNotebookEditor: boolean; activeKernel: INotebookKernelInfo | undefined; readonly onDidChangeKernel: Event; + getId(): string; getDomNode(): HTMLElement; getInnerWebview(): Webview | undefined; @@ -161,6 +168,8 @@ export interface INotebookEditor extends ICompositeCodeEditor { */ focus(): void; + hasFocus(): boolean; + /** * Select & focus cell */ @@ -261,12 +270,12 @@ export interface INotebookEditor extends ICompositeCodeEditor { /** * Render the output in webview layer */ - createInset(cell: ICellViewModel, output: IOutput, shadowContent: string, offset: number): void; + createInset(cell: ICellViewModel, output: IProcessedOutput, shadowContent: string, offset: number): void; /** * Remove the output from the webview layer */ - removeInset(output: IOutput): void; + removeInset(output: IProcessedOutput): void; /** * Send message to the webview for outputs. @@ -357,7 +366,8 @@ export interface INotebookEditor extends ICompositeCodeEditor { } export interface INotebookCellList { - elementAt(position: number): ICellViewModel; + readonly contextKeyService: IContextKeyService; + elementAt(position: number): ICellViewModel | undefined; elementHeight(element: ICellViewModel): number; onWillScroll: Event; onDidChangeFocus: Event>; @@ -367,8 +377,8 @@ export interface INotebookCellList { scrollLeft: number; length: number; rowsContainer: HTMLElement; - readonly onDidRemoveOutput: Event; - readonly onDidHideOutput: Event; + readonly onDidRemoveOutput: Event; + readonly onDidHideOutput: Event; readonly onMouseUp: Event>; readonly onMouseDown: Event>; detachViewModel(): void; @@ -407,6 +417,7 @@ export interface INotebookCellList { } export interface BaseCellRenderTemplate { + contextKeyService: IContextKeyService; container: HTMLElement; cellContainer: HTMLElement; toolbar: ToolBar; @@ -445,7 +456,7 @@ export interface IOutputTransformContribution { */ dispose(): void; - render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; + render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput; } export interface CellFindMatch { @@ -463,11 +474,6 @@ export enum CellRevealPosition { Center } -export enum CellRunState { - Idle, - Running -} - export enum CellEditState { /** * Default state. @@ -499,7 +505,6 @@ export interface CellViewModelStateChangeEvent { metadataChanged?: boolean; selectionChanged?: boolean; focusModeChanged?: boolean; - runStateChanged?: boolean; editStateChanged?: boolean; languageChanged?: boolean; foldingStateChanged?: boolean; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 759d7a54e93..3c2c26c9c58 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { MutableDisposable } from 'vs/base/common/lifecycle'; +import { MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -18,6 +18,7 @@ import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/noteb import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; @@ -25,8 +26,12 @@ export class NotebookEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.notebook'; private editorMemento: IEditorMemento; private readonly groupListener = this._register(new MutableDisposable()); - private _widget: NotebookEditorWidget; + private _widget?: NotebookEditorWidget; private _rootElement!: HTMLElement; + private dimension: DOM.Dimension | null = null; + private _widgetDisposableStore: DisposableStore = new DisposableStore(); + private readonly _onDidFocusWidget = this._register(new Emitter()); + public get onDidFocus(): Event { return this._onDidFocusWidget.event; } constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -36,7 +41,7 @@ export class NotebookEditor extends BaseEditor { @IEditorGroupsService editorGroupService: IEditorGroupsService) { super(NotebookEditor.ID, telemetryService, themeService, storageService); - this._widget = this.instantiationService.createInstance(NotebookEditorWidget); + // this._widget = this.instantiationService.createInstance(NotebookEditorWidget); this.editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); } @@ -45,12 +50,14 @@ export class NotebookEditor extends BaseEditor { set viewModel(newModel: NotebookViewModel | undefined) { - this._widget.viewModel = newModel; - this._onDidChangeModel.fire(); + if (this._widget) { + this._widget.viewModel = newModel; + this._onDidChangeModel.fire(); + } } get viewModel() { - return this._widget.viewModel; + return this._widget?.viewModel; } get minimumWidth(): number { return 375; } @@ -71,9 +78,9 @@ export class NotebookEditor extends BaseEditor { protected createEditor(parent: HTMLElement): void { this._rootElement = DOM.append(parent, DOM.$('.notebook-editor')); - this._widget.createEditor(); - this._register(this.onDidFocus(() => this._widget.updateEditorFocus())); - this._register(this.onDidBlur(() => this._widget.updateEditorFocus())); + // this._widget.createEditor(); + this._register(this.onDidFocus(() => this._widget?.updateEditorFocus())); + this._register(this.onDidBlur(() => this._widget?.updateEditorFocus())); } getDomNode() { @@ -89,8 +96,8 @@ export class NotebookEditor extends BaseEditor { this.saveEditorViewState(this.input); } - this._widget.onWillHide(); - super.onHide(); + this._widget?.onWillHide(); + super.onWillHide(); } setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { @@ -111,28 +118,70 @@ export class NotebookEditor extends BaseEditor { focus() { super.focus(); - this._widget.focus(); + this._widget?.focus(); } async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { if (this.input instanceof NotebookEditorInput) { - this.saveEditorViewState(this.input); + if (!this.input.isDisposed()) { + // set a new input, let's hide previous input + this.saveEditorViewState(this.input as NotebookEditorInput); + this._widget?.onWillHide(); + } } await super.setInput(input, options, token); - const model = await input.resolve(); + // input attached + Event.once(input.onDispose)(() => { + // make sure the editor widget is removed from the view + const existingEditorWidgetForInput = NotebookRegistry.getNotebookEditorWidget(this.input as NotebookEditorInput); + if (existingEditorWidgetForInput) { + existingEditorWidgetForInput?.getDomNode().remove(); + existingEditorWidgetForInput?.dispose(); + NotebookRegistry.releaseNotebookEditorWidget(this.input as NotebookEditorInput); + } + }); + + this._widgetDisposableStore.clear(); + + const existingEditorWidgetForInput = NotebookRegistry.getNotebookEditorWidget(input); + if (existingEditorWidgetForInput) { + // hide current widget + this._widget?.onWillHide(); + // previous widget is then detached + // set the new one + this._widget = existingEditorWidgetForInput; + NotebookRegistry.claimNotebookEditorWidget(input, this._widget); + } else { + // hide current widget + this._widget?.onWillHide(); + // create a new widget + this._widget = this.instantiationService.createInstance(NotebookEditorWidget); + this._widget.createEditor(); + NotebookRegistry.claimNotebookEditorWidget(input, this._widget); + } + + if (this.dimension) { + this._widget.layout(this.dimension, this._rootElement); + } + + const model = await input.resolve(this._widget!.getId()); const viewState = this.loadTextEditorViewState(input); - this._widget.setModel(model.notebook, viewState, options); + + await this._widget.setModel(model.notebook, viewState, options); + this._widgetDisposableStore.add(this._widget.onDidFocus(() => this._onDidFocusWidget.fire())); } clearInput(): void { - // de-ref widget + const existingEditorWidgetForInput = NotebookRegistry.getNotebookEditorWidget(this.input as NotebookEditorInput); + existingEditorWidgetForInput?.onWillHide(); + this._widget = undefined; super.clearInput(); } private saveEditorViewState(input: NotebookEditorInput): void { - if (this.group) { + if (this.group && this._widget) { const state = this._widget.getEditorViewState(); this.editorMemento.saveEditorState(this.group, input.resource, state); } @@ -149,8 +198,23 @@ export class NotebookEditor extends BaseEditor { layout(dimension: DOM.Dimension): void { DOM.toggleClass(this._rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); DOM.toggleClass(this._rootElement, 'narrow-width', dimension.width < 600); + this.dimension = dimension; - this._widget.layout(dimension, this._rootElement); + if (this._input === undefined || this._widget === undefined) { + return; + } + + if (this._input.resource?.toString() !== this._widget?.viewModel?.uri.toString()) { + // input and widget mismatch + // this happens when + // 1. open document A, pin the document + // 2. open document B + // 3. close document B + // 4. a layout is triggered + return; + } + + this._widget?.layout(this.dimension, this._rootElement); } protected saveState(): void { @@ -168,7 +232,6 @@ export class NotebookEditor extends BaseEditor { //#endregion dispose() { - this._widget.dispose(); super.dispose(); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 3fc6eb2d78d..728eee516d2 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -14,23 +14,8 @@ import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; let NOTEBOOK_EDITOR_INPUT_HANDLE = 0; export class NotebookEditorInput extends EditorInput { - - private static readonly _instances = new Map(); - - static getOrCreate(instantiationService: IInstantiationService, resource: URI, name: string, viewType: string | undefined) { - const key = resource.toString() + viewType; - let input = NotebookEditorInput._instances.get(key); - if (!input) { - input = instantiationService.createInstance(class extends NotebookEditorInput { - dispose() { - NotebookEditorInput._instances.delete(key); - super.dispose(); - } - }, resource, name, viewType); - - NotebookEditorInput._instances.set(key, input); - } - return input; + static create(instantiationService: IInstantiationService, resource: URI, name: string, viewType: string | undefined) { + return instantiationService.createInstance(NotebookEditorInput, resource, name, viewType); } static readonly ID: string = 'workbench.input.notebook'; @@ -132,7 +117,7 @@ export class NotebookEditorInput extends EditorInput { } _move(group: GroupIdentifier, newResource: URI): { editor: IEditorInput } | undefined { - const editorInput = NotebookEditorInput.getOrCreate(this.instantiationService, newResource, basename(newResource), this.viewType); + const editorInput = NotebookEditorInput.create(this.instantiationService, newResource, basename(newResource), this.viewType); return { editor: editorInput }; } @@ -144,12 +129,12 @@ export class NotebookEditorInput extends EditorInput { return; } - async resolve(): Promise { + async resolve(editorId?: string): Promise { if (!await this.notebookService.canResolve(this.viewType!)) { throw new Error(`Cannot open notebook of type '${this.viewType}'`); } - this.textModel = await this.notebookService.modelManager.resolve(this.resource, this.viewType!); + this.textModel = await this.notebookService.modelManager.resolve(this.resource, this.viewType!, editorId); this._register(this.textModel.onDidChangeDirty(() => { this._onDidChangeDirty.fire(); @@ -173,12 +158,14 @@ export class NotebookEditorInput extends EditorInput { return false; } - dispose() { + clearTextModel() { if (this.textModel) { this.notebookService.destoryNotebookDocument(this.textModel!.notebook.viewType, this.textModel!.notebook); this.textModel.dispose(); } + } + dispose() { super.dispose(); } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts index 983be6092bd..2263ff20d0e 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -10,7 +10,7 @@ import { CancellationTokenSource } from 'vs/base/common/cancellation'; import { Color, RGBA } from 'vs/base/common/color'; import { onUnexpectedError } from 'vs/base/common/errors'; import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import { combinedDisposable, DisposableStore, Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/notebook'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; @@ -25,12 +25,12 @@ import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/c 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 } from 'vs/platform/theme/common/colorRegistry'; +import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground, errorForeground, transparent } 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, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, IEditableCellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, IEditableCellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_HAS_MULTIPLE_KERNELS } 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'; @@ -39,15 +39,17 @@ import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, Note 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, IOutput, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto } 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 { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { generateUuid } from 'vs/base/common/uuid'; import { Memento, MementoObject } from 'vs/workbench/common/memento'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; +import { URI } from 'vs/base/common/uri'; +import { PANEL_BORDER } from 'vs/workbench/common/theme'; +import { debugIconStartForeground } from 'vs/workbench/contrib/debug/browser/debugToolBar'; const $ = DOM.$; @@ -66,8 +68,6 @@ export class NotebookEditorOptions extends EditorOptions { } } - - export class NotebookEditorWidget extends Disposable implements INotebookEditor { static readonly ID: string = 'workbench.editor.notebook'; private static readonly EDITOR_MEMENTOS = new Map>(); @@ -76,6 +76,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private webview: BackLayerWebView | null = null; private webviewTransparentCover: HTMLElement | null = null; private list: INotebookCellList | undefined; + private dndController: CellDragAndDropController | null = null; private renderedEditors: Map = new Map(); private eventDispatcher: NotebookEventDispatcher | undefined; private notebookViewModel: NotebookViewModel | undefined; @@ -87,18 +88,21 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private editorEditable: IContextKey | null = null; private editorRunnable: IContextKey | null = null; private editorExecutingNotebook: IContextKey | null = null; + private notebookHasMultipleKernels: IContextKey | null = null; private outputRenderer: OutputRenderer; protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; private scrollBeyondLastLine: boolean; private readonly memento: Memento; - + private _isDisposed: boolean = false; + private readonly _onDidFocusWidget = this._register(new Emitter()); + public get onDidFocus(): Event { return this._onDidFocusWidget.event; } constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, @INotebookService private notebookService: INotebookService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IContextKeyService readonly contextKeyService: IContextKeyService, @ILayoutService private readonly _layoutService: ILayoutService ) { super(); @@ -116,21 +120,42 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } }); + + this.notebookService.addNotebookEditor(this); } - private readonly _onDidChangeModel = new Emitter(); - readonly onDidChangeModel: Event = this._onDidChangeModel.event; + private _uuid = generateUuid(); + public getId(): string { + return this._uuid; + } + private readonly _onDidChangeModel = new Emitter(); + readonly onDidChangeModel: Event = this._onDidChangeModel.event; + + private readonly _onDidFocusEditorWidget = new Emitter(); + readonly onDidFocusEditorWidget = this._onDidFocusEditorWidget.event; set viewModel(newModel: NotebookViewModel | undefined) { this.notebookViewModel = newModel; - this._onDidChangeModel.fire(); + this._onDidChangeModel.fire(newModel?.notebookDocument); } get viewModel() { return this.notebookViewModel; } + get uri() { + return this.notebookViewModel?.uri; + } + + get textModel() { + return this.notebookViewModel?.notebookDocument; + } + + hasModel() { + return !!this.notebookViewModel; + } + private _activeKernel: INotebookKernelInfo | undefined = undefined; private readonly _onDidChangeKernel = new Emitter(); readonly onDidChangeKernel: Event = this._onDidChangeKernel.event; @@ -155,11 +180,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#region Editor Core protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { - const mementoKey = `${this.getId()}${key}`; + const mementoKey = `${NotebookEditorWidget.ID}${key}`; let editorMemento = NotebookEditorWidget.EDITOR_MEMENTOS.get(mementoKey); if (!editorMemento) { - editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); + editorMemento = new EditorMemento(NotebookEditorWidget.ID, key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); NotebookEditorWidget.EDITOR_MEMENTOS.set(mementoKey, editorMemento); } @@ -170,12 +195,6 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor return this.memento.getMemento(scope); } - - getId(): string { - return NotebookEditorWidget.ID; - } - - public get isNotebookEditor() { return true; } @@ -186,6 +205,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.overlayContainer)); } + hasFocus() { + return this.editorFocus?.get() || false; + } + createEditor(): void { this.overlayContainer = document.createElement('div'); const id = generateUuid(); @@ -204,6 +227,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); this.editorRunnable.set(true); this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); + this.notebookHasMultipleKernels = NOTEBOOK_HAS_MULTIPLE_KERNELS.bindTo(this.contextKeyService); + this.notebookHasMultipleKernels.set(false); const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); @@ -232,10 +257,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private createCellList(): void { DOM.addClass(this.body, 'cell-list-container'); - const dndController = this._register(new CellDragAndDropController(this, this.body)); - const renders = [ - this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, dndController), - this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this, dndController, this.renderedEditors), + this.dndController = this._register(new CellDragAndDropController(this, this.body)); + const getScopedContextKeyService = (container?: HTMLElement) => this.list!.contextKeyService.createScoped(container); + const renderers = [ + this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, this.dndController, getScopedContextKeyService), + this.instantiationService.createInstance(MarkdownCellRenderer, this, this.dndController, this.renderedEditors, getScopedContextKeyService), ]; this.list = this.instantiationService.createInstance( @@ -243,7 +269,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor 'NotebookCellList', this.body, this.instantiationService.createInstance(NotebookCellListDelegate), - renders, + renderers, this.contextKeyService, { setRowLineHeight: false, @@ -282,20 +308,12 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } }, ); - dndController.setList(this.list); + this.dndController.setList(this.list); - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.webview.webview.onDidBlur(() => this.updateEditorFocus()); - this.webview.webview.onDidFocus(() => this.updateEditorFocus()); - this._register(this.webview.onMessage(message => { - if (this.viewModel) { - this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); - } - })); - this.list.rowsContainer.appendChild(this.webview.element); + // create Webview this._register(this.list); - this._register(combinedDisposable(...renders)); + this._register(combinedDisposable(...renderers)); // transparent cover this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); @@ -327,6 +345,10 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); this._register(this.list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this))); + + const widgetFocusTracker = DOM.trackFocus(this.getDomNode()); + this._register(widgetFocusTracker); + this._register(widgetFocusTracker.onDidFocus(() => this._onDidFocusWidget.fire())); } getDomNode() { @@ -347,21 +369,23 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor focus() { this.editorFocus?.set(true); this.list?.domFocus(); + this._onDidFocusEditorWidget.fire(); } async setModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined, options: EditorOptions | undefined): Promise { - if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(textModel) || this.webview === null) { + if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(textModel)) { this.detachModel(); await this.attachModel(textModel, viewState); } - const availableKernels = this.notebookService.getContributedNotebookKernels(textModel.viewType, textModel.uri); - this.activeKernel = availableKernels[0]; + // clear state + this.dndController?.clearGlobalDragState(); + + this._setKernels(textModel); this.localStore.add(this.notebookService.onDidChangeKernels(() => { if (this.activeKernel === undefined) { - const availableKernels = this.notebookService.getContributedNotebookKernels(textModel.viewType, textModel.uri); - this.activeKernel = availableKernels[0]; + this._setKernels(textModel); } })); @@ -396,11 +420,45 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.viewModel?.dispose(); // avoid event this.notebookViewModel = undefined; - this.webview?.clearInsets(); - this.webview?.clearPreloadsCache(); + // this.webview?.clearInsets(); + // this.webview?.clearPreloadsCache(); + this.webview?.dispose(); + this.webview?.element.remove(); + this.webview = null; this.list?.clear(); } + private _setKernels(textModel: NotebookTextModel) { + const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; + const availableKernels = this.notebookService.getContributedNotebookKernels(textModel.viewType, textModel.uri); + + if (provider.kernel && availableKernels.length > 0) { + this.notebookHasMultipleKernels!.set(true); + } else if (availableKernels.length > 1) { + this.notebookHasMultipleKernels!.set(true); + } else { + this.notebookHasMultipleKernels!.set(false); + } + + if (provider && provider.kernel) { + // it has a builtin kernel, don't automatically choose a kernel + this.loadKernelPreloads(provider.providerExtensionLocation, provider.kernel); + return; + } + + // the provider doesn't have a builtin kernel, choose a kernel + this.activeKernel = availableKernels[0]; + if (this.activeKernel) { + this.loadKernelPreloads(this.activeKernel.extensionLocation, this.activeKernel); + } + } + + private loadKernelPreloads(extensionLocation: URI, kernel: INotebookKernelInfoDto) { + if (kernel.preloads) { + this.webview?.updateKernelPreloads([extensionLocation], kernel.preloads.map(preload => URI.revive(preload))); + } + } + private updateForMetadata(): void { this.editorEditable?.set(!!this.viewModel!.metadata?.editable); this.editorRunnable?.set(!!this.viewModel!.metadata?.runnable); @@ -408,13 +466,25 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); } - private async attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { - if (!this.webview) { - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); - } - + private async createWebview(id: string, document: URI): Promise { + this.webview = this.instantiationService.createInstance(BackLayerWebView, this, id, document); await this.webview.waitForInitialization(); + this.webview.webview.onDidBlur(() => this.updateEditorFocus()); + this.webview.webview.onDidFocus(() => { + this.updateEditorFocus(); + this._onDidFocusWidget.fire(); + }); + + this.localStore.add(this.webview.onMessage(message => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.getId(), message); + } + })); + this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview.element); + } + + private async attachModel(textModel: NotebookTextModel, viewState: INotebookEditorViewState | undefined) { + await this.createWebview(this.getId(), textModel.uri); this.eventDispatcher = new NotebookEventDispatcher(); this.viewModel = this.instantiationService.createInstance(NotebookViewModel, textModel.viewType, textModel, this.eventDispatcher, this.getLayoutInfo()); @@ -458,8 +528,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.webview!.element.style.height = `${scrollHeight}px`; if (this.webview?.insetMapping) { - let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; - let removedItems: IOutput[] = []; + let updateItems: { cell: CodeCellViewModel, output: IProcessedOutput, cellTop: number }[] = []; + let removedItems: IProcessedOutput[] = []; this.webview?.insetMapping.forEach((value, key) => { const cell = value.cell; const viewIndex = this.list?.getViewIndex(cell); @@ -501,6 +571,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor })); this.list!.layout(); + this.dndController?.clearGlobalDragState(); // restore list state at last, it must be after list layout this.restoreListViewState(viewState); @@ -532,8 +603,15 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - getEditorViewState() { - const state = this.notebookViewModel!.getEditorViewState(); + getEditorViewState(): INotebookEditorViewState { + const state = this.notebookViewModel?.getEditorViewState(); + if (!state) { + return { + editingCells: {}, + editorViewStates: {} + }; + } + if (this.list) { state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; let cellHeights: { [key: number]: number } = {}; @@ -698,6 +776,8 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor private readonly _onMouseDown: Emitter = this._register(new Emitter()); public readonly onMouseDown: Event = this._onMouseDown.event; + private pendingLayouts = new WeakMap(); + //#endregion //#region Cell operations @@ -712,12 +792,27 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.list?.updateElementHeight2(cell, height); }; + if (this.pendingLayouts.has(cell)) { + this.pendingLayouts.get(cell)!.dispose(); + } + let r: () => void; - DOM.scheduleAtNextAnimationFrame(() => { + const layoutDisposable = DOM.scheduleAtNextAnimationFrame(() => { + if (this._isDisposed) { + return; + } + + this.pendingLayouts.delete(cell); + relayout(cell, height); r(); }); + this.pendingLayouts.set(cell, toDisposable(() => { + layoutDisposable.dispose(); + r(); + })); + return new Promise(resolve => { r = resolve; }); } @@ -1021,15 +1116,17 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor try { this.editorExecutingNotebook!.set(true); this.notebookViewModel!.currentTokenSource = tokenSource; + const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; + if (provider) { + const viewType = provider.id; + const notebookUri = this.notebookViewModel!.uri; - if (this._activeKernel) { - await this.notebookService.executeNotebook2(this.notebookViewModel!.viewType, this.notebookViewModel!.uri, this._activeKernel.id, tokenSource.token); - } else { - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = this.notebookViewModel!.uri; - return await this.notebookService.executeNotebook(viewType, notebookUri, tokenSource.token); + if (this._activeKernel) { + await this.notebookService.executeNotebook2(this.notebookViewModel!.viewType, this.notebookViewModel!.uri, this._activeKernel.id, tokenSource.token); + } else if (provider.kernel) { + return await this.notebookService.executeNotebook(viewType, notebookUri, true, tokenSource.token); + } else { + return await this.notebookService.executeNotebook(viewType, notebookUri, false, tokenSource.token); } } @@ -1050,13 +1147,18 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } async executeNotebookCell(cell: ICellViewModel): Promise { + if (cell.cellKind === CellKind.Markdown) { + cell.editState = CellEditState.Preview; + return; + } + if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).runnable) { return; } const tokenSource = new CancellationTokenSource(); try { - this._executeNotebookCell(cell, tokenSource); + await this._executeNotebookCell(cell, tokenSource); } finally { tokenSource.dispose(); } @@ -1070,10 +1172,13 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor if (provider) { const viewType = provider.id; const notebookUri = this.notebookViewModel!.uri; + if (this._activeKernel) { return await this.notebookService.executeNotebookCell2(viewType, notebookUri, cell.handle, this._activeKernel.id, tokenSource.token); + } else if (provider.kernel) { + return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, true, tokenSource.token); } else { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); + return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, false, tokenSource.token); } } } finally { @@ -1136,7 +1241,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.list?.triggerScrollFromMouseWheelEvent(event); } - createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { + createInset(cell: CodeCellViewModel, output: IProcessedOutput, shadowContent: string, offset: number) { if (!this.webview) { return; } @@ -1154,7 +1259,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } - removeInset(output: IOutput) { + removeInset(output: IProcessedOutput) { if (!this.webview) { return; } @@ -1162,7 +1267,7 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor this.webview!.removeInset(output); } - hideInset(output: IOutput) { + hideInset(output: IProcessedOutput) { if (!this.webview) { return; } @@ -1188,13 +1293,20 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor //#endregion dispose() { + this._isDisposed = true; + this.notebookService.removeNotebookEditor(this); const keys = Object.keys(this._contributions); for (let i = 0, len = keys.length; i < len; i++) { const contributionId = keys[i]; this._contributions[contributionId].dispose(); } + this.localStore.clear(); + this.list?.clear(); + this.webview?.dispose(); + this.overlayContainer.remove(); + this.viewModel?.dispose(); // this._layoutService.container.removeChild(this.overlayContainer); @@ -1208,7 +1320,11 @@ export class NotebookEditorWidget extends Disposable implements INotebookEditor } } -const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; +export const notebookCellBorder = registerColor('notebook.cellBorderColor', { + dark: transparent(PANEL_BORDER, .6), + light: transparent(PANEL_BORDER, .4), + hc: null +}, nls.localize('notebook.cellBorderColor', "The border color for notebook cells.")); export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { light: new Color(new RGBA(102, 175, 224)), @@ -1216,12 +1332,29 @@ export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator hc: new Color(new RGBA(0, 73, 122)) }, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); +export const cellStatusIconSuccess = registerColor('notebookStatusSuccessIcon.foreground', { + light: debugIconStartForeground, + dark: debugIconStartForeground, + hc: debugIconStartForeground +}, nls.localize('notebookStatusSuccessIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); + +export const cellStatusIconError = registerColor('notebookStatusErrorIcon.foreground', { + light: errorForeground, + dark: errorForeground, + hc: errorForeground +}, nls.localize('notebookStatusErrorIcon.foreground', "The error icon color of notebook cells in the cell status bar.")); + +export const cellStatusIconRunning = registerColor('notebookStatusRunningIcon.foreground', { + light: foreground, + dark: foreground, + hc: foreground +}, nls.localize('notebookStatusRunningIcon.foreground', "The running icon color of notebook cells in the cell status bar.")); + export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { - dark: new Color(new RGBA(255, 255, 255, 0.06)), - light: new Color(new RGBA(237, 239, 249)), + dark: notebookCellBorder, + light: notebookCellBorder, hc: null -} - , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); +}, nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); // TODO currently also used for toolbar border, if we keep all of this, pick a generic name export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperator', { @@ -1230,6 +1363,11 @@ export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperat hc: contrastBorder }, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); +export const cellStatusBarItemHover = registerColor('notebook.cellStatusBarItemHoverBackground', { + light: new Color(new RGBA(0, 0, 0, 0.08)), + dark: new Color(new RGBA(255, 255, 255, 0.15)), + hc: new Color(new RGBA(255, 255, 255, 0.15)), +}, nls.localize('notebook.cellStatusBarItemHoverBackground', "The background color of notebook cell status bar items.")); registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { @@ -1237,7 +1375,8 @@ registerThemingParticipant((theme, collector) => { 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 = 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, @@ -1301,6 +1440,36 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); } + const editorBorderColor = theme.getColor(notebookCellBorder); + if (editorBorderColor) { + collector.addRule(`.notebookOverlay .monaco-list-row .cell-editor-part:before { outline: solid 1px ${editorBorderColor}; }`); + } + + const headingBorderColor = theme.getColor(notebookCellBorder); + if (headingBorderColor) { + collector.addRule(`.notebookOverlay .cell.markdown h1 { border-color: ${headingBorderColor}; }`); + } + + const cellStatusSuccessIcon = theme.getColor(cellStatusIconSuccess); + if (cellStatusSuccessIcon) { + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: ${cellStatusSuccessIcon} }`); + } + + const cellStatusErrorIcon = theme.getColor(cellStatusIconError); + if (cellStatusErrorIcon) { + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-error { color: ${cellStatusErrorIcon} }`); + } + + const cellStatusRunningIcon = theme.getColor(cellStatusIconRunning); + if (cellStatusRunningIcon) { + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-sync { color: ${cellStatusRunningIcon} }`); + } + + const cellStatusBarHoverBg = theme.getColor(cellStatusBarItemHover); + if (cellStatusBarHoverBg) { + collector.addRule(`.monaco-workbench .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: ${cellStatusBarHoverBg}; }`); + } + // const widgetShadowColor = theme.getColor(widgetShadow); // if (widgetShadowColor) { // collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts index f6ddc3cb0de..9af645bc91d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts @@ -6,6 +6,8 @@ import { CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { BrandedService, IConstructorSignature1 } from 'vs/platform/instantiation/common/instantiation'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; export type IOutputTransformCtor = IConstructorSignature1; @@ -19,6 +21,18 @@ export namespace NotebookRegistry { export function getOutputTransformContributions(): IOutputTransformDescription[] { return NotebookRegistryImpl.INSTANCE.getNotebookOutputTransform(); } + + export function claimNotebookEditorWidget(editorInput: NotebookEditorInput, widget: NotebookEditorWidget) { + NotebookRegistryImpl.INSTANCE.claimNotebookEditorWidget(editorInput, widget); + } + + export function releaseNotebookEditorWidget(editorInput: NotebookEditorInput) { + NotebookRegistryImpl.INSTANCE.releaseNotebookEditorWidget(editorInput); + } + + export function getNotebookEditorWidget(editorInput: NotebookEditorInput): NotebookEditorWidget | undefined { + return NotebookRegistryImpl.INSTANCE.getNotebookEditorWidget(editorInput); + } } export function registerOutputTransform(id: string, kind: CellOutputKind, ctor: { new(editor: INotebookEditor, ...services: Services): IOutputTransformContribution }): void { @@ -30,6 +44,7 @@ class NotebookRegistryImpl { static readonly INSTANCE = new NotebookRegistryImpl(); private readonly outputTransforms: IOutputTransformDescription[]; + private readonly notebookEditorWidgetOwnership = new Map(); constructor() { this.outputTransforms = []; @@ -42,4 +57,16 @@ class NotebookRegistryImpl { getNotebookOutputTransform(): IOutputTransformDescription[] { return this.outputTransforms.slice(0); } + + claimNotebookEditorWidget(editorInput: NotebookEditorInput, widget: NotebookEditorWidget) { + this.notebookEditorWidgetOwnership.set(editorInput, widget); + } + + releaseNotebookEditorWidget(editorInput: NotebookEditorInput) { + this.notebookEditorWidgetOwnership.delete(editorInput); + } + + getNotebookEditorWidget(editorInput: NotebookEditorInput) { + return this.notebookEditorWidgetOwnership.get(editorInput); + } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts index f8bc6c5e8e6..466c69d0cdd 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -6,12 +6,12 @@ import * as nls from 'vs/nls'; import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { URI } from 'vs/base/common/uri'; +import { URI, UriComponents } from 'vs/base/common/uri'; import { notebookProviderExtensionPoint, notebookRendererExtensionPoint } from 'vs/workbench/contrib/notebook/browser/extensionPoint'; import { NotebookProviderInfo } 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, INotebookMimeTypeSelector, INotebookRendererInfo, NotebookDocumentMetadata, CellEditType, ICellDto2, INotebookKernelInfo } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { INotebookTextModel, INotebookRendererInfo, NotebookDocumentMetadata, ICellDto2, 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 } 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'; @@ -22,7 +22,10 @@ import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/mode import { NotebookEditorModelManager } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/common/notebookService'; import * as glob from 'vs/base/common/glob'; -import { basename } from 'vs/base/common/resources'; +import { basename } from 'vs/base/common/path'; +import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -41,7 +44,6 @@ export class NotebookProviderInfoStore { add(info: NotebookProviderInfo): void { if (this.contributedEditors.has(info.id)) { - console.log(`Custom editor with id '${info.id}' already registered`); return; } this.contributedEditors.set(info.id, info); @@ -69,7 +71,6 @@ export class NotebookOutputRendererInfoStore { add(info: NotebookOutputRendererInfo): void { if (this.contributedRenderers.has(info.id)) { - console.log(`Custom notebook output renderer with id '${info.id}' already registered`); return; } this.contributedRenderers.set(info.id, info); @@ -98,13 +99,22 @@ class ModelData implements IDisposable { export class NotebookService extends Disposable implements INotebookService, ICustomEditorViewTypesHandler { _serviceBrand: undefined; private readonly _notebookProviders = new Map(); - private readonly _notebookRenderers = new Map(); + private readonly _notebookRenderers = new Map(); private readonly _notebookKernels = new Map(); notebookProviderInfoStore: NotebookProviderInfoStore = new NotebookProviderInfoStore(); notebookRenderersInfoStore: NotebookOutputRendererInfoStore = new NotebookOutputRendererInfoStore(); - private readonly _models: { [modelId: string]: ModelData; }; - private _onDidChangeActiveEditor = new Emitter<{ viewType: string, uri: URI }>(); - onDidChangeActiveEditor: Event<{ viewType: string, uri: URI }> = this._onDidChangeActiveEditor.event; + private readonly _models = new Map(); + private _onDidChangeActiveEditor = new Emitter(); + onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; + private _onDidChangeVisibleEditors = new Emitter(); + onDidChangeVisibleEditors: Event = this._onDidChangeVisibleEditors.event; + private readonly _onNotebookEditorAdd: Emitter = this._register(new Emitter()); + public readonly onNotebookEditorAdd: Event = this._onNotebookEditorAdd.event; + private readonly _onNotebookEditorsRemove: Emitter = this._register(new Emitter()); + public readonly onNotebookEditorsRemove: Event = this._onNotebookEditorsRemove.event; + private readonly _onNotebookDocumentRemove: Emitter = this._register(new Emitter()); + public readonly onNotebookDocumentRemove: Event = this._onNotebookDocumentRemove.event; + private readonly _notebookEditors = new Map(); private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; @@ -114,15 +124,17 @@ export class NotebookService extends Disposable implements INotebookService, ICu private cutItems: NotebookCellTextModel[] | undefined; modelManager: NotebookEditorModelManager; + private _displayOrder: { userOrder: string[], defaultOrder: string[] } = Object.create(null); constructor( @IExtensionService private readonly extensionService: IExtensionService, @IEditorService private readonly editorService: IEditorService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IAccessibilityService private readonly accessibilityService: IAccessibilityService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { super(); - this._models = {}; this.modelManager = this.instantiationService.createInstance(NotebookEditorModelManager); notebookProviderExtensionPoint.setHandler((extensions) => { @@ -135,6 +147,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu displayName: notebookContribution.displayName, selector: notebookContribution.selector || [], providerDisplayName: extension.description.isBuiltin ? nls.localize('builtinProviderDisplayName', "Built-in") : extension.description.displayName || extension.description.identifier.value, + providerExtensionLocation: extension.description.extensionLocation })); } } @@ -158,6 +171,26 @@ export class NotebookService extends Disposable implements INotebookService, ICu }); this.editorService.registerCustomEditorViewTypesHandler('Notebook', this); + + const updateOrder = () => { + let userOrder = this.configurationService.getValue('notebook.displayOrder'); + this._displayOrder = { + defaultOrder: this.accessibilityService.isScreenReaderOptimized() ? ACCESSIBLE_NOTEBOOK_DISPLAY_ORDER : NOTEBOOK_DISPLAY_ORDER, + userOrder: userOrder + }; + }; + + updateOrder(); + + this._register(this.configurationService.onDidChangeConfiguration(e => { + if (e.affectedKeys.indexOf('notebook.displayOrder') >= 0) { + updateOrder(); + } + })); + + this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => { + updateOrder(); + })); } getViewTypes(): ICustomEditorInfo[] { @@ -170,6 +203,9 @@ export class NotebookService extends Disposable implements INotebookService, ICu async canResolve(viewType: string): Promise { if (!this._notebookProviders.has(viewType)) { + await this.extensionService.whenInstalledExtensionsRegistered(); + // 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(`onNotebookEditor:${viewType}`); } @@ -178,6 +214,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu registerNotebookController(viewType: string, extensionData: NotebookExtensionDescription, controller: IMainNotebookController) { this._notebookProviders.set(viewType, { extensionData, controller }); + this.notebookProviderInfoStore.get(viewType)!.kernel = controller.kernel; this._onDidChangeViewTypes.fire(); } @@ -186,12 +223,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu this._onDidChangeViewTypes.fire(); } - registerNotebookRenderer(handle: number, extensionData: NotebookExtensionDescription, type: string, selectors: INotebookMimeTypeSelector, preloads: URI[]) { - this._notebookRenderers.set(handle, { extensionData, type, selectors, preloads }); + registerNotebookRenderer(id: string, renderer: INotebookRendererInfo) { + this._notebookRenderers.set(id, renderer); } - unregisterNotebookRenderer(handle: number) { - this._notebookRenderers.delete(handle); + unregisterNotebookRenderer(id: string) { + this._notebookRenderers.delete(id); } registerNotebookKernel(notebook: INotebookKernelInfo): void { @@ -236,7 +273,7 @@ export class NotebookService extends Disposable implements INotebookService, ICu private _notebookKernelMatch(resource: URI, selectors: (string | glob.IRelativePattern)[]): boolean { for (let i = 0; i < selectors.length; i++) { const pattern = typeof selectors[i] !== 'string' ? selectors[i] : selectors[i].toString(); - if (glob.match(pattern, basename(resource).toLowerCase())) { + if (glob.match(pattern, basename(resource.fsPath).toLowerCase())) { return true; } } @@ -244,27 +281,20 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } - getRendererInfo(handle: number): INotebookRendererInfo | undefined { - const renderer = this._notebookRenderers.get(handle); + getRendererInfo(id: string): INotebookRendererInfo | undefined { + const renderer = this._notebookRenderers.get(id); - if (renderer) { - return { - id: renderer.extensionData.id, - extensionLocation: URI.revive(renderer.extensionData.location), - preloads: renderer.preloads - }; - } - - return; + return renderer; } - async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise { + async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[], editorId?: string): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - const notebookModel = await provider.controller.createNotebook(viewType, uri, true, false); + const notebookModel = await provider.controller.createNotebook(viewType, uri, { metadata, languages, cells }, false, editorId); + await this.transformTextModelOutputs(notebookModel!); if (!notebookModel) { return undefined; } @@ -273,73 +303,291 @@ export class NotebookService extends Disposable implements INotebookService, ICu const modelId = MODEL_ID(uri); const modelData = new ModelData( notebookModel, - (model) => this._onWillDispose(model), + (model) => this._onWillDisposeDocument(model), ); - this._models[modelId] = modelData; - - notebookModel.metadata = metadata; - notebookModel.languages = languages; - - notebookModel.applyEdit(notebookModel.versionId, [ - { - editType: CellEditType.Insert, - index: 0, - cells: cells - } - ]); - + this._models.set(modelId, modelData); return modelData.model; } - async resolveNotebook(viewType: string, uri: URI, forceReload: boolean): Promise { + async resolveNotebook(viewType: string, uri: URI, forceReload: boolean, editorId?: string): Promise { const provider = this._notebookProviders.get(viewType); if (!provider) { return undefined; } - let notebookModel: NotebookTextModel | undefined; - - notebookModel = await provider.controller.createNotebook(viewType, uri, false, forceReload); + const notebookModel = await provider.controller.createNotebook(viewType, uri, undefined, forceReload, editorId); + await this.transformTextModelOutputs(notebookModel!); // new notebook model created const modelId = MODEL_ID(uri); const modelData = new ModelData( notebookModel!, - (model) => this._onWillDispose(model), + (model) => this._onWillDisposeDocument(model), ); - this._models[modelId] = modelData; + this._models.set(modelId, modelData); return modelData.model; } - async executeNotebook(viewType: string, uri: URI, token: CancellationToken): Promise { - let provider = this._notebookProviders.get(viewType); + private async _fillInTransformedOutputs( + renderers: Set, + requestItems: IOutputRenderRequestCellInfo[], + renderFunc: (rendererId: string, items: IOutputRenderRequestCellInfo[]) => Promise | undefined>, + lookUp: (key: T) => { outputs: IProcessedOutput[] } + ) { + for (let id of renderers) { + const requestsPerRenderer: IOutputRenderRequestCellInfo[] = requestItems.map(req => { + return { + key: req.key, + outputs: req.outputs.filter(output => output.handlerId === id) + }; + }); - if (provider) { - return provider.controller.executeNotebook(viewType, uri, token); + 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[] = []; + 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) => { + if (output.outputKind === CellOutputKind.Rich) { + // TODO no string[] casting + const ret = this._transformMimeTypes(output, 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 }); + 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) => { + const outputs = cell.outputs; + const outputRequest: IOutputRenderRequestOutputInfo[] = []; + outputs.map((output, index) => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this._transformMimeTypes(output, 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 }); + renderers.add(orderedMimeTypes[pickedMimeTypeIndex].rendererId!); + } + } + }); + + requestItems.push({ key: [editIndex, cellIndex], outputs: outputRequest }); + }); + } + }); + + 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) => { + const outputs = splice[2]; + const outputRequest: IOutputRenderRequestOutputInfo[] = []; + outputs.map((output, index) => { + if (output.outputKind === CellOutputKind.Rich) { + const ret = this._transformMimeTypes(output, 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 }); + 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, + 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; } - async executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise { + private _transformMimeTypes(output: IDisplayOutput, documentDisplayOrder: string[]): ITransformedDisplayOutputDto { + let mimeTypes = Object.keys(output.data); + let coreDisplayOrder = this._displayOrder; + const sorted = sortMimeTypes(mimeTypes, coreDisplayOrder?.userOrder || [], documentDisplayOrder, coreDisplayOrder?.defaultOrder || []); + + let orderMimeTypes: IOrderedMimeType[] = []; + + sorted.forEach(mimeType => { + let handlers = this.findBestMatchedRenderer(mimeType); + + if (handlers.length) { + const handler = handlers[0]; + + 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 + }); + } + + if (mimeTypeSupportedByCore(mimeType)) { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: BUILTIN_RENDERER_ID + }); + } + } else { + orderMimeTypes.push({ + mimeType: mimeType, + isResolved: false, + rendererId: BUILTIN_RENDERER_ID + }); + } + }); + + return { + outputKind: output.outputKind, + data: output.data, + orderedMimeTypes: orderMimeTypes, + pickedMimeTypeIndex: 0 + }; + } + + findBestMatchedRenderer(mimeType: string): readonly NotebookOutputRendererInfo[] { + return this.notebookRenderersInfoStore.getContributedRenderer(mimeType); + } + + async executeNotebook(viewType: string, uri: URI, useAttachedKernel: boolean, token: CancellationToken): Promise { + let provider = this._notebookProviders.get(viewType); + + if (provider) { + return provider.controller.executeNotebook(viewType, uri, useAttachedKernel, token); + } + + return; + } + + async executeNotebookCell(viewType: string, uri: URI, handle: number, useAttachedKernel: boolean, token: CancellationToken): Promise { const provider = this._notebookProviders.get(viewType); if (provider) { - await provider.controller.executeNotebookCell(uri, handle, token); + await provider.controller.executeNotebookCell(uri, handle, useAttachedKernel, token); } } async executeNotebook2(viewType: string, uri: URI, kernelId: string, token: CancellationToken): Promise { const kernel = this._notebookKernels.get(kernelId); if (kernel) { - kernel.executeNotebook(viewType, uri, undefined, token); + await kernel.executeNotebook(viewType, uri, undefined, token); } } async executeNotebookCell2(viewType: string, uri: URI, handle: number, kernelId: string, token: CancellationToken): Promise { const kernel = this._notebookKernels.get(kernelId); if (kernel) { - kernel.executeNotebook(viewType, uri, handle, token); + await kernel.executeNotebook(viewType, uri, handle, token); } } @@ -364,16 +612,47 @@ export class NotebookService extends Disposable implements INotebookService, ICu return ret; } - destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void { - let provider = this._notebookProviders.get(viewType); + removeNotebookEditor(editor: INotebookEditor) { + let editorCache = this._notebookEditors.get(editor.getId()); - if (provider) { - provider.controller.removeNotebookDocument(notebook); + if (editorCache) { + this._notebookEditors.delete(editor.getId()); + this._onNotebookEditorsRemove.fire([editor]); } } - updateActiveNotebookDocument(viewType: string, resource: URI): void { - this._onDidChangeActiveEditor.fire({ viewType, uri: resource }); + addNotebookEditor(editor: INotebookEditor) { + this._notebookEditors.set(editor.getId(), editor); + this._onNotebookEditorAdd.fire(editor); + } + + listNotebookEditors(): INotebookEditor[] { + return [...this._notebookEditors].map(e => e[1]); + } + + listVisibleNotebookEditors(): INotebookEditor[] { + return this.editorService.visibleEditorPanes + .filter(pane => (pane as any).isNotebookEditor) + .map(pane => pane.getControl() as INotebookEditor) + .filter(editor => !!editor) + .filter(editor => this._notebookEditors.has(editor.getId())); + } + + listNotebookDocuments(): NotebookTextModel[] { + return [...this._models].map(e => e[1].model); + } + + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void { + this._onWillDisposeDocument(notebook); + } + + updateActiveNotebookEditor(editor: INotebookEditor | null) { + this._onDidChangeActiveEditor.fire(editor ? editor.getId() : null); + } + + updateVisibleNotebookEditor(editors: string[]) { + const alreadyCreated = editors.filter(editorId => this._notebookEditors.has(editorId)); + this._onDidChangeVisibleEditors.fire(alreadyCreated); } setToCopy(items: NotebookCellTextModel[]) { @@ -404,21 +683,41 @@ export class NotebookService extends Disposable implements INotebookService, ICu return false; } - onDidReceiveMessage(viewType: string, uri: URI, message: any): void { + onDidReceiveMessage(viewType: string, editorId: string, message: any): void { let provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.onDidReceiveMessage(uri, message); + return provider.controller.onDidReceiveMessage(editorId, message); } } - private _onWillDispose(model: INotebookTextModel): void { + private _onWillDisposeDocument(model: INotebookTextModel): void { let modelId = MODEL_ID(model.uri); - let modelData = this._models[modelId]; - delete this._models[modelId]; - modelData?.dispose(); + let modelData = this._models.get(modelId); + this._models.delete(modelId); - // this._onModelRemoved.fire(model); + if (modelData) { + // delete editors and documents + const willRemovedEditors: INotebookEditor[] = []; + this._notebookEditors.forEach(editor => { + if (editor.textModel === modelData!.model) { + willRemovedEditors.push(editor); + } + }); + + willRemovedEditors.forEach(e => this._notebookEditors.delete(e.getId())); + + let provider = this._notebookProviders.get(modelData!.model.viewType); + + if (provider) { + provider.controller.removeNotebookDocument(modelData!.model); + } + + + this._onNotebookEditorsRemove.fire(willRemovedEditors.map(e => e)); + this._onNotebookDocumentRemove.fire([modelData.model.uri]); + modelData?.dispose(); + } } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index 6849223732a..43e575d93a4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts @@ -21,7 +21,8 @@ import { IListService, IWorkbenchListOptions, WorkbenchList } from 'vs/platform/ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CellRevealPosition, CellRevealType, CursorAtBoundary, getVisibleCells, ICellRange, ICellViewModel, INotebookCellList, reduceCellRanges, CellEditState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CellViewModel, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { diff, IOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { diff, IProcessedOutput, NOTEBOOK_EDITOR_CURSOR_BOUNDARY, CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { clamp } from 'vs/base/common/numbers'; export class NotebookCellList extends WorkbenchList implements IDisposable, IStyleController, INotebookCellList { get onWillScroll(): Event { return this.view.onWillScroll; } @@ -34,10 +35,10 @@ export class NotebookCellList extends WorkbenchList implements ID private _viewModelStore = new DisposableStore(); private styleElement?: HTMLStyleElement; - private readonly _onDidRemoveOutput = new Emitter(); - readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; - private readonly _onDidHideOutput = new Emitter(); - readonly onDidHideOutput: Event = this._onDidHideOutput.event; + private readonly _onDidRemoveOutput = new Emitter(); + readonly onDidRemoveOutput: Event = this._onDidRemoveOutput.event; + private readonly _onDidHideOutput = new Emitter(); + readonly onDidHideOutput: Event = this._onDidHideOutput.event; private _viewModel: NotebookViewModel | null = null; private _hiddenRangeIds: string[] = []; @@ -66,10 +67,18 @@ export class NotebookCellList extends WorkbenchList implements ID }); this._previousFocusedElements = e.elements; - // Force focus out of webview if focus is in webview and I press an arrow key to focus the next cell - if (document.activeElement && document.activeElement.tagName.toLowerCase() === 'webview') { - this.focusView(); - } + // if focus is in the list, but is not inside the focused element, then reset focus + setTimeout(() => { + if (DOM.isAncestor(document.activeElement, this.rowsContainer)) { + const focusedElement = this.getFocusedElements()[0]; + if (focusedElement) { + const focusedDomElement = this.domElementOfElement(focusedElement); + if (focusedDomElement && !DOM.isAncestor(document.activeElement, focusedDomElement)) { + focusedDomElement.focus(); + } + } + } + }, 0); })); const notebookEditorCursorAtBoundaryContext = NOTEBOOK_EDITOR_CURSOR_BOUNDARY.bindTo(contextKeyService); @@ -126,8 +135,14 @@ export class NotebookCellList extends WorkbenchList implements ID } - elementAt(position: number): ICellViewModel { - return this.element(this.view.indexAt(position)); + elementAt(position: number): ICellViewModel | undefined { + if (!this.view.length) { + return undefined; + } + + const idx = this.view.indexAt(position); + const clamped = clamp(idx, 0, this.view.length - 1); + return this.element(clamped); } elementHeight(element: ICellViewModel): number { @@ -170,8 +185,8 @@ export class NotebookCellList extends WorkbenchList implements ID if (e.synchronous) { viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IOutput[] = []; - const deletedOutputs: IOutput[] = []; + const hideOutputs: IProcessedOutput[] = []; + const deletedOutputs: IProcessedOutput[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); @@ -190,8 +205,8 @@ export class NotebookCellList extends WorkbenchList implements ID } else { DOM.scheduleAtNextAnimationFrame(() => { viewDiffs.reverse().forEach((diff) => { - const hideOutputs: IOutput[] = []; - const deletedOutputs: IOutput[] = []; + const hideOutputs: IProcessedOutput[] = []; + const deletedOutputs: IProcessedOutput[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); @@ -216,7 +231,7 @@ export class NotebookCellList extends WorkbenchList implements ID const viewSelections = model.selectionHandles.map(handle => { return model.getCellByHandle(handle); }).filter(cell => !!cell).map(cell => this._getViewIndexUpperBound(cell!)); - this.setFocus(viewSelections); + this.setFocus(viewSelections, undefined, true); })); const hiddenRanges = model.getHiddenRanges(); @@ -312,8 +327,8 @@ export class NotebookCellList extends WorkbenchList implements ID viewDiffs.reverse().forEach((diff) => { // remove output in the webview - const hideOutputs: IOutput[] = []; - const deletedOutputs: IOutput[] = []; + const hideOutputs: IProcessedOutput[] = []; + const deletedOutputs: IProcessedOutput[] = []; for (let i = diff.start; i < diff.start + diff.deleteCount; i++) { const cell = this.element(i); @@ -342,7 +357,7 @@ export class NotebookCellList extends WorkbenchList implements ID } }); - if (!selectionsLeft.length && this._viewModel!.viewCells) { + if (!selectionsLeft.length && this._viewModel!.viewCells.length) { // after splice, the selected cells are deleted this._viewModel!.selectionHandles = [this._viewModel!.viewCells[0].handle]; } @@ -395,12 +410,12 @@ export class NotebookCellList extends WorkbenchList implements ID } } - setFocus(indexes: number[], browserEvent?: UIEvent): void { + setFocus(indexes: number[], browserEvent?: UIEvent, ignoreTextModelUpdate?: boolean): void { if (!indexes.length) { return; } - if (this._viewModel) { + if (this._viewModel && !ignoreTextModelUpdate) { this._viewModel.selectionHandles = indexes.map(index => this.element(index)).map(cell => cell.handle); } 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 b6d6134632f..cbc254803dd 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 { IOutput, IRenderOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput, IRenderOutput } 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'; @@ -33,7 +33,7 @@ export class OutputRenderer { } } - renderNoop(output: IOutput, container: HTMLElement): IRenderOutput { + renderNoop(output: IProcessedOutput, container: HTMLElement): IRenderOutput { const contentNode = document.createElement('p'); contentNode.innerText = `No renderer could be found for output. It has the following output type: ${output.outputKind}`; @@ -43,7 +43,7 @@ export class OutputRenderer { }; } - render(output: IOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { + render(output: IProcessedOutput, container: HTMLElement, preferredMimeType: string | undefined): IRenderOutput { let transform = this._mimeTypeMapping[output.outputKind]; if (transform) { 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 fc11f14dec0..667ac683adb 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 @@ -15,6 +15,8 @@ import { IModeService } from 'vs/editor/common/services/modeService'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; import { URI } from 'vs/base/common/uri'; import { MarkdownRenderer } from 'vs/workbench/contrib/notebook/browser/view/renderers/mdRenderer'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { handleANSIOutput } from 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; class RichRenderer implements IOutputTransformContribution { private _mdRenderer: MarkdownRenderer; @@ -24,7 +26,8 @@ class RichRenderer implements IOutputTransformContribution { public notebookEditor: INotebookEditor, @IInstantiationService private readonly instantiationService: IInstantiationService, @IModelService private readonly modelService: IModelService, - @IModeService private readonly modeService: IModeService + @IModeService private readonly modeService: IModeService, + @IThemeService private readonly themeService: IThemeService ) { this._mdRenderer = instantiationService.createInstance(MarkdownRenderer, undefined); this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this)); @@ -209,8 +212,8 @@ class RichRenderer implements IOutputTransformContribution { renderPlainText(output: any, container: HTMLElement) { let data = output.data['text/plain']; let str = isArray(data) ? data.join('') : data; - const contentNode = document.createElement('p'); - contentNode.innerText = str; + const contentNode = DOM.$('.output-plaintext'); + contentNode.appendChild(handleANSIOutput(str, this.themeService)); container.appendChild(contentNode); return { 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 32ed484dd50..0a4d2a75b88 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 @@ -3,7 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IRenderOutput, CellOutputKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import * as DOM from 'vs/base/browser/dom'; +import { IRenderOutput, CellOutputKind, IStreamOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { registerOutputTransform } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; import { INotebookEditor, IOutputTransformContribution } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -13,8 +14,8 @@ class StreamRenderer implements IOutputTransformContribution { ) { } - render(output: any, container: HTMLElement): IRenderOutput { - const contentNode = document.createElement('p'); + render(output: IStreamOutput, container: HTMLElement): IRenderOutput { + const contentNode = DOM.$('.output-stream'); contentNode.innerText = output.text; container.appendChild(contentNode); return { 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 d00e478f35c..56cb965048f 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -12,14 +12,27 @@ import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { IOpenerService, matchesScheme } from 'vs/platform/opener/common/opener'; import { CELL_MARGIN, CELL_RUN_GUTTER } 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 { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IProcessedOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; +import { asWebviewUri } from 'vs/workbench/contrib/webview/common/webviewUri'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { dirname, joinPath } from 'vs/base/common/resources'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Schemas } from 'vs/base/common/network'; +import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IFileService } from 'vs/platform/files/common/files'; +import { VSBuffer } from 'vs/base/common/buffer'; +import { getExtensionForMimeType } from 'vs/base/common/mime'; + +export interface WebviewIntialized { + __vscode_notebook_message: boolean; + type: 'initialized' +} export interface IDimensionMessage { __vscode_notebook_message: boolean; @@ -61,6 +74,13 @@ export interface IBlurOutputMessage { focusNext?: boolean; } +export interface IClickedDataUrlMessage { + __vscode_notebook_message: string; + type: 'clicked-data-url'; + data: string; + downloadName?: string; +} + export interface IClearMessage { type: 'clear'; } @@ -104,12 +124,13 @@ export interface IScrollRequestMessage { export interface IUpdatePreloadResourceMessage { type: 'preload'; resources: string[]; + source: string; } interface ICachedInset { outputId: string; cell: CodeCellViewModel; - preloads: ReadonlySet; + preloads: ReadonlySet; cachedCreation: ICreationRequestMessage; } @@ -121,28 +142,37 @@ function html(strings: TemplateStringsArray, ...values: any[]): string { return str; } -type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage; +type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage | IBlurOutputMessage | WebviewIntialized | IClickedDataUrlMessage; let version = 0; export class BackLayerWebView extends Disposable { element: HTMLElement; webview!: WebviewElement; - insetMapping: Map = new Map(); - hiddenInsetMapping: Set = new Set(); - reversedInsetMapping: Map = new Map(); + 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()); public readonly onMessage: Event = this._onMessage.event; + private _loaded!: Promise; private _initalized: Promise; + private _disposed = false; constructor( public notebookEditor: INotebookEditor, + public id: string, + public documentUri: URI, @IWebviewService readonly webviewService: IWebviewService, @IOpenerService readonly openerService: IOpenerService, @INotebookService private readonly notebookService: INotebookService, - @IEnvironmentService private readonly environmentService: IEnvironmentService + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IWorkbenchEnvironmentService private readonly workbenchEnvironmentService: IWorkbenchEnvironmentService, + @IFileDialogService private readonly fileDialogService: IFileDialogService, + @IFileService private readonly fileService: IFileService, ) { super(); this.element = document.createElement('div'); @@ -153,7 +183,7 @@ export class BackLayerWebView extends Disposable { this.element.style.margin = `0px 0 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px`; const pathsPath = getPathFromAmdModule(require, 'vs/loader.js'); - const loader = URI.file(pathsPath).with({ scheme: WebviewResourceScheme }); + const loader = asWebviewUri(this.workbenchEnvironmentService, this.id, URI.file(pathsPath)); let coreDependencies = ''; let resolveFunc: () => void; @@ -162,9 +192,11 @@ export class BackLayerWebView extends Disposable { resolveFunc = resolve; }); + const baseUrl = asWebviewUri(this.workbenchEnvironmentService, this.id, dirname(documentUri)); + if (!isWeb) { coreDependencies = ``; - const htmlContent = this.generateContent(8, coreDependencies); + const htmlContent = this.generateContent(8, coreDependencies, baseUrl.toString()); this.initialize(htmlContent); resolveFunc!(); } else { @@ -180,18 +212,20 @@ export class BackLayerWebView extends Disposable { ${loaderJs} `; - const htmlContent = this.generateContent(8, coreDependencies); + + const htmlContent = this.generateContent(8, coreDependencies, baseUrl.toString()); this.initialize(htmlContent); resolveFunc!(); }); } } - generateContent(outputNodePadding: number, coreDependencies: string) { + generateContent(outputNodePadding: number, coreDependencies: string, baseUrl: string) { return html` +