diff --git a/.eslintrc.json b/.eslintrc.json index 06033278bd..0eecdb3218 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" ] } @@ -106,6 +111,14 @@ "rxjs/*" ] }, + { + "target": "**/{vs,sql}/base/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/{vs,sql}/base/node/**", "restrictions": [ @@ -155,13 +168,22 @@ "*" // node modules ] }, + { + "target": "**/{vs,sql}/base/parts/*/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/base/parts/*/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/{vs,sql}/base/parts/*/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -191,10 +213,10 @@ "typemoq", "sinon", "vs/nls", - "azdata", + "azdata", "**/{vs,sql}/base/common/**", - "**/{vs,sql}/base/test/common/**", "**/{vs,sql}/base/parts/*/common/**", + "**/{vs,sql}/base/test/common/**", "**/{vs,sql}/platform/*/common/**", "**/{vs,sql}/platform/*/test/common/**" ] @@ -221,15 +243,25 @@ "*" // node modules ] }, + { + "target": "**/{vs,sql}/platform/*/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/base/parts/*/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/platform/*/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/{vs,sql}/platform/*/electron-browser/**", "restrictions": [ "vs/nls", "azdata", "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser,node}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/platform/*/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -442,18 +474,34 @@ "**/{vs,sql}/**/{common,worker}/**" ] }, + { + "target": "**/{vs,sql}/workbench/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/base/parts/*/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/platform/*/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/editor/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention + "**/{vs,sql}/workbench/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/api/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/services/*/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/{vs,sql}/workbench/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/{vs,sql}/base/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/base/parts/*/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/platform/*/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/editor/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/base/parts/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/platform/*/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/editor/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention - "**/{vs,sql}/workbench/{common,browser,node,electron-browser,api}/**", - "**/{vs,sql}/workbench/services/*/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/services/*/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -465,7 +513,7 @@ "**/{vs,sql}/base/**", "**/{vs,sql}/platform/**", "**/{vs,sql}/editor/**", - "**/{vs,sql}/workbench/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", "vs/workbench/contrib/files/common/editors/fileEditorInput", "**/{vs,sql}/workbench/services/**", "**/{vs,sql}/workbench/test/**", @@ -537,16 +585,30 @@ "*" // node modules ] }, + { + "target": "**/{vs,sql}/workbench/services/**/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/**/{common,browser,worker,electron-sandbox}/**", + "**/{vs,sql}/platform/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/editor/**", + "**/{vs,sql}/workbench/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/api/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/services/**/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/{vs,sql}/workbench/services/**/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/{vs,sql}/base/**/{common,browser,worker,node,electron-browser}/**", - "**/{vs,sql}/platform/**/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/editor/**", - "**/{vs,sql}/workbench/{common,browser,node,electron-browser,api}/**", - "**/{vs,sql}/workbench/services/**/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -559,11 +621,11 @@ "**/{vs,sql}/base/**", "**/{vs,sql}/platform/**", "**/{vs,sql}/editor/**", - "**/{vs,sql}/workbench/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/workbench/services/**", "**/{vs,sql}/workbench/contrib/**", "**/{vs,sql}/workbench/test/**", - "**" + "*" ] }, { @@ -663,17 +725,32 @@ "*" // node modules ] }, + { + "target": "**/{vs,sql}/workbench/contrib/**/electron-sandbox/**", + "restrictions": [ + "vs/nls", + "vs/css!./**/*", + "**/{vs,sql}/base/**/{common,browser,worker,electron-sandbox}/**", + "**/{vs,sql}/platform/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/editor/**", + "**/{vs,sql}/workbench/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/api/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/services/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/contrib/**/{common,browser,electron-sandbox}/**" + ] + }, { "target": "**/{vs,sql}/workbench/contrib/**/electron-browser/**", "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/{vs,sql}/base/**/{common,browser,worker,node,electron-browser}/**", - "**/{vs,sql}/platform/**/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/**/{common,browser,worker,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", "**/{vs,sql}/editor/**", - "**/{vs,sql}/workbench/{common,browser,node,electron-browser,api}/**", - "**/{vs,sql}/workbench/services/**/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/workbench/contrib/**/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/workbench/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/api/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/services/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/contrib/**/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -693,10 +770,10 @@ "restrictions": [ "vs/nls", "vs/css!./**/*", - "**/{vs,sql}/base/**/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/base/parts/**/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/platform/**/{common,browser,node,electron-browser}/**", - "**/{vs,sql}/code/**/{common,browser,node,electron-browser}/**", + "**/{vs,sql}/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/code/**/{common,browser,node,electron-sandbox,electron-browser}/**", "*" // node modules ] }, @@ -724,6 +801,54 @@ "*" // node modules ] }, + { + "target": "**/src/{vs,sql}/workbench/workbench.common.main.ts", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/**/{common,browser}/**", + "**/{vs,sql}/base/parts/**/{common,browser}/**", + "**/{vs,sql}/platform/**/{common,browser}/**", + "**/{vs,sql}/editor/**", + "**/{vs,sql}/workbench/**/{common,browser}/**" + ] + }, + { + "target": "**/src/{vs,sql}/workbench/workbench.web.main.ts", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/**/{common,browser}/**", + "**/{vs,sql}/base/parts/**/{common,browser}/**", + "**/{vs,sql}/platform/**/{common,browser}/**", + "**/{vs,sql}/editor/**", + "**/{vs,sql}/workbench/**/{common,browser}/**", + "**/{vs,sql}/workbench/workbench.common.main" + ] + }, + { + "target": "**/src/{vs,sql}/workbench/workbench.sandbox.main.ts", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/base/parts/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/platform/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/editor/**", + "**/{vs,sql}/workbench/**/{common,browser,electron-sandbox}/**", + "**/{vs,sql}/workbench/workbench.common.main" + ] + }, + { + "target": "**/src/{vs,sql}/workbench/workbench.desktop.main.ts", + "restrictions": [ + "vs/nls", + "**/{vs,sql}/base/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/base/parts/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/platform/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/editor/**", + "**/{vs,sql}/workbench/**/{common,browser,node,electron-sandbox,electron-browser}/**", + "**/{vs,sql}/workbench/workbench.common.main", + "**/{vs,sql}/workbench/workbench.sandbox.main" + ] + }, { "target": "**/extensions/**", "restrictions": "**/*" diff --git a/.vscode/launch.json b/.vscode/launch.json index 36be679916..9fc255d056 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", @@ -108,38 +105,11 @@ ], "browserLaunchLocation": "workspace" }, - { - "type": "chrome", - "request": "launch", - "name": "Launch azuredatastudio with new notebook command", - "windows": { - "runtimeExecutable": "${workspaceFolder}/scripts/sql.bat" - }, - "osx": { - "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" - }, - "linux": { - "runtimeExecutable": "${workspaceFolder}/scripts/sql.sh" - }, - "urlFilter": "*index.html*", - "runtimeArgs": [ - "--inspect=5875", - "--command=notebook.command.new" - ], - "skipFiles": [ - "**/winjs*.js" - ], - "webRoot": "${workspaceFolder}", - "timeout": 45000 - }, { "type": "chrome", "request": "launch", "name": "Launch ADS (Web) (TBD)", - "runtimeExecutable": "yarn", - "runtimeArgs": [ - "web" - ], + "program": "${workspaceFolder}/scripts/code-web.js", "presentation": { "group": "0_vscode", "order": 2 diff --git a/.yarnrc b/.yarnrc index d86b284e83..00b2ebda69 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/common/symbols.ts b/build/azure-pipelines/common/symbols.ts deleted file mode 100644 index 079695f733..0000000000 --- a/build/azure-pipelines/common/symbols.ts +++ /dev/null @@ -1,228 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as request from 'request'; -import { createReadStream, createWriteStream, unlink, mkdir } from 'fs'; -import * as github from 'github-releases'; -import { join } from 'path'; -import { tmpdir } from 'os'; -import { promisify } from 'util'; - -const BASE_URL = 'https://rink.hockeyapp.net/api/2/'; -const HOCKEY_APP_TOKEN_HEADER = 'X-HockeyAppToken'; - -export interface IVersions { - app_versions: IVersion[]; -} - -export interface IVersion { - id: number; - version: string; -} - -export interface IApplicationAccessor { - accessToken: string; - appId: string; -} - -export interface IVersionAccessor extends IApplicationAccessor { - id: string; -} - -enum Platform { - WIN_32 = 'win32-ia32', - WIN_64 = 'win32-x64', - LINUX_64 = 'linux-x64', - MAC_OS = 'darwin-x64' -} - -function symbolsZipName(platform: Platform, electronVersion: string, insiders: boolean): string { - return `${insiders ? 'insiders' : 'stable'}-symbols-v${electronVersion}-${platform}.zip`; -} - -const SEED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; -async function tmpFile(name: string): Promise { - let res = ''; - for (let i = 0; i < 8; i++) { - res += SEED.charAt(Math.floor(Math.random() * SEED.length)); - } - - const tmpParent = join(tmpdir(), res); - - await promisify(mkdir)(tmpParent); - - return join(tmpParent, name); -} - -function getVersions(accessor: IApplicationAccessor): Promise { - return asyncRequest({ - url: `${BASE_URL}/apps/${accessor.appId}/app_versions`, - method: 'GET', - headers: { - [HOCKEY_APP_TOKEN_HEADER]: accessor.accessToken - } - }); -} - -function createVersion(accessor: IApplicationAccessor, version: string): Promise { - return asyncRequest({ - url: `${BASE_URL}/apps/${accessor.appId}/app_versions/new`, - method: 'POST', - headers: { - [HOCKEY_APP_TOKEN_HEADER]: accessor.accessToken - }, - formData: { - bundle_version: version - } - }); -} - -function updateVersion(accessor: IVersionAccessor, symbolsPath: string) { - return asyncRequest({ - url: `${BASE_URL}/apps/${accessor.appId}/app_versions/${accessor.id}`, - method: 'PUT', - headers: { - [HOCKEY_APP_TOKEN_HEADER]: accessor.accessToken - }, - formData: { - dsym: createReadStream(symbolsPath) - } - }); -} - -function asyncRequest(options: request.UrlOptions & request.CoreOptions): Promise { - return new Promise((resolve, reject) => { - request(options, (error, _response, body) => { - if (error) { - reject(error); - } else { - resolve(JSON.parse(body)); - } - }); - }); -} - -function downloadAsset(repository: any, assetName: string, targetPath: string, electronVersion: string) { - return new Promise((resolve, reject) => { - repository.getReleases({ tag_name: `v${electronVersion}` }, (err: any, releases: any) => { - if (err) { - reject(err); - } else { - const asset = releases[0].assets.filter((asset: any) => asset.name === assetName)[0]; - if (!asset) { - reject(new Error(`Asset with name ${assetName} not found`)); - } else { - repository.downloadAsset(asset, (err: any, reader: any) => { - if (err) { - reject(err); - } else { - const writer = createWriteStream(targetPath); - writer.on('error', reject); - writer.on('close', resolve); - reader.on('error', reject); - - reader.pipe(writer); - } - }); - } - } - }); - }); -} - -interface IOptions { - repository: string; - platform: Platform; - versions: { code: string; insiders: boolean; electron: string; }; - access: { hockeyAppToken: string; hockeyAppId: string; githubToken: string }; -} - -async function ensureVersionAndSymbols(options: IOptions) { - - // Check version does not exist - console.log(`HockeyApp: checking for existing version ${options.versions.code} (${options.platform})`); - const versions = await getVersions({ accessToken: options.access.hockeyAppToken, appId: options.access.hockeyAppId }); - if (!Array.isArray(versions.app_versions)) { - throw new Error(`Unexpected response: ${JSON.stringify(versions)}`); - } - - if (versions.app_versions.some(v => v.version === options.versions.code)) { - console.log(`HockeyApp: Returning without uploading symbols because version ${options.versions.code} (${options.platform}) was already found`); - return; - } - - // Download symbols for platform and electron version - const symbolsName = symbolsZipName(options.platform, options.versions.electron, options.versions.insiders); - const symbolsPath = await tmpFile('symbols.zip'); - console.log(`HockeyApp: downloading symbols ${symbolsName} for electron ${options.versions.electron} (${options.platform}) into ${symbolsPath}`); - await downloadAsset(new (github as any)({ repo: options.repository, token: options.access.githubToken }), symbolsName, symbolsPath, options.versions.electron); - - // Create version - console.log(`HockeyApp: creating new version ${options.versions.code} (${options.platform})`); - const version = await createVersion({ accessToken: options.access.hockeyAppToken, appId: options.access.hockeyAppId }, options.versions.code); - - // Upload symbols - console.log(`HockeyApp: uploading symbols for version ${options.versions.code} (${options.platform})`); - await updateVersion({ id: String(version.id), accessToken: options.access.hockeyAppToken, appId: options.access.hockeyAppId }, symbolsPath); - - // Cleanup - await promisify(unlink)(symbolsPath); -} - -// Environment -const pakage = require('../../../package.json'); -const product = require('../../../product.json'); -const repository = product.electronRepository; -const electronVersion = require('../../lib/electron').getElectronVersion(); -const insiders = product.quality !== 'stable'; -let codeVersion = pakage.version; -if (insiders) { - codeVersion = `${codeVersion}-insider`; -} -const githubToken = process.argv[2]; -const hockeyAppToken = process.argv[3]; -const is64 = process.argv[4] === 'x64'; -const hockeyAppId = process.argv[5]; - -if (process.argv.length !== 6) { - throw new Error(`HockeyApp: Unexpected number of arguments. Got ${process.argv}`); -} - -let platform: Platform; -if (process.platform === 'darwin') { - platform = Platform.MAC_OS; -} else if (process.platform === 'win32') { - platform = is64 ? Platform.WIN_64 : Platform.WIN_32; -} else { - platform = Platform.LINUX_64; -} - -// Create version and upload symbols in HockeyApp -if (repository && codeVersion && electronVersion && (product.quality === 'stable' || product.quality === 'insider')) { - ensureVersionAndSymbols({ - repository, - platform, - versions: { - code: codeVersion, - insiders, - electron: electronVersion - }, - access: { - githubToken, - hockeyAppToken, - hockeyAppId - } - }).then(() => { - console.log('HockeyApp: done'); - }).catch(error => { - console.error(`HockeyApp: error ${error} (AppID: ${hockeyAppId})`); - - return process.exit(1); - }); -} else { - console.log(`HockeyApp: skipping due to unexpected context (repository: ${repository}, codeVersion: ${codeVersion}, electronVersion: ${electronVersion}, quality: ${product.quality})`); -} \ No newline at end of file diff --git a/build/azure-pipelines/darwin/app-entitlements.plist b/build/azure-pipelines/darwin/app-entitlements.plist new file mode 100644 index 0000000000..90031d937b --- /dev/null +++ b/build/azure-pipelines/darwin/app-entitlements.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 9623d2e660..4961bfa344 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -65,12 +65,13 @@ steps: # ./scripts/test-integration.sh --tfs "Integration Tests" # displayName: Run Integration Tests (Electron) -# - task: PublishPipelineArtifact@0 -# inputs: -# artifactName: crash-dump-macos -# targetPath: .build/crashes -# displayName: 'Publish Crash Reports' -# condition: succeededOrFailed() +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-macos + targetPath: .build/crashes + displayName: 'Publish Crash Reports' + continueOnError: true + condition: failed() - task: PublishTestResults@2 displayName: Publish Tests Results diff --git a/build/azure-pipelines/darwin/entitlements.plist b/build/azure-pipelines/darwin/entitlements.plist deleted file mode 100644 index 6631ffa6f2..0000000000 --- a/build/azure-pipelines/darwin/entitlements.plist +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 175ec6c111..9c5b047ceb 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -157,24 +157,18 @@ steps: artifactName: crash-dump-macos targetPath: .build/crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - 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-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: | @@ -248,16 +242,28 @@ steps: SessionTimeout: 60 displayName: Notarization +- script: | + set -e + APP_ROOT=$(agent.builddirectory)/VSCode-darwin + APP_NAME="`ls $APP_ROOT | head -n 1`" + "$APP_ROOT/$APP_NAME/Contents/Resources/app/bin/code" --export-default-configuration=.build + displayName: Verify start after signing (export configuration) + - script: | set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ - VSCODE_HOCKEYAPP_TOKEN="$(vscode-hockeyapp-token)" \ ./build/azure-pipelines/darwin/publish.sh displayName: Publish +- script: | + AZURE_STORAGE_ACCESS_KEY="$(ticino-storage-key)" \ + yarn gulp upload-vscode-configuration + displayName: Upload configuration (for Bing settings search) + continueOnError: true + - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 displayName: 'Component Detection' continueOnError: true diff --git a/build/azure-pipelines/darwin/publish.sh b/build/azure-pipelines/darwin/publish.sh index f0375d2a3c..07734e194f 100755 --- a/build/azure-pipelines/darwin/publish.sh +++ b/build/azure-pipelines/darwin/publish.sh @@ -17,11 +17,3 @@ node build/azure-pipelines/common/createAsset.js \ archive-unsigned \ "vscode-server-darwin.zip" \ ../vscode-server-darwin.zip - -# publish hockeyapp symbols -# node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" x64 "$VSCODE_HOCKEYAPP_ID_MACOS" -# Skip hockey app because build failure. -# https://github.com/microsoft/vscode/issues/90491 - -# upload configuration -yarn gulp upload-vscode-configuration diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index 3e392c8920..cb871f65bd 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -81,6 +81,14 @@ steps: # displayName: 'Publish Crash Reports' # condition: succeededOrFailed() +- task: PublishPipelineArtifact@0 + inputs: + artifactName: crash-dump-linux + targetPath: .build/crashes + displayName: 'Publish Crash Reports' + continueOnError: true + condition: failed() + - task: PublishTestResults@2 displayName: Publish Tests Results inputs: diff --git a/build/azure-pipelines/linux/product-build-linux-multiarch.yml b/build/azure-pipelines/linux/product-build-linux-multiarch.yml index 68ae4ee8b6..485f8dcfba 100644 --- a/build/azure-pipelines/linux/product-build-linux-multiarch.yml +++ b/build/azure-pipelines/linux/product-build-linux-multiarch.yml @@ -107,7 +107,6 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - VSCODE_HOCKEYAPP_TOKEN="$(vscode-hockeyapp-token)" \ ./build/azure-pipelines/linux/multiarch/$(VSCODE_ARCH)/publish.sh displayName: Publish diff --git a/build/azure-pipelines/linux/product-build-linux.yml b/build/azure-pipelines/linux/product-build-linux.yml index 76428b860f..5d7bccf467 100644 --- a/build/azure-pipelines/linux/product-build-linux.yml +++ b/build/azure-pipelines/linux/product-build-linux.yml @@ -145,7 +145,8 @@ steps: artifactName: crash-dump-linux targetPath: .build/crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - script: | set -e @@ -178,7 +179,6 @@ steps: AZURE_DOCUMENTDB_MASTERKEY="$(builds-docdb-key-readwrite)" \ AZURE_STORAGE_ACCESS_KEY_2="$(vscode-storage-key)" \ VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ - VSCODE_HOCKEYAPP_TOKEN="$(vscode-hockeyapp-token)" \ ./build/azure-pipelines/linux/publish.sh displayName: Publish diff --git a/build/azure-pipelines/linux/publish.sh b/build/azure-pipelines/linux/publish.sh index b168ab9cf7..7e360be6cb 100755 --- a/build/azure-pipelines/linux/publish.sh +++ b/build/azure-pipelines/linux/publish.sh @@ -27,11 +27,6 @@ rm -rf $ROOT/vscode-server-*.tar.* node build/azure-pipelines/common/createAsset.js "server-$PLATFORM_LINUX" archive-unsigned "$SERVER_TARBALL_FILENAME" "$SERVER_TARBALL_PATH" -# Publish hockeyapp symbols -# node build/azure-pipelines/common/symbols.js "$VSCODE_MIXIN_PASSWORD" "$VSCODE_HOCKEYAPP_TOKEN" "x64" "$VSCODE_HOCKEYAPP_ID_LINUX64" -# Skip hockey app because build failure. -# https://github.com/microsoft/vscode/issues/90491 - # Publish DEB PLATFORM_DEB="linux-deb-x64" DEB_ARCH="amd64" diff --git a/build/azure-pipelines/mixin.js b/build/azure-pipelines/mixin.js index 6ab3b75140..203727861e 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('✔︎')); diff --git a/build/azure-pipelines/product-build.yml b/build/azure-pipelines/product-build.yml index a98b5f4f77..7b6d2bcbbd 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 1f665c8b3d..db6524be03 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 - condition: succeeded() - 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) - - script: | set -e yarn postinstall diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 15c764990c..c404caa6e6 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -72,12 +72,13 @@ steps: # .\scripts\test-integration.bat --tfs "Integration Tests" # displayName: Run Integration Tests (Electron) -# - task: PublishPipelineArtifact@0 -# displayName: 'Publish Crash Reports' -# inputs: -# artifactName: crash-dump-windows -# targetPath: .build\crashes -# condition: succeededOrFailed() +- task: PublishPipelineArtifact@0 + displayName: 'Publish Crash Reports' + inputs: + artifactName: crash-dump-windows + targetPath: .build\crashes + continueOnError: true + condition: failed() - task: PublishTestResults@2 displayName: Publish Tests Results 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 0000000000..01be34aa9a --- /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/product-build-win32.yml b/build/azure-pipelines/win32/product-build-win32.yml index 75dc54e359..466d3dd332 100644 --- a/build/azure-pipelines/win32/product-build-win32.yml +++ b/build/azure-pipelines/win32/product-build-win32.yml @@ -154,7 +154,8 @@ steps: artifactName: crash-dump-windows-$(VSCODE_ARCH) targetPath: .build\crashes displayName: 'Publish Crash Reports' - condition: succeededOrFailed() + continueOnError: true + condition: failed() - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 inputs: @@ -235,7 +236,6 @@ steps: $ErrorActionPreference = "Stop" $env:AZURE_STORAGE_ACCESS_KEY_2 = "$(vscode-storage-key)" $env:AZURE_DOCUMENTDB_MASTERKEY = "$(builds-docdb-key-readwrite)" - $env:VSCODE_HOCKEYAPP_TOKEN = "$(vscode-hockeyapp-token)" $env:VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" .\build\azure-pipelines\win32\publish.ps1 displayName: Publish diff --git a/build/azure-pipelines/win32/publish.ps1 b/build/azure-pipelines/win32/publish.ps1 index c9ff9d09ec..a225f9d5fd 100644 --- a/build/azure-pipelines/win32/publish.ps1 +++ b/build/azure-pipelines/win32/publish.ps1 @@ -16,22 +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 } -# Skip hockey app because build failure. -# https://github.com/microsoft/vscode/issues/90491 -# publish hockeyapp symbols -# $hockeyAppId = if ("$Arch" -eq "ia32") { "$env:VSCODE_HOCKEYAPP_ID_WIN32" } else { "$env:VSCODE_HOCKEYAPP_ID_WIN64" } -# exec { node build/azure-pipelines/common/symbols.js "$env:VSCODE_MIXIN_PASSWORD" "$env:VSCODE_HOCKEYAPP_TOKEN" "$Arch" $hockeyAppId } +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 fdee2f55cd..65c8a28380 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 0000000000..7307d01c0e --- /dev/null +++ b/build/darwin/sign.js @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 0000000000..e299ea5606 --- /dev/null +++ b/build/darwin/sign.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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.editor.js b/build/gulpfile.editor.js index a1297d1802..7a06e69086 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -127,6 +127,7 @@ const createESMSourcesAndResourcesTask = task.define('extract-editor-esm', () => const compileEditorESMTask = task.define('compile-editor-esm', () => { const KEEP_PREV_ANALYSIS = false; + const FAIL_ON_PURPOSE = false; console.log(`Launching the TS compiler at ${path.join(__dirname, '../out-editor-esm')}...`); let result; if (process.platform === 'win32') { @@ -142,7 +143,7 @@ const compileEditorESMTask = task.define('compile-editor-esm', () => { console.log(result.stdout.toString()); console.log(result.stderr.toString()); - if (result.status !== 0) { + if (FAIL_ON_PURPOSE || result.status !== 0) { console.log(`The TS Compilation failed, preparing analysis folder...`); const destPath = path.join(__dirname, '../../vscode-monaco-editor-esm-analysis'); const keepPrevAnalysis = (KEEP_PREV_ANALYSIS && fs.existsSync(destPath)); diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 2dde039a25..8666a15cc0 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -60,6 +60,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/**', @@ -85,7 +86,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 f468ba892c..55fc01effb 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -77,6 +77,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', @@ -184,6 +185,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 4407f02985..1e0fd4b09a 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -66,6 +66,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); @@ -89,12 +90,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, @@ -113,8 +114,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 => { @@ -146,6 +149,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 e9778b7a9c..3cd602a96e 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', @@ -73,7 +67,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 2e078558b2..561e777391 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', @@ -81,7 +75,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 dd2b3cd1d6..93a6992e41 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -138,6 +138,10 @@ "name": "vs/workbench/contrib/relauncher", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/sash", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/scm", "project": "vscode-workbench" @@ -338,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 d093dfda8c..0733d83e34 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,sql}/**/electron-browser/**', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 6c52cab5b2..172b52e9a1 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,sql}/**/electron-browser/**', diff --git a/build/lib/treeshaking.js b/build/lib/treeshaking.js index 34b517f76c..a9ac363113 100644 --- a/build/lib/treeshaking.js +++ b/build/lib/treeshaking.js @@ -420,7 +420,7 @@ function markNodes(languageService, options) { // (they can be the declaration of a module import) continue; } - if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + if (options.shakeLevel === 2 /* ClassMembers */ && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { enqueue_black(declaration.name); for (let j = 0; j < declaration.members.length; j++) { const member = declaration.members[j]; @@ -614,6 +614,34 @@ function generateResult(languageService, shakeLevel) { } //#endregion //#region Utils +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration) { + if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { + for (const heritageClause of declaration.heritageClauses) { + for (const type of heritageClause.types) { + const symbol = findSymbolFromHeritageType(checker, type); + if (symbol) { + const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); + if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { + return true; + } + } + } + } + } + return false; +} +function findSymbolFromHeritageType(checker, type) { + if (ts.isExpressionWithTypeArguments(type)) { + return findSymbolFromHeritageType(checker, type.expression); + } + if (ts.isIdentifier(type)) { + return getRealNodeSymbol(checker, type)[0]; + } + if (ts.isPropertyAccessExpression(type)) { + return findSymbolFromHeritageType(checker, type.name); + } + return null; +} /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ diff --git a/build/lib/treeshaking.ts b/build/lib/treeshaking.ts index 80822c4c87..495541a898 100644 --- a/build/lib/treeshaking.ts +++ b/build/lib/treeshaking.ts @@ -536,7 +536,7 @@ function markNodes(languageService: ts.LanguageService, options: ITreeShakingOpt continue; } - if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration))) { + if (options.shakeLevel === ShakeLevel.ClassMembers && (ts.isClassDeclaration(declaration) || ts.isInterfaceDeclaration(declaration)) && !isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program, checker, declaration)) { enqueue_black(declaration.name!); for (let j = 0; j < declaration.members.length; j++) { @@ -752,6 +752,36 @@ function generateResult(languageService: ts.LanguageService, shakeLevel: ShakeLe //#region Utils +function isLocalCodeExtendingOrInheritingFromDefaultLibSymbol(program: ts.Program, checker: ts.TypeChecker, declaration: ts.ClassDeclaration | ts.InterfaceDeclaration): boolean { + if (!program.isSourceFileDefaultLibrary(declaration.getSourceFile()) && declaration.heritageClauses) { + for (const heritageClause of declaration.heritageClauses) { + for (const type of heritageClause.types) { + const symbol = findSymbolFromHeritageType(checker, type); + if (symbol) { + const decl = symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]); + if (decl && program.isSourceFileDefaultLibrary(decl.getSourceFile())) { + return true; + } + } + } + } + } + return false; +} + +function findSymbolFromHeritageType(checker: ts.TypeChecker, type: ts.ExpressionWithTypeArguments | ts.Expression | ts.PrivateIdentifier): ts.Symbol | null { + if (ts.isExpressionWithTypeArguments(type)) { + return findSymbolFromHeritageType(checker, type.expression); + } + if (ts.isIdentifier(type)) { + return getRealNodeSymbol(checker, type)[0]; + } + if (ts.isPropertyAccessExpression(type)) { + return findSymbolFromHeritageType(checker, type.name); + } + return null; +} + /** * Returns the node's symbol and the `import` node (if the symbol resolved from a different module) */ diff --git a/build/lib/util.js b/build/lib/util.js index bff8653056..fa80fc7f32 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 806173699f..bd4eee5395 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 5df163f163..9d2734da6d 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 baae3ffde5..300c1355d3 100644 --- a/build/package.json +++ b/build/package.json @@ -35,6 +35,7 @@ "applicationinsights": "1.0.8", "azure-storage": "^2.1.0", "documentdb": "1.13.0", + "electron-osx-sign": "^0.4.16", "github-releases": "^0.4.1", "gulp-bom": "^1.0.0", "gulp-sourcemaps": "^1.11.0", @@ -48,7 +49,7 @@ "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-node-resolve": "^5.2.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 46e30ba21c..2b54228502 100644 --- a/build/win32/code.iss +++ b/build/win32/code.iss @@ -151,7 +151,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 dff6224b1a..8b029e0e28 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -634,6 +634,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== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -669,6 +674,11 @@ binary-search-bounds@2.0.3: resolved "https://registry.yarnpkg.com/binary-search-bounds/-/binary-search-bounds-2.0.3.tgz#5ff8616d6dd2ca5388bc85b2d6266e2b9da502dc" integrity sha1-X/hhbW3SylOIvIWy1iZuK52lAtw= +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" @@ -731,11 +741,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" @@ -900,6 +928,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= + component-emitter@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -1003,7 +1036,7 @@ debug-fabulous@0.0.X: lazy-debug-legacy "0.0.X" object-assign "4.1.0" -debug@2.X, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3: +debug@2.X, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, 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== @@ -1163,6 +1196,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" @@ -2004,6 +2049,13 @@ isarray@1.0.0, 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" @@ -2720,6 +2772,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" + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -3462,10 +3523,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" @@ -3700,11 +3761,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 09478ca403..f1ce13c720 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 97155fafc8..eb76b0e1d0 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/docker/package.json b/extensions/docker/package.json index aa4ef1c294..c1b91dc283 100644 --- a/extensions/docker/package.json +++ b/extensions/docker/package.json @@ -12,9 +12,9 @@ "contributes": { "languages": [{ "id": "dockerfile", - "extensions": [ ".dockerfile" ], - "filenames": [ "Dockerfile" ], - "aliases": [ "Dockerfile" ], + "extensions": [ ".dockerfile", ".containerfile" ], + "filenames": [ "Dockerfile", "Containerfile" ], + "aliases": [ "Dockerfile", "Containerfile" ], "configuration": "./language-configuration.json" }], "grammars": [{ diff --git a/extensions/git/package.json b/extensions/git/package.json index ea99da5140..d33bafbad5 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -105,6 +105,12 @@ "category": "Git", "icon": "$(add)" }, + { + "command": "git.stageAllMerge", + "title": "%command.stageAllMerge%", + "category": "Git", + "icon": "$(add)" + }, { "command": "git.stageSelectedRanges", "title": "%command.stageSelectedRanges%", @@ -490,6 +496,10 @@ "command": "git.stageAllUntracked", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" }, + { + "command": "git.stageAllMerge", + "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" + }, { "command": "git.stageSelectedRanges", "when": "config.git.enabled && !git.missing && gitOpenRepositoryCount != 0" @@ -897,12 +907,12 @@ ], "scm/resourceGroup/context": [ { - "command": "git.stageAll", + "command": "git.stageAllMerge", "when": "scmProvider == git && scmResourceGroup == merge", "group": "1_modification" }, { - "command": "git.stageAll", + "command": "git.stageAllMerge", "when": "scmProvider == git && scmResourceGroup == merge", "group": "inline" }, @@ -1678,10 +1688,7 @@ "description": "%config.terminalAuthentication%" }, "git.githubAuthentication": { - "type": "boolean", - "scope": "resource", - "default": true, - "description": "%config.githubAuthentication%" + "deprecationMessage": "This setting is now deprecated, please use `github.gitAuthentication` instead." } } }, @@ -1843,26 +1850,27 @@ { "view": "scm", "contents": "%view.workbench.scm.empty%", - "when": "config.git.enabled && !git.missing && workbenchState == empty" + "when": "config.git.enabled && git.state == initialized && workbenchState == empty" }, { "view": "scm", "contents": "%view.workbench.scm.folder%", - "when": "config.git.enabled && !git.missing && workbenchState == folder" + "when": "config.git.enabled && git.state == initialized && workbenchState == folder" }, { "view": "scm", "contents": "%view.workbench.scm.workspace%", - "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount != 0" + "when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0" }, { "view": "scm", "contents": "%view.workbench.scm.emptyWorkspace%", - "when": "config.git.enabled && !git.missing && workbenchState == workspace && workspaceFolderCount == 0" + "when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount == 0" }, { "view": "explorer", - "contents": "%view.workbench.cloneRepository%" + "contents": "%view.workbench.cloneRepository%", + "when": "config.git.enabled" } ] }, diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index a735b58aa7..98d4ce4019 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -14,6 +14,7 @@ "command.stageAll": "Stage All Changes", "command.stageAllTracked": "Stage All Tracked Changes", "command.stageAllUntracked": "Stage All Untracked Changes", + "command.stageAllMerge": "Stage All Merge Changes", "command.stageSelectedRanges": "Stage Selected Ranges", "command.revertSelectedRanges": "Revert Selected Ranges", "command.stageChange": "Stage Change", @@ -55,7 +56,7 @@ "command.pushToForce": "Push to... (Force)", "command.pushFollowTags": "Push (Follow Tags)", "command.pushFollowTagsForce": "Push (Follow Tags, Force)", - "command.addRemote": "Add Remote", + "command.addRemote": "Add Remote...", "command.removeRemote": "Remove Remote", "command.sync": "Sync", "command.syncRebase": "Sync (Rebase)", @@ -146,7 +147,6 @@ "config.untrackedChanges.hidden": "Untracked changes are hidden and excluded from several actions.", "config.showCommitInput": "Controls whether to show the commit input in the Git source control panel.", "config.terminalAuthentication": "Controls whether to enable VS Code to be the authentication handler for git processes spawned in the integrated terminal. Note: terminals need to be restarted to pick up a change in this setting.", - "config.githubAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", "colors.added": "Color for added resources.", "colors.modified": "Color for modified resources.", "colors.deleted": "Color for deleted resources.", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index dd74f5bb65..f7326a39f2 100644 --- a/extensions/git/src/api/api1.ts +++ b/extensions/git/src/api/api1.ts @@ -5,7 +5,7 @@ import { Model } from '../model'; import { Repository as BaseRepository, Resource } from '../repository'; -import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider, CredentialsProvider } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, GitExtension, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode'; import { mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -159,6 +159,10 @@ export class ApiRepository implements Repository { return this._repository.getBranch(name); } + getBranches(query: BranchQuery): Promise { + return this._repository.getBranches(query); + } + setBranchUpstream(name: string, upstream: string): Promise { return this._repository.setBranchUpstream(name, upstream); } diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index e1b68f1ac0..2a8d1cdf9b 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -121,6 +121,7 @@ export interface RepositoryUIState { export interface LogOptions { /** Max number of log entries to retrieve. If not specified, the default is 32. */ readonly maxEntries?: number; + readonly path?: string; } export interface CommitOptions { @@ -131,6 +132,11 @@ export interface CommitOptions { empty?: boolean; } +export interface BranchQuery { + readonly remote?: boolean; + readonly contains?: string; +} + export interface Repository { readonly rootUri: Uri; @@ -170,6 +176,7 @@ export interface Repository { createBranch(name: string, checkout: boolean, ref?: string): Promise; deleteBranch(name: string, force?: boolean): Promise; getBranch(name: string): Promise; + getBranches(query: BranchQuery): Promise; setBranchUpstream(name: string, upstream: string): Promise; getMergeBase(ref1: string, ref2: string): Promise; @@ -202,6 +209,7 @@ export interface RemoteSourceProvider { readonly icon?: string; // codicon name readonly supportsQuery?: boolean; getRemoteSources(query?: string): ProviderResult; + publishRepository?(repository: Repository): Promise; } export interface Credentials { diff --git a/extensions/git/src/askpass.ts b/extensions/git/src/askpass.ts index 0a177470d4..385a67aae1 100644 --- a/extensions/git/src/askpass.ts +++ b/extensions/git/src/askpass.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { window, InputBoxOptions, Uri, OutputChannel, Disposable } from 'vscode'; +import { window, InputBoxOptions, Uri, OutputChannel, Disposable, workspace } from 'vscode'; import { IDisposable, EmptyDisposable, toDisposable } from './util'; import * as path from 'path'; import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer'; @@ -31,6 +31,13 @@ export class Askpass implements IIPCHandler { } async handle({ request, host }: { request: string, host: string }): Promise { + const config = workspace.getConfiguration('git', null); + const enabled = config.get('enabled'); + + if (!enabled) { + return ''; + } + const uri = Uri.parse(host); const authority = uri.authority.replace(/^.*@/, ''); const password = /password/i.test(request); diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 4a2ecfa7ab..4450cc36df 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -19,6 +19,7 @@ import { grep, isDescendant, pathEquals } from './util'; import { Log, LogLevel } from './log'; import { GitTimelineItem } from './timelineProvider'; import { throttle, debounce } from './decorators'; +import { ApiRepository } from './api/api1'; const localize = nls.loadMessageBundle(); @@ -216,6 +217,11 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] { return [...heads, ...tags, ...remoteHeads]; } +function sanitizeRemoteName(name: string) { + name = name.trim(); + return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-'); +} + class TagItem implements QuickPickItem { get label(): string { return this.ref.name ?? ''; } get description(): string { return this.ref.commit?.substr(0, 8) ?? ''; } @@ -287,6 +293,7 @@ class RemoteSourceProviderQuickPick { } } catch (err) { this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }]; + console.error(err); } finally { this.quickpick.busy = false; } @@ -527,7 +534,7 @@ export class CommandCenter { .map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('clonefrom', "Clone from {0}", provider.name), alwaysShow: true, provider })); quickpick.placeholder = providers.length === 0 - ? localize('provide url', "Provide repository URL.") + ? localize('provide url', "Provide repository URL") : localize('provide url or pick', "Provide repository URL or pick a repository source."); const updatePicks = (value?: string) => { @@ -993,37 +1000,14 @@ export class CommandCenter { @command('git.stageAll', { repository: true }) async stageAll(repository: Repository): Promise { - const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[]; - const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources); + const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]; + const uris = resources.map(r => r.resourceUri); - try { - for (const deletionConflict of deletionConflicts) { - await this._stageDeletionConflict(repository, deletionConflict.resourceUri); - } - } catch (err) { - if (/Cancelled/.test(err.message)) { - return; - } - - throw err; + if (uris.length > 0) { + const config = workspace.getConfiguration('git', Uri.file(repository.root)); + const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges'); + await repository.add(uris, untrackedChanges === 'mixed' ? undefined : { update: true }); } - - if (unresolved.length > 0) { - const message = unresolved.length > 1 - ? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", merge.length) - : localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(merge[0].resourceUri.fsPath)); - - const yes = localize('yes', "Yes"); - const pick = await window.showWarningMessage(message, { modal: true }, yes); - - if (pick !== yes) { - return; - } - } - - const config = workspace.getConfiguration('git', Uri.file(repository.root)); - const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges'); - await repository.add([], untrackedChanges === 'mixed' ? undefined : { update: true }); } private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise { @@ -1079,6 +1063,43 @@ export class CommandCenter { await repository.add(uris); } + @command('git.stageAllMerge', { repository: true }) + async stageAllMerge(repository: Repository): Promise { + const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[]; + const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources); + + try { + for (const deletionConflict of deletionConflicts) { + await this._stageDeletionConflict(repository, deletionConflict.resourceUri); + } + } catch (err) { + if (/Cancelled/.test(err.message)) { + return; + } + + throw err; + } + + if (unresolved.length > 0) { + const message = unresolved.length > 1 + ? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", merge.length) + : localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(merge[0].resourceUri.fsPath)); + + const yes = localize('yes', "Yes"); + const pick = await window.showWarningMessage(message, { modal: true }, yes); + + if (pick !== yes) { + return; + } + } + + const uris = resources.map(r => r.resourceUri); + + if (uris.length > 0) { + await repository.add(uris); + } + } + @command('git.stageChange') async stageChange(uri: Uri, changes: LineChange[], index: number): Promise { const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0]; @@ -1998,9 +2019,17 @@ export class CommandCenter { const remotes = repository.remotes; if (remotes.length === 0) { - if (!pushOptions.silent) { - window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to.")); + if (pushOptions.silent) { + return; } + + const addRemote = localize('addremote', 'Add Remote'); + const result = await window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."), addRemote); + + if (result === addRemote) { + await this.addRemote(repository); + } + return; } @@ -2117,48 +2146,79 @@ export class CommandCenter { @command('git.addRemote', { repository: true }) async addRemote(repository: Repository): Promise { - const remotes = repository.remotes; + const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>(); + quickpick.ignoreFocusOut = true; - const sanitize = (name: string) => { - name = name.trim(); - return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-'); + const providers = this.model.getRemoteProviders() + .map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('addfrom', "Add remote from {0}", provider.name), alwaysShow: true, provider })); + + quickpick.placeholder = providers.length === 0 + ? localize('provide url', "Provide repository URL") + : localize('provide url or pick', "Provide repository URL or pick a repository source."); + + const updatePicks = (value?: string) => { + if (value) { + quickpick.items = [{ + label: localize('addFrom', "Add remote from URL"), + description: value, + alwaysShow: true, + url: value + }, + ...providers]; + } else { + quickpick.items = providers; + } }; + quickpick.onDidChangeValue(updatePicks); + updatePicks(); + + const result = await getQuickPickResult(quickpick); + let url: string | undefined; + + if (result) { + if (result.url) { + url = result.url; + } else if (result.provider) { + const quickpick = new RemoteSourceProviderQuickPick(result.provider); + const remote = await quickpick.pick(); + + if (remote) { + if (typeof remote.url === 'string') { + url = remote.url; + } else if (remote.url.length > 0) { + url = await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") }); + } + } + } + } + + if (!url) { + return; + } + const resultName = await window.showInputBox({ placeHolder: localize('remote name', "Remote name"), prompt: localize('provide remote name', "Please provide a remote name"), ignoreFocusOut: true, validateInput: (name: string) => { - if (sanitize(name)) { - return null; + if (!sanitizeRemoteName(name)) { + return localize('remote name format invalid', "Remote name format invalid"); + } else if (repository.remotes.find(r => r.name === name)) { + return localize('remote already exists', "Remote '{0}' already exists.", name); } - return localize('remote name format invalid', "Remote name format invalid"); + + return null; } }); - const name = sanitize(resultName || ''); + const name = sanitizeRemoteName(resultName || ''); if (!name) { return; } - if (remotes.find(r => r.name === name)) { - window.showErrorMessage(localize('remote already exists', "Remote '{0}' already exists.", name)); - return; - } - - const url = await window.showInputBox({ - placeHolder: localize('remote url', "Remote URL"), - prompt: localize('provide remote URL', "Enter URL for remote \"{0}\"", name), - ignoreFocusOut: true - }); - - if (!url) { - return; - } - await repository.addRemote(name, url); - return name; } @@ -2268,15 +2328,38 @@ export class CommandCenter { @command('git.publish', { repository: true }) async publish(repository: Repository): Promise { + const branchName = repository.HEAD && repository.HEAD.name || ''; const remotes = repository.remotes; if (remotes.length === 0) { - window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to.")); + const providers = this.model.getRemoteProviders().filter(p => !!p.publishRepository); + + if (providers.length === 0) { + window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to.")); + return; + } + + let provider: RemoteSourceProvider; + + if (providers.length === 1) { + provider = providers[0]; + } else { + const picks = providers + .map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('publish to', "Publish to {0}", provider.name), alwaysShow: true, provider })); + const placeHolder = localize('pick provider', "Pick a provider to publish the branch '{0}' to:", branchName); + const choice = await window.showQuickPick(picks, { placeHolder }); + + if (!choice) { + return; + } + + provider = choice.provider; + } + + await provider.publishRepository!(new ApiRepository(repository)); return; } - const branchName = repository.HEAD && repository.HEAD.name || ''; - if (remotes.length === 1) { return await repository.pushTo(remotes[0].name, branchName, true); } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index ed8b89ea98..5ebff9c193 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -15,7 +15,7 @@ import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, import { CancellationToken, Progress, Uri } from 'vscode'; import { URI } from 'vscode-uri'; import { detectEncoding } from './encoding'; -import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions } from './api/git'; +import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -848,6 +848,9 @@ export class Repository { async log(options?: LogOptions): Promise { const maxEntries = options?.maxEntries ?? 32; const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--']; + if (options?.path) { + args.push(options.path); + } const result = await this.run(args); if (result.exitCode) { @@ -1220,15 +1223,13 @@ export class Repository { args.push('-A'); } - args.push('--'); - if (paths && paths.length) { - args.push.apply(args, paths.map(sanitizePath)); + for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + await this.run([...args, '--', ...chunk]); + } } else { - args.push('.'); + await this.run([...args, '--', '.']); } - - await this.run(args); } async rm(paths: string[]): Promise { @@ -1437,10 +1438,11 @@ export class Repository { const limiter = new Limiter(5); const promises: Promise[] = []; + const args = ['clean', '-f', '-q']; for (const paths of groups) { for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { - promises.push(limiter.queue(() => this.run(['clean', '-f', '-q', '--', ...chunk]))); + promises.push(limiter.queue(() => this.run([...args, '--', ...chunk]))); } } @@ -1472,19 +1474,19 @@ export class Repository { // In case there are no branches, we must use rm --cached if (!result.stdout) { - args = ['rm', '--cached', '-r', '--']; + args = ['rm', '--cached', '-r']; } else { - args = ['reset', '-q', treeish, '--']; - } - - if (paths && paths.length) { - args.push.apply(args, paths.map(sanitizePath)); - } else { - args.push('.'); + args = ['reset', '-q', treeish]; } try { - await this.run(args); + if (paths && paths.length > 0) { + for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + await this.run([...args, '--', ...chunk]); + } + } else { + await this.run([...args, '--', '.']); + } } catch (err) { // In case there are merge conflicts to be resolved, git reset will output // some "needs merge" data. We try to get around that. @@ -1789,13 +1791,17 @@ export class Repository { .map(([ref]) => ({ name: ref, type: RefType.Head } as Branch)); } - async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate' }): Promise { + async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate', contains?: string }): Promise { const args = ['for-each-ref', '--format', '%(refname) %(objectname)']; if (opts && opts.sort && opts.sort !== 'alphabetically') { args.push('--sort', `-${opts.sort}`); } + if (opts?.contains) { + args.push('--contains', opts.contains); + } + const result = await this.run(args); const fn = (line: string): Ref | null => { @@ -1913,6 +1919,11 @@ export class Repository { } } + async getBranches(query: BranchQuery): Promise { + const refs = await this.getRefs({ contains: query.contains }); + return refs.filter(value => (value.type !== RefType.Tag) && (query.remote || !value.remote)); + } + // TODO: Support core.commentChar stripCommitMessageComments(message: string): string { return message.replace(/^\s*#.*$\n?/gm, '').trim(); @@ -1963,10 +1974,10 @@ export class Repository { } async updateSubmodules(paths: string[]): Promise { - const args = ['submodule', 'update', '--']; + const args = ['submodule', 'update']; for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { - await this.run([...args, ...chunk]); + await this.run([...args, '--', ...chunk]); } } diff --git a/extensions/git/src/github.ts b/extensions/git/src/github.ts deleted file mode 100644 index a031607ba8..0000000000 --- a/extensions/git/src/github.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CredentialsProvider, Credentials } from './api/git'; -import { IDisposable, filterEvent, EmptyDisposable } from './util'; -import { workspace, Uri, AuthenticationSession, authentication } from 'vscode'; -import { Askpass } from './askpass'; - -export class GitHubCredentialProvider implements CredentialsProvider { - - async getCredentials(host: Uri): Promise { - if (!/github\.com/i.test(host.authority)) { - return; - } - - const session = await this.getSession(); - return { username: session.account.id, password: await session.getAccessToken() }; - } - - private async getSession(): Promise { - const authenticationSessions = await authentication.getSessions('github', ['repo']); - - if (authenticationSessions.length) { - return await authenticationSessions[0]; - } else { - return await authentication.login('github', ['repo']); - } - } -} - -export class GithubCredentialProviderManager { - - private providerDisposable: IDisposable = EmptyDisposable; - private readonly disposable: IDisposable; - - private _enabled = false; - private set enabled(enabled: boolean) { - if (this._enabled === enabled) { - return; - } - - this._enabled = enabled; - - if (enabled) { - this.providerDisposable = this.askpass.registerCredentialsProvider(new GitHubCredentialProvider()); - } else { - this.providerDisposable.dispose(); - } - } - - constructor(private readonly askpass: Askpass) { - this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))(this.refresh, this); - this.refresh(); - } - - private refresh(): void { - this.enabled = workspace.getConfiguration('git', null).get('githubAuthentication', true); - } - - dispose(): void { - this.enabled = false; - this.disposable.dispose(); - } -} diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 7c9669b6ae..456b485cb7 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -22,7 +22,6 @@ import { GitExtensionImpl } from './api/extension'; // import * as fs from 'fs'; import { GitTimelineProvider } from './timelineProvider'; import { registerAPICommands } from './api/api1'; -import { GithubCredentialProviderManager } from './github'; import { TerminalEnvironmentManager } from './terminal'; const deactivateTasks: { (): Promise; }[] = []; @@ -44,9 +43,6 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann const terminalEnvironmentManager = new TerminalEnvironmentManager(context, env); disposables.push(terminalEnvironmentManager); - const githubCredentialProviderManager = new GithubCredentialProviderManager(askpass); - context.subscriptions.push(githubCredentialProviderManager); - const git = new Git({ gitPath: info.path, version: info.version, env }); const model = new Model(git, askpass, context.globalState, outputChannel); disposables.push(model); diff --git a/extensions/git/src/model.ts b/extensions/git/src/model.ts index 1231028d6f..e698782f19 100644 --- a/extensions/git/src/model.ts +++ b/extensions/git/src/model.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel } from 'vscode'; +import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel, commands } from 'vscode'; import { Repository, RepositoryState } from './repository'; import { memoize, sequentialize, debounce } from './decorators'; import { dispose, anyEvent, filterEvent, isDescendant, firstIndex, pathEquals, toDisposable } from './util'; @@ -12,8 +12,9 @@ import * as path from 'path'; import * as fs from 'fs'; import * as nls from 'vscode-nls'; import { fromGitUri } from './uri'; -import { GitErrorCodes, APIState as State, RemoteSourceProvider, CredentialsProvider } from './api/git'; +import { APIState as State, RemoteSourceProvider, CredentialsProvider } from './api/git'; import { Askpass } from './askpass'; +import { IRemoteSourceProviderRegistry } from './remoteProvider'; const localize = nls.loadMessageBundle(); @@ -45,7 +46,7 @@ interface OpenRepository extends Disposable { repository: Repository; } -export class Model { +export class Model implements IRemoteSourceProviderRegistry { private _onDidOpenRepository = new EventEmitter(); readonly onDidOpenRepository: Event = this._onDidOpenRepository.event; @@ -73,9 +74,16 @@ export class Model { setState(state: State): void { this._state = state; this._onDidChangeState.fire(state); + commands.executeCommand('setContext', 'git.state', state); } - private remoteProviders = new Set(); + private remoteSourceProviders = new Set(); + + private _onDidAddRemoteSourceProvider = new EventEmitter(); + readonly onDidAddRemoteSourceProvider = this._onDidAddRemoteSourceProvider.event; + + private _onDidRemoveRemoteSourceProvider = new EventEmitter(); + readonly onDidRemoveRemoteSourceProvider = this._onDidRemoveRemoteSourceProvider.event; private disposables: Disposable[] = []; @@ -92,6 +100,7 @@ export class Model { const onPossibleGitRepositoryChange = filterEvent(onGitRepositoryChange, uri => !this.getRepository(uri)); onPossibleGitRepositoryChange(this.onPossibleGitRepositoryChange, this, this.disposables); + this.setState('uninitialized'); this.doInitialScan().finally(() => this.setState('initialized')); } @@ -115,31 +124,27 @@ export class Model { return; } - for (const folder of workspace.workspaceFolders || []) { + await Promise.all((workspace.workspaceFolders || []).map(async folder => { const root = folder.uri.fsPath; + const children = await new Promise((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r))); + const promises = children + .filter(child => child !== '.git') + .map(child => this.openRepository(path.join(root, child))); - try { - const children = await new Promise((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r))); + const folderConfig = workspace.getConfiguration('git', folder.uri); + const paths = folderConfig.get('scanRepositories') || []; - children - .filter(child => child !== '.git') - .forEach(child => this.openRepository(path.join(root, child))); - - const folderConfig = workspace.getConfiguration('git', folder.uri); - const paths = folderConfig.get('scanRepositories') || []; - - for (const possibleRepositoryPath of paths) { - if (path.isAbsolute(possibleRepositoryPath)) { - console.warn(localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting.")); - continue; - } - - this.openRepository(path.join(root, possibleRepositoryPath)); + for (const possibleRepositoryPath of paths) { + if (path.isAbsolute(possibleRepositoryPath)) { + console.warn(localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting.")); + continue; } - } catch (err) { - // noop + + promises.push(this.openRepository(path.join(root, possibleRepositoryPath))); } - } + + await Promise.all(promises); + })); } private onPossibleGitRepositoryChange(uri: Uri): void { @@ -248,16 +253,12 @@ export class Model { } const dotGit = await this.git.getRepositoryDotGit(repositoryRoot); - const repository = new Repository(this.git.open(repositoryRoot, dotGit), this.globalState, this.outputChannel); + const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this.globalState, this.outputChannel); this.open(repository); await repository.status(); } catch (err) { - if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) { - return; - } - - // console.error('Failed to find repository:', err); + // noop } } @@ -451,8 +452,13 @@ export class Model { } registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable { - this.remoteProviders.add(provider); - return toDisposable(() => this.remoteProviders.delete(provider)); + this.remoteSourceProviders.add(provider); + this._onDidAddRemoteSourceProvider.fire(provider); + + return toDisposable(() => { + this.remoteSourceProviders.delete(provider); + this._onDidRemoveRemoteSourceProvider.fire(provider); + }); } registerCredentialsProvider(provider: CredentialsProvider): Disposable { @@ -460,7 +466,7 @@ export class Model { } getRemoteProviders(): RemoteSourceProvider[] { - return [...this.remoteProviders.values()]; + return [...this.remoteSourceProviders.values()]; } dispose(): void { diff --git a/extensions/git/src/remoteProvider.ts b/extensions/git/src/remoteProvider.ts new file mode 100644 index 0000000000..24f941b799 --- /dev/null +++ b/extensions/git/src/remoteProvider.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, Event } from 'vscode'; +import { RemoteSourceProvider } from './api/git'; + +export interface IRemoteSourceProviderRegistry { + readonly onDidAddRemoteSourceProvider: Event; + readonly onDidRemoveRemoteSourceProvider: Event; + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + getRemoteProviders(): RemoteSourceProvider[]; +} diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index ad2cf89802..e7c162dd9a 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -7,7 +7,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode'; import * as nls from 'vscode-nls'; -import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions } from './api/git'; +import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery } from './api/git'; import { AutoFetcher } from './autofetch'; import { debounce, memoize, throttle } from './decorators'; import { Commit, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; @@ -16,6 +16,7 @@ import { toGitUri } from './uri'; import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent } from './util'; import { IFileWatcher, watch } from './watch'; import { Log, LogLevel } from './log'; +import { IRemoteSourceProviderRegistry } from './remoteProvider'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -251,11 +252,13 @@ export class Resource implements SourceControlResourceState { } get resourceDecoration(): Decoration { - const title = this.tooltip; - const letter = this.letter; - const color = this.color; - const priority = this.priority; - return { bubble: true, title, letter, color, priority }; + return { + bubble: this.type !== Status.DELETED && this.type !== Status.INDEX_DELETED, + title: this.tooltip, + letter: this.letter, + color: this.color, + priority: this.priority + }; } constructor( @@ -279,6 +282,7 @@ export const enum Operation { Clean = 'Clean', Branch = 'Branch', GetBranch = 'GetBranch', + GetBranches = 'GetBranches', SetBranchUpstream = 'SetBranchUpstream', HashObject = 'HashObject', Checkout = 'Checkout', @@ -678,6 +682,7 @@ export class Repository implements Disposable { constructor( private readonly repository: BaseRepository, + remoteSourceProviderRegistry: IRemoteSourceProviderRegistry, globalState: Memento, outputChannel: OutputChannel ) { @@ -773,7 +778,7 @@ export class Repository implements Disposable { } }, null, this.disposables); - const statusBar = new StatusBarCommands(this); + const statusBar = new StatusBarCommands(this, remoteSourceProviderRegistry); this.disposables.push(statusBar); statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables); this._sourceControl.statusBarCommands = statusBar.commands; @@ -1049,6 +1054,10 @@ export class Repository implements Disposable { return await this.run(Operation.GetBranch, () => this.repository.getBranch(name)); } + async getBranches(query: BranchQuery): Promise { + return await this.run(Operation.GetBranches, () => this.repository.getBranches(query)); + } + async setBranchUpstream(name: string, upstream: string): Promise { await this.run(Operation.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream)); } diff --git a/extensions/git/src/statusbar.ts b/extensions/git/src/statusbar.ts index 2d2288f3fa..43bf093160 100644 --- a/extensions/git/src/statusbar.ts +++ b/extensions/git/src/statusbar.ts @@ -7,7 +7,8 @@ import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode import { Repository, Operation } from './repository'; import { anyEvent, dispose, filterEvent } from './util'; import * as nls from 'vscode-nls'; -import { Branch } from './api/git'; +import { Branch, RemoteSourceProvider } from './api/git'; +import { IRemoteSourceProviderRegistry } from './remoteProvider'; const localize = nls.loadMessageBundle(); @@ -39,41 +40,45 @@ class CheckoutStatusBar { } interface SyncStatusBarState { - enabled: boolean; - isSyncRunning: boolean; - hasRemotes: boolean; - HEAD: Branch | undefined; + readonly enabled: boolean; + readonly isSyncRunning: boolean; + readonly hasRemotes: boolean; + readonly HEAD: Branch | undefined; + readonly remoteSourceProviders: RemoteSourceProvider[]; } class SyncStatusBar { - private static StartState: SyncStatusBarState = { - enabled: true, - isSyncRunning: false, - hasRemotes: false, - HEAD: undefined - }; - private _onDidChange = new EventEmitter(); get onDidChange(): Event { return this._onDidChange.event; } private disposables: Disposable[] = []; - private _state: SyncStatusBarState = SyncStatusBar.StartState; + private _state: SyncStatusBarState; private get state() { return this._state; } private set state(state: SyncStatusBarState) { this._state = state; this._onDidChange.fire(); } - constructor(private repository: Repository) { - repository.onDidRunGitStatus(this.onModelChange, this, this.disposables); - repository.onDidChangeOperations(this.onOperationsChange, this, this.disposables); + constructor(private repository: Repository, private remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) { + repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables); + repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables); + + anyEvent(remoteSourceProviderRegistry.onDidAddRemoteSourceProvider, remoteSourceProviderRegistry.onDidRemoveRemoteSourceProvider) + (this.onDidChangeRemoteSourceProviders, this, this.disposables); const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync')); onEnablementChange(this.updateEnablement, this, this.disposables); this.updateEnablement(); - this._onDidChange.fire(); + this._state = { + enabled: true, + isSyncRunning: false, + hasRemotes: false, + HEAD: undefined, + remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders() + .filter(p => !!p.publishRepository) + }; } private updateEnablement(): void { @@ -83,7 +88,7 @@ class SyncStatusBar { this.state = { ... this.state, enabled }; } - private onOperationsChange(): void { + private onDidChangeOperations(): void { const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) || this.repository.operations.isRunning(Operation.Push) || this.repository.operations.isRunning(Operation.Pull); @@ -91,7 +96,7 @@ class SyncStatusBar { this.state = { ...this.state, isSyncRunning }; } - private onModelChange(): void { + private onDidRunGitStatus(): void { this.state = { ...this.state, hasRemotes: this.repository.remotes.length > 0, @@ -99,9 +104,34 @@ class SyncStatusBar { }; } + private onDidChangeRemoteSourceProviders(): void { + this.state = { + ...this.state, + remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders() + .filter(p => !!p.publishRepository) + }; + } + get command(): Command | undefined { - if (!this.state.enabled || !this.state.hasRemotes) { - return undefined; + if (!this.state.enabled) { + return; + } + + if (!this.state.hasRemotes) { + if (this.state.remoteSourceProviders.length === 0) { + return; + } + + const tooltip = this.state.remoteSourceProviders.length === 1 + ? localize('publish to', "Publish to {0}", this.state.remoteSourceProviders[0].name) + : localize('publish to...', "Publish to..."); + + return { + command: 'git.publish', + title: `$(cloud-upload)`, + tooltip, + arguments: [this.repository.sourceControl] + }; } const HEAD = this.state.HEAD; @@ -152,38 +182,21 @@ class SyncStatusBar { export class StatusBarCommands { + readonly onDidChange: Event; + private syncStatusBar: SyncStatusBar; private checkoutStatusBar: CheckoutStatusBar; private disposables: Disposable[] = []; - constructor(repository: Repository) { - this.syncStatusBar = new SyncStatusBar(repository); + constructor(repository: Repository, remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) { + this.syncStatusBar = new SyncStatusBar(repository, remoteSourceProviderRegistry); this.checkoutStatusBar = new CheckoutStatusBar(repository); - } - - get onDidChange(): Event { - return anyEvent( - this.syncStatusBar.onDidChange, - this.checkoutStatusBar.onDidChange - ); + this.onDidChange = anyEvent(this.syncStatusBar.onDidChange, this.checkoutStatusBar.onDidChange); } get commands(): Command[] { - const result: Command[] = []; - - const checkout = this.checkoutStatusBar.command; - - if (checkout) { - result.push(checkout); - } - - const sync = this.syncStatusBar.command; - - if (sync) { - result.push(sync); - } - - return result; + return [this.checkoutStatusBar.command, this.syncStatusBar.command] + .filter((c): c is Command => !!c); } dispose(): void { diff --git a/extensions/git/src/terminal.ts b/extensions/git/src/terminal.ts index c8679a8271..780d52eb49 100644 --- a/extensions/git/src/terminal.ts +++ b/extensions/git/src/terminal.ts @@ -34,7 +34,8 @@ export class TerminalEnvironmentManager { } private refresh(): void { - this.enabled = workspace.getConfiguration('git', null).get('terminalAuthentication', true); + const config = workspace.getConfiguration('git', null); + this.enabled = config.get('enabled', true) && config.get('terminalAuthentication', true); } dispose(): void { diff --git a/extensions/github-authentication/build/generateconfig.js b/extensions/github-authentication/build/generateconfig.js deleted file mode 100644 index 58a5ecbacb..0000000000 --- a/extensions/github-authentication/build/generateconfig.js +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -const fs = require('fs'); -const path = require('path'); - -const schemes = ['OSS', 'INSIDERS', 'STABLE', 'EXPLORATION', 'VSO', 'VSO_PPE', 'VSO_DEV']; - -function main() { - let content = {}; - - for (const scheme of schemes) { - const id = process.env[`${scheme}_GITHUB_ID`]; - const secret = process.env[`${scheme}_GITHUB_SECRET`]; - - if (id && secret) { - content[scheme] = { id, secret }; - } - } - - const githubAppId = process.env.GITHUB_APP_ID; - const githubAppSecret = process.env.GITHUB_APP_SECRET; - - if (githubAppId && githubAppSecret) { - content.GITHUB_APP = { id: githubAppId, secret: githubAppSecret } - } - - if (Object.keys(content).length > 0) { - fs.writeFileSync(path.join(__dirname, '../src/common/config.json'), JSON.stringify(content)); - } -} - -main(); diff --git a/extensions/github-authentication/src/common/clientRegistrar.ts b/extensions/github-authentication/src/common/clientRegistrar.ts deleted file mode 100644 index 0cae2366e6..0000000000 --- a/extensions/github-authentication/src/common/clientRegistrar.ts +++ /dev/null @@ -1,102 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Uri, env } from 'vscode'; -import * as fs from 'fs'; -import * as path from 'path'; - -export interface ClientDetails { - id?: string; - secret?: string; -} - -export interface ClientConfig { - OSS: ClientDetails; - INSIDERS: ClientDetails; - STABLE: ClientDetails; - EXPLORATION: ClientDetails; - - VSO: ClientDetails; - VSO_PPE: ClientDetails; - VSO_DEV: ClientDetails; - - GITHUB_APP: ClientDetails; -} - -export class Registrar { - private _config: ClientConfig; - - constructor() { - try { - const fileContents = fs.readFileSync(path.join(env.appRoot, 'extensions/github-authentication/src/common/config.json')).toString(); - this._config = JSON.parse(fileContents); - } catch (e) { - this._config = { - OSS: {}, - INSIDERS: {}, - STABLE: {}, - EXPLORATION: {}, - VSO: {}, - VSO_PPE: {}, - VSO_DEV: {}, - GITHUB_APP: {} - }; - } - } - - getGitHubAppDetails(): ClientDetails { - if (!this._config.GITHUB_APP.id || !this._config.GITHUB_APP.secret) { - throw new Error(`No GitHub App client configuration available`); - } - - return this._config.GITHUB_APP; - } - - getClientDetails(callbackUri: Uri): ClientDetails { - let details: ClientDetails | undefined; - switch (callbackUri.scheme) { - case 'code-oss': - details = this._config.OSS; - break; - - case 'vscode-insiders': - details = this._config.INSIDERS; - break; - - case 'vscode': - details = this._config.STABLE; - break; - - case 'vscode-exploration': - details = this._config.EXPLORATION; - break; - - case 'https': - switch (callbackUri.authority) { - case 'online.visualstudio.com': - details = this._config.VSO; - break; - case 'online-ppe.core.vsengsaas.visualstudio.com': - details = this._config.VSO_PPE; - break; - case 'online.dev.core.vsengsaas.visualstudio.com': - details = this._config.VSO_DEV; - break; - } - - default: - throw new Error(`Unrecognized callback ${callbackUri}`); - } - - if (!details.id || !details.secret) { - throw new Error(`No client configuration available for ${callbackUri}`); - } - - return details; - } -} - -const ClientRegistrar = new Registrar(); -export default ClientRegistrar; diff --git a/extensions/github-authentication/src/extension.ts b/extensions/github-authentication/src/extension.ts index 444bd903bf..d5c8d203c0 100644 --- a/extensions/github-authentication/src/extension.ts +++ b/extensions/github-authentication/src/extension.ts @@ -25,6 +25,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.authentication.registerAuthenticationProvider({ id: 'github', displayName: 'GitHub', + supportsMultipleAccounts: false, onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), login: async (scopeList: string[]) => { diff --git a/extensions/github-authentication/src/github.ts b/extensions/github-authentication/src/github.ts index c9443eab4d..34fff37b46 100644 --- a/extensions/github-authentication/src/github.ts +++ b/extensions/github-authentication/src/github.ts @@ -21,20 +21,8 @@ interface SessionData { accessToken: string; } -// TODO remove -interface OldSessionData { - id: string; - accountName: string; - scopes: string[]; - accessToken: string; -} - -function isOldSessionData(x: any): x is OldSessionData { - return !!x.accountName; -} - export class GitHubAuthenticationProvider { - private _sessions: vscode.AuthenticationSession[] = []; + private _sessions: vscode.AuthenticationSession2[] = []; private _githubServer = new GitHubServer(); public async initialize(): Promise { @@ -44,14 +32,12 @@ export class GitHubAuthenticationProvider { // Ignore, network request failed } - // TODO revert Cannot validate tokens from auth server, no available clientId - // await this.validateSessions(); this.pollForChange(); } private pollForChange() { setTimeout(async () => { - let storedSessions: vscode.AuthenticationSession[]; + let storedSessions: vscode.AuthenticationSession2[]; try { storedSessions = await this.readSessions(); } catch (e) { @@ -94,13 +80,13 @@ export class GitHubAuthenticationProvider { }, 1000 * 30); } - private async readSessions(): Promise { + private async readSessions(): Promise { const storedSessions = await keychain.getToken(); if (storedSessions) { try { - const sessionData: (SessionData | OldSessionData)[] = JSON.parse(storedSessions); - const sessionPromises = sessionData.map(async (session: SessionData | OldSessionData): Promise => { - const needsUserInfo = isOldSessionData(session) || !session.account; + const sessionData: SessionData[] = JSON.parse(storedSessions); + const sessionPromises = sessionData.map(async (session: SessionData): Promise => { + const needsUserInfo = !session.account; let userInfo: { id: string, accountName: string }; if (needsUserInfo) { userInfo = await this._githubServer.getUserInfo(session.accessToken); @@ -109,15 +95,11 @@ export class GitHubAuthenticationProvider { return { id: session.id, account: { - displayName: isOldSessionData(session) - ? session.accountName - : session.account?.displayName ?? userInfo!.accountName, - id: isOldSessionData(session) - ? userInfo!.id - : session.account?.id ?? userInfo!.id + displayName: session.account?.displayName ?? userInfo!.accountName, + id: session.account?.id ?? userInfo!.id }, scopes: session.scopes, - getAccessToken: () => Promise.resolve(session.accessToken) + accessToken: session.accessToken }; }); @@ -136,57 +118,30 @@ export class GitHubAuthenticationProvider { } private async storeSessions(): Promise { - const sessionData: SessionData[] = await Promise.all(this._sessions.map(async session => { - const resolvedAccessToken = await session.getAccessToken(); - return { - id: session.id, - account: session.account, - scopes: session.scopes, - accessToken: resolvedAccessToken - }; - })); - - await keychain.setToken(JSON.stringify(sessionData)); + await keychain.setToken(JSON.stringify(this._sessions)); } - get sessions(): vscode.AuthenticationSession[] { + get sessions(): vscode.AuthenticationSession2[] { return this._sessions; } - public async login(scopes: string): Promise { - const token = scopes === 'vso' ? await this.loginAndInstallApp(scopes) : await this._githubServer.login(scopes); + public async login(scopes: string): Promise { + const token = await this._githubServer.login(scopes); const session = await this.tokenToSession(token, scopes.split(' ')); await this.setToken(session); return session; } - public async loginAndInstallApp(scopes: string): Promise { - const token = await this._githubServer.login(scopes); - const hasUserInstallation = await this._githubServer.hasUserInstallation(token); - if (hasUserInstallation) { - return token; - } else { - return this._githubServer.installApp(); - } - } - public async manuallyProvideToken(): Promise { this._githubServer.manuallyProvideToken(); } - private async tokenToSession(token: string, scopes: string[]): Promise { + private async tokenToSession(token: string, scopes: string[]): Promise { const userInfo = await this._githubServer.getUserInfo(token); - return { - id: uuid(), - getAccessToken: () => Promise.resolve(token), - account: { - displayName: userInfo.accountName, - id: userInfo.id - }, - scopes: scopes - }; + return new vscode.AuthenticationSession2(uuid(), token, { displayName: userInfo.accountName, id: userInfo.id }, scopes); } - private async setToken(session: vscode.AuthenticationSession): Promise { + + private async setToken(session: vscode.AuthenticationSession2): Promise { const sessionIndex = this._sessions.findIndex(s => s.id === session.id); if (sessionIndex > -1) { this._sessions.splice(sessionIndex, 1, session); @@ -201,14 +156,6 @@ export class GitHubAuthenticationProvider { const sessionIndex = this._sessions.findIndex(session => session.id === id); if (sessionIndex > -1) { this._sessions.splice(sessionIndex, 1); - // TODO revert - // Cannot revoke tokens from auth server, no clientId available - // const token = await session.getAccessToken(); - // try { - // await this._githubServer.revokeToken(token); - // } catch (_) { - // // ignore, should still remove from keychain - // } } await this.storeSessions(); diff --git a/extensions/github-authentication/src/githubServer.ts b/extensions/github-authentication/src/githubServer.ts index c7882c676f..e2e252e8ee 100644 --- a/extensions/github-authentication/src/githubServer.ts +++ b/extensions/github-authentication/src/githubServer.ts @@ -9,7 +9,6 @@ import * as vscode from 'vscode'; import * as uuid from 'uuid'; import { PromiseAdapter, promiseFromEvent } from './common/utils'; import Logger from './common/logger'; -import ClientRegistrar from './common/clientRegistrar'; const localize = nls.loadMessageBundle(); @@ -24,8 +23,8 @@ class UriEventHandler extends vscode.EventEmitter implements vscode. export const uriHandler = new UriEventHandler; -const exchangeCodeForToken: (state: string, host: string, getPath: (code: string) => string) => PromiseAdapter = - (state, host, getPath) => async (uri, resolve, reject) => { +const exchangeCodeForToken: (state: string) => PromiseAdapter = + (state) => async (uri, resolve, reject) => { Logger.info('Exchanging code for token...'); const query = parseQuery(uri); const code = query.code; @@ -36,8 +35,8 @@ const exchangeCodeForToken: (state: string, host: string, getPath: (code: string } const post = https.request({ - host: host, - path: getPath(code), + host: AUTH_RELAY_SERVER, + path: `/token?code=${code}&state=${state}`, method: 'POST', headers: { Accept: 'application/json' @@ -81,26 +80,13 @@ export class GitHubServer { const state = uuid(); const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - let uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code`); - if (scopes === 'vso') { - const clientDetails = ClientRegistrar.getGitHubAppDetails(); - uri = vscode.Uri.parse(`https://github.com/login/oauth/authorize?redirect_uri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&client_id=${clientDetails.id}`); - } + const uri = vscode.Uri.parse(`https://${AUTH_RELAY_SERVER}/authorize/?callbackUri=${encodeURIComponent(callbackUri.toString())}&scope=${scopes}&state=${state}&responseType=code`); vscode.env.openExternal(uri); - return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, - scopes === 'vso' ? 'github.com' : AUTH_RELAY_SERVER, - (code) => { - if (scopes === 'vso') { - const clientDetails = ClientRegistrar.getGitHubAppDetails(); - return `/login/oauth/access_token?client_id=${clientDetails.id}&client_secret=${clientDetails.secret}&state=${state}&code=${code}`; - } else { - return `/token?code=${code}&state=${state}`; - } - })).finally(() => { - this.updateStatusBarItem(false); - }); + return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state)).finally(() => { + this.updateStatusBarItem(false); + }); } private updateStatusBarItem(isStart?: boolean) { @@ -130,51 +116,6 @@ export class GitHubServer { } } - public async hasUserInstallation(token: string): Promise { - return new Promise((resolve, reject) => { - Logger.info('Getting user installations...'); - const post = https.request({ - host: 'api.github.com', - path: `/user/installations`, - method: 'GET', - headers: { - Accept: 'application/vnd.github.machine-man-preview+json', - Authorization: `token ${token}`, - 'User-Agent': 'Visual-Studio-Code' - } - }, result => { - const buffer: Buffer[] = []; - result.on('data', (chunk: Buffer) => { - buffer.push(chunk); - }); - result.on('end', () => { - if (result.statusCode === 200) { - const json = JSON.parse(Buffer.concat(buffer).toString()); - Logger.info('Got installation info!'); - const hasInstallation = json.installations.some((installation: { app_slug: string }) => installation.app_slug === 'microsoft-visual-studio-code'); - resolve(hasInstallation); - } else { - reject(new Error(result.statusMessage)); - } - }); - }); - - post.end(); - post.on('error', err => { - reject(err); - }); - }); - } - - public async installApp(): Promise { - const clientDetails = ClientRegistrar.getGitHubAppDetails(); - const state = uuid(); - const uri = vscode.Uri.parse(`https://github.com/apps/microsoft-visual-studio-code/installations/new?state=${state}`); - - vscode.env.openExternal(uri); - return promiseFromEvent(uriHandler.event, exchangeCodeForToken(state, 'github.com', (code) => `/login/oauth/access_token?client_id=${clientDetails.id}&client_secret=${clientDetails.secret}&state=${state}&code=${code}`)); - } - public async getUserInfo(token: string): Promise<{ id: string, accountName: string }> { return new Promise((resolve, reject) => { Logger.info('Getting account info...'); @@ -210,91 +151,4 @@ export class GitHubServer { }); }); } - - public async validateToken(token: string): Promise { - return new Promise(async (resolve, reject) => { - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - const clientDetails = ClientRegistrar.getClientDetails(callbackUri); - const detailsString = `${clientDetails.id}:${clientDetails.secret}`; - - const payload = JSON.stringify({ access_token: token }); - - Logger.info('Validating token...'); - const post = https.request({ - host: 'api.github.com', - path: `/applications/${clientDetails.id}/token`, - method: 'POST', - headers: { - Authorization: `Basic ${Buffer.from(detailsString).toString('base64')}`, - 'User-Agent': 'Visual-Studio-Code', - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload) - } - }, result => { - const buffer: Buffer[] = []; - result.on('data', (chunk: Buffer) => { - buffer.push(chunk); - }); - result.on('end', () => { - if (result.statusCode === 200) { - Logger.info('Validated token!'); - resolve(); - } else { - Logger.info(`Validating token failed: ${result.statusMessage}`); - reject(new Error(result.statusMessage)); - } - }); - }); - - post.write(payload); - post.end(); - post.on('error', err => { - Logger.error(err.message); - reject(new Error(NETWORK_ERROR)); - }); - }); - } - - public async revokeToken(token: string): Promise { - return new Promise(async (resolve, reject) => { - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.github-authentication/did-authenticate`)); - const clientDetails = ClientRegistrar.getClientDetails(callbackUri); - const detailsString = `${clientDetails.id}:${clientDetails.secret}`; - - const payload = JSON.stringify({ access_token: token }); - - Logger.info('Revoking token...'); - const post = https.request({ - host: 'api.github.com', - path: `/applications/${clientDetails.id}/token`, - method: 'DELETE', - headers: { - Authorization: `Basic ${Buffer.from(detailsString).toString('base64')}`, - 'User-Agent': 'Visual-Studio-Code', - 'Content-Type': 'application/json', - 'Content-Length': Buffer.byteLength(payload) - } - }, result => { - const buffer: Buffer[] = []; - result.on('data', (chunk: Buffer) => { - buffer.push(chunk); - }); - result.on('end', () => { - if (result.statusCode === 204) { - Logger.info('Revoked token!'); - resolve(); - } else { - Logger.info(`Revoking token failed: ${result.statusMessage}`); - reject(new Error(result.statusMessage)); - } - }); - }); - - post.write(payload); - post.end(); - post.on('error', err => { - reject(err); - }); - }); - } } diff --git a/extensions/github/.vscodeignore b/extensions/github/.vscodeignore new file mode 100644 index 0000000000..ee85b88450 --- /dev/null +++ b/extensions/github/.vscodeignore @@ -0,0 +1,8 @@ +src/** +!src/common/config.json +out/** +build/** +extension.webpack.config.js +tsconfig.json +yarn.lock +README.md diff --git a/extensions/github/README.md b/extensions/github/README.md new file mode 100644 index 0000000000..4807d25369 --- /dev/null +++ b/extensions/github/README.md @@ -0,0 +1,7 @@ +# GitHub for Visual Studio Code + +**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled. + +## Features + +This extension provides GitHub features for VS Code. diff --git a/src/vs/platform/dialogs/node/dialogs.ts b/extensions/github/extension.webpack.config.js similarity index 60% rename from src/vs/platform/dialogs/node/dialogs.ts rename to extensions/github/extension.webpack.config.js index 13a3ff5696..35b95ccffc 100644 --- a/src/vs/platform/dialogs/node/dialogs.ts +++ b/extensions/github/extension.webpack.config.js @@ -3,13 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; +//@ts-check -export interface INativeOpenDialogOptions { - forceNewWindow?: boolean; +'use strict'; - defaultPath?: string; +const withDefaults = require('../shared.webpack.config'); - telemetryEventName?: string; - telemetryExtraData?: ITelemetryData; -} +module.exports = withDefaults({ + context: __dirname, + entry: { + extension: './src/extension.ts' + } +}); diff --git a/extensions/github/package.json b/extensions/github/package.json new file mode 100644 index 0000000000..6535ac92a3 --- /dev/null +++ b/extensions/github/package.json @@ -0,0 +1,67 @@ +{ + "name": "github", + "displayName": "%displayName%", + "description": "%description%", + "publisher": "vscode", + "version": "0.0.1", + "engines": { + "vscode": "^1.41.0" + }, + "enableProposedApi": true, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "extensionDependencies": [ + "vscode.git" + ], + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "github.publish", + "title": "Publish to GitHub" + } + ], + "configuration": [ + { + "title": "GitHub", + "properties": { + "github.gitAuthentication": { + "type": "boolean", + "scope": "resource", + "default": true, + "description": "%config.gitAuthentication%" + } + } + } + ], + "viewsWelcome": [ + { + "view": "scm", + "contents": "%welcome.publishFolder%", + "when": "config.git.enabled && git.state == initialized && workbenchState == folder" + }, + { + "view": "scm", + "contents": "%welcome.publishWorkspaceFolder%", + "when": "config.git.enabled && git.state == initialized && workbenchState == workspace && workspaceFolderCount != 0" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run compile", + "compile": "gulp compile-extension:github", + "watch": "gulp watch-extension:github" + }, + "dependencies": { + "@octokit/rest": "^17.9.1", + "tunnel": "^0.0.6", + "vscode-nls": "^4.1.2" + }, + "devDependencies": { + "@types/node": "^10.12.21" + } +} diff --git a/extensions/github/package.nls.json b/extensions/github/package.nls.json new file mode 100644 index 0000000000..28906a39f0 --- /dev/null +++ b/extensions/github/package.nls.json @@ -0,0 +1,7 @@ +{ + "displayName": "GitHub", + "description": "GitHub", + "config.gitAuthentication": "Controls whether to enable automatic GitHub authentication for git commands within VS Code.", + "welcome.publishFolder": "You can also directly publish this folder to a GitHub repository.\n[$(github) Publish to GitHub](command:github.publish)", + "welcome.publishWorkspaceFolder": "You can also directly publish a workspace folder to a GitHub repository.\n[$(github) Publish to GitHub](command:github.publish)" +} diff --git a/extensions/github/src/auth.ts b/extensions/github/src/auth.ts new file mode 100644 index 0000000000..df4bf778d1 --- /dev/null +++ b/extensions/github/src/auth.ts @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AuthenticationSession, authentication, window } from 'vscode'; +import { Agent, globalAgent } from 'https'; +import { Octokit } from '@octokit/rest'; +import { httpsOverHttp } from 'tunnel'; +import { URL } from 'url'; + +function getAgent(url: string | undefined = process.env.HTTPS_PROXY): Agent { + if (!url) { + return globalAgent; + } + + try { + const { hostname, port, username, password } = new URL(url); + const auth = username && password && `${username}:${password}`; + return httpsOverHttp({ proxy: { host: hostname, port, proxyAuth: auth } }); + } catch (e) { + window.showErrorMessage(`HTTPS_PROXY environment variable ignored: ${e.message}`); + return globalAgent; + } +} + +const scopes = ['repo']; + +export async function getSession(): Promise { + const authenticationSessions = await authentication.getSessions('github', scopes); + + if (authenticationSessions.length) { + return await authenticationSessions[0]; + } else { + return await authentication.login('github', scopes); + } +} + +let _octokit: Promise | undefined; + +export function getOctokit(): Promise { + if (!_octokit) { + _octokit = getSession().then(async session => { + const token = await session.getAccessToken(); + const agent = getAgent(); + + return new Octokit({ + request: { agent }, + userAgent: 'GitHub VSCode', + auth: `token ${token}` + }); + }).then(null, async err => { + _octokit = undefined; + throw err; + }); + } + + return _octokit; +} + diff --git a/extensions/github/src/commands.ts b/extensions/github/src/commands.ts new file mode 100644 index 0000000000..1a3fd851c0 --- /dev/null +++ b/extensions/github/src/commands.ts @@ -0,0 +1,22 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { API as GitAPI } from './typings/git'; +import { publishRepository } from './publish'; + +export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] { + const disposables = []; + + disposables.push(vscode.commands.registerCommand('github.publish', async () => { + try { + publishRepository(gitAPI); + } catch (err) { + vscode.window.showErrorMessage(err.message); + } + })); + + return disposables; +} diff --git a/extensions/github/src/credentialProvider.ts b/extensions/github/src/credentialProvider.ts new file mode 100644 index 0000000000..55743eb5df --- /dev/null +++ b/extensions/github/src/credentialProvider.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CredentialsProvider, Credentials, API as GitAPI } from './typings/git'; +import { workspace, Uri, Disposable } from 'vscode'; +import { getSession } from './auth'; + +const EmptyDisposable: Disposable = { dispose() { } }; + +class GitHubCredentialProvider implements CredentialsProvider { + + async getCredentials(host: Uri): Promise { + if (!/github\.com/i.test(host.authority)) { + return; + } + + const session = await getSession(); + return { username: session.account.id, password: await session.getAccessToken() }; + } +} + +export class GithubCredentialProviderManager { + + private providerDisposable: Disposable = EmptyDisposable; + private readonly disposable: Disposable; + + private _enabled = false; + private set enabled(enabled: boolean) { + if (this._enabled === enabled) { + return; + } + + this._enabled = enabled; + + if (enabled) { + this.providerDisposable = this.gitAPI.registerCredentialsProvider(new GitHubCredentialProvider()); + } else { + this.providerDisposable.dispose(); + } + } + + constructor(private gitAPI: GitAPI) { + this.disposable = workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('github')) { + this.refresh(); + } + }); + + this.refresh(); + } + + private refresh(): void { + const config = workspace.getConfiguration('github', null); + const enabled = config.get('gitAuthentication', true); + this.enabled = !!enabled; + } + + dispose(): void { + this.enabled = false; + this.disposable.dispose(); + } +} diff --git a/extensions/github/src/extension.ts b/extensions/github/src/extension.ts new file mode 100644 index 0000000000..41a8e541bc --- /dev/null +++ b/extensions/github/src/extension.ts @@ -0,0 +1,19 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { GithubRemoteSourceProvider } from './remoteSourceProvider'; +import { GitExtension } from './typings/git'; +import { registerCommands } from './commands'; +import { GithubCredentialProviderManager } from './credentialProvider'; + +export async function activate(context: vscode.ExtensionContext) { + const gitExtension = vscode.extensions.getExtension('vscode.git')!.exports; + const gitAPI = gitExtension.getAPI(1); + + context.subscriptions.push(...registerCommands(gitAPI)); + context.subscriptions.push(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI))); + context.subscriptions.push(new GithubCredentialProviderManager(gitAPI)); +} diff --git a/extensions/github/src/publish.ts b/extensions/github/src/publish.ts new file mode 100644 index 0000000000..fdb3bd90e9 --- /dev/null +++ b/extensions/github/src/publish.ts @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as nls from 'vscode-nls'; +import * as path from 'path'; +import { promises as fs } from 'fs'; +import { API as GitAPI, Repository } from './typings/git'; +import { getOctokit } from './auth'; + +const localize = nls.loadMessageBundle(); + +function sanitizeRepositoryName(value: string): string { + return value.trim().replace(/[^a-z0-9_.]/ig, '-'); +} + +function getPick(quickpick: vscode.QuickPick): Promise { + return Promise.race([ + new Promise(c => quickpick.onDidAccept(() => quickpick.selectedItems.length > 0 && c(quickpick.selectedItems[0]))), + new Promise(c => quickpick.onDidHide(() => c(undefined))) + ]); +} + +export async function publishRepository(gitAPI: GitAPI, repository?: Repository): Promise { + if (!vscode.workspace.workspaceFolders?.length) { + return; + } + + let folder: vscode.WorkspaceFolder; + + if (vscode.workspace.workspaceFolders.length === 1) { + folder = vscode.workspace.workspaceFolders[0]; + } else { + const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder })); + const placeHolder = localize('pick folder', "Pick a folder to publish to GitHub"); + const pick = await vscode.window.showQuickPick(picks, { placeHolder }); + + if (!pick) { + return; + } + + folder = pick.folder; + } + + let quickpick = vscode.window.createQuickPick(); + quickpick.ignoreFocusOut = true; + + quickpick.placeholder = 'Repository Name'; + quickpick.value = folder.name; + quickpick.show(); + quickpick.busy = true; + + const octokit = await getOctokit(); + const user = await octokit.users.getAuthenticated({}); + const owner = user.data.login; + quickpick.busy = false; + + let repo: string | undefined; + + const onDidChangeValue = async () => { + const sanitizedRepo = sanitizeRepositoryName(quickpick.value); + + if (!sanitizedRepo) { + quickpick.items = []; + } else { + quickpick.items = [{ label: `$(repo) Publish to GitHub private repository`, description: `$(github) ${owner}/${sanitizedRepo}`, alwaysShow: true, repo: sanitizedRepo }]; + } + }; + + onDidChangeValue(); + + while (true) { + const listener = quickpick.onDidChangeValue(onDidChangeValue); + const pick = await getPick(quickpick); + listener.dispose(); + + repo = pick?.repo; + + if (repo) { + try { + quickpick.busy = true; + await octokit.repos.get({ owner, repo: repo }); + quickpick.items = [{ label: `$(error) GitHub repository already exists`, description: `$(github) ${owner}/${repo}`, alwaysShow: true }]; + } catch { + break; + } finally { + quickpick.busy = false; + } + } + } + + quickpick.dispose(); + + if (!repo) { + return; + } + + quickpick = vscode.window.createQuickPick(); + quickpick.placeholder = localize('ignore', "Select which files should be included in the repository."); + quickpick.canSelectMany = true; + quickpick.show(); + + try { + quickpick.busy = true; + + const repositoryPath = folder.uri.fsPath; + const currentPath = path.join(repositoryPath); + const children = await fs.readdir(currentPath); + quickpick.items = children.map(name => ({ label: name })); + quickpick.selectedItems = quickpick.items; + quickpick.busy = false; + + const result = await Promise.race([ + new Promise(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))), + new Promise(c => quickpick.onDidHide(() => c(undefined))) + ]); + + if (!result) { + return; + } + + const ignored = new Set(children); + result.forEach(c => ignored.delete(c.label)); + + const raw = [...ignored].map(i => `/${i}`).join('\n'); + await fs.writeFile(path.join(repositoryPath, '.gitignore'), raw, 'utf8'); + } finally { + quickpick.dispose(); + } + + const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => { + progress.report({ message: 'Publishing to GitHub private repository', increment: 25 }); + + const res = await octokit.repos.createForAuthenticatedUser({ + name: repo!, + private: true + }); + + const createdGithubRepository = res.data; + + progress.report({ message: 'Creating first commit', increment: 25 }); + + if (!repository) { + repository = await gitAPI.init(folder.uri) || undefined; + + if (!repository) { + return; + } + + await repository.commit('first commit', { all: true }); + } + + progress.report({ message: 'Uploading files', increment: 25 }); + await repository.addRemote('origin', createdGithubRepository.clone_url); + await repository.push('origin', 'master', true); + + return createdGithubRepository; + }); + + if (!githubRepository) { + return; + } + + const openInGitHub = 'Open In GitHub'; + const action = await vscode.window.showInformationMessage(`Successfully published the '${owner}/${repo}' repository on GitHub.`, openInGitHub); + + if (action === openInGitHub) { + vscode.commands.executeCommand('vscode.open', vscode.Uri.parse(githubRepository.html_url)); + } +} diff --git a/extensions/github/src/remoteSourceProvider.ts b/extensions/github/src/remoteSourceProvider.ts new file mode 100644 index 0000000000..503447b777 --- /dev/null +++ b/extensions/github/src/remoteSourceProvider.ts @@ -0,0 +1,67 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { API as GitAPI, RemoteSourceProvider, RemoteSource, Repository } from './typings/git'; +import { getOctokit } from './auth'; +import { Octokit } from '@octokit/rest'; +import { publishRepository } from './publish'; + +function asRemoteSource(raw: any): RemoteSource { + return { + name: `$(github) ${raw.full_name}`, + description: raw.description || undefined, + url: raw.clone_url + }; +} + +export class GithubRemoteSourceProvider implements RemoteSourceProvider { + + readonly name = 'GitHub'; + readonly icon = 'github'; + readonly supportsQuery = true; + + private userReposCache: RemoteSource[] = []; + + constructor(private gitAPI: GitAPI) { } + + async getRemoteSources(query?: string): Promise { + const octokit = await getOctokit(); + const [fromUser, fromQuery] = await Promise.all([ + this.getUserRemoteSources(octokit, query), + this.getQueryRemoteSources(octokit, query) + ]); + + const userRepos = new Set(fromUser.map(r => r.name)); + + return [ + ...fromUser, + ...fromQuery.filter(r => !userRepos.has(r.name)) + ]; + } + + private async getUserRemoteSources(octokit: Octokit, query?: string): Promise { + if (!query) { + const user = await octokit.users.getAuthenticated({}); + const username = user.data.login; + const res = await octokit.repos.listForUser({ username, sort: 'updated', per_page: 100 }); + this.userReposCache = res.data.map(asRemoteSource); + } + + return this.userReposCache; + } + + private async getQueryRemoteSources(octokit: Octokit, query?: string): Promise { + if (!query) { + return []; + } + + const raw = await octokit.search.repos({ q: query, sort: 'updated' }); + return raw.data.items.map(asRemoteSource); + } + + publishRepository(repository: Repository): Promise { + return publishRepository(this.gitAPI, repository); + } +} diff --git a/extensions/github/src/typings/git.d.ts b/extensions/github/src/typings/git.d.ts new file mode 100644 index 0000000000..2a8d1cdf9b --- /dev/null +++ b/extensions/github/src/typings/git.d.ts @@ -0,0 +1,295 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Uri, Event, Disposable, ProviderResult } from 'vscode'; +export { ProviderResult } from 'vscode'; + +export interface Git { + readonly path: string; +} + +export interface InputBox { + value: string; +} + +export const enum RefType { + Head, + RemoteHead, + Tag +} + +export interface Ref { + readonly type: RefType; + readonly name?: string; + readonly commit?: string; + readonly remote?: string; +} + +export interface UpstreamRef { + readonly remote: string; + readonly name: string; +} + +export interface Branch extends Ref { + readonly upstream?: UpstreamRef; + readonly ahead?: number; + readonly behind?: number; +} + +export interface Commit { + readonly hash: string; + readonly message: string; + readonly parents: string[]; + readonly authorDate?: Date; + readonly authorName?: string; + readonly authorEmail?: string; + readonly commitDate?: Date; +} + +export interface Submodule { + readonly name: string; + readonly path: string; + readonly url: string; +} + +export interface Remote { + readonly name: string; + readonly fetchUrl?: string; + readonly pushUrl?: string; + readonly isReadOnly: boolean; +} + +export const enum Status { + INDEX_MODIFIED, + INDEX_ADDED, + INDEX_DELETED, + INDEX_RENAMED, + INDEX_COPIED, + + MODIFIED, + DELETED, + UNTRACKED, + IGNORED, + INTENT_TO_ADD, + + ADDED_BY_US, + ADDED_BY_THEM, + DELETED_BY_US, + DELETED_BY_THEM, + BOTH_ADDED, + BOTH_DELETED, + BOTH_MODIFIED +} + +export interface Change { + + /** + * Returns either `originalUri` or `renameUri`, depending + * on whether this change is a rename change. When + * in doubt always use `uri` over the other two alternatives. + */ + readonly uri: Uri; + readonly originalUri: Uri; + readonly renameUri: Uri | undefined; + readonly status: Status; +} + +export interface RepositoryState { + readonly HEAD: Branch | undefined; + readonly refs: Ref[]; + readonly remotes: Remote[]; + readonly submodules: Submodule[]; + readonly rebaseCommit: Commit | undefined; + + readonly mergeChanges: Change[]; + readonly indexChanges: Change[]; + readonly workingTreeChanges: Change[]; + + readonly onDidChange: Event; +} + +export interface RepositoryUIState { + readonly selected: boolean; + readonly onDidChange: Event; +} + +/** + * Log options. + */ +export interface LogOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number; + readonly path?: string; +} + +export interface CommitOptions { + all?: boolean | 'tracked'; + amend?: boolean; + signoff?: boolean; + signCommit?: boolean; + empty?: boolean; +} + +export interface BranchQuery { + readonly remote?: boolean; + readonly contains?: string; +} + +export interface Repository { + + readonly rootUri: Uri; + readonly inputBox: InputBox; + readonly state: RepositoryState; + readonly ui: RepositoryUIState; + + getConfigs(): Promise<{ key: string; value: string; }[]>; + getConfig(key: string): Promise; + setConfig(key: string, value: string): Promise; + getGlobalConfig(key: string): Promise; + + getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; + detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>; + buffer(ref: string, path: string): Promise; + show(ref: string, path: string): Promise; + getCommit(ref: string): Promise; + + clean(paths: string[]): Promise; + + apply(patch: string, reverse?: boolean): Promise; + diff(cached?: boolean): Promise; + diffWithHEAD(): Promise; + diffWithHEAD(path: string): Promise; + diffWith(ref: string): Promise; + diffWith(ref: string, path: string): Promise; + diffIndexWithHEAD(): Promise; + diffIndexWithHEAD(path: string): Promise; + diffIndexWith(ref: string): Promise; + diffIndexWith(ref: string, path: string): Promise; + diffBlobs(object1: string, object2: string): Promise; + diffBetween(ref1: string, ref2: string): Promise; + diffBetween(ref1: string, ref2: string, path: string): Promise; + + hashObject(data: string): Promise; + + createBranch(name: string, checkout: boolean, ref?: string): Promise; + deleteBranch(name: string, force?: boolean): Promise; + getBranch(name: string): Promise; + getBranches(query: BranchQuery): Promise; + setBranchUpstream(name: string, upstream: string): Promise; + + getMergeBase(ref1: string, ref2: string): Promise; + + status(): Promise; + checkout(treeish: string): Promise; + + addRemote(name: string, url: string): Promise; + removeRemote(name: string): Promise; + renameRemote(name: string, newName: string): Promise; + + fetch(remote?: string, ref?: string, depth?: number): Promise; + pull(unshallow?: boolean): Promise; + push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise; + + blame(path: string): Promise; + log(options?: LogOptions): Promise; + + commit(message: string, opts?: CommitOptions): Promise; +} + +export interface RemoteSource { + readonly name: string; + readonly description?: string; + readonly url: string | string[]; +} + +export interface RemoteSourceProvider { + readonly name: string; + readonly icon?: string; // codicon name + readonly supportsQuery?: boolean; + getRemoteSources(query?: string): ProviderResult; + publishRepository?(repository: Repository): Promise; +} + +export interface Credentials { + readonly username: string; + readonly password: string; +} + +export interface CredentialsProvider { + getCredentials(host: Uri): ProviderResult; +} + +export type APIState = 'uninitialized' | 'initialized'; + +export interface API { + readonly state: APIState; + readonly onDidChangeState: Event; + readonly git: Git; + readonly repositories: Repository[]; + readonly onDidOpenRepository: Event; + readonly onDidCloseRepository: Event; + + toGitUri(uri: Uri, ref: string): Uri; + getRepository(uri: Uri): Repository | null; + init(root: Uri): Promise; + + registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; + registerCredentialsProvider(provider: CredentialsProvider): Disposable; +} + +export interface GitExtension { + + readonly enabled: boolean; + readonly onDidChangeEnablement: Event; + + /** + * Returns a specific API version. + * + * Throws error if git extension is disabled. You can listed to the + * [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event + * to know when the extension becomes enabled/disabled. + * + * @param version Version number. + * @returns API instance + */ + getAPI(version: 1): API; +} + +export const enum GitErrorCodes { + BadConfigFile = 'BadConfigFile', + AuthenticationFailed = 'AuthenticationFailed', + NoUserNameConfigured = 'NoUserNameConfigured', + NoUserEmailConfigured = 'NoUserEmailConfigured', + NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified', + NotAGitRepository = 'NotAGitRepository', + NotAtRepositoryRoot = 'NotAtRepositoryRoot', + Conflict = 'Conflict', + StashConflict = 'StashConflict', + UnmergedChanges = 'UnmergedChanges', + PushRejected = 'PushRejected', + RemoteConnectionError = 'RemoteConnectionError', + DirtyWorkTree = 'DirtyWorkTree', + CantOpenResource = 'CantOpenResource', + GitNotFound = 'GitNotFound', + CantCreatePipe = 'CantCreatePipe', + CantAccessRemote = 'CantAccessRemote', + RepositoryNotFound = 'RepositoryNotFound', + RepositoryIsLocked = 'RepositoryIsLocked', + BranchNotFullyMerged = 'BranchNotFullyMerged', + NoRemoteReference = 'NoRemoteReference', + InvalidBranchName = 'InvalidBranchName', + BranchAlreadyExists = 'BranchAlreadyExists', + NoLocalChanges = 'NoLocalChanges', + NoStashFound = 'NoStashFound', + LocalChangesOverwritten = 'LocalChangesOverwritten', + NoUpstreamBranch = 'NoUpstreamBranch', + IsInSubmodule = 'IsInSubmodule', + WrongCase = 'WrongCase', + CantLockRef = 'CantLockRef', + CantRebaseMultipleBranches = 'CantRebaseMultipleBranches', + PatchDoesNotApply = 'PatchDoesNotApply', + NoPathFound = 'NoPathFound', + UnknownPath = 'UnknownPath', +} diff --git a/extensions/github/src/typings/ref.d.ts b/extensions/github/src/typings/ref.d.ts new file mode 100644 index 0000000000..b00931a633 --- /dev/null +++ b/extensions/github/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// + +declare module 'tunnel'; diff --git a/extensions/vscode-account/tsconfig.json b/extensions/github/tsconfig.json similarity index 100% rename from extensions/vscode-account/tsconfig.json rename to extensions/github/tsconfig.json diff --git a/extensions/github/yarn.lock b/extensions/github/yarn.lock new file mode 100644 index 0000000000..6e8d2a7cc4 --- /dev/null +++ b/extensions/github/yarn.lock @@ -0,0 +1,305 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@octokit/auth-token@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f" + integrity sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg== + dependencies: + "@octokit/types" "^2.0.0" + +"@octokit/core@^2.4.3": + version "2.5.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-2.5.0.tgz#4706258893a7ac6ab35d58d2fb9f2d2ba19a41a5" + integrity sha512-uvzmkemQrBgD8xuGbjhxzJN1darJk9L2cS+M99cHrDG2jlSVpxNJVhoV86cXdYBqdHCc9Z995uLCczaaHIYA6Q== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/graphql" "^4.3.1" + "@octokit/request" "^5.4.0" + "@octokit/types" "^2.0.0" + before-after-hook "^2.1.0" + universal-user-agent "^5.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.1.tgz#16d5c0e7a83e3a644d1ddbe8cded6c3d038d31d7" + integrity sha512-pOPHaSz57SFT/m3R5P8MUu4wLPszokn5pXcB/pzavLTQf2jbU+6iayTvzaY6/BiotuRS0qyEUkx3QglT4U958A== + dependencies: + "@octokit/types" "^2.11.1" + is-plain-object "^3.0.0" + universal-user-agent "^5.0.0" + +"@octokit/graphql@^4.3.1": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.4.0.tgz#4540b48bbf796b837b311ba6ea5104760db530ca" + integrity sha512-Du3hAaSROQ8EatmYoSAJjzAz3t79t9Opj/WY1zUgxVUGfIKn0AEjg+hlOLscF6fv6i/4y/CeUvsWgIfwMkTccw== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^2.0.0" + universal-user-agent "^5.0.0" + +"@octokit/plugin-paginate-rest@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.0.tgz#9ae0c14c1b90ec0d96d2ef1b44706b4505a91cee" + integrity sha512-KoNxC3PLNar8UJwR+1VMQOw2IoOrrFdo5YOiDKnBhpVbKpw+zkBKNMNKwM44UWL25Vkn0Sl3nYIEGKY+gW5ebw== + dependencies: + "@octokit/types" "^2.12.1" + +"@octokit/plugin-request-log@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" + integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== + +"@octokit/plugin-rest-endpoint-methods@^3.11.1": + version "3.12.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.12.0.tgz#313938ece4267e98687179cdbc32bc09e06f2379" + integrity sha512-cpUTZR0V2B8lz351oKLXx01BxBF3sMOhvm0glEMIS9XWSpkHCeQDPLPh8TQmepY8e+AhhgWxuWdn1PLDPQlTpw== + dependencies: + "@octokit/types" "^4.0.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.0.tgz#94ca7293373654400fbb2995f377f9473e00834b" + integrity sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw== + dependencies: + "@octokit/types" "^2.0.0" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": + version "5.4.2" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.2.tgz#74f8e5bbd39dc738a1b127629791f8ad1b3193ee" + integrity sha512-zKdnGuQ2TQ2vFk9VU8awFT4+EYf92Z/v3OlzRaSh4RIP0H6cvW1BFPXq4XYvNez+TPQjqN+0uSkCYnMFFhcFrw== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^2.11.1" + deprecation "^2.0.0" + is-plain-object "^3.0.0" + node-fetch "^2.3.0" + once "^1.4.0" + universal-user-agent "^5.0.0" + +"@octokit/rest@^17.9.1": + version "17.9.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-17.9.1.tgz#ffa31614b2a2330fac82dcff8bb5e88c63fa7cbb" + integrity sha512-TCTqCMNs21ToN5rIx15sIETuR19WsAy9wI4GnSVY8yZxeOKzeQs8uvIeijy8Gb6DbcYGXDf+xJ1MNDePPss0DA== + dependencies: + "@octokit/core" "^2.4.3" + "@octokit/plugin-paginate-rest" "^2.2.0" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "^3.11.1" + +"@octokit/types@^2.0.0", "@octokit/types@^2.11.1", "@octokit/types@^2.12.1": + version "2.16.2" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.16.2.tgz#4c5f8da3c6fecf3da1811aef678fda03edac35d2" + integrity sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q== + dependencies: + "@types/node" ">= 8" + +"@octokit/types@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-4.0.0.tgz#57425d275d33b02922e8822eabf57466ef243969" + integrity sha512-wbjL8HhCLdBOAmvPJPkMqF4bf6AWzsas78I7+slJt5LAjuAL+kTlWtXHr2V9VnOuEFItZdzfgFTpMjSBkFeVZg== + dependencies: + "@types/node" ">= 8" + +"@types/node@>= 8": + version "14.0.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.1.tgz#5d93e0a099cd0acd5ef3d5bde3c086e1f49ff68c" + integrity sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA== + +"@types/node@^10.12.21": + version "10.17.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.14.tgz#b6c60ebf2fb5e4229fdd751ff9ddfae0f5f31541" + integrity sha512-G0UmX5uKEmW+ZAhmZ6PLTQ5eu/VPaT+d/tdLd5IFsKRPcbe6lPxocBtcYBFSaLaCW8O60AX90e91Nsp8lVHCNw== + +before-after-hook@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +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" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +is-plain-object@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928" + integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg== + dependencies: + isobject "^4.0.0" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" + integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== + +macos-release@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" + integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch@^2.3.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +os-name@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" + integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== + dependencies: + macos-release "^2.2.0" + windows-release "^3.1.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" + integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + +universal-user-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" + integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q== + dependencies: + os-name "^3.1.0" + +vscode-nls@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167" + integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw== + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +windows-release@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.0.tgz#dce167e9f8be733f21c849ebd4d03fe66b29b9f0" + integrity sha512-2HetyTg1Y+R+rUgrKeUEhAG/ZuOmTrI1NBb3ZyAGQMYmOJjBBPe4MTodghRkmLJZHwkuPi02anbeGP+Zf401LQ== + dependencies: + execa "^1.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/extensions/image-preview/media/main.js b/extensions/image-preview/media/main.js index f7c905ae82..65febc80ee 100644 --- a/extensions/image-preview/media/main.js +++ b/extensions/image-preview/media/main.js @@ -279,6 +279,9 @@ image.classList.add('scale-to-fit'); image.addEventListener('load', () => { + if (hasLoadedImage) { + return; + } hasLoadedImage = true; vscode.postMessage({ @@ -297,7 +300,11 @@ } }); - image.addEventListener('error', () => { + image.addEventListener('error', e => { + if (hasLoadedImage) { + return; + } + hasLoadedImage = true; document.body.classList.add('error'); document.body.classList.remove('loading'); diff --git a/extensions/image-preview/src/preview.ts b/extensions/image-preview/src/preview.ts index 7f375919ac..afdb5f6669 100644 --- a/extensions/image-preview/src/preview.ts +++ b/extensions/image-preview/src/preview.ts @@ -179,7 +179,7 @@ class Preview extends Disposable { private async render() { if (this._previewState !== PreviewState.Disposed) { - this.webviewEditor.webview.html = await this.getWebiewContents(); + this.webviewEditor.webview.html = await this.getWebviewContents(); } } @@ -203,7 +203,7 @@ class Preview extends Disposable { } } - private async getWebiewContents(): Promise { + private async getWebviewContents(): Promise { const version = Date.now().toString(); const settings = { isMac: process.platform === 'darwin', @@ -249,9 +249,9 @@ class Preview extends Disposable { // Avoid adding cache busting if there is already a query string if (resource.query) { - return webviewEditor.webview.asWebviewUri(resource).toString(true); + return webviewEditor.webview.asWebviewUri(resource).toString(); } - return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(true); + return webviewEditor.webview.asWebviewUri(resource).with({ query: `version=${version}` }).toString(); } private extensionResource(path: string) { diff --git a/extensions/json-language-features/client/src/jsonMain.ts b/extensions/json-language-features/client/src/jsonMain.ts index ebdbf7e661..3d0ea7060e 100644 --- a/extensions/json-language-features/client/src/jsonMain.ts +++ b/extensions/json-language-features/client/src/jsonMain.ts @@ -213,17 +213,13 @@ export function activate(context: ExtensionContext) { return Promise.reject(new Error(localize('untitled.schema', 'Unable to load {0}', uri.toString()))); } if (uri.scheme !== 'http' && uri.scheme !== 'https') { - if (schemaDownloadEnabled) { - return workspace.openTextDocument(uri).then(doc => { - schemaDocuments[uri.toString()] = true; - return doc.getText(); - }, error => { - return Promise.reject(error); - }); - } else { - return Promise.reject(localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); - } - } else { + return workspace.openTextDocument(uri).then(doc => { + schemaDocuments[uri.toString()] = true; + return doc.getText(); + }, error => { + return Promise.reject(error); + }); + } else if (schemaDownloadEnabled) { if (telemetryReporter && uri.authority === 'schema.management.azure.com') { /* __GDPR__ "json.schema" : { @@ -242,6 +238,8 @@ export function activate(context: ExtensionContext) { } return Promise.reject(new ResponseError(error.status, getErrorStatusDescription(error.status) + '\n' + extraInfo)); }); + } else { + return Promise.reject(localize('schemaDownloadDisabled', 'Downloading schemas is disabled through setting \'{0}\'', SettingIds.enableSchemaDownload)); } }); diff --git a/extensions/json-language-features/server/README.md b/extensions/json-language-features/server/README.md index f68b03fcce..edf55ce63f 100644 --- a/extensions/json-language-features/server/README.md +++ b/extensions/json-language-features/server/README.md @@ -62,14 +62,14 @@ The server supports the following settings: - json - `format` - `enable`: Whether the server should register the formatting support. This option is only applicable if the client supports *dynamicRegistration* for *rangeFormatting* and `initializationOptions.provideFormatter` is not defined. - - `schema`: Configures association of file names to schema URL or schemas and/or associations of schema URL to schema content. - - `fileMatch`: an array of file names or paths (separated by `/`). `*` can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there at least one matching pattern and the last matching pattern is not an exclusion pattern. - - `url`: The URL of the schema, optional when also a schema is provided. - - `schema`: The schema content. - - `resultLimit`: The max number foldig ranges and otline symbols to be computed (for performance reasons) + - `schemas`: Configures association of file names to schema URL or schemas and/or associations of schema URL to schema content. + - `fileMatch`: an array of file names or paths (separated by `/`). `*` can be used as a wildcard. Exclusion patterns can also be defined and start with '!'. A file matches when there at least one matching pattern and the last matching pattern is not an exclusion pattern. + - `url`: The URL of the schema, optional when also a schema is provided. + - `schema`: The schema content. + - `resultLimit`: The max number foldig ranges and otline symbols to be computed (for performance reasons) ```json - { + { "http": { "proxy": "", "proxyStrictSSL": true @@ -86,7 +86,7 @@ The server supports the following settings: ], "url": "http://json.schemastore.org/foo", "schema": { - "type": "array" + "type": "array" } } ] diff --git a/extensions/json/package.json b/extensions/json/package.json index 901584e1d9..af713956bf 100644 --- a/extensions/json/package.json +++ b/extensions/json/package.json @@ -26,6 +26,7 @@ ".webmanifest", ".js.map", ".css.map", + ".ts.map", ".har", ".jslintrc", ".jsonld" diff --git a/extensions/json/test/colorize-results/test_json.json b/extensions/json/test/colorize-results/test_json.json index fc3dec2172..6f94bec76e 100644 --- a/extensions/json/test/colorize-results/test_json.json +++ b/extensions/json/test/colorize-results/test_json.json @@ -389,7 +389,7 @@ "t": "source.json meta.structure.dictionary.json meta.structure.dictionary.value.json meta.structure.dictionary.json meta.structure.dictionary.value.json string.quoted.double.json constant.character.escape.json", "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" @@ -1165,4 +1165,4 @@ "hc_black": "default: #FFFFFF" } } -] +] \ No newline at end of file diff --git a/extensions/markdown-language-features/media/markdown.css b/extensions/markdown-language-features/media/markdown.css index d4efc854d4..e4afd26c3f 100644 --- a/extensions/markdown-language-features/media/markdown.css +++ b/extensions/markdown-language-features/media/markdown.css @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ html, body { - font-family: var(--vscode-markdown-font-family, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif); - font-size: var(--vscode-markdown-font-size, 14px); + font-family: var(--markdown-font-family, system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif); + font-size: var(--markdown-font-size, 14px); padding: 0 26px; - line-height: var(--vscode-markdown-line-height, 22px); + line-height: var(--markdown-line-height, 22px); word-wrap: break-word; } @@ -157,7 +157,7 @@ blockquote { } code { - font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + font-family: var(--vscode-editor-font-family, Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"); font-size: 1em; line-height: 1.357em; } diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 4bba6c740b..c23fb33eb8 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -211,7 +211,7 @@ }, "markdown.preview.fontFamily": { "type": "string", - "default": "-apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', 'Ubuntu', 'Droid Sans', sans-serif", + "default": "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', 'Ubuntu', 'Droid Sans', sans-serif", "description": "%markdown.preview.fontFamily.desc%", "scope": "resource" }, diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json index fbbab99951..6cf645a410 100644 --- a/extensions/markdown-language-features/package.nls.json +++ b/extensions/markdown-language-features/package.nls.json @@ -1,7 +1,7 @@ { "displayName": "Markdown Language Features", "description": "Provides rich language support for Markdown.", - "markdown.preview.breaks.desc": "Sets how line-breaks are rendered in the markdown preview. Setting it to 'true' creates a
for every newline.", + "markdown.preview.breaks.desc": "Sets how line-breaks are rendered in the markdown preview. Setting it to 'true' creates a
for newlines inside paragraphs.", "markdown.preview.linkify": "Enable or disable conversion of URL-like text to links in the markdown preview.", "markdown.preview.doubleClickToSwitchToEditor.desc": "Double click in the markdown preview to switch to the editor.", "markdown.preview.fontFamily.desc": "Controls the font family used in the markdown preview.", diff --git a/extensions/markdown-language-features/src/features/previewContentProvider.ts b/extensions/markdown-language-features/src/features/previewContentProvider.ts index 50b9b46f17..db21c81e25 100644 --- a/extensions/markdown-language-features/src/features/previewContentProvider.ts +++ b/extensions/markdown-language-features/src/features/previewContentProvider.ts @@ -152,9 +152,9 @@ export class MarkdownContentProvider { private getSettingsOverrideStyles(config: MarkdownPreviewConfiguration): string { return [ - config.fontFamily ? `--vscode-markdown-font-family: ${config.fontFamily};` : '', - isNaN(config.fontSize) ? '' : `--vscode-markdown-font-size: ${config.fontSize}px;`, - isNaN(config.lineHeight) ? '' : `--vscode-markdown-line-height: ${config.lineHeight};`, + config.fontFamily ? `--markdown-font-family: ${config.fontFamily};` : '', + isNaN(config.fontSize) ? '' : `--markdown-font-size: ${config.fontSize}px;`, + isNaN(config.lineHeight) ? '' : `--markdown-line-height: ${config.lineHeight};`, ].join(' '); } diff --git a/extensions/vscode-account/.vscodeignore b/extensions/microsoft-authentication/.vscodeignore similarity index 100% rename from extensions/vscode-account/.vscodeignore rename to extensions/microsoft-authentication/.vscodeignore diff --git a/extensions/vscode-account/extension.webpack.config.js b/extensions/microsoft-authentication/extension.webpack.config.js similarity index 100% rename from extensions/vscode-account/extension.webpack.config.js rename to extensions/microsoft-authentication/extension.webpack.config.js diff --git a/extensions/vscode-account/media/auth.css b/extensions/microsoft-authentication/media/auth.css similarity index 100% rename from extensions/vscode-account/media/auth.css rename to extensions/microsoft-authentication/media/auth.css diff --git a/extensions/vscode-account/media/auth.html b/extensions/microsoft-authentication/media/auth.html similarity index 100% rename from extensions/vscode-account/media/auth.html rename to extensions/microsoft-authentication/media/auth.html diff --git a/extensions/vscode-account/package.json b/extensions/microsoft-authentication/package.json similarity index 63% rename from extensions/vscode-account/package.json rename to extensions/microsoft-authentication/package.json index 0deacbea46..dde513aa5e 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/microsoft-authentication/package.json @@ -1,5 +1,5 @@ { - "name": "vscode-account", + "name": "microsoft-authentication", "publisher": "vscode", "displayName": "%displayName%", "description": "%description%", @@ -16,25 +16,10 @@ ], "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", "main": "./out/extension.js", - "contributes": { - "configuration": { - "title": "Microsoft Account", - "properties": { - "microsoftAccount.logLevel": { - "type": "string", - "enum": [ - "info", - "trace" - ], - "default": "info" - } - } - } - }, "scripts": { "vscode:prepublish": "npm run compile", - "compile": "gulp compile-extension:vscode-account", - "watch": "gulp watch-extension:vscode-account" + "compile": "gulp compile-extension:microsoft-authentication", + "watch": "gulp watch-extension:microsoft-authentication" }, "devDependencies": { "typescript": "^3.7.4", diff --git a/extensions/vscode-account/package.nls.json b/extensions/microsoft-authentication/package.nls.json similarity index 100% rename from extensions/vscode-account/package.nls.json rename to extensions/microsoft-authentication/package.nls.json diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/microsoft-authentication/src/AADHelper.ts similarity index 81% rename from extensions/vscode-account/src/AADHelper.ts rename to extensions/microsoft-authentication/src/AADHelper.ts index c5b4046a97..c3f50126db 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/microsoft-authentication/src/AADHelper.ts @@ -82,9 +82,6 @@ export class AzureActiveDirectoryService { } public async initialize(): Promise { - // TODO remove, temporary migration - await keychain.migrateToken(); - const storedData = await keychain.getToken(); if (storedData) { try { @@ -204,13 +201,9 @@ export class AzureActiveDirectoryService { }, 1000 * 30); } - private convertToSession(token: IToken): vscode.AuthenticationSession { - return { - id: token.sessionId, - getAccessToken: () => this.resolveAccessToken(token), - account: token.account, - scopes: token.scope.split(' ') - }; + private async convertToSession(token: IToken): Promise { + const resolvedToken = await this.resolveAccessToken(token); + return new vscode.AuthenticationSession2(token.sessionId, resolvedToken, token.account, token.scope.split(' ')); } private async resolveAccessToken(token: IToken): Promise { @@ -243,77 +236,81 @@ export class AzureActiveDirectoryService { } } - get sessions(): vscode.AuthenticationSession[] { - return this._tokens.map(token => this.convertToSession(token)); + get sessions(): Promise { + return Promise.all(this._tokens.map(token => this.convertToSession(token))); } - public async login(scope: string): Promise { + public async login(scope: string): Promise { Logger.info('Logging in...'); - - if (vscode.env.uiKind === vscode.UIKind.Web) { - await this.loginWithoutLocalServer(scope); - return; - } - - const nonce = crypto.randomBytes(16).toString('base64'); - const { server, redirectPromise, codePromise } = createServer(nonce); - - let token: IToken | undefined; - try { - const port = await startServer(server); - vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`)); - - const redirectReq = await redirectPromise; - if ('err' in redirectReq) { - const { err, res } = redirectReq; - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); - res.end(); - throw err; + return new Promise(async (resolve, reject) => { + if (vscode.env.uiKind === vscode.UIKind.Web) { + resolve(this.loginWithoutLocalServer(scope)); + return; } - const host = redirectReq.req.headers.host || ''; - const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; - const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; - - const state = `${updatedPort},${encodeURIComponent(nonce)}`; - - const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); - const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); - const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`; - - await redirectReq.res.writeHead(302, { Location: loginUrl }); - redirectReq.res.end(); - - const codeRes = await codePromise; - const res = codeRes.res; + const nonce = crypto.randomBytes(16).toString('base64'); + const { server, redirectPromise, codePromise } = createServer(nonce); + let token: IToken | undefined; try { - if ('err' in codeRes) { - throw codeRes.err; - } - token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope); - this.setToken(token, scope); - Logger.info('Login successful'); - res.writeHead(302, { Location: '/' }); - res.end(); - } catch (err) { - res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); - res.end(); - throw new Error(err.message); - } - } catch (e) { - Logger.error(e.message); + const port = await startServer(server); + vscode.env.openExternal(vscode.Uri.parse(`http://localhost:${port}/signin?nonce=${encodeURIComponent(nonce)}`)); - // If the error was about starting the server, try directly hitting the login endpoint instead - if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { - await this.loginWithoutLocalServer(scope); + const redirectReq = await redirectPromise; + if ('err' in redirectReq) { + const { err, res } = redirectReq; + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + throw err; + } + + const host = redirectReq.req.headers.host || ''; + const updatedPortStr = (/^[^:]+:(\d+)$/.exec(Array.isArray(host) ? host[0] : host) || [])[1]; + const updatedPort = updatedPortStr ? parseInt(updatedPortStr, 10) : port; + + const state = `${updatedPort},${encodeURIComponent(nonce)}`; + + const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); + const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); + const loginUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize?response_type=code&response_mode=query&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUrl)}&state=${state}&scope=${encodeURIComponent(scope)}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}`; + + await redirectReq.res.writeHead(302, { Location: loginUrl }); + redirectReq.res.end(); + + const codeRes = await codePromise; + const res = codeRes.res; + + try { + if ('err' in codeRes) { + throw codeRes.err; + } + token = await this.exchangeCodeForToken(codeRes.code, codeVerifier, scope); + this.setToken(token, scope); + Logger.info('Login successful'); + res.writeHead(302, { Location: '/' }); + const session = await this.convertToSession(token); + resolve(session); + res.end(); + } catch (err) { + res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); + res.end(); + reject(err.message); + } + } catch (e) { + Logger.error(e.message); + + // If the error was about starting the server, try directly hitting the login endpoint instead + if (e.message === 'Error listening to server' || e.message === 'Closed' || e.message === 'Timeout waiting for port') { + await this.loginWithoutLocalServer(scope); + } + + reject(e.message); + } finally { + setTimeout(() => { + server.close(); + }, 5000); } - throw new Error(e.message); - } finally { - setTimeout(() => { - server.close(); - }, 5000); - } + }); } private getCallbackEnvironment(callbackUri: vscode.Uri): string { @@ -333,8 +330,8 @@ export class AzureActiveDirectoryService { } } - private async loginWithoutLocalServer(scope: string): Promise { - const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.vscode-account`)); + private async loginWithoutLocalServer(scope: string): Promise { + const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.microsoft-authentication`)); const nonce = crypto.randomBytes(16).toString('base64'); const port = (callbackUri.authority.match(/:([0-9]*)$/) || [])[1] || (callbackUri.scheme === 'https' ? 443 : 80); const callbackEnvironment = this.getCallbackEnvironment(callbackUri); @@ -348,7 +345,7 @@ export class AzureActiveDirectoryService { }); vscode.env.openExternal(uri); - const timeoutPromise = new Promise((_: (value: IToken) => void, reject) => { + const timeoutPromise = new Promise((_: (value: vscode.AuthenticationSession2) => void, reject) => { const wait = setTimeout(() => { clearTimeout(wait); reject('Login timed out.'); @@ -358,9 +355,9 @@ export class AzureActiveDirectoryService { return Promise.race([this.handleCodeResponse(state, codeVerifier, scope), timeoutPromise]); } - private async handleCodeResponse(state: string, codeVerifier: string, scope: string) { + private async handleCodeResponse(state: string, codeVerifier: string, scope: string): Promise { let uriEventListener: vscode.Disposable; - return new Promise((resolve: (value: IToken) => void, reject) => { + return new Promise((resolve: (value: vscode.AuthenticationSession2) => void, reject) => { uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => { try { const query = parseQuery(uri); @@ -374,7 +371,8 @@ export class AzureActiveDirectoryService { const token = await this.exchangeCodeForToken(code, codeVerifier, scope); this.setToken(token, scope); - resolve(token); + const session = await this.convertToSession(token); + resolve(session); } catch (err) { reject(err); } diff --git a/extensions/vscode-account/src/authServer.ts b/extensions/microsoft-authentication/src/authServer.ts similarity index 100% rename from extensions/vscode-account/src/authServer.ts rename to extensions/microsoft-authentication/src/authServer.ts diff --git a/extensions/vscode-account/src/extension.ts b/extensions/microsoft-authentication/src/extension.ts similarity index 91% rename from extensions/vscode-account/src/extension.ts rename to extensions/microsoft-authentication/src/extension.ts index 4f19901e18..874640c78c 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/microsoft-authentication/src/extension.ts @@ -20,15 +20,15 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.authentication.registerAuthenticationProvider({ id: 'microsoft', displayName: 'Microsoft', + supportsMultipleAccounts: true, onDidChangeSessions: onDidChangeSessions.event, getSessions: () => Promise.resolve(loginService.sessions), login: async (scopes: string[]) => { try { telemetryReporter.sendTelemetryEvent('login'); - await loginService.login(scopes.sort().join(' ')); - const session = loginService.sessions[loginService.sessions.length - 1]; + const session = await loginService.login(scopes.sort().join(' ')); onDidChangeSessions.fire({ added: [session.id], removed: [], changed: [] }); - return loginService.sessions[0]!; + return session; } catch (e) { telemetryReporter.sendTelemetryEvent('loginFailed'); throw e; diff --git a/extensions/vscode-account/src/keychain.ts b/extensions/microsoft-authentication/src/keychain.ts similarity index 80% rename from extensions/vscode-account/src/keychain.ts rename to extensions/microsoft-authentication/src/keychain.ts index 5cb90812b4..c451a1002b 100644 --- a/extensions/vscode-account/src/keychain.ts +++ b/extensions/microsoft-authentication/src/keychain.ts @@ -43,26 +43,9 @@ export class Keychain { this.keytar = keytar; } - // TODO remove, temporary migration - async migrateToken(): Promise { - const oldServiceId = `${vscode.env.uriScheme}-vscode.login`; - try { - const data = await this.keytar.getPassword(oldServiceId, ACCOUNT_ID); - if (data) { - Logger.info('Migrating token...'); - this.setToken(data); - await this.keytar.deletePassword(oldServiceId, ACCOUNT_ID); - Logger.info('Migration successful'); - } - } catch (e) { - Logger.error(`Migrating token failed: ${e}`); - } - } - async setToken(token: string): Promise { try { - Logger.trace('Writing to keychain', token); return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); } catch (e) { Logger.error(`Setting token failed: ${e}`); @@ -85,9 +68,7 @@ export class Keychain { async getToken(): Promise { try { - const result = await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); - Logger.trace('Reading from keychain', result); - return result; + return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); } catch (e) { // Ignore Logger.error(`Getting token failed: ${e}`); diff --git a/extensions/vscode-account/src/logger.ts b/extensions/microsoft-authentication/src/logger.ts similarity index 72% rename from extensions/vscode-account/src/logger.ts rename to extensions/microsoft-authentication/src/logger.ts index de4a8ee3c7..a44e4bdd35 100644 --- a/extensions/vscode-account/src/logger.ts +++ b/extensions/microsoft-authentication/src/logger.ts @@ -5,25 +5,13 @@ import * as vscode from 'vscode'; -type LogLevel = 'Trace' | 'Info' | 'Error'; - -enum Level { - Trace = 'trace', - Info = 'Info' -} +type LogLevel = 'Info' | 'Error'; class Log { private output: vscode.OutputChannel; - private level: Level; constructor() { this.output = vscode.window.createOutputChannel('Microsoft Authentication'); - this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info; - vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('microsoftAccount.logLevel')) { - this.level = vscode.workspace.getConfiguration('microsoftAccount').get('logLevel') || Level.Info; - } - }); } private data2String(data: any): string { @@ -44,12 +32,6 @@ class Log { this.logLevel('Error', message, data); } - public trace(message: string, data?: any): void { - if (this.level === Level.Trace) { - this.logLevel('Trace', message, data); - } - } - public logLevel(level: LogLevel, message: string, data?: any): void { this.output.appendLine(`[${level} - ${this.now()}] ${message}`); if (data) { diff --git a/extensions/vscode-account/src/typings/refs.d.ts b/extensions/microsoft-authentication/src/typings/refs.d.ts similarity index 100% rename from extensions/vscode-account/src/typings/refs.d.ts rename to extensions/microsoft-authentication/src/typings/refs.d.ts diff --git a/extensions/vscode-account/src/utils.ts b/extensions/microsoft-authentication/src/utils.ts similarity index 100% rename from extensions/vscode-account/src/utils.ts rename to extensions/microsoft-authentication/src/utils.ts diff --git a/extensions/microsoft-authentication/tsconfig.json b/extensions/microsoft-authentication/tsconfig.json new file mode 100644 index 0000000000..1225709307 --- /dev/null +++ b/extensions/microsoft-authentication/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../shared.tsconfig.json", + "compilerOptions": { + "outDir": "./out", + "experimentalDecorators": true, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "include": [ + "src/**/*" + ] +} diff --git a/extensions/vscode-account/yarn.lock b/extensions/microsoft-authentication/yarn.lock similarity index 100% rename from extensions/vscode-account/yarn.lock rename to extensions/microsoft-authentication/yarn.lock diff --git a/extensions/package.json b/extensions/package.json index 343e305375..178ddba617 100644 --- a/extensions/package.json +++ b/extensions/package.json @@ -3,7 +3,7 @@ "version": "0.0.1", "description": "Dependencies shared by all extensions", "dependencies": { - "typescript": "3.9.1-rc" + "typescript": "3.9.4" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/python/cgmanifest.json b/extensions/python/cgmanifest.json index 97a9d1d111..6b1df10ed8 100644 --- a/extensions/python/cgmanifest.json +++ b/extensions/python/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "MagicStack/MagicPython", "repositoryUrl": "https://github.com/MagicStack/MagicPython", - "commitHash": "0b09c1fca238d22e15ac5712d03f9bf6da626f9c" + "commitHash": "b4b2e6eb16fee36aea0788bf0aa1853c25f7d276" } }, "license": "MIT", diff --git a/extensions/python/syntaxes/MagicPython.tmLanguage.json b/extensions/python/syntaxes/MagicPython.tmLanguage.json index a6920a0630..b8822299e6 100644 --- a/extensions/python/syntaxes/MagicPython.tmLanguage.json +++ b/extensions/python/syntaxes/MagicPython.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/MagicStack/MagicPython/commit/0b09c1fca238d22e15ac5712d03f9bf6da626f9c", + "version": "https://github.com/MagicStack/MagicPython/commit/b4b2e6eb16fee36aea0788bf0aa1853c25f7d276", "name": "MagicPython", "scopeName": "source.python", "patterns": [ @@ -31,6 +31,9 @@ { "include": "#function-declaration" }, + { + "include": "#generator" + }, { "include": "#statement-keyword" }, @@ -291,6 +294,9 @@ { "include": "#lambda" }, + { + "include": "#generator" + }, { "include": "#illegal-operator" }, @@ -306,6 +312,9 @@ { "include": "#list" }, + { + "include": "#odd-function-call" + }, { "include": "#round-braces" }, @@ -388,6 +397,9 @@ }, { "include": "#member-access-base" + }, + { + "include": "#member-access-attribute" } ] }, @@ -413,6 +425,11 @@ } ] }, + "member-access-attribute": { + "comment": "Highlight attribute access in otherwise non-specialized cases.", + "name": "meta.attribute.python", + "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" + }, "special-names": { "name": "constant.other.caps.python", "match": "(?x)\n \\b\n # we want to see \"enough\", meaning 2 or more upper-case\n # letters in the beginning of the constant\n #\n # for more details refer to:\n # https://github.com/MagicStack/MagicPython/issues/42\n (\n _* [[:upper:]] [_\\d]* [[:upper:]]\n )\n [[:upper:]\\d]* (_\\w*)?\n \\b\n" @@ -459,6 +476,21 @@ } ] }, + "odd-function-call": { + "comment": "A bit obscured function call where there may have been an\narbitrary number of other operations to get the function.\nE.g. \"arr[idx](args)\"\n", + "begin": "(?x)\n (?<= \\] | \\) ) \\s*\n (?=\\()\n", + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.definition.arguments.end.python" + } + }, + "patterns": [ + { + "include": "#function-arguments" + } + ] + }, "round-braces": { "begin": "\\(", "end": "\\)", @@ -1195,6 +1227,26 @@ } ] }, + "generator": { + "comment": "Match \"for ... in\" construct used in generators and for loops to\ncorrectly identify the \"in\" as a control flow keyword.\n", + "begin": "\\bfor\\b", + "beginCaptures": { + "0": { + "name": "keyword.control.flow.python" + } + }, + "end": "\\bin\\b", + "endCaptures": { + "0": { + "name": "keyword.control.flow.python" + } + }, + "patterns": [ + { + "include": "#expression" + } + ] + }, "function-declaration": { "name": "meta.function.python", "begin": "(?x)\n \\s*\n (?:\\b(async) \\s+)? \\b(def)\\s+\n (?=\n [[:alpha:]_][[:word:]]* \\s* \\(\n )\n", @@ -1407,6 +1459,7 @@ "include": "#special-names" }, { + "name": "meta.indexed-name.python", "match": "(?x)\n \\b ([[:alpha:]_]\\w*) \\b\n" } ] @@ -1524,6 +1577,7 @@ }, "function-call": { "name": "meta.function-call.python", + "comment": "Regular function call of the type \"name(args)\"", "begin": "(?x)\n \\b(?=\n ([[:alpha:]_]\\w*) \\s* (\\()\n )\n", "end": "(\\))", "endCaptures": { diff --git a/extensions/python/test/colorize-results/test-freeze-56377_py.json b/extensions/python/test/colorize-results/test-freeze-56377_py.json index 63889e26fe..3eb0f85a2a 100644 --- a/extensions/python/test/colorize-results/test-freeze-56377_py.json +++ b/extensions/python/test/colorize-results/test-freeze-56377_py.json @@ -254,13 +254,13 @@ }, { "c": "in", - "t": "source.python keyword.operator.logical.python", + "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.operator.logical.python: #569CD6", - "light_plus": "keyword.operator.logical.python: #0000FF", - "dark_vs": "keyword.operator.logical.python: #569CD6", - "light_vs": "keyword.operator.logical.python: #0000FF", - "hc_black": "keyword.operator.logical.python: #569CD6" + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" } }, { @@ -298,7 +298,7 @@ }, { "c": "request", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -565,7 +565,7 @@ "t": "source.python string.quoted.single.python constant.character.escape.python", "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" diff --git a/extensions/python/test/colorize-results/test_py.json b/extensions/python/test/colorize-results/test_py.json index c61d2db761..c0a1445364 100644 --- a/extensions/python/test/colorize-results/test_py.json +++ b/extensions/python/test/colorize-results/test_py.json @@ -430,7 +430,7 @@ }, { "c": "size", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -914,13 +914,13 @@ }, { "c": "in", - "t": "source.python meta.function.python meta.function.parameters.python keyword.operator.logical.python", + "t": "source.python meta.function.python meta.function.parameters.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.operator.logical.python: #569CD6", - "light_plus": "keyword.operator.logical.python: #0000FF", - "dark_vs": "keyword.operator.logical.python: #569CD6", - "light_vs": "keyword.operator.logical.python: #0000FF", - "hc_black": "keyword.operator.logical.python: #569CD6" + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" } }, { @@ -2113,13 +2113,13 @@ }, { "c": "in", - "t": "source.python keyword.operator.logical.python", + "t": "source.python keyword.control.flow.python", "r": { - "dark_plus": "keyword.operator.logical.python: #569CD6", - "light_plus": "keyword.operator.logical.python: #0000FF", - "dark_vs": "keyword.operator.logical.python: #569CD6", - "light_vs": "keyword.operator.logical.python: #0000FF", - "hc_black": "keyword.operator.logical.python: #569CD6" + "dark_plus": "keyword.control: #C586C0", + "light_plus": "keyword.control: #AF00DB", + "dark_vs": "keyword.control: #569CD6", + "light_vs": "keyword.control: #0000FF", + "hc_black": "keyword.control: #C586C0" } }, { @@ -4544,7 +4544,7 @@ }, { "c": "fn", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4621,7 +4621,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4918,7 +4918,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python", + "t": "source.python meta.member.access.python meta.attribute.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -4973,7 +4973,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python meta.item-access.python", + "t": "source.python meta.member.access.python meta.item-access.python meta.indexed-name.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -5182,7 +5182,7 @@ }, { "c": "memo", - "t": "source.python meta.member.access.python meta.item-access.python", + "t": "source.python meta.member.access.python meta.item-access.python meta.indexed-name.python", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -6797,4 +6797,4 @@ "hc_black": "string: #CE9178" } } -] +] \ No newline at end of file diff --git a/extensions/r/cgmanifest.json b/extensions/r/cgmanifest.json index 7cf785d714..62f2751e6c 100644 --- a/extensions/r/cgmanifest.json +++ b/extensions/r/cgmanifest.json @@ -6,11 +6,11 @@ "git": { "name": "Ikuyadeu/vscode-R", "repositoryUrl": "https://github.com/Ikuyadeu/vscode-R", - "commitHash": "bc79e9245682ee09b4f0b742b927a37702d91b82" + "commitHash": "e03ba9cb9b19412f48c73ea73deb6746d50bbf23" } }, "license": "MIT", - "version": "1.1.8" + "version": "1.3.0" } ], "version": 1 diff --git a/extensions/r/syntaxes/r.tmLanguage.json b/extensions/r/syntaxes/r.tmLanguage.json index 2b6c4368da..ad947f6284 100644 --- a/extensions/r/syntaxes/r.tmLanguage.json +++ b/extensions/r/syntaxes/r.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Ikuyadeu/vscode-R/commit/bc79e9245682ee09b4f0b742b927a37702d91b82", + "version": "https://github.com/Ikuyadeu/vscode-R/commit/e03ba9cb9b19412f48c73ea73deb6746d50bbf23", "name": "R", "scopeName": "source.r", "patterns": [ @@ -168,20 +168,12 @@ "match": "(\\-|\\+|\\*|\\/|%\\/%|%%|%\\*%|%o%|%x%|\\^)", "name": "keyword.operator.arithmetic.r" }, - { - "match": "<=|>=", - "name": "keyword.operator.comparison.r" - }, - { - "match": "==", - "name": "keyword.operator.comarison.r" - }, { "match": "(:=|<-|<<-|->|->>)", "name": "keyword.operator.assignment.r" }, { - "match": "(!=|<>|<|>|%in%)", + "match": "(==|<=|>=|!=|<>|<|>|%in%)", "name": "keyword.operator.comparison.r" }, { diff --git a/extensions/search-result/package.json b/extensions/search-result/package.json index 6a55968bda..ffb5321ae4 100644 --- a/extensions/search-result/package.json +++ b/extensions/search-result/package.json @@ -43,8 +43,5 @@ "path": "./syntaxes/searchResult.tmLanguage.json" } ] - }, - "devDependencies": { - "vscode": "^1.1.36" } } diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index 23e0615a71..eac084ddbc 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -12,7 +12,7 @@ const mappings = [ ['css', 'source.css'], ['dart', 'source.dart'], ['diff', 'source.diff'], - ['dockerfile', 'source.dockerfile', '(?:dockerfile|Dockerfile)'], + ['dockerfile', 'source.dockerfile', '(?:dockerfile|Dockerfile|containerfile|Containerfile)'], ['fs', 'source.fsharp'], ['go', 'source.go'], ['groovy', 'source.groovy'], diff --git a/extensions/search-result/yarn.lock b/extensions/search-result/yarn.lock index 4ff83ab98b..fb57ccd13a 100644 --- a/extensions/search-result/yarn.lock +++ b/extensions/search-result/yarn.lock @@ -2,601 +2,3 @@ # yarn lockfile v1 -agent-base@4, agent-base@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== - dependencies: - es6-promisify "^5.0.0" - -ajv@^6.5.5: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -asn1@~0.2.3: - version "0.2.4" - resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" - integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" - integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= - -aws-sign2@~0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" - integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= - -aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" - integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= - dependencies: - tweetnacl "^0.14.3" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -browser-stdout@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" - integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" - integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= - -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - -commander@2.15.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" - integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" - integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= - dependencies: - assert-plus "^1.0.0" - -debug@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== - dependencies: - ms "^2.1.1" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= - -diff@3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" - integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -es6-promise@^4.0.3: - version "4.2.8" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" - integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= - dependencies: - es6-promise "^4.0.3" - -escape-string-regexp@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -extend@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -extsprintf@1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" - integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= - -extsprintf@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" - integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" - integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" - integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= - dependencies: - assert-plus "^1.0.0" - -glob@7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" - integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.2: - version "7.1.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" - integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -growl@1.10.5: - version "1.10.5" - resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" - integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= - -har-validator@~5.1.0: - version "5.1.3" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" - integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== - dependencies: - ajv "^6.5.5" - har-schema "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -he@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" - integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= - -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== - dependencies: - agent-base "4" - debug "3.1.0" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -https-proxy-agent@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" - integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== - dependencies: - agent-base "^4.3.0" - debug "^3.1.0" - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" - integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" - integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" - integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-schema@0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" - integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= - -jsprim@^1.2.2: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" - integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.2.3" - verror "1.10.0" - -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -minimatch@3.0.4, minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -mkdirp@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -mocha@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" - integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== - dependencies: - browser-stdout "1.3.1" - commander "2.15.1" - debug "3.1.0" - diff "3.5.0" - escape-string-regexp "1.0.5" - glob "7.1.2" - growl "1.10.5" - he "1.1.1" - minimatch "3.0.4" - mkdirp "0.5.1" - supports-color "5.4.0" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" - integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= - -psl@^1.1.24: - version "1.4.0" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2" - integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw== - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" - integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -qs@~6.5.2: - version "6.5.2" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== - -querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - -request@^2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== - dependencies: - aws-sign2 "~0.7.0" - aws4 "^1.8.0" - caseless "~0.12.0" - combined-stream "~1.0.6" - extend "~3.0.2" - forever-agent "~0.6.1" - form-data "~2.3.2" - har-validator "~5.1.0" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.4.3" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - -safe-buffer@^5.0.1, safe-buffer@^5.1.2: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== - -safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -source-map-support@^0.5.0: - version "0.5.16" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" - integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -sshpk@^1.7.0: - version "1.16.1" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" - integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== - dependencies: - asn1 "~0.2.3" - assert-plus "^1.0.0" - bcrypt-pbkdf "^1.0.0" - dashdash "^1.12.0" - ecc-jsbn "~0.1.1" - getpass "^0.1.1" - jsbn "~0.1.0" - safer-buffer "^2.0.2" - tweetnacl "~0.14.0" - -supports-color@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" - integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== - dependencies: - has-flag "^3.0.0" - -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" - integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -url-parse@^1.4.4: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -uuid@^3.3.2: - version "3.3.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866" - integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" - integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -vscode-test@^0.4.1: - version "0.4.3" - resolved "https://registry.yarnpkg.com/vscode-test/-/vscode-test-0.4.3.tgz#461ebf25fc4bc93d77d982aed556658a2e2b90b8" - integrity sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w== - dependencies: - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - -vscode@^1.1.36: - version "1.1.36" - resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.36.tgz#5e1a0d1bf4977d0c7bc5159a9a13d5b104d4b1b6" - integrity sha512-cGFh9jmGLcTapCpPCKvn8aG/j9zVQ+0x5hzYJq5h5YyUXVGa1iamOaB2M2PZXoumQPES4qeAP1FwkI0b6tL4bQ== - dependencies: - glob "^7.1.2" - mocha "^5.2.0" - request "^2.88.0" - semver "^5.4.1" - source-map-support "^0.5.0" - url-parse "^1.4.4" - vscode-test "^0.4.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= diff --git a/extensions/sql/cgmanifest.json b/extensions/sql/cgmanifest.json index 859d678224..56b576cdc7 100644 --- a/extensions/sql/cgmanifest.json +++ b/extensions/sql/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "Microsoft/vscode-mssql", "repositoryUrl": "https://github.com/Microsoft/vscode-mssql", - "commitHash": "a542fe96780e6b274adb281810d419a512fb5bb4" + "commitHash": "37a22725186b5b481b2882a78c7b9fe024c13946" } }, "license": "MIT", diff --git a/extensions/sql/syntaxes/sql.tmLanguage.json b/extensions/sql/syntaxes/sql.tmLanguage.json index 2c12fd5076..30ba9e5881 100644 --- a/extensions/sql/syntaxes/sql.tmLanguage.json +++ b/extensions/sql/syntaxes/sql.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/vscode-mssql/commit/a542fe96780e6b274adb281810d419a512fb5bb4", + "version": "https://github.com/Microsoft/vscode-mssql/commit/37a22725186b5b481b2882a78c7b9fe024c13946", "name": "SQL", "scopeName": "source.sql", "patterns": [ diff --git a/extensions/theme-defaults/themes/dark_plus.json b/extensions/theme-defaults/themes/dark_plus.json index f83f763b9a..fdadd4e5c3 100644 --- a/extensions/theme-defaults/themes/dark_plus.json +++ b/extensions/theme-defaults/themes/dark_plus.json @@ -104,7 +104,7 @@ "variable.other.enummember" ], "settings": { - "foreground": "#51B6C4", + "foreground": "#4FC1FF", } }, { diff --git a/extensions/theme-defaults/themes/light_plus.json b/extensions/theme-defaults/themes/light_plus.json index 00c2a6cb47..a5ea49d4d3 100644 --- a/extensions/theme-defaults/themes/light_plus.json +++ b/extensions/theme-defaults/themes/light_plus.json @@ -104,7 +104,7 @@ "variable.other.enummember" ], "settings": { - "foreground": "#328267", + "foreground": "#0070C1", } }, { @@ -169,7 +169,7 @@ "keyword.control.anchor.regexp" ], "settings": { - "foreground": "#ff0000" + "foreground": "#EE0000" } }, { @@ -181,7 +181,7 @@ { "scope": "constant.character.escape", "settings": { - "foreground": "#ff0000" + "foreground": "#EE0000" } }, { diff --git a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json index 8b1fe2dd80..935573463e 100644 --- a/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json +++ b/extensions/theme-monokai-dimmed/themes/dimmed-monokai-color-theme.json @@ -42,7 +42,23 @@ "pickerGroup.foreground": "#b0b0b0", "terminal.ansiWhite": "#ffffff", "inputOption.activeBorder": "#3655b5", - "focusBorder": "#3655b5" + "focusBorder": "#3655b5", + "terminal.ansiBlack": "#1e1e1e", + "terminal.ansiRed": "#C4265E", // the bright color with ~75% transparent on the background + "terminal.ansiGreen": "#86B42B", + "terminal.ansiYellow": "#B3B42B", + "terminal.ansiBlue": "#6A7EC8", + "terminal.ansiMagenta": "#8C6BC8", + "terminal.ansiCyan": "#56ADBC", + "terminal.ansiWhite": "#e3e3dd", + "terminal.ansiBrightBlack": "#666666", + "terminal.ansiBrightRed": "#f92672", + "terminal.ansiBrightGreen": "#A6E22E", + "terminal.ansiBrightYellow": "#e2e22e", // hue shifted #A6E22E + "terminal.ansiBrightBlue": "#819aff", // hue shifted #AE81FF + "terminal.ansiBrightMagenta": "#AE81FF", + "terminal.ansiBrightCyan": "#66D9EF", + "terminal.ansiBrightWhite": "#f8f8f2" }, "tokenColors": [ { diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index a3df8c6b32..b3bb41d3eb 100644 --- a/extensions/theme-seti/cgmanifest.json +++ b/extensions/theme-seti/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "seti-ui", "repositoryUrl": "https://github.com/jesseweed/seti-ui", - "commitHash": "8f22764c37feb7f706465f5186132111a2401b6b" + "commitHash": "f3b2775662b0075aab56e5f0c03269f21f3f0f30" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index b4b8366f16..5dc3bb8d9b 100644 Binary files a/extensions/theme-seti/icons/seti.woff and b/extensions/theme-seti/icons/seti.woff differ diff --git a/extensions/theme-seti/icons/vs-seti-icon-theme.json b/extensions/theme-seti/icons/vs-seti-icon-theme.json index 16de3aceb1..157faca436 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -1030,12 +1030,6 @@ "fontCharacter": "\\E074", "fontColor": "#cc3e44" }, - "_rmd_light": { - "fontColor": "#498ba7" - }, - "_rmd": { - "fontColor": "#519aba" - }, "_rollup_light": { "fontCharacter": "\\E075", "fontColor": "#b8383d" @@ -1496,7 +1490,7 @@ "test.tsx": "_react_2", "re": "_reasonml", "r": "_R", - "rmd": "_rmd", + "rmd": "_R", "erb": "_html_erb", "erb.html": "_html_erb", "html.erb": "_html_erb", @@ -1697,7 +1691,7 @@ "sql": "_db", "swift": "_swift", "typescript": "_typescript", - "typescriptreact": "_react", + "typescriptreact": "_typescript", "xml": "_xml", "yaml": "_yml", "argdown": "_argdown", @@ -1833,7 +1827,7 @@ "test.tsx": "_react_2_light", "re": "_reasonml_light", "r": "_R_light", - "rmd": "_rmd_light", + "rmd": "_R_light", "erb": "_html_erb_light", "erb.html": "_html_erb_light", "html.erb": "_html_erb_light", @@ -1974,7 +1968,7 @@ "sql": "_db_light", "swift": "_swift_light", "typescript": "_typescript_light", - "typescriptreact": "_react_light", + "typescriptreact": "_typescript_light", "xml": "_xml_light", "yaml": "_yml_light", "argdown": "_argdown_light", @@ -2061,5 +2055,5 @@ "Schema Compare": "scmp" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/8f22764c37feb7f706465f5186132111a2401b6b" -} + "version": "https://github.com/jesseweed/seti-ui/commit/f3b2775662b0075aab56e5f0c03269f21f3f0f30" +} \ No newline at end of file diff --git a/extensions/vscode-web-playground/package.json b/extensions/vscode-web-playground/package.json index 46b6102c10..05fa8bdeb7 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 0a5236436a..31924f9e86 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -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.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 1e2f612076..9ed1180847 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,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/sql", + "eslint": "eslint -c .eslintrc.json --rulesdir ./build/lib/eslint --ext .ts --ext .js ./src/vs ./extensions", "sqllint": "eslint --no-eslintrc -c .eslintrc.sql.ts.json --rulesdir ./build/lib/eslint --ext .ts ./src/sql" }, "dependencies": { @@ -81,11 +81,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", "zone.js": "^0.8.4" @@ -126,7 +125,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", @@ -172,7 +171,7 @@ "opn": "^6.0.0", "optimist": "0.3.5", "p-all": "^1.0.0", - "playwright": "0.15.0", + "playwright": "1.0.1", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -183,7 +182,7 @@ "temp-write": "^3.4.0", "ts-loader": "^4.4.2", "typemoq": "^0.3.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 1cd117586e..af4d60eab4 100644 --- a/product.json +++ b/product.json @@ -10,9 +10,11 @@ "win32NameVersion": "Azure Data Studio", "win32RegValueName": "azuredatastudio", "win32AppId": "{{760C8610-682F-4E9B-9C9C-4E1120308DF2}", + "win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85479}", "win32x64AppId": "{{6591F69E-6588-4980-81ED-C8FCBD7EC4B8}", "win32UserAppId": "{{EA2AB3AA-446D-450F-9948-AE5768C1161A}", "win32x64UserAppId": "{{1F0FD1CE-9703-4734-8F50-5B5CEEDAE6B9}", + "win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7D}", "win32AppUserModelId": "Microsoft.azuredatastudio", "win32ShellNameShort": "Azure Data Studio", "darwinBundleIdentifier": "com.azuredatastudio.oss", @@ -60,6 +62,7 @@ ] }, "extensionAllowedProposedApi": [ + "ms-vscode.vscode-js-profile-flame", "ms-vscode.vscode-js-profile-table", "ms-vscode.references-view" ], diff --git a/remote/package.json b/remote/package.json index 3e8943349a..23ecbfdc19 100644 --- a/remote/package.json +++ b/remote/package.json @@ -38,11 +38,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", "zone.js": "^0.8.4" diff --git a/remote/web/package.json b/remote/web/package.json index c5ab0ad2d0..bf81ca6488 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -23,11 +23,10 @@ "slickgrid": "github:anthonydresser/SlickGrid#2.3.33", "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", "zone.js": "^0.8.4" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index ba75b1f9cd..66574ac713 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -353,30 +353,25 @@ xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -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== zone.js@^0.8.4: version "0.8.29" diff --git a/remote/yarn.lock b/remote/yarn.lock index f5da6262f7..477a8fc362 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -740,30 +740,25 @@ xtend@^4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -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 b7441ea015..e78d39835b 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/src/main.js b/src/main.js index 4f31e22afb..d0cc84e82b 100644 --- a/src/main.js +++ b/src/main.js @@ -42,6 +42,9 @@ app.setPath('userData', userDataPath); // Set temp directory based on crash-reporter-directory CLI argument // The crash reporter will store crashes in temp folder so we need // to change that location accordingly. + +// If a crash-reporter-directory is specified we setup the crash reporter +// right from the beginning as early as possible to monitor all processes. let crashReporterDirectory = args['crash-reporter-directory']; if (crashReporterDirectory) { crashReporterDirectory = path.normalize(crashReporterDirectory); @@ -54,8 +57,20 @@ if (crashReporterDirectory) { app.exit(1); } } + + // Crashes are stored in the temp directory by default, so we + // need to change that directory to the provided one console.log(`Found --crash-reporter-directory argument. Setting temp directory to be '${crashReporterDirectory}'`); app.setPath('temp', crashReporterDirectory); + + // Start crash reporter + const { crashReporter } = require('electron'); + crashReporter.start({ + companyName: 'Microsoft', + productName: product.nameShort, + submitURL: '', + uploadToServer: false + }); } // Set logs path before app 'ready' event if running portable @@ -71,7 +86,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/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 7d1c6edc64..1ffa912f5f 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -444,5 +444,10 @@ declare module 'azdata' { getAssessmentItems(ownerUri: string, targetType: sqlAssessment.SqlAssessmentTargetType): Promise; generateAssessmentScript(items: SqlAssessmentResultItem[]): Promise; } -} + export interface TreeItem2 extends vscode.TreeItem2 { + payload?: IConnectionProfile; + childProvider?: string; + type?: ExtensionNodeType; + } +} diff --git a/src/sql/base/browser/ui/scrollableSplitview/scrollableSplitview.ts b/src/sql/base/browser/ui/scrollableSplitview/scrollableSplitview.ts index 1beea6c142..c18ae84014 100644 --- a/src/sql/base/browser/ui/scrollableSplitview/scrollableSplitview.ts +++ b/src/sql/base/browser/ui/scrollableSplitview/scrollableSplitview.ts @@ -272,13 +272,17 @@ export class ScrollableSplitView extends HeightMap implements IDisposable { // Add sash if (this.options.enableResizing && this.viewItems.length > 1) { - const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; - const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) } : { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }; - const sash = new Sash(this.sashContainer, layoutProvider, { - orientation, - orthogonalStartSash: this.orthogonalStartSash, - orthogonalEndSash: this.orthogonalEndSash - }); + const sash = this.orientation === Orientation.HORIZONTAL + ? new Sash(this.sashContainer, { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) }, { + orientation: Orientation.HORIZONTAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }) + : new Sash(this.sashContainer, { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }, { + orientation: Orientation.VERTICAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }); const sashEventMapper = this.orientation === Orientation.VERTICAL ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey } as ISashEvent) @@ -375,13 +379,17 @@ export class ScrollableSplitView extends HeightMap implements IDisposable { // Add sash if (this.options.enableResizing && this.viewItems.length > 1) { - const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; - const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) } : { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }; - const sash = new Sash(this.sashContainer, layoutProvider, { - orientation, - orthogonalStartSash: this.orthogonalStartSash, - orthogonalEndSash: this.orthogonalEndSash - }); + const sash = this.orientation === Orientation.HORIZONTAL + ? new Sash(this.sashContainer, { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) }, { + orientation: Orientation.HORIZONTAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }) + : new Sash(this.sashContainer, { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }, { + orientation: Orientation.VERTICAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }); const sashEventMapper = this.orientation === Orientation.VERTICAL ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey } as ISashEvent) diff --git a/src/sql/base/browser/ui/table/media/slickColorTheme.css b/src/sql/base/browser/ui/table/media/slickColorTheme.css index 6f250a2fea..974446b2fb 100644 --- a/src/sql/base/browser/ui/table/media/slickColorTheme.css +++ b/src/sql/base/browser/ui/table/media/slickColorTheme.css @@ -125,11 +125,11 @@ background: transparent; } -.vs .monaco-workbench input { +.monaco-workbench.vs input { color: var(--color-content); } -.vs .monaco-workbench .input { +.monaco-workbench.vs .input { background-color: white; } @@ -248,7 +248,7 @@ background: transparent; } -.vs-dark .monaco-workbench input, .vs-dark .monaco-workbench .input { +.monaco-workbench.vs-dark input, .monaco-workbench.vs-dark .input { color: var(--color-content); background-color: #3C3C3C; } @@ -299,7 +299,7 @@ background: transparent; } -.hc-black .monaco-workbench input { +.monaco-workbench.hc-black input { color: #000; background-color: #FFF; } diff --git a/src/sql/media/overwriteVsIcons.css b/src/sql/media/overwriteVsIcons.css index cb51d28519..b13d3a0c02 100644 --- a/src/sql/media/overwriteVsIcons.css +++ b/src/sql/media/overwriteVsIcons.css @@ -7,11 +7,11 @@ background-size: 10px; } -.vs .monaco-workbench .tab > .tab-close .action-label.close-editor-action { +.monaco-workbench.vs .tab > .tab-close .action-label.close-editor-action { background: url('icons/close.svg') center center no-repeat; } -.vs-dark .monaco-workbench .tab > .tab-close .action-label.close-editor-action, -.hc-black .monaco-workbench .tab > .tab-close .action-label.close-editor-action { +.monaco-workbench.vs-dark .tab > .tab-close .action-label.close-editor-action, +.monaco-workbench.hc-black .tab > .tab-close .action-label.close-editor-action { background: url('icons/close_inverse.svg') center center no-repeat; -} \ No newline at end of file +} diff --git a/src/sql/platform/clipboard/browser/clipboardService.ts b/src/sql/platform/clipboard/browser/clipboardService.ts index 82a8ba42b0..a90b15136f 100644 --- a/src/sql/platform/clipboard/browser/clipboardService.ts +++ b/src/sql/platform/clipboard/browser/clipboardService.ts @@ -38,35 +38,35 @@ export class BrowserClipboardService implements IClipboardService { /** * Reads text from the system find pasteboard. */ - readFindText(): string { + readFindText(): Promise { return this._vsClipboardService.readFindText(); } /** * Writes text to the system find pasteboard. */ - writeFindText(text: string): void { - this._vsClipboardService.writeFindText(text); + writeFindText(text: string): Promise { + return this._vsClipboardService.writeFindText(text); } /** * Writes resources to the system clipboard. */ - writeResources(resources: URI[]): void { - this._vsClipboardService.writeResources(resources); + writeResources(resources: URI[]): Promise { + return this._vsClipboardService.writeResources(resources); } /** * Reads resources from the system clipboard. */ - readResources(): URI[] { + readResources(): Promise { return this._vsClipboardService.readResources(); } /** * Find out if resources are copied to the clipboard. */ - hasResources(): boolean { + hasResources(): Promise { return this._vsClipboardService.hasResources(); } @@ -74,4 +74,14 @@ export class BrowserClipboardService implements IClipboardService { // eslint-disable-next-line no-sync return this._vsClipboardService.readTextSync(); } + + readFindTextSync(): string { + // eslint-disable-next-line no-sync + return this._vsClipboardService.readFindTextSync(); + } + + writeFindTextSync(text: string): void { + // eslint-disable-next-line no-sync + return this._vsClipboardService.writeFindTextSync(text); + } } diff --git a/src/sql/workbench/browser/modelComponents/media/markdown.css b/src/sql/workbench/browser/modelComponents/media/markdown.css index 186bf93c5b..68e12bd63c 100644 --- a/src/sql/workbench/browser/modelComponents/media/markdown.css +++ b/src/sql/workbench/browser/modelComponents/media/markdown.css @@ -68,18 +68,18 @@ modelview-dom-component .showEditorSelection .code-line .code-line:hover:before border-left: none; } -.vs-dark .monaco-workbench modelview-dom-component .showEditorSelection .code-active-line:before -.hc-black .monaco-workbench modelview-dom-component .showEditorSelection .code-active-line:before { +.monaco-workbench.vs-dark modelview-dom-component .showEditorSelection .code-active-line:before +.monaco-workbench.hc-black modelview-dom-component .showEditorSelection .code-active-line:before { border-left: 3px solid rgba(255, 255, 255, 0.4); } -.vs-dark .monaco-workbench modelview-dom-component .showEditorSelection .code-line:hover:before -.hc-black .monaco-workbench modelview-dom-component .showEditorSelection .code-line:hover:before { +.monaco-workbench.vs-dark modelview-dom-component .showEditorSelection .code-line:hover:before +.monaco-workbench.hc-black modelview-dom-component .showEditorSelection .code-line:hover:before { border-left: 3px solid rgba(255, 255, 255, 0.60); } -.vs-dark .monaco-workbench modelview-dom-component .showEditorSelection .code-line .code-line:hover:before -.hc-black .monaco-workbench modelview-dom-component .showEditorSelection .code-line .code-line:hover:before { +.monaco-workbench.vs-dark modelview-dom-component .showEditorSelection .code-line .code-line:hover:before +.monaco-workbench.hc-black modelview-dom-component .showEditorSelection .code-line .code-line:hover:before { border-left: none; } @@ -205,15 +205,15 @@ modelview-dom-component pre { background-color: rgba(220, 220, 220, 0.4); } -.vs-dark .monaco-workbench modelview-dom-component pre { +.monaco-workbench.vs-dark modelview-dom-component pre { background-color: rgba(10, 10, 10, 0.4); } -.hc-black .monaco-workbench modelview-dom-component pre { +.monaco-workbench.hc-black modelview-dom-component pre { background-color: rgb(0, 0, 0); } -.hc-black .monaco-workbench modelview-dom-component h1 { +.monaco-workbench.hc-black modelview-dom-component h1 { border-color: rgb(0, 0, 0); } @@ -221,7 +221,7 @@ modelview-dom-component table > thead > tr > th { border-color: rgba(0, 0, 0, 0.69); } -.vs-dark .monaco-workbench modelview-dom-component table > thead > tr > th { +.monaco-workbench.vs-dark modelview-dom-component table > thead > tr > th { border-color: rgba(255, 255, 255, 0.69); } @@ -231,8 +231,8 @@ modelview-dom-component table > tbody > tr + tr > td { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench modelview-dom-component h1, -.vs-dark .monaco-workbench modelview-dom-component hr, -.vs-dark .monaco-workbench modelview-dom-component table > tbody > tr + tr > td { +.monaco-workbench.vs-dark modelview-dom-component h1, +.monaco-workbench.vs-dark modelview-dom-component hr, +.monaco-workbench.vs-dark modelview-dom-component table > tbody > tr + tr > td { border-color: rgba(255, 255, 255, 0.18); } diff --git a/src/sql/workbench/common/editor/query/queryEditorInput.ts b/src/sql/workbench/common/editor/query/queryEditorInput.ts index 2352a53acd..fd7402a100 100644 --- a/src/sql/workbench/common/editor/query/queryEditorInput.ts +++ b/src/sql/workbench/common/editor/query/queryEditorInput.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { EditorInput, GroupIdentifier, IRevertOptions, ISaveOptions, IEditorInput, TextResourceEditorInput } from 'vs/workbench/common/editor'; +import { EditorInput, GroupIdentifier, IRevertOptions, ISaveOptions, IEditorInput } from 'vs/workbench/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IConnectionManagementService, IConnectableInput, INewConnectionParams, RunQueryOnConnectionMode } from 'sql/platform/connection/common/connectionManagement'; @@ -17,6 +17,7 @@ import { IQueryModelService } from 'sql/workbench/services/query/common/queryMod import { ExecutionPlanOptions } from 'azdata'; import { startsWith } from 'vs/base/common/strings'; import { IRange } from 'vs/editor/common/core/range'; +import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput'; const MAX_SIZE = 13; @@ -119,7 +120,7 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab constructor( private _description: string, - protected _text: TextResourceEditorInput, + protected _text: AbstractTextResourceEditorInput, protected _results: QueryResultsInput, @IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService, @IQueryModelService private readonly queryModelService: IQueryModelService, @@ -168,7 +169,7 @@ export abstract class QueryEditorInput extends EditorInput implements IConnectab // Getters for private properties public get uri(): string { return this.resource!.toString(true); } - public get text(): TextResourceEditorInput { return this._text; } + public get text(): AbstractTextResourceEditorInput { return this._text; } public get results(): QueryResultsInput { return this._results; } // Description is shown beside the tab name in the combobox of open editors public getDescription(): string { return this._description; } diff --git a/src/sql/workbench/common/workspaceActions.ts b/src/sql/workbench/common/workspaceActions.ts index e2ad175bd0..0f997b44d9 100644 --- a/src/sql/workbench/common/workspaceActions.ts +++ b/src/sql/workbench/common/workspaceActions.ts @@ -5,9 +5,9 @@ import { Action } from 'vs/base/common/actions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -// eslint-disable-next-line code-layering,code-import-patterns -import { IElectronService } from 'vs/platform/electron/node/electron'; import { URI } from 'vs/base/common/uri'; +// eslint-disable-next-line code-layering,code-import-patterns +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; export class ShowFileInFolderAction extends Action { diff --git a/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css b/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css index dc97818b39..8355072d08 100644 --- a/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css +++ b/src/sql/workbench/contrib/connection/browser/media/connectionViewlet.css @@ -18,7 +18,7 @@ .server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group { font-weight: normal; } /* High Contrast Theming */ -.hc-black .monaco-workbench .server-explorer-viewlet .server-group { +.monaco-workbench.hc-black .server-explorer-viewlet .server-group { line-height: 20px; } diff --git a/src/sql/workbench/contrib/dashboard/browser/contents/webviewContent.component.ts b/src/sql/workbench/contrib/dashboard/browser/contents/webviewContent.component.ts index 640136a2a9..25978b4b7d 100644 --- a/src/sql/workbench/contrib/dashboard/browser/contents/webviewContent.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/contents/webviewContent.component.ts @@ -102,7 +102,7 @@ export class WebviewContent extends AngularDisposable implements OnInit, IDashbo {}, { allowScripts: true - }); + }, undefined); this._webview.mountTo(this._el.nativeElement); diff --git a/src/sql/workbench/contrib/dashboard/browser/widgets/webview/webviewWidget.component.ts b/src/sql/workbench/contrib/dashboard/browser/widgets/webview/webviewWidget.component.ts index 6c49f762ff..164f089e93 100644 --- a/src/sql/workbench/contrib/dashboard/browser/widgets/webview/webviewWidget.component.ts +++ b/src/sql/workbench/contrib/dashboard/browser/widgets/webview/webviewWidget.component.ts @@ -102,7 +102,7 @@ export class WebviewWidget extends DashboardWidget implements IDashboardWidget, {}, { allowScripts: true, - }); + }, undefined); this._webview.mountTo(this._el.nativeElement); this._onMessageDisposable = this._webview.onMessage(e => { diff --git a/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css b/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css index 7c17be9b6d..f0aef69dd4 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css +++ b/src/sql/workbench/contrib/dataExplorer/browser/media/connectionViewletPanel.css @@ -18,7 +18,7 @@ .server-explorer-viewlet:lang(ko) .monaco-tree .monaco-tree-row .server-group { font-weight: normal; } /* High Contrast Theming */ -.hc-black .monaco-workbench .server-explorer-viewlet .server-group { +.monaco-workbench.hc-black .server-explorer-viewlet .server-group { line-height: 20px; } diff --git a/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts b/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts index d590de6758..b74d58f4b2 100644 --- a/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts +++ b/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts @@ -15,7 +15,7 @@ import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; import { IEditorInput, EditorInput, IUntitledTextResourceEditorInput } from 'vs/workbench/common/editor'; import { ITextEditorOptions, 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 { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { QueryEditorInput } from 'sql/workbench/common/editor/query/queryEditorInput'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -72,7 +72,7 @@ suite('Editor Replacer Contribution', () => { instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response?.override); const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this @@ -87,7 +87,7 @@ suite('Editor Replacer Contribution', () => { instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.other'), undefined, 'sql'); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response?.override); const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this @@ -102,7 +102,7 @@ suite('Editor Replacer Contribution', () => { instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.notebook'), undefined, undefined); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response?.override); const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this @@ -117,7 +117,7 @@ suite('Editor Replacer Contribution', () => { instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.iynb'), undefined, 'notebook'); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response?.override); const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this @@ -135,7 +135,7 @@ suite('Editor Replacer Contribution', () => { const service = accessor.untitledTextEditorService; const input = instantiationService.createInstance(UntitledTextEditorInput, service.create()); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response?.override); const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this @@ -153,7 +153,7 @@ suite('Editor Replacer Contribution', () => { const service = accessor.untitledTextEditorService; const untitled = instantiationService.createInstance(UntitledTextEditorInput, service.create()); const input = instantiationService.createInstance(UntitledQueryEditorInput, '', untitled, undefined); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response === undefined); contrib.dispose(); @@ -167,7 +167,7 @@ suite('Editor Replacer Contribution', () => { const accessor = instantiationService.createInstance(ServiceAccessor); const service = accessor.untitledTextEditorService; const input = instantiationService.createInstance(UntitledTextEditorInput, service.create({ associatedResource: URI.file('/test/file.unknown') })); - const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup, OpenEditorContext.NEW_EDITOR); assert(response === undefined); contrib.dispose(); @@ -191,10 +191,10 @@ class MockEditorService extends TestEditorService { }); } - fireOpenEditor(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) { + fireOpenEditor(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, context: OpenEditorContext, id?: string) { for (const handler of this.overridenOpens) { let response: IOpenEditorOverride | undefined; - if (response = handler.open(editor, options, group)) { + if (response = handler.open(editor, options, group, context, id)) { return response; } } diff --git a/src/sql/workbench/contrib/modelView/browser/webview.component.ts b/src/sql/workbench/contrib/modelView/browser/webview.component.ts index d678400501..309d7eb289 100644 --- a/src/sql/workbench/contrib/modelView/browser/webview.component.ts +++ b/src/sql/workbench/contrib/modelView/browser/webview.component.ts @@ -71,7 +71,7 @@ export default class WebViewComponent extends ComponentBase implements IComponen {}, { allowScripts: true - }); + }, undefined); this._webview.mountTo(this._el.nativeElement); diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.css b/src/sql/workbench/contrib/notebook/browser/notebook.css index 3d3c767dc2..75563360c0 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.css +++ b/src/sql/workbench/contrib/notebook/browser/notebook.css @@ -219,8 +219,8 @@ background: url("./media/light/new_notebook.svg") center center no-repeat; } -.vs-dark .monaco-workbench .notebook-action.new-notebook, -.hc-black .monaco-workbench .notebook-action.new-notebook { +.monaco-workbench.vs-dark .notebook-action.new-notebook, +.monaco-workbench.hc-black .notebook-action.new-notebook { background: url("./media/dark/new_notebook_inverse.svg") center center no-repeat; } diff --git a/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css b/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css index a74f8ce461..6967b2bd11 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css +++ b/src/sql/workbench/contrib/objectExplorer/browser/media/serverTreeActions.css @@ -8,8 +8,8 @@ background-image: url('add_server.svg'); } -.vs-dark .monaco-workbench .add-server-action, -.hc-black .monaco-workbench .add-server-action { +.monaco-workbench.vs-dark .add-server-action, +.monaco-workbench.hc-black .add-server-action { background-image: url('add_server_inverse.svg'); } @@ -17,8 +17,8 @@ background-image: url('new_servergroup.svg'); } -.vs-dark .monaco-workbench .add-server-group-action, -.hc-black .monaco-workbench .add-server-group-action { +.monaco-workbench.vs-dark .add-server-group-action, +.monaco-workbench.hc-black .add-server-group-action { background-image: url('new_servergroup_inverse.svg'); } @@ -26,7 +26,7 @@ background-image: url('connected_active_server.svg'); } -.vs-dark .monaco-workbench .active-connections-action, -.hc-black .monaco-workbench .active-connections-action{ +.monaco-workbench.vs-dark .active-connections-action, +.monaco-workbench.hc-black .active-connections-action{ background-image: url('connected_active_server_inverse.svg'); } diff --git a/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css b/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css index 976880f0c6..74a4e1f99f 100644 --- a/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css +++ b/src/sql/workbench/contrib/query/browser/media/binarydiffeditor.css @@ -11,10 +11,10 @@ border-left: 3px solid #DDD; } -.vs-dark .monaco-workbench .binarydiff-right { +.monaco-workbench.vs-dark .binarydiff-right { border-left: 3px solid rgb(20, 20, 20); } -.hc-black .monaco-workbench .binarydiff-right { +.monaco-workbench.hc-black .binarydiff-right { border-left: 3px solid #6FC3DF; -} \ No newline at end of file +} diff --git a/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css b/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css index 5a2cc4d9cd..c2f3548ea3 100644 --- a/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css +++ b/src/sql/workbench/contrib/query/browser/media/editorGroupsControl.css @@ -3,11 +3,11 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.vs .monaco-workbench > .editor > .content.drag { +.monaco-workbench.vs > .editor > .content.drag { background-color: #ECECEC; } -.vs-dark .monaco-workbench > .editor > .content.drag { +.monaco-workbench.vs-dark > .editor > .content.drag { background-color: #2D2D2D; } @@ -28,17 +28,17 @@ } .vs #monaco-workbench-editor-drop-overlay, -.vs .monaco-workbench > .editor.empty > .content.dropfeedback { +.monaco-workbench.vs > .editor.empty > .content.dropfeedback { background-color: rgba(51,153,255, 0.18); } .vs-dark #monaco-workbench-editor-drop-overlay, -.vs-dark .monaco-workbench > .editor.empty > .content.dropfeedback { +.monaco-workbench.vs-dark > .editor.empty > .content.dropfeedback { background-color: rgba(83, 89, 93, 0.5); } .hc-black #monaco-workbench-editor-drop-overlay, -.hc-black .monaco-workbench > .editor.empty > .content.dropfeedback { +.monaco-workbench.hc-black > .editor.empty > .content.dropfeedback { background: none !important; outline: 2px dashed #f38518; outline-offset: -2px; @@ -67,63 +67,63 @@ box-sizing: content-box; } -.vs .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.dragging { +.monaco-workbench.vs > .editor > .content.vertical-layout > .one-editor-silo.dragging { border-left: 1px solid #E7E7E7; border-right: 1px solid #E7E7E7; } -.vs .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.dragging { +.monaco-workbench.vs > .editor > .content.horizontal-layout > .one-editor-silo.dragging { border-top: 1px solid #E7E7E7; border-bottom: 1px solid #E7E7E7; } -.vs .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-two, -.vs .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three { +.monaco-workbench.vs > .editor > .content.vertical-layout > .one-editor-silo.editor-two, +.monaco-workbench.vs > .editor > .content.vertical-layout > .one-editor-silo.editor-three { border-left: 1px solid #E7E7E7; } -.vs .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, -.vs .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { +.monaco-workbench.vs > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, +.monaco-workbench.vs > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { border-top: 1px solid #E7E7E7; } -.vs-dark .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.dragging { +.monaco-workbench.vs-dark > .editor > .content.vertical-layout > .one-editor-silo.dragging { border-left: 1px solid #444; border-right: 1px solid #444; } -.vs-dark .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.dragging { +.monaco-workbench.vs-dark > .editor > .content.horizontal-layout > .one-editor-silo.dragging { border-top: 1px solid #444; border-bottom: 1px solid #444; } -.vs-dark .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-two, -.vs-dark .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three { +.monaco-workbench.vs-dark > .editor > .content.vertical-layout > .one-editor-silo.editor-two, +.monaco-workbench.vs-dark > .editor > .content.vertical-layout > .one-editor-silo.editor-three { border-left: 1px solid #444; } -.vs-dark .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, -.vs-dark .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { +.monaco-workbench.vs-dark > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, +.monaco-workbench.vs-dark > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { border-top: 1px solid #444; } -.hc-black .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.dragging { +.monaco-workbench.hc-black > .editor > .content.vertical-layout > .one-editor-silo.dragging { border-left: 1px solid #6FC3DF; border-right: 1px solid #6FC3DF; } -.hc-black .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.dragging { +.monaco-workbench.hc-black > .editor > .content.horizontal-layout > .one-editor-silo.dragging { border-top: 1px solid #6FC3DF; border-bottom: 1px solid #6FC3DF; } -.hc-black .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-two, -.hc-black .monaco-workbench > .editor > .content.vertical-layout > .one-editor-silo.editor-three { +.monaco-workbench.hc-black > .editor > .content.vertical-layout > .one-editor-silo.editor-two, +.monaco-workbench.hc-black > .editor > .content.vertical-layout > .one-editor-silo.editor-three { border-left: 1px solid #6FC3DF; } -.hc-black .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, -.hc-black .monaco-workbench > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { +.monaco-workbench.hc-black > .editor > .content.horizontal-layout > .one-editor-silo.editor-two, +.monaco-workbench.hc-black > .editor > .content.horizontal-layout > .one-editor-silo.editor-three { border-top: 1px solid #6FC3DF; } @@ -149,4 +149,4 @@ .monaco-workbench > .editor > .content > .one-editor-silo > .container > .editor-container { height: calc(100% - 35px); /* Editor is below editor title */ -} \ No newline at end of file +} diff --git a/src/sql/workbench/contrib/query/browser/media/editorpart.css b/src/sql/workbench/contrib/query/browser/media/editorpart.css index 9beb3c5442..4489ddaef0 100644 --- a/src/sql/workbench/contrib/query/browser/media/editorpart.css +++ b/src/sql/workbench/contrib/query/browser/media/editorpart.css @@ -3,15 +3,15 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.vs .monaco-workbench .monaco-editor-background { +.monaco-workbench.vs .monaco-editor-background { background-color: white; } -.vs-dark .monaco-workbench .monaco-editor-background { +.monaco-workbench.vs-dark .monaco-editor-background { background-color: #1E1E1E; } -.hc-black .monaco-workbench .monaco-editor-background { +.monaco-workbench.hc-black .monaco-editor-background { background-color: #000; } @@ -24,11 +24,11 @@ background-image: url('letterpress.svg'); } -.vs-dark .monaco-workbench .part.editor.empty { +.monaco-workbench.vs-dark .part.editor.empty { background-image: url('letterpress-dark.svg'); } -.hc-black .monaco-workbench .part.editor.empty { +.monaco-workbench.hc-black .part.editor.empty { background-image: url('letterpress-hc.svg'); } @@ -38,4 +38,4 @@ .monaco-workbench .part.editor { background-size: 260px 260px; } -} \ No newline at end of file +} diff --git a/src/sql/workbench/contrib/query/browser/media/notabstitle.css b/src/sql/workbench/contrib/query/browser/media/notabstitle.css index a4ca1dcd5d..b33a7276ce 100644 --- a/src/sql/workbench/contrib/query/browser/media/notabstitle.css +++ b/src/sql/workbench/contrib/query/browser/media/notabstitle.css @@ -28,11 +28,11 @@ opacity: 1; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action { background: url('close-dirty.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action, -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action, +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title.dirty .title-actions .close-editor-action { background: url('close-dirty-inverse.svg') center center no-repeat; } diff --git a/src/sql/workbench/contrib/query/browser/media/tabstitle.css b/src/sql/workbench/contrib/query/browser/media/tabstitle.css index c7ae079e16..7e5756ef18 100644 --- a/src/sql/workbench/contrib/query/browser/media/tabstitle.css +++ b/src/sql/workbench/contrib/query/browser/media/tabstitle.css @@ -5,11 +5,11 @@ /* Title Container */ -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.tabs { background: #F3F3F3; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.tabs { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.tabs { background: #252526; } @@ -51,56 +51,56 @@ padding-left: 10px; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) { background-color: #ECECEC; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:not(.active) { background-color: #2D2D2D; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { border-left-color: #F3F3F3; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child { border-right-color: #F3F3F3; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { border-left-color: #252526; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active:last-child { border-right-color: #252526; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child, -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child, +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab:first-child { border-left-color: transparent; } -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab { border-left-color: #6FC3DF; } -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active { +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active { outline: 2px solid #f38518; outline-offset: -1px; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { background-color: #DDECFF; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { background-color: #383B3D; } -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container.dropfeedback, +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback { background: none !important; outline: 2px dashed #f38518; outline-offset: -2px; @@ -117,20 +117,20 @@ height: 16px; /* tweak the icon size of the editor labels when icons are enabled */ } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { opacity: 0.7 !important; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { opacity: 0.5 !important; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active .tab-label, +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.active .tab-label, .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dropfeedback .tab-label { opacity: 1 !important; } -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label { opacity: 1 !important; } @@ -172,21 +172,21 @@ margin-right: 0.5em; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action { background: url('close-dirty.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action, -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action, +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action { background: url('close-dirty-inverse.svg') center center no-repeat; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover { background: url('close.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover, -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover, +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.dirty .close-editor-action:hover { background: url('close-inverse.svg') center center no-repeat; } @@ -202,12 +202,12 @@ background-position-x: calc(100% - 6px); /* to the right of the tab label */ } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { background-image: url('close-dirty.svg'); } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty, -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty, +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab.no-close-button.dirty { background-image: url('close-dirty-inverse.svg'); } @@ -217,4 +217,4 @@ cursor: default; flex: initial; padding-left: 4px; -} \ No newline at end of file +} diff --git a/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css b/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css index 48300eef8b..7d59b5590d 100644 --- a/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css +++ b/src/sql/workbench/contrib/query/browser/media/textdiffeditor.css @@ -3,20 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.vs .monaco-workbench .textdiff-editor-action.next { +.monaco-workbench.vs .textdiff-editor-action.next { background: url('next-diff.svg') center center no-repeat; } -.vs .monaco-workbench .textdiff-editor-action.previous { +.monaco-workbench.vs .textdiff-editor-action.previous { background: url('previous-diff.svg') center center no-repeat; } -.vs-dark .monaco-workbench .textdiff-editor-action.next, -.hc-black .monaco-workbench .textdiff-editor-action.next { +.monaco-workbench.vs-dark .textdiff-editor-action.next, +.monaco-workbench.hc-black .textdiff-editor-action.next { background: url('next-diff-inverse.svg') center center no-repeat; } -.vs-dark .monaco-workbench .textdiff-editor-action.previous, -.hc-black .monaco-workbench .textdiff-editor-action.previous { +.monaco-workbench.vs-dark .textdiff-editor-action.previous, +.monaco-workbench.hc-black .textdiff-editor-action.previous { background: url('previous-diff-inverse.svg') center center no-repeat; -} \ No newline at end of file +} diff --git a/src/sql/workbench/contrib/query/browser/media/titlecontrol.css b/src/sql/workbench/contrib/query/browser/media/titlecontrol.css index 4e89271f38..a6a275ae7c 100644 --- a/src/sql/workbench/contrib/query/browser/media/titlecontrol.css +++ b/src/sql/workbench/contrib/query/browser/media/titlecontrol.css @@ -26,23 +26,23 @@ cursor: pointer; } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { color: rgba(51, 51, 51, 0.5); } -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a, -.vs .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a { +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a, +.monaco-workbench.vs > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a { color: #333333; } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .title-label a, +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title .tabs-container > .tab .tab-label a { color: rgba(255, 255, 255, 0.5); } -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a, -.vs-dark .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a { +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.active .title-label a, +.monaco-workbench.vs-dark > .part.editor > .content > .one-editor-silo > .container > .title.active .tabs-container > .tab .tab-label a { color: white; } @@ -59,8 +59,8 @@ background-repeat: no-repeat; } -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label, -.hc-black .monaco-workbench > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label { +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .title-actions .action-label, +.monaco-workbench.hc-black > .part.editor > .content > .one-editor-silo > .container > .title .editor-actions .action-label { line-height: initial; } @@ -94,8 +94,8 @@ background: url('close.svg') center center no-repeat; } -.vs-dark .monaco-workbench .close-editor-action, -.hc-black .monaco-workbench .close-editor-action { +.monaco-workbench.vs-dark .close-editor-action, +.monaco-workbench.hc-black .close-editor-action { background: url('close-inverse.svg') center center no-repeat; } @@ -103,8 +103,8 @@ background: url('split-editor-vertical.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action, -.hc-black .monaco-workbench > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action { +.monaco-workbench.vs-dark > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action, +.monaco-workbench.hc-black > .part.editor > .content.vertical-layout > .one-editor-silo > .container > .title .split-editor-action { background: url('split-editor-vertical-inverse.svg') center center no-repeat; } @@ -112,8 +112,8 @@ background: url('split-editor-horizontal.svg') center center no-repeat; } -.vs-dark .monaco-workbench > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action, -.hc-black .monaco-workbench > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action { +.monaco-workbench.vs-dark > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action, +.monaco-workbench.hc-black > .part.editor > .content.horizontal-layout > .one-editor-silo > .container > .title .split-editor-action { background: url('split-editor-horizontal-inverse.svg') center center no-repeat; } @@ -121,7 +121,7 @@ background: url('stackview.svg') center center no-repeat; } -.vs-dark .monaco-workbench .show-group-editors-action, -.hc-black .monaco-workbench .show-group-editors-action { +.monaco-workbench.vs-dark .show-group-editors-action, +.monaco-workbench.hc-black .show-group-editors-action { background: url('stackview-inverse.svg') center center no-repeat; -} \ No newline at end of file +} diff --git a/src/sql/workbench/contrib/webview/browser/webViewDialog.ts b/src/sql/workbench/contrib/webview/browser/webViewDialog.ts index 7506567f7a..6054293f88 100644 --- a/src/sql/workbench/contrib/webview/browser/webViewDialog.ts +++ b/src/sql/workbench/contrib/webview/browser/webViewDialog.ts @@ -94,7 +94,7 @@ export class WebViewDialog extends Modal { {}, { allowScripts: true - }); + }, undefined); this._webview.mountTo(this._body); diff --git a/src/sql/workbench/contrib/welcome/gettingStarted/electron-browser/enablePreviewFeatures.ts b/src/sql/workbench/contrib/welcome/gettingStarted/electron-browser/enablePreviewFeatures.ts index be1424fe48..b953d7ff5b 100644 --- a/src/sql/workbench/contrib/welcome/gettingStarted/electron-browser/enablePreviewFeatures.ts +++ b/src/sql/workbench/contrib/welcome/gettingStarted/electron-browser/enablePreviewFeatures.ts @@ -8,7 +8,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; export class NativeEnablePreviewFeatures extends AbstractEnablePreviewFeatures { diff --git a/src/sql/workbench/contrib/welcome2/gettingStarted/electron-browser/enablePreviewFeatures.ts b/src/sql/workbench/contrib/welcome2/gettingStarted/electron-browser/enablePreviewFeatures.ts index be1424fe48..b953d7ff5b 100644 --- a/src/sql/workbench/contrib/welcome2/gettingStarted/electron-browser/enablePreviewFeatures.ts +++ b/src/sql/workbench/contrib/welcome2/gettingStarted/electron-browser/enablePreviewFeatures.ts @@ -8,7 +8,7 @@ import { IStorageService } from 'vs/platform/storage/common/storage'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IElectronService } from 'vs/platform/electron/node/electron'; +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; export class NativeEnablePreviewFeatures extends AbstractEnablePreviewFeatures { diff --git a/src/vs/base/browser/contextmenu.ts b/src/vs/base/browser/contextmenu.ts index c52d1e8129..5e61d17445 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 78dc3d2ea8..1de9ee0cbf 100644 --- a/src/vs/base/browser/markdownRenderer.ts +++ b/src/vs/base/browser/markdownRenderer.ts @@ -16,16 +16,22 @@ import { escape } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { renderCodicons, markdownEscapeEscapedCodicons } from 'vs/base/common/codicons'; +import { resolvePath } from 'vs/base/common/resources'; + +export interface MarkedOptions extends marked.MarkedOptions { + baseUrl?: never; +} export interface MarkdownRenderOptions extends FormattedTextRenderOptions { codeBlockRenderer?: (modeId: string, value: string) => Promise; codeBlockRenderCallback?: () => void; + baseUrl?: URI; } /** * Create html nodes for the given content element. */ -export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: marked.MarkedOptions = {}): HTMLElement { +export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement { const element = createElement(options); const _uriMassage = function (part: string): string { @@ -82,6 +88,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende if (href) { ({ href, dimensions } = parseHrefAndDimensions(href)); href = _href(href, true); + if (options.baseUrl) { + href = resolvePath(options.baseUrl, href).toString(); + } attributes.push(`src="${href}"`); } if (text) { @@ -101,6 +110,12 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende text = removeMarkdownEscapes(text); } href = _href(href, false); + if (options.baseUrl) { + const hasScheme = /^\w[\w\d+.-]*:/.test(href); + if (!hasScheme) { + href = resolvePath(options.baseUrl, href).toString(); + } + } title = removeMarkdownEscapes(title); href = removeMarkdownEscapes(href); if ( @@ -187,6 +202,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; @@ -202,18 +224,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 da7b37b0e0..ee4cda4222 100644 --- a/src/vs/base/browser/ui/actionbar/actionbar.ts +++ b/src/vs/base/browser/ui/actionbar/actionbar.ts @@ -31,6 +31,7 @@ export interface IActionViewItem extends IDisposable { export interface IBaseActionViewItemOptions { draggable?: boolean; isMenu?: boolean; + useEventAsContext?: boolean; } export class BaseActionViewItem extends Disposable implements IActionViewItem { @@ -178,7 +179,7 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem { onClick(event: DOM.EventLike): void { DOM.EventHelper.stop(event, true); - const context = types.isUndefinedOrNull(this._context) ? undefined : this._context; + const context = types.isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : undefined : this._context; this.actionRunner.run(this._action, context); } @@ -404,6 +405,7 @@ export interface IActionBarOptions { ariaLabel?: string; animated?: boolean; triggerKeys?: ActionTrigger; + allowContextMenu?: boolean; } const defaultOptions: IActionBarOptions = { @@ -633,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 ab67bd300f..ba25be2a8e 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 3cba707513..73e0627590 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 2faf0490a3..f1f6e9c6ac 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/dropdown/dropdown.ts b/src/vs/base/browser/ui/dropdown/dropdown.ts index 6c87e8734e..8421f003f3 100644 --- a/src/vs/base/browser/ui/dropdown/dropdown.ts +++ b/src/vs/base/browser/ui/dropdown/dropdown.ts @@ -14,6 +14,7 @@ import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes'; import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, addClasses } from 'vs/base/browser/dom'; import { IContextMenuDelegate } from 'vs/base/browser/contextmenu'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Emitter } from 'vs/base/common/event'; export interface ILabelRenderer { (container: HTMLElement): IDisposable | null; @@ -29,7 +30,10 @@ export class BaseDropdown extends ActionRunner { private boxContainer?: HTMLElement; private _label?: HTMLElement; private contents?: HTMLElement; + private visible: boolean | undefined; + private _onDidChangeVisibility = new Emitter(); + readonly onDidChangeVisibility = this._onDidChangeVisibility.event; constructor(container: HTMLElement, options: IBaseDropdownOptions) { super(); @@ -48,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]) { @@ -101,11 +105,17 @@ export class BaseDropdown extends ActionRunner { } show(): void { - this.visible = true; + if (!this.visible) { + this.visible = true; + this._onDidChangeVisibility.fire(true); + } } hide(): void { - this.visible = false; + if (this.visible) { + this.visible = false; + this._onDidChangeVisibility.fire(false); + } } isVisible(): boolean { @@ -256,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 }); } @@ -303,6 +314,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { this.element.tabIndex = 0; this.element.setAttribute('role', 'button'); this.element.setAttribute('aria-haspopup', 'true'); + this.element.setAttribute('aria-expanded', 'false'); this.element.title = this._action.label || ''; return null; @@ -321,6 +333,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem { } this.dropdownMenu = this._register(new DropdownMenu(container, options)); + this._register(this.dropdownMenu.onDidChangeVisibility(visible => this.element?.setAttribute('aria-expanded', `${visible}`))); this.dropdownMenu.menuOptions = { actionViewItemProvider: this.actionViewItemProvider, diff --git a/src/vs/base/browser/ui/grid/gridview.ts b/src/vs/base/browser/ui/grid/gridview.ts index 6a22c4fdf4..9de82ec58c 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 2e6a7e14ff..353df14170 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 Source EULA. 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 0000000000..5d77846870 --- /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 Source EULA. 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/listPaging.ts b/src/vs/base/browser/ui/list/listPaging.ts index bc00bfac4f..8c730ca86f 100644 --- a/src/vs/base/browser/ui/list/listPaging.ts +++ b/src/vs/base/browser/ui/list/listPaging.ts @@ -165,7 +165,7 @@ export class PagedList implements IDisposable { } get onDidChangeFocus(): Event> { - return Event.map(this.list.onDidChangeFocus, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidChangeFocus, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); } get onDidOpen(): Event> { @@ -173,11 +173,11 @@ export class PagedList implements IDisposable { } get onDidChangeSelection(): Event> { - return Event.map(this.list.onDidChangeSelection, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); } get onPin(): Event> { - return Event.map(this.list.onDidPin, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes })); + return Event.map(this.list.onDidPin, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent })); } get onContextMenu(): Event> { diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 7b6cef6131..f3e9a95aa3 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); @@ -278,18 +283,20 @@ export class ListView implements ISpliceable, IDisposable { this.rowsContainer = document.createElement('div'); this.rowsContainer.className = 'monaco-list-rows'; - if (options.transformOptimization) { + const transformOptimization = getOrDefault(options, o => o.transformOptimization, DefaultOptions.transformOptimization); + if (transformOptimization) { this.rowsContainer.style.transform = 'translate3d(0px, 0px, 0px)'; } 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); @@ -319,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) { @@ -651,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 b03c0d8c3a..7547418095 100644 --- a/src/vs/base/browser/ui/list/listWidget.ts +++ b/src/vs/base/browser/ui/list/listWidget.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./list'; -import { localize } from 'vs/nls'; import { IDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { isNumber } from 'vs/base/common/types'; import { range, firstIndex, binarySearch } from 'vs/base/common/arrays'; @@ -17,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'; @@ -26,6 +25,7 @@ import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice'; import { clamp } from 'vs/base/common/numbers'; import { matchesPrefix } from 'vs/base/common/filters'; import { IDragAndDropData } from 'vs/base/browser/dnd'; +import { alert } from 'vs/base/browser/ui/aria/aria'; interface ITraitChangeEvent { indexes: number[]; @@ -344,6 +344,7 @@ class TypeLabelController implements IDisposable { private automaticKeyboardNavigation = true; private triggered = false; + private previouslyFocused = -1; private readonly enabledDisposables = new DisposableStore(); private readonly disposables = new DisposableStore(); @@ -393,6 +394,7 @@ class TypeLabelController implements IDisposable { const onInput = Event.reduce(Event.any(onChar, onClear), (r, i) => i === null ? null : ((r || '') + i)); onInput(this.onInput, this, this.enabledDisposables); + onClear(this.onClear, this, this.enabledDisposables); this.enabled = true; this.triggered = false; @@ -408,6 +410,19 @@ class TypeLabelController implements IDisposable { this.triggered = false; } + private onClear(): void { + const focus = this.list.getFocus(); + if (focus.length > 0 && focus[0] === this.previouslyFocused) { + // List: re-anounce element on typing end since typed keys will interupt aria label of focused element + // Do not announce if there was a focus change at the end to prevent duplication https://github.com/microsoft/vscode/issues/95961 + const ariaLabel = this.list.options.accessibilityProvider?.getAriaLabel(this.list.element(focus[0])); + if (ariaLabel) { + alert(ariaLabel); + } + } + this.previouslyFocused = -1; + } + private onInput(word: string | null): void { if (!word) { this.state = TypeLabelControllerState.Idle; @@ -426,6 +441,7 @@ class TypeLabelController implements IDisposable { const labelStr = label && label.toString(); if (typeof labelStr === 'undefined' || matchesPrefix(word, labelStr)) { + this.previouslyFocused = start; this.list.setFocus([index]); this.list.reveal(index); return; @@ -1091,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 { @@ -1272,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 { @@ -1363,7 +1376,7 @@ export class List implements ISpliceable, IDisposable { set ariaLabel(value: string) { this._ariaLabel = value; - this.view.domNode.setAttribute('aria-label', localize('aria list', "{0}. Use the navigation keys to navigate.", value)); + this.view.domNode.setAttribute('aria-label', value); } domFocus(): void { diff --git a/src/vs/base/browser/ui/menu/menu.css b/src/vs/base/browser/ui/menu/menu.css index b549947f17..de8e55b969 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 a716e451a6..fcf6e34aab 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 f1554b811a..f803cb6f63 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -140,8 +140,8 @@ export class MenuBar extends Disposable { eventHandled = false; } - // Never allow default tab behavior - if (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab)) { + // Never allow default tab behavior when not compact + if (this.options.compactMode === undefined && (event.equals(KeyCode.Tab | KeyMod.Shift) || event.equals(KeyCode.Tab))) { event.preventDefault(); } @@ -315,7 +315,7 @@ export class MenuBar extends Disposable { createOverflowMenu(): void { const label = this.options.compactMode !== undefined ? nls.localize('mAppMenu', 'Application Menu') : nls.localize('mMore', 'More'); const title = this.options.compactMode !== undefined ? label : undefined; - const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': label, 'title': title, 'aria-haspopup': true }); + const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': this.options.compactMode !== undefined ? 0 : -1, 'aria-label': label, 'title': title, 'aria-haspopup': true }); const titleElement = $('div.menubar-menu-title.toolbar-toggle-more' + menuBarMoreIcon.cssSelector, { 'role': 'none', 'aria-hidden': true }); buttonElement.appendChild(titleElement); @@ -326,7 +326,7 @@ export class MenuBar extends Disposable { let event = new StandardKeyboardEvent(e as KeyboardEvent); let eventHandled = true; - if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) { + if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter) || (this.options.compactMode !== undefined && event.equals(KeyCode.Space))) && !this.isOpen) { this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX }; this.openedViaKeyboard = true; this.focusState = MenubarState.OPEN; @@ -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/mouseCursor/mouseCursor.css b/src/vs/base/browser/ui/mouseCursor/mouseCursor.css new file mode 100644 index 0000000000..1a035d319a --- /dev/null +++ b/src/vs/base/browser/ui/mouseCursor/mouseCursor.css @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-mouse-cursor-text { + cursor: text; +} + +/* The following selector looks a bit funny, but that is needed to cover all the workbench and the editor!! */ +.vs-dark .mac .monaco-mouse-cursor-text, .hc-black .mac .monaco-mouse-cursor-text, +.vs-dark.mac .monaco-mouse-cursor-text, .hc-black.mac .monaco-mouse-cursor-text { + cursor: -webkit-image-set(url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAAL0lEQVQoz2NgCD3x//9/BhBYBWdhgFVAiVW4JBFKGIa4AqD0//9D3pt4I4tAdAMAHTQ/j5Zom30AAAAASUVORK5CYII=') 1x, url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAQAAADZc7J/AAAAz0lEQVRIx2NgYGBY/R8I/vx5eelX3n82IJ9FxGf6tksvf/8FiTMQAcAGQMDvSwu09abffY8QYSAScNk45G198eX//yev73/4///701eh//kZSARckrNBRvz//+8+6ZohwCzjGNjdgQxkAg7B9WADeBjIBqtJCbhRA0YNoIkBSNmaPEMoNmA0FkYNoFKhapJ6FGyAH3nauaSmPfwI0v/3OukVi0CIZ+F25KrtYcx/CTIy0e+rC7R1Z4KMICVTQQ14feVXIbR695u14+Ir4gwAAD49E54wc1kWAAAAAElFTkSuQmCC') 2x) 5 8, text; +} diff --git a/src/vs/base/browser/ui/mouseCursor/mouseCursor.ts b/src/vs/base/browser/ui/mouseCursor/mouseCursor.ts new file mode 100644 index 0000000000..93844870f3 --- /dev/null +++ b/src/vs/base/browser/ui/mouseCursor/mouseCursor.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./mouseCursor'; + +export const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`; diff --git a/src/vs/base/browser/ui/sash/sash.css b/src/vs/base/browser/ui/sash/sash.css index de8ac93f4b..5302729c3d 100644 --- a/src/vs/base/browser/ui/sash/sash.css +++ b/src/vs/base/browser/ui/sash/sash.css @@ -13,13 +13,6 @@ pointer-events: none; } -.monaco-sash.vertical { - cursor: ew-resize; - top: 0; - width: 4px; - height: 100%; -} - .monaco-sash.mac.vertical { cursor: col-resize; } @@ -32,13 +25,6 @@ cursor: w-resize; } -.monaco-sash.horizontal { - cursor: ns-resize; - left: 0; - width: 100%; - height: 4px; -} - .monaco-sash.mac.horizontal { cursor: row-resize; } @@ -51,52 +37,11 @@ cursor: n-resize; } -.monaco-sash:not(.disabled).orthogonal-start::before, -.monaco-sash:not(.disabled).orthogonal-end::after { - content: ' '; - height: 8px; - width: 8px; - z-index: 100; - display: block; - cursor: all-scroll; - position: absolute; -} - -.monaco-sash.orthogonal-start.vertical::before { - left: -2px; - top: -4px; -} - -.monaco-sash.orthogonal-end.vertical::after { - left: -2px; - bottom: -4px; -} - -.monaco-sash.orthogonal-start.horizontal::before { - top: -2px; - left: -4px; -} - -.monaco-sash.orthogonal-end.horizontal::after { - top: -2px; - right: -4px; -} - .monaco-sash.disabled { cursor: default !important; pointer-events: none !important; } -/** Touch **/ - -.monaco-sash.touch.vertical { - width: 20px; -} - -.monaco-sash.touch.horizontal { - height: 20px; -} - /** Debug **/ .monaco-sash.debug { @@ -110,4 +55,4 @@ .monaco-sash.debug:not(.disabled).orthogonal-start::before, .monaco-sash.debug:not(.disabled).orthogonal-end::after { background: red; -} \ No newline at end of file +} diff --git a/src/vs/base/browser/ui/sash/sash.ts b/src/vs/base/browser/ui/sash/sash.ts index a867faea03..853185876e 100644 --- a/src/vs/base/browser/ui/sash/sash.ts +++ b/src/vs/base/browser/ui/sash/sash.ts @@ -5,7 +5,6 @@ import 'vs/css!./sash'; import { IDisposable, dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { isIPad } from 'vs/base/browser/browser'; import { isMacintosh } from 'vs/base/common/platform'; import * as types from 'vs/base/common/types'; import { EventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; @@ -39,9 +38,18 @@ export interface ISashEvent { } export interface ISashOptions { - orientation?: Orientation; - orthogonalStartSash?: Sash; - orthogonalEndSash?: Sash; + readonly orientation: Orientation; + readonly orthogonalStartSash?: Sash; + readonly orthogonalEndSash?: Sash; + readonly size?: number; +} + +export interface IVerticalSashOptions extends ISashOptions { + readonly orientation: Orientation.VERTICAL; +} + +export interface IHorizontalSashOptions extends ISashOptions { + readonly orientation: Orientation.HORIZONTAL; } export const enum Orientation { @@ -56,12 +64,20 @@ export const enum SashState { Enabled } +let globalSize = 4; +const onDidChangeGlobalSize = new Emitter(); +export function setGlobalSashSize(size: number): void { + globalSize = size; + onDidChangeGlobalSize.fire(size); +} + export class Sash extends Disposable { private el: HTMLElement; private layoutProvider: ISashLayoutProvider; private hidden: boolean; private orientation!: Orientation; + private size: number; private _state: SashState = SashState.Enabled; get state(): SashState { return this._state; } @@ -127,7 +143,9 @@ export class Sash extends Disposable { this._orthogonalEndSash = sash; } - constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions = {}) { + constructor(container: HTMLElement, layoutProvider: IVerticalSashLayoutProvider, options: ISashOptions); + constructor(container: HTMLElement, layoutProvider: IHorizontalSashLayoutProvider, options: ISashOptions); + constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions) { super(); this.el = append(container, $('.monaco-sash')); @@ -142,12 +160,21 @@ export class Sash extends Disposable { this._register(Gesture.addTarget(this.el)); this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this)); - if (isIPad) { - // see also https://ux.stackexchange.com/questions/39023/what-is-the-optimum-button-size-of-touch-screen-applications - addClass(this.el, 'touch'); - } + if (typeof options.size === 'number') { + this.size = options.size; - this.setOrientation(options.orientation || Orientation.VERTICAL); + if (options.orientation === Orientation.VERTICAL) { + this.el.style.width = `${this.size}px`; + } else { + this.el.style.height = `${this.size}px`; + } + } else { + this.size = globalSize; + this._register(onDidChangeGlobalSize.event(size => { + this.size = size; + this.layout(); + })); + } this.hidden = false; this.layoutProvider = layoutProvider; @@ -155,11 +182,7 @@ export class Sash extends Disposable { this.orthogonalStartSash = options.orthogonalStartSash; this.orthogonalEndSash = options.orthogonalEndSash; - toggleClass(this.el, 'debug', DEBUG); - } - - setOrientation(orientation: Orientation): void { - this.orientation = orientation; + this.orientation = options.orientation || Orientation.VERTICAL; if (this.orientation === Orientation.HORIZONTAL) { addClass(this.el, 'horizontal'); @@ -169,9 +192,9 @@ export class Sash extends Disposable { addClass(this.el, 'vertical'); } - if (this.layoutProvider) { - this.layout(); - } + toggleClass(this.el, 'debug', DEBUG); + + this.layout(); } private onMouseDown(e: MouseEvent): void { @@ -331,11 +354,9 @@ export class Sash extends Disposable { } layout(): void { - const size = isIPad ? 20 : 4; - if (this.orientation === Orientation.VERTICAL) { const verticalProvider = (this.layoutProvider); - this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (size / 2) + 'px'; + this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px'; if (verticalProvider.getVerticalSashTop) { this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px'; @@ -346,7 +367,7 @@ export class Sash extends Disposable { } } else { const horizontalProvider = (this.layoutProvider); - this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (size / 2) + 'px'; + this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px'; if (horizontalProvider.getHorizontalSashLeft) { this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px'; @@ -384,15 +405,15 @@ export class Sash extends Disposable { private getOrthogonalSash(e: MouseEvent): Sash | undefined { if (this.orientation === Orientation.VERTICAL) { - if (e.offsetY <= 4) { + if (e.offsetY <= this.size) { return this.orthogonalStartSash; - } else if (e.offsetY >= this.el.clientHeight - 4) { + } else if (e.offsetY >= this.el.clientHeight - this.size) { return this.orthogonalEndSash; } } else { - if (e.offsetX <= 4) { + if (e.offsetX <= this.size) { return this.orthogonalStartSash; - } else if (e.offsetX >= this.el.clientWidth - 4) { + } else if (e.offsetX >= this.el.clientWidth - this.size) { return this.orthogonalEndSash; } } diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 0e3a67b820..da2d9c7db6 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 bfea4cce66..cbe11a810c 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 9ff40c339f..938bed518f 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 d2dc1c966e..7ff01fb105 100644 --- a/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts +++ b/src/vs/base/browser/ui/selectBox/selectBoxCustom.ts @@ -90,8 +90,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi private _isVisible: boolean; private selectBoxOptions: ISelectBoxOptions; - - public selectElement: HTMLSelectElement; // {{SQL CARBON EDIT}} + public selectElement: HTMLSelectElement; // {{SQL CARBON EDIT}} + private container?: HTMLElement; private options: ISelectOptionItem[] = []; private selected: number; private readonly _onDidSelect: Emitter; @@ -310,6 +310,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(); @@ -454,7 +455,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; @@ -469,7 +470,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; @@ -739,7 +740,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.css b/src/vs/base/browser/ui/splitview/paneview.css index 35c2e52696..add5d05934 100644 --- a/src/vs/base/browser/ui/splitview/paneview.css +++ b/src/vs/base/browser/ui/splitview/paneview.css @@ -16,7 +16,12 @@ flex-direction: column; } +.monaco-pane-view .pane.horizontal:not(.expanded) { + flex-direction: row; +} + .monaco-pane-view .pane > .pane-header { + height: 22px; font-size: 11px; font-weight: bold; text-transform: uppercase; @@ -26,6 +31,12 @@ align-items: center; } +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header { + flex-direction: column; + height: 100%; + width: 22px; +} + .monaco-pane-view .pane > .pane-header > .twisties { width: 20px; display: flex; @@ -36,6 +47,11 @@ flex-shrink: 0; } +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .twisties { + margin-top: 2px; + margin-bottom: 2px; +} + .monaco-pane-view .pane > .pane-header.expanded > .twisties::before { transform: rotate(90deg); } @@ -132,6 +148,7 @@ width: 100%; height: 100%; min-height: 22px; + min-width: 19px; pointer-events: none; /* very important to not take events away from the parent */ transition: opacity 150ms ease-out; diff --git a/src/vs/base/browser/ui/splitview/paneview.ts b/src/vs/base/browser/ui/splitview/paneview.ts index 35a2a7b023..2c353f3b5f 100644 --- a/src/vs/base/browser/ui/splitview/paneview.ts +++ b/src/vs/base/browser/ui/splitview/paneview.ts @@ -106,7 +106,7 @@ export abstract class Pane extends Disposable implements IView { get minimumSize(): number { const headerSize = this.headerSize; const expanded = !this.headerVisible || this.isExpanded(); - const minimumBodySize = expanded ? this.minimumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0; + const minimumBodySize = expanded ? this.minimumBodySize : 0; return headerSize + minimumBodySize; } @@ -114,7 +114,7 @@ export abstract class Pane extends Disposable implements IView { get maximumSize(): number { const headerSize = this.headerSize; const expanded = !this.headerVisible || this.isExpanded(); - const maximumBodySize = expanded ? this.maximumBodySize : this._orientation === Orientation.HORIZONTAL ? 50 : 0; + const maximumBodySize = expanded ? this.maximumBodySize : 0; return headerSize + maximumBodySize; } @@ -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'); @@ -141,6 +141,10 @@ export abstract class Pane extends Disposable implements IView { return false; } + if (this.element) { + toggleClass(this.element, 'expanded', expanded); + } + this._expanded = !!expanded; this.updateHeader(); @@ -185,12 +189,21 @@ export abstract class Pane extends Disposable implements IView { this._orientation = orientation; + if (this.element) { + toggleClass(this.element, 'horizontal', this.orientation === Orientation.HORIZONTAL); + toggleClass(this.element, 'vertical', this.orientation === Orientation.VERTICAL); + } + if (this.header) { this.updateHeader(); } } render(): void { + toggleClass(this.element, 'expanded', this.isExpanded()); + toggleClass(this.element, 'horizontal', this.orientation === Orientation.HORIZONTAL); + toggleClass(this.element, 'vertical', this.orientation === Orientation.VERTICAL); + this.header = $('.pane-header'); append(this.element, this.header); this.header.setAttribute('tabindex', '0'); @@ -250,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; } @@ -262,7 +273,6 @@ export abstract class Pane extends Disposable implements IView { protected updateHeader(): void { const expanded = !this.headerVisible || this.isExpanded(); - this.header.style.height = `${this.headerSize}px`; this.header.style.lineHeight = `${this.headerSize}px`; toggleClass(this.header, 'hidden', !this.headerVisible); toggleClass(this.header, 'expanded', expanded); @@ -272,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 43c1387fae..33d90e3490 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 { @@ -689,13 +689,17 @@ export class SplitView extends Disposable { // Add sash if (this.viewItems.length > 1) { - const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL; - const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) } : { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }; - const sash = new Sash(this.sashContainer, layoutProvider, { - orientation, - orthogonalStartSash: this.orthogonalStartSash, - orthogonalEndSash: this.orthogonalEndSash - }); + const sash = this.orientation === Orientation.VERTICAL + ? new Sash(this.sashContainer, { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) }, { + orientation: Orientation.HORIZONTAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }) + : new Sash(this.sashContainer, { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) }, { + orientation: Orientation.VERTICAL, + orthogonalStartSash: this.orthogonalStartSash, + orthogonalEndSash: this.orthogonalEndSash + }); const sashEventMapper = this.orientation === Orientation.VERTICAL ? (e: IBaseSashEvent) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey }) @@ -951,7 +955,7 @@ export class SplitView extends Disposable { position += this.viewItems[i].size; if (this.sashItems[i].sash === sash) { - return Math.min(position, this.contentSize - 2); + return position; } } diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index 2220e1cb57..f6283d69bd 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/browser/ui/tree/treeDefaults.ts b/src/vs/base/browser/ui/tree/treeDefaults.ts index b25ac6b433..302a2c3799 100644 --- a/src/vs/base/browser/ui/tree/treeDefaults.ts +++ b/src/vs/base/browser/ui/tree/treeDefaults.ts @@ -13,7 +13,7 @@ export class CollapseAllAction extends Action { super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'collapse-all', enabled); } - async run(context?: any): Promise { + async run(): Promise { this.viewer.collapseAll(); this.viewer.setSelection([]); this.viewer.setFocus([]); diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 5b094ade7d..5543500af1 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -764,7 +764,7 @@ export class IdleValue { this._handle.dispose(); } - getValue(): T { + get value(): T { if (!this._didRun) { this._handle.dispose(); this._executor(); diff --git a/src/vs/base/common/charCode.ts b/src/vs/base/common/charCode.ts index 8342cb7875..ac05ce4d6d 100644 --- a/src/vs/base/common/charCode.ts +++ b/src/vs/base/common/charCode.ts @@ -342,7 +342,17 @@ export const enum CharCode { * Unicode Character 'LINE SEPARATOR' (U+2028) * http://www.fileformat.info/info/unicode/char/2028/index.htm */ - LINE_SEPARATOR_2028 = 8232, + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, // http://www.fileformat.info/info/unicode/category/Sk/list.htm U_CIRCUMFLEX = 0x005E, // U+005E CIRCUMFLEX @@ -422,4 +432,4 @@ export const enum CharCode { * http://www.fileformat.info/info/unicode/char/feff/index.htm */ UTF8_BOM = 65279 -} \ No newline at end of file +} diff --git a/src/vs/base/common/codicons.ts b/src/vs/base/common/codicons.ts index 9c7c88cd86..5b8d56a031 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,8 +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/comparers.ts b/src/vs/base/common/comparers.ts index 367eb79508..690da82c24 100644 --- a/src/vs/base/common/comparers.ts +++ b/src/vs/base/common/comparers.ts @@ -6,7 +6,12 @@ import { sep } from 'vs/base/common/path'; import { IdleValue } from 'vs/base/common/async'; -const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { +// When comparing large numbers of strings, such as in sorting large arrays, is better for +// performance to create an Intl.Collator object and use the function provided by its compare +// property than it is to use String.prototype.localeCompare() + +// A collator with numeric sorting enabled, and no sensitivity to case or to accents +const intlFileNameCollatorBaseNumeric: IdleValue<{ collator: Intl.Collator, collatorIsNumeric: boolean }> = new IdleValue(() => { const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); return { collator: collator, @@ -14,20 +19,64 @@ const intlFileNameCollator: IdleValue<{ collator: Intl.Collator, collatorIsNumer }; }); +// A collator with numeric sorting enabled. +const intlFileNameCollatorNumeric: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: true }); + return { + collator: collator + }; +}); + +// A collator with numeric sorting enabled, and sensitivity to accents and diacritics but not case. +const intlFileNameCollatorNumericCaseInsenstive: IdleValue<{ collator: Intl.Collator }> = new IdleValue(() => { + const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'accent' }); + return { + collator: collator + }; +}); + export function compareFileNames(one: string | null, other: string | null, caseSensitive = false): number { const a = one || ''; const b = other || ''; - const result = intlFileNameCollator.getValue().collator.compare(a, b); + const result = intlFileNameCollatorBaseNumeric.value.collator.compare(a, b); // Using the numeric option in the collator will // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && a !== b) { + if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && a !== b) { return a < b ? -1 : 1; } return result; } +/** Compares filenames by name then extension, sorting numbers numerically instead of alphabetically. */ +export function compareFileNamesNumeric(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one, true); + const [otherName, otherExtension] = extractNameAndExtension(other, true); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; + let result; + + // Check for name differences, comparing numbers numerically instead of alphabetically. + result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); + if (result !== 0) { + return result; + } + + // Check for case insensitive extension differences, comparing numbers numerically instead of alphabetically. + result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Disambiguate the extension case if needed. + if (oneExtension !== otherExtension) { + return collatorNumeric.compare(oneExtension, otherExtension); + } + + return 0; +} + const FileNameMatch = /^(.*?)(\.([^.]*))?$/; export function noIntlCompareFileNames(one: string | null, other: string | null, caseSensitive = false): number { @@ -54,19 +103,19 @@ export function compareFileExtensions(one: string | null, other: string | null): const [oneName, oneExtension] = extractNameAndExtension(one); const [otherName, otherExtension] = extractNameAndExtension(other); - let result = intlFileNameCollator.getValue().collator.compare(oneExtension, otherExtension); + let result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneExtension, otherExtension); if (result === 0) { // Using the numeric option in the collator will // make compare(`foo1`, `foo01`) === 0. We must disambiguate. - if (intlFileNameCollator.getValue().collatorIsNumeric && oneExtension !== otherExtension) { + if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && oneExtension !== otherExtension) { return oneExtension < otherExtension ? -1 : 1; } // Extensions are equal, compare filenames - result = intlFileNameCollator.getValue().collator.compare(oneName, otherName); + result = intlFileNameCollatorBaseNumeric.value.collator.compare(oneName, otherName); - if (intlFileNameCollator.getValue().collatorIsNumeric && result === 0 && oneName !== otherName) { + if (intlFileNameCollatorBaseNumeric.value.collatorIsNumeric && result === 0 && oneName !== otherName) { return oneName < otherName ? -1 : 1; } } @@ -74,10 +123,63 @@ export function compareFileExtensions(one: string | null, other: string | null): return result; } -function extractNameAndExtension(str?: string | null): [string, string] { +/** Compares filenames by extenson, then by name. Sorts numbers numerically, not alphabetically. */ +export function compareFileExtensionsNumeric(one: string | null, other: string | null): number { + const [oneName, oneExtension] = extractNameAndExtension(one, true); + const [otherName, otherExtension] = extractNameAndExtension(other, true); + const collatorNumeric = intlFileNameCollatorNumeric.value.collator; + const collatorNumericCaseInsensitive = intlFileNameCollatorNumericCaseInsenstive.value.collator; + let result; + + // Check for extension differences, ignoring differences in case and comparing numbers numerically. + result = compareAndDisambiguateByLength(collatorNumericCaseInsensitive, oneExtension, otherExtension); + if (result !== 0) { + return result; + } + + // Compare names. + result = compareAndDisambiguateByLength(collatorNumeric, oneName, otherName); + if (result !== 0) { + return result; + } + + // Disambiguate extension case if needed. + if (oneExtension !== otherExtension) { + return collatorNumeric.compare(oneExtension, otherExtension); + } + + return 0; +} + +/** Extracts the name and extension from a full filename, with optional special handling for dotfiles */ +function extractNameAndExtension(str?: string | null, dotfilesAsNames = false): [string, string] { const match = str ? FileNameMatch.exec(str) as Array : ([] as Array); - return [(match && match[1]) || '', (match && match[3]) || '']; + let result: [string, string] = [(match && match[1]) || '', (match && match[3]) || '']; + + // if the dotfilesAsNames option is selected, treat an empty filename with an extension, + // or a filename that starts with a dot, as a dotfile name + if (dotfilesAsNames && (!result[0] && result[1] || result[0] && result[0].charAt(0) === '.')) { + result = [result[0] + '.' + result[1], '']; + } + + return result; +} + +function compareAndDisambiguateByLength(collator: Intl.Collator, one: string, other: string) { + // Check for differences + let result = collator.compare(one, other); + if (result !== 0) { + return result; + } + + // In a numeric comparison, `foo1` and `foo01` will compare as equivalent. + // Disambiguate by sorting the shorter string first. + if (one.length !== other.length) { + return one.length < other.length ? -1 : 1; + } + + return 0; } function comparePathComponents(one: string, other: string, caseSensitive = false): number { diff --git a/src/vs/base/common/event.ts b/src/vs/base/common/event.ts index 0c97345d08..1fb56b5efe 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 f4da5c246e..0543114117 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 53a8fca091..d2c8abd615 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 586292a7f1..db8d580fb7 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/iterator.ts b/src/vs/base/common/iterator.ts index e1c2de039d..3015e91c2e 100644 --- a/src/vs/base/common/iterator.ts +++ b/src/vs/base/common/iterator.ts @@ -5,6 +5,10 @@ export namespace Iterable { + export function is(thing: any): thing is IterableIterator { + return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; + } + const _empty: Iterable = Object.freeze([]); export function empty(): Iterable { return _empty; diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index e63d1f2b75..2d47df6c1b 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { once } from 'vs/base/common/functional'; +import { Iterable } from 'vs/base/common/iterator'; /** * Enables logging of potentially leaked disposables. @@ -54,26 +55,26 @@ export function isDisposable(thing: E): thing is E & IDisposab export function dispose(disposable: T): T; export function dispose(disposable: T | undefined): T | undefined; +export function dispose = IterableIterator>(disposables: IterableIterator): A; export function dispose(disposables: Array): Array; export function dispose(disposables: ReadonlyArray): ReadonlyArray; -export function dispose(disposables: T | T[] | undefined): T | T[] | undefined { - if (Array.isArray(disposables)) { - disposables.forEach(d => { +export function dispose(arg: T | IterableIterator | undefined): any { + if (Iterable.is(arg)) { + for (let d of arg) { if (d) { markTracked(d); d.dispose(); } - }); - return []; - } else if (disposables) { - markTracked(disposables); - disposables.dispose(); - return disposables; - } else { - return undefined; + } + return arg; + } else if (arg) { + markTracked(arg); + arg.dispose(); + return arg; } } + export function combinedDisposable(...disposables: IDisposable[]): IDisposable { disposables.forEach(markTracked); return trackDisposable({ dispose: () => dispose(disposables) }); diff --git a/src/vs/base/common/map.ts b/src/vs/base/common/map.ts index eb56c470b0..f862ecaa76 100644 --- a/src/vs/base/common/map.ts +++ b/src/vs/base/common/map.ts @@ -206,7 +206,7 @@ export class UriIterator implements IKeyIterator { cmp(a: string): number { if (this._states[this._stateIdx] === UriIteratorState.Scheme) { - return compareSubstringIgnoreCase(a, this._value.scheme); + return compare(a, this._value.scheme); } else if (this._states[this._stateIdx] === UriIteratorState.Authority) { return compareSubstringIgnoreCase(a, this._value.authority); } else if (this._states[this._stateIdx] === UriIteratorState.Path) { @@ -486,11 +486,9 @@ export class ResourceMap implements Map { readonly [Symbol.toStringTag] = 'ResourceMap'; protected readonly map: Map; - protected readonly ignoreCase?: boolean; - constructor() { - this.map = new Map(); - this.ignoreCase = false; // in the future this should be an uri-comparator + constructor(other?: ResourceMap) { + this.map = other ? new Map(other.map) : new Map(); } set(resource: URI, value: T): this { @@ -550,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(); } } @@ -580,18 +565,23 @@ export const enum Touch { AsNew = 2 } -export class LinkedMap { +export class LinkedMap implements Map { + + readonly [Symbol.toStringTag] = 'LinkedMap'; private _map: Map>; private _head: Item | undefined; private _tail: Item | undefined; private _size: number; + private _state: number; + constructor() { this._map = new Map>(); this._head = undefined; this._tail = undefined; this._size = 0; + this._state = 0; } clear(): void { @@ -599,6 +589,7 @@ export class LinkedMap { this._head = undefined; this._tail = undefined; this._size = 0; + this._state++; } isEmpty(): boolean { @@ -632,7 +623,7 @@ export class LinkedMap { return item.value; } - set(key: K, value: V, touch: Touch = Touch.None): void { + set(key: K, value: V, touch: Touch = Touch.None): this { let item = this._map.get(key); if (item) { item.value = value; @@ -658,6 +649,7 @@ export class LinkedMap { this._map.set(key, item); this._size++; } + return this; } delete(key: K): boolean { @@ -690,6 +682,7 @@ export class LinkedMap { } forEach(callbackfn: (value: V, key: K, map: LinkedMap) => void, thisArg?: any): void { + const state = this._state; let current = this._head; while (current) { if (thisArg) { @@ -697,38 +690,25 @@ export class LinkedMap { } else { callbackfn(current.value, current.key, this); } + if (this._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } current = current.next; } } - values(): V[] { - const result: V[] = []; - let current = this._head; - while (current) { - result.push(current.value); - current = current.next; - } - return result; - } - - keys(): K[] { - const result: K[] = []; - let current = this._head; - while (current) { - result.push(current.key); - current = current.next; - } - return result; - } - - /* VS Code / Monaco editor runs on es5 which has no Symbol.iterator keys(): IterableIterator { - const current = this._head; + const map = this; + const state = this._state; + let current = this._head; const iterator: IterableIterator = { [Symbol.iterator]() { return iterator; }, - next():IteratorResult { + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } if (current) { const result = { value: current.key, done: false }; current = current.next; @@ -742,12 +722,17 @@ export class LinkedMap { } values(): IterableIterator { - const current = this._head; + const map = this; + const state = this._state; + let current = this._head; const iterator: IterableIterator = { [Symbol.iterator]() { return iterator; }, - next():IteratorResult { + next(): IteratorResult { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } if (current) { const result = { value: current.value, done: false }; current = current.next; @@ -759,7 +744,34 @@ export class LinkedMap { }; return iterator; } - */ + + entries(): IterableIterator<[K, V]> { + const map = this; + const state = this._state; + let current = this._head; + const iterator: IterableIterator<[K, V]> = { + [Symbol.iterator]() { + return iterator; + }, + next(): IteratorResult<[K, V]> { + if (map._state !== state) { + throw new Error(`LinkedMap got modified during iteration.`); + } + if (current) { + const result: IteratorResult<[K, V]> = { value: [current.key, current.value], done: false }; + current = current.next; + return result; + } else { + return { value: undefined, done: true }; + } + } + }; + return iterator; + } + + [Symbol.iterator](): IterableIterator<[K, V]> { + return this.entries(); + } protected trimOld(newSize: number) { if (newSize >= this.size) { @@ -781,6 +793,7 @@ export class LinkedMap { if (current) { current.previous = undefined; } + this._state++; } private addItemFirst(item: Item): void { @@ -794,6 +807,7 @@ export class LinkedMap { this._head.previous = item; } this._head = item; + this._state++; } private addItemLast(item: Item): void { @@ -807,6 +821,7 @@ export class LinkedMap { this._tail.next = item; } this._tail = item; + this._state++; } private removeItem(item: Item): void { @@ -843,6 +858,7 @@ export class LinkedMap { } item.next = undefined; item.previous = undefined; + this._state++; } private touch(item: Item, touch: Touch): void { @@ -879,6 +895,7 @@ export class LinkedMap { item.next = this._head; this._head.previous = item; this._head = item; + this._state++; } else if (touch === Touch.AsNew) { if (item === this._tail) { return; @@ -902,6 +919,7 @@ export class LinkedMap { item.previous = this._tail; this._tail.next = item; this._tail = item; + this._state++; } } @@ -953,17 +971,18 @@ export class LRUCache extends LinkedMap { this.checkTrim(); } - get(key: K): V | undefined { - return super.get(key, Touch.AsNew); + get(key: K, touch: Touch = Touch.AsNew): V | undefined { + return super.get(key, touch); } peek(key: K): V | undefined { return super.get(key, Touch.None); } - set(key: K, value: V): void { + set(key: K, value: V): this { super.set(key, value, Touch.AsNew); this.checkTrim(); + return this; } private checkTrim() { diff --git a/src/vs/base/common/mime.ts b/src/vs/base/common/mime.ts index 14065f4401..03f3019ac9 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 93eeff2ef3..23dfb36986 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 33117bfa71..e1f944c21a 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -6,7 +6,7 @@ import * as extpath from 'vs/base/common/extpath'; import * as paths from 'vs/base/common/path'; import { URI, uriToFsPath } from 'vs/base/common/uri'; -import { equalsIgnoreCase } from 'vs/base/common/strings'; +import { equalsIgnoreCase, compare as strCompare, compareIgnoreCase } from 'vs/base/common/strings'; import { Schemas } from 'vs/base/common/network'; import { isLinux, isWindows } from 'vs/base/common/platform'; import { CharCode } from 'vs/base/common/charCode'; @@ -17,248 +17,363 @@ export function originalFSPath(uri: URI): string { return uriToFsPath(uri, true); } -/** - * Creates a key from a resource URI to be used to resource comparison and for resource maps. - * URI queries are included, fragments are ignored. - */ -export function getComparisonKey(resource: URI, caseInsensitivePath = hasToIgnoreCase(resource)): string { - let path = resource.path || '/'; - if (caseInsensitivePath) { - path = path.toLowerCase(); - } - return resource.with({ authority: resource.authority.toLowerCase(), path: path, fragment: null }).toString(); +//#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; } -export 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; -} +export class ExtUri implements IExtUri { -export function basenameOrAuthority(resource: URI): string { - return basename(resource) || resource.authority; -} + constructor(private _ignorePathCasing: (uri: URI) => boolean) { } -/** - * Tests whether a `candidate` URI is a parent or equal of a given `base` URI. - * URI queries must match, fragments are ignored. - * @param base A uri which is "longer" - * @param parentCandidate A uri which is "shorter" then `base` - */ -export function isEqualOrParent(base: URI, parentCandidate: URI, ignoreCase = hasToIgnoreCase(base)): boolean { - if (base.scheme === parentCandidate.scheme) { - if (base.scheme === Schemas.file) { - return extpath.isEqualOrParent(originalFSPath(base), originalFSPath(parentCandidate), ignoreCase) && base.query === parentCandidate.query; - } - if (isEqualAuthority(base.authority, parentCandidate.authority)) { - return extpath.isEqualOrParent(base.path || '/', parentCandidate.path || '/', ignoreCase, '/') && base.query === parentCandidate.query; - } - } - return false; -} - -/** - * Tests whether the two authorities are the same - */ -export function isEqualAuthority(a1: string, a2: string) { - return a1 === a2 || equalsIgnoreCase(a1, a2); -} - -/** - * Tests whether two resources are the same. URI queries must match, fragments are ignored unless requested. - */ -export function isEqual(first: URI | undefined, second: URI | undefined, caseInsensitivePath = hasToIgnoreCase(first), ignoreFragment = true): 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); -} - -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 - } - } - 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; + compare(uri1: URI, uri2: URI, ignoreFragment: boolean = false): number { + // scheme + let ret = strCompare(uri1.scheme, uri2.scheme); + if (ret === 0) { + // authority + ret = compareIgnoreCase(uri1.authority, uri2.authority); + if (ret === 0) { + // 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); + } } } } - fromPath = toPath.substr(0, i) + fromPath.substr(i); + return ret; + } + + 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 (!uri1 || !uri2) { + return false; + } + if (uri1.scheme !== uri2.scheme || !isEqualAuthority(uri1.authority, uri2.authority)) { + return false; + } + 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); + } + + 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); + } + } + return false; + } + + // --- 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 + }); + } + + 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 + } + } + + 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; } - 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 + * 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 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 - }); - } - 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) - }); -} +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[] = []; diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index e0f93e78b8..9b48f33401 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 0000000000..b72cb49ec3 --- /dev/null +++ b/src/vs/base/common/skipList.ts @@ -0,0 +1,203 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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/common/strings.ts b/src/vs/base/common/strings.ts index e3d30cd1ac..2bc6957841 100644 --- a/src/vs/base/common/strings.ts +++ b/src/vs/base/common/strings.ts @@ -728,6 +728,14 @@ export function isBasicASCII(str: string): boolean { return IS_BASIC_ASCII.test(str); } +export const UNUSUAL_LINE_TERMINATORS = /[\u2028\u2029\u0085]/; // LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL) +/** + * Returns true if `str` contains unusual line terminators, like LS, PS or NEL + */ +export function containsUnusualLineTerminators(str: string): boolean { + return UNUSUAL_LINE_TERMINATORS.test(str); +} + export function containsFullWidthCharacter(str: string): boolean { for (let i = 0, len = str.length; i < len; i++) { if (isFullWidthCharacter(str.charCodeAt(i))) { 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 eca7a8bee3..ab14d788a6 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 Source EULA. 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 9446ecd994..d152801fd6 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 ad98ceb8d1..5a37a46ca9 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 09e271f9bc..580b7d0993 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'; @@ -27,7 +27,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; @@ -40,8 +40,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 6fbb7e1dcc..ac38fa4af7 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 69678de891..0000000000 --- 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 Source EULA. 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 1109070d6b..0ec100d418 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 da1be68828..a10ca84d48 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 0000000000..9a004cb440 --- /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 Source EULA. 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 0000000000..47abbf0d02 --- /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 Source EULA. 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 0000000000..812e97e3a1 --- /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 Source EULA. 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 0000000000..3259e66e8c --- /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 Source EULA. 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/browser/comparers.test.ts b/src/vs/base/test/browser/comparers.test.ts index 0ae54df942..44e22ebc30 100644 --- a/src/vs/base/test/browser/comparers.test.ts +++ b/src/vs/base/test/browser/comparers.test.ts @@ -3,48 +3,294 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareFileNames, compareFileExtensions } from 'vs/base/common/comparers'; +import { compareFileNames, compareFileExtensions, compareFileNamesNumeric, compareFileExtensionsNumeric } from 'vs/base/common/comparers'; import * as assert from 'assert'; +const compareLocale = (a: string, b: string) => a.localeCompare(b); +const compareLocaleNumeric = (a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }); + + suite('Comparers', () => { test('compareFileNames', () => { + // + // Comparisons with the same results as compareFileNamesNumeric + // + + // name-only comparisons assert(compareFileNames(null, null) === 0, 'null should be equal'); assert(compareFileNames(null, 'abc') < 0, 'null should be come before real values'); assert(compareFileNames('', '') === 0, 'empty should be equal'); assert(compareFileNames('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileNames('.abc', '.abc') === 0, 'equal full names should be equal'); - assert(compareFileNames('.env', '.env.example') < 0, 'filenames with extensions should come after those without'); - assert(compareFileNames('.env.example', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNames('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNames('Z', 'a') > 0, 'Z comes after a regardless of case'); + + // name plus extension comparisons + assert(compareFileNames('bbb.aaa', 'aaa.bbb') > 0, 'files with extensions are compared first by filename'); + + // dotfile comparisons + assert(compareFileNames('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNames('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNames('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNames('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNames('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNames(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNames('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNames('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNames('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNames('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons assert(compareFileNames('1', '1') === 0, 'numerically equal full names should be equal'); assert(compareFileNames('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); assert(compareFileNames('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); assert(compareFileNames('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNames('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNames('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + + // + // Comparisons with different results than compareFileNamesNumeric + // + + // name-only comparisons + assert(compareFileNames('a', 'A') !== compareLocale('a', 'A'), 'the same letter does not sort by locale'); + assert(compareFileNames('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter does not sort by locale'); + assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNames), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); + assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNames), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents do not sort in locale order'); + + // name plus extension comparisons + assert(compareFileNames('aggregate.go', 'aggregate_repo.go') > 0, 'compares the whole name all at once by locale'); + + // numeric comparisons + assert(compareFileNames('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileNames('abc.txt1', 'abc.txt01') > 0, 'same name plus extensions with equal numbers sort in unicode order'); + assert(compareFileNames('art01', 'Art01') !== 'art01'.localeCompare('Art01', undefined, { numeric: true }), + 'a numerically equivalent word of a different case does not compare numerically based on locale'); + }); test('compareFileExtensions', () => { + // + // Comparisons with the same results as compareFileExtensionsNumeric + // + + // name-only comparisons assert(compareFileExtensions(null, null) === 0, 'null should be equal'); - assert(compareFileExtensions(null, '.abc') < 0, 'null should come before real files'); assert(compareFileExtensions(null, 'abc') < 0, 'null should come before real files without extension'); assert(compareFileExtensions('', '') === 0, 'empty should be equal'); assert(compareFileExtensions('abc', 'abc') === 0, 'equal names should be equal'); - assert(compareFileExtensions('.abc', '.abc') === 0, 'equal full names should be equal'); + assert(compareFileExtensions('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensions('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons assert(compareFileExtensions('file.ext', 'file.ext') === 0, 'equal full names should be equal'); assert(compareFileExtensions('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); - assert(compareFileExtensions('.ext', 'a.ext') < 0, 'if equal extensions, filenames should be compared, empty filename should come before others'); - assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files should be compared by extensions'); + assert(compareFileExtensions('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); assert(compareFileExtensions('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extensions even if filenames compare differently'); + assert(compareFileExtensions('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensions('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensions('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + + // dotfile comparisons + assert(compareFileExtensions('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensions('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensions(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensions('.env', 'aaa.env') < 0, 'if equal extensions, filenames should be compared, empty filename should come before others'); + assert(compareFileExtensions('.MD', 'a.md') < 0, 'if extensions differ in case, files sort by extension in unicode order'); + + // numeric comparisons assert(compareFileExtensions('1', '1') === 0, 'numerically equal full names should be equal'); assert(compareFileExtensions('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); assert(compareFileExtensions('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensions('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensions('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensions('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); assert(compareFileExtensions('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); assert(compareFileExtensions('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); assert(compareFileExtensions('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); - assert(compareFileExtensions('file2.ext2', 'file1.ext10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); - assert(compareFileExtensions('file.ext01', 'file.ext1') < 0, 'extensions with equal numbers should be in alphabetical order'); + assert(compareFileExtensions('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + + // Same extension comparison that has the same result as compareFileExtensionsNumeric, but a different result than compareFileNames + // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. + assert(compareFileExtensions('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); + + // + // Comparisons with different results from compareFileExtensionsNumeric + // + + // name-only comparisions + assert(compareFileExtensions('a', 'A') !== compareLocale('a', 'A'), 'the same letter of different case does not sort by locale'); + assert(compareFileExtensions('â', 'Â') !== compareLocale('â', 'Â'), 'the same accented letter of different case does not sort by locale'); + assert.notDeepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensions), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases do not sort in locale order'); + assert.notDeepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensions), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents do not sort in locale order'); + + // name plus extension comparisons + assert(compareFileExtensions('a.MD', 'a.md') !== compareLocale('MD', 'md'), 'case differences in extensions do not sort by locale'); + assert(compareFileExtensions('a.md', 'A.md') !== compareLocale('a', 'A'), 'case differences in names do not sort by locale'); + + // dotfile comparisons + assert(compareFileExtensions('.env', '.aaa.env') < 0, 'a dotfile with an extension is treated as a name plus an extension - equal extensions'); + assert(compareFileExtensions('.env', '.env.aaa') > 0, 'a dotfile with an extension is treated as a name plus an extension - unequal extensions'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensions('.env', 'aaa') > 0, 'filenames without extensions come before dotfiles'); + assert(compareFileExtensions('.md', 'A.MD') > 0, 'a file with an uppercase extension sorts before a dotfile of the same lowercase extension'); + + // numeric comparisons + assert(compareFileExtensions('abc.txt01', 'abc.txt1') < 0, 'extensions with equal numbers sort in unicode order'); + assert(compareFileExtensions('art01', 'Art01') !== compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case does not compare by locale'); + assert(compareFileExtensions('abc02.txt', 'abc002.txt') > 0, 'filenames with equivalent numbers and leading zeros sort in unicode order'); + assert(compareFileExtensions('txt.abc01', 'txt.abc1') < 0, 'extensions with equivalent numbers sort in unicode order'); + + }); + + test('compareFileNamesNumeric', () => { + + // + // Comparisons with the same results as compareFileNames + // + + // name-only comparisons + assert(compareFileNamesNumeric(null, null) === 0, 'null should be equal'); + assert(compareFileNamesNumeric(null, 'abc') < 0, 'null should be come before real values'); + assert(compareFileNamesNumeric('', '') === 0, 'empty should be equal'); + assert(compareFileNamesNumeric('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileNamesNumeric('z', 'A') > 0, 'z comes is after A regardless of case'); + assert(compareFileNamesNumeric('Z', 'a') > 0, 'Z comes after a regardless of case'); + + // name plus extension comparisons + assert(compareFileNamesNumeric('file.ext', 'file.ext') === 0, 'equal full names should be equal'); + assert(compareFileNamesNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileNamesNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileNamesNumeric('bbb.aaa', 'aaa.bbb') > 0, 'files should be compared by names even if extensions compare differently'); + + // dotfile comparisons + assert(compareFileNamesNumeric('.abc', '.abc') === 0, 'equal dotfile names should be equal'); + assert(compareFileNamesNumeric('.env.', '.gitattributes') < 0, 'filenames starting with dots and with extensions should still sort properly'); + assert(compareFileNamesNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileNamesNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + assert(compareFileNamesNumeric('.aaa_env', '.aaa.env') < 0, 'and underscore in a dotfile name will sort before a dot'); + + // dotfile vs non-dotfile comparisons + assert(compareFileNamesNumeric(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileNamesNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileNamesNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileNamesNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + assert(compareFileNamesNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileNamesNumeric('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileNamesNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileNamesNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileNamesNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileNamesNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileNamesNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + + // + // Comparisons with different results than compareFileNames + // + + // name-only comparisons + assert(compareFileNamesNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter sorts by locale'); + assert(compareFileNamesNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileNamesNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileNamesNumeric), ['email', 'Email', 'émail', 'Émail'].sort(compareLocale), 'the same base characters with different case or accents sort in locale order'); + + // name plus extensions comparisons + assert(compareFileNamesNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'compares the name first, then the extension'); + + // numeric comparisons + assert(compareFileNamesNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest number first'); + assert(compareFileNamesNumeric('abc.txt1', 'abc.txt01') < 0, 'same name plus extensions with equal numbers sort shortest number first'); + assert(compareFileNamesNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + + }); + + test('compareFileExtensionsNumeric', () => { + + // + // Comparisons with the same result as compareFileExtensions + // + + // name-only comparisons + assert(compareFileExtensionsNumeric(null, null) === 0, 'null should be equal'); + assert(compareFileExtensionsNumeric(null, 'abc') < 0, 'null should come before real files without extensions'); + assert(compareFileExtensionsNumeric('', '') === 0, 'empty should be equal'); + assert(compareFileExtensionsNumeric('abc', 'abc') === 0, 'equal names should be equal'); + assert(compareFileExtensionsNumeric('z', 'A') > 0, 'z comes after A'); + assert(compareFileExtensionsNumeric('Z', 'a') > 0, 'Z comes after a'); + + // name plus extension comparisons + assert(compareFileExtensionsNumeric('file.ext', 'file.ext') === 0, 'equal full filenames should be equal'); + assert(compareFileExtensionsNumeric('a.ext', 'b.ext') < 0, 'if equal extensions, filenames should be compared'); + assert(compareFileExtensionsNumeric('file.aaa', 'file.bbb') < 0, 'files with equal names should be compared by extensions'); + assert(compareFileExtensionsNumeric('bbb.aaa', 'aaa.bbb') < 0, 'files should be compared by extension first'); + assert(compareFileExtensionsNumeric('agg.go', 'aggrepo.go') < 0, 'shorter names sort before longer names'); + assert(compareFileExtensionsNumeric('agg.go', 'agg_repo.go') < 0, 'shorter names short before longer names even when the longer name contains an underscore'); + assert(compareFileExtensionsNumeric('a.MD', 'b.md') < 0, 'when extensions are the same except for case, the files sort by name'); + + // dotfile comparisons + assert(compareFileExtensionsNumeric('.abc', '.abc') === 0, 'equal dotfiles should be equal'); + assert(compareFileExtensionsNumeric('.md', '.Gitattributes') > 0, 'dotfiles sort alphabetically regardless of case'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsNumeric(null, '.abc') < 0, 'null should come before dotfiles'); + assert(compareFileExtensionsNumeric('.env', 'aaa.env') < 0, 'dotfiles come before filenames with extensions'); + assert(compareFileExtensionsNumeric('.MD', 'a.md') < 0, 'dotfiles sort before lowercase files'); + + // numeric comparisons + assert(compareFileExtensionsNumeric('1', '1') === 0, 'numerically equal full names should be equal'); + assert(compareFileExtensionsNumeric('abc1.txt', 'abc1.txt') === 0, 'equal filenames with numbers should be equal'); + assert(compareFileExtensionsNumeric('abc1.txt', 'abc2.txt') < 0, 'filenames with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsNumeric('abc2.txt', 'abc10.txt') < 0, 'filenames with numbers should be in numerical order'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc010.txt') < 0, 'filenames with numbers that have leading zeros sort numerically'); + assert(compareFileExtensionsNumeric('abc1.10.txt', 'abc1.2.txt') > 0, 'numbers with dots between them are treated as two separate numbers, not one decimal number'); + assert(compareFileExtensionsNumeric('abc2.txt2', 'abc1.txt10') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc1') === 0, 'equal extensions with numbers should be equal'); + assert(compareFileExtensionsNumeric('txt.abc1', 'txt.abc2') < 0, 'extensions with numbers should be in numerical order, not alphabetical order'); + assert(compareFileExtensionsNumeric('txt.abc2', 'txt.abc10') < 0, 'extensions with numbers should be in numerical order even when they are multiple digits long'); + assert(compareFileExtensionsNumeric('a.ext1', 'b.ext1') < 0, 'if equal extensions with numbers, filenames should be compared'); + assert(compareFileExtensionsNumeric('a10.txt', 'A2.txt') > 0, 'filenames with number and case differences compare numerically'); + + // Same extension comparison that has the same result as compareFileExtensions, but a different result than compareFileNames + // This is an edge case caused by compareFileNames comparing the whole name all at once instead of the name and then the extension. + assert(compareFileExtensionsNumeric('aggregate.go', 'aggregate_repo.go') < 0, 'when extensions are equal, names sort in dictionary order'); + + // + // Comparisons with different results than compareFileExtensions + // + + // name-only comparisons + assert(compareFileExtensionsNumeric('a', 'A') === compareLocale('a', 'A'), 'the same letter of different case sorts by locale'); + assert(compareFileExtensionsNumeric('â', 'Â') === compareLocale('â', 'Â'), 'the same accented letter of different case sorts by locale'); + assert.deepEqual(['artichoke', 'Artichoke', 'art', 'Art'].sort(compareFileExtensionsNumeric), ['artichoke', 'Artichoke', 'art', 'Art'].sort(compareLocale), 'words with the same root and different cases sort in locale order'); + assert.deepEqual(['email', 'Email', 'émail', 'Émail'].sort(compareFileExtensionsNumeric), ['email', 'Email', 'émail', 'Émail'].sort((a, b) => a.localeCompare(b)), 'the same base characters with different case or accents sort in locale order'); + + // name plus extension comparisons + assert(compareFileExtensionsNumeric('a.MD', 'a.md') === compareLocale('MD', 'md'), 'case differences in extensions sort by locale'); + assert(compareFileExtensionsNumeric('a.md', 'A.md') === compareLocale('a', 'A'), 'case differences in names sort by locale'); + + // dotfile comparisons + assert(compareFileExtensionsNumeric('.env', '.aaa.env') > 0, 'dotfiles sort alphabetically when they contain multiple dots'); + assert(compareFileExtensionsNumeric('.env', '.env.aaa') < 0, 'dotfiles with the same root sort shortest first'); + + // dotfile vs non-dotfile comparisons + assert(compareFileExtensionsNumeric('.env', 'aaa') < 0, 'dotfiles come before filenames without extensions'); + assert(compareFileExtensionsNumeric('.md', 'A.MD') < 0, 'dotfiles sort before uppercase files'); + + // numeric comparisons + assert(compareFileExtensionsNumeric('abc.txt01', 'abc.txt1') > 0, 'extensions with equal numbers should be in shortest-first order'); + assert(compareFileExtensionsNumeric('art01', 'Art01') === compareLocaleNumeric('art01', 'Art01'), 'a numerically equivalent word of a different case compares numerically based on locale'); + assert(compareFileExtensionsNumeric('abc02.txt', 'abc002.txt') < 0, 'filenames with equivalent numbers and leading zeros sort shortest string first'); + assert(compareFileExtensionsNumeric('txt.abc01', 'txt.abc1') > 0, 'extensions with equivalent numbers sort shortest extension first'); + }); }); diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index cfce25ade1..385e421e37 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/base/test/common/map.test.ts b/src/vs/base/test/common/map.test.ts index 3650bff666..6c3ba81fa5 100644 --- a/src/vs/base/test/common/map.test.ts +++ b/src/vs/base/test/common/map.test.ts @@ -13,8 +13,8 @@ suite('Map', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('bk', 'bv'); - assert.deepStrictEqual(map.keys(), ['ak', 'bk']); - assert.deepStrictEqual(map.values(), ['av', 'bv']); + assert.deepStrictEqual([...map.keys()], ['ak', 'bk']); + assert.deepStrictEqual([...map.values()], ['av', 'bv']); assert.equal(map.first, 'av'); assert.equal(map.last, 'bv'); }); @@ -23,16 +23,16 @@ suite('Map', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('ak', 'av', Touch.AsOld); - assert.deepStrictEqual(map.keys(), ['ak']); - assert.deepStrictEqual(map.values(), ['av']); + assert.deepStrictEqual([...map.keys()], ['ak']); + assert.deepStrictEqual([...map.values()], ['av']); }); test('LinkedMap - Touch New one', () => { let map = new LinkedMap(); map.set('ak', 'av'); map.set('ak', 'av', Touch.AsNew); - assert.deepStrictEqual(map.keys(), ['ak']); - assert.deepStrictEqual(map.values(), ['av']); + assert.deepStrictEqual([...map.keys()], ['ak']); + assert.deepStrictEqual([...map.values()], ['av']); }); test('LinkedMap - Touch Old two', () => { @@ -40,8 +40,8 @@ suite('Map', () => { map.set('ak', 'av'); map.set('bk', 'bv'); map.set('bk', 'bv', Touch.AsOld); - assert.deepStrictEqual(map.keys(), ['bk', 'ak']); - assert.deepStrictEqual(map.values(), ['bv', 'av']); + assert.deepStrictEqual([...map.keys()], ['bk', 'ak']); + assert.deepStrictEqual([...map.values()], ['bv', 'av']); }); test('LinkedMap - Touch New two', () => { @@ -49,8 +49,8 @@ suite('Map', () => { map.set('ak', 'av'); map.set('bk', 'bv'); map.set('ak', 'av', Touch.AsNew); - assert.deepStrictEqual(map.keys(), ['bk', 'ak']); - assert.deepStrictEqual(map.values(), ['bv', 'av']); + assert.deepStrictEqual([...map.keys()], ['bk', 'ak']); + assert.deepStrictEqual([...map.values()], ['bv', 'av']); }); test('LinkedMap - Touch Old from middle', () => { @@ -59,8 +59,8 @@ suite('Map', () => { map.set('bk', 'bv'); map.set('ck', 'cv'); map.set('bk', 'bv', Touch.AsOld); - assert.deepStrictEqual(map.keys(), ['bk', 'ak', 'ck']); - assert.deepStrictEqual(map.values(), ['bv', 'av', 'cv']); + assert.deepStrictEqual([...map.keys()], ['bk', 'ak', 'ck']); + assert.deepStrictEqual([...map.values()], ['bv', 'av', 'cv']); }); test('LinkedMap - Touch New from middle', () => { @@ -69,8 +69,8 @@ suite('Map', () => { map.set('bk', 'bv'); map.set('ck', 'cv'); map.set('bk', 'bv', Touch.AsNew); - assert.deepStrictEqual(map.keys(), ['ak', 'ck', 'bk']); - assert.deepStrictEqual(map.values(), ['av', 'cv', 'bv']); + assert.deepStrictEqual([...map.keys()], ['ak', 'ck', 'bk']); + assert.deepStrictEqual([...map.values()], ['av', 'cv', 'bv']); }); test('LinkedMap - basics', function () { @@ -129,6 +129,61 @@ suite('Map', () => { assert.ok(!map.has('1')); }); + test('LinkedMap - Iterators', () => { + const map = new LinkedMap(); + map.set(1, 1); + map.set(2, 2); + map.set(3, 3); + + for (const elem of map.keys()) { + assert.ok(elem); + } + + for (const elem of map.values()) { + assert.ok(elem); + } + + for (const elem of map.entries()) { + assert.ok(elem); + } + + { + const keys = map.keys(); + const values = map.values(); + const entries = map.entries(); + map.get(1); + keys.next(); + values.next(); + entries.next(); + } + + { + const keys = map.keys(); + const values = map.values(); + const entries = map.entries(); + map.get(1, Touch.AsNew); + + let exceptions: number = 0; + try { + keys.next(); + } catch (err) { + exceptions++; + } + try { + values.next(); + } catch (err) { + exceptions++; + } + try { + entries.next(); + } catch (err) { + exceptions++; + } + + assert.strictEqual(exceptions, 3); + } + }); + test('LinkedMap - LRU Cache simple', () => { const cache = new LRUCache(5); @@ -136,10 +191,10 @@ suite('Map', () => { assert.strictEqual(cache.size, 5); cache.set(6, 6); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [2, 3, 4, 5, 6]); + assert.deepStrictEqual([...cache.keys()], [2, 3, 4, 5, 6]); cache.set(7, 7); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [3, 4, 5, 6, 7]); + assert.deepStrictEqual([...cache.keys()], [3, 4, 5, 6, 7]); let values: number[] = []; [3, 4, 5, 6, 7].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [3, 4, 5, 6, 7]); @@ -150,11 +205,11 @@ suite('Map', () => { [1, 2, 3, 4, 5].forEach(value => cache.set(value, value)); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [1, 2, 3, 4, 5]); + assert.deepStrictEqual([...cache.keys()], [1, 2, 3, 4, 5]); cache.get(3); - assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]); + assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]); cache.peek(4); - assert.deepStrictEqual(cache.keys(), [1, 2, 4, 5, 3]); + assert.deepStrictEqual([...cache.keys()], [1, 2, 4, 5, 3]); let values: number[] = []; [1, 2, 3, 4, 5].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [1, 2, 3, 4, 5]); @@ -169,7 +224,7 @@ suite('Map', () => { assert.strictEqual(cache.size, 10); cache.limit = 5; assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [6, 7, 8, 9, 10]); + assert.deepStrictEqual([...cache.keys()], [6, 7, 8, 9, 10]); cache.limit = 20; assert.strictEqual(cache.size, 5); for (let i = 11; i <= 20; i++) { @@ -181,7 +236,7 @@ suite('Map', () => { values.push(cache.get(i)!); assert.strictEqual(cache.get(i), i); } - assert.deepStrictEqual(cache.values(), values); + assert.deepStrictEqual([...cache.values()], values); }); test('LinkedMap - LRU Cache limit with ratio', () => { @@ -193,11 +248,11 @@ suite('Map', () => { assert.strictEqual(cache.size, 10); cache.set(11, 11); assert.strictEqual(cache.size, 5); - assert.deepStrictEqual(cache.keys(), [7, 8, 9, 10, 11]); + assert.deepStrictEqual([...cache.keys()], [7, 8, 9, 10, 11]); let values: number[] = []; - cache.keys().forEach(key => values.push(cache.get(key)!)); + [...cache.keys()].forEach(key => values.push(cache.get(key)!)); assert.deepStrictEqual(values, [7, 8, 9, 10, 11]); - assert.deepStrictEqual(cache.values(), values); + assert.deepStrictEqual([...cache.values()], values); }); test('LinkedMap - toJSON / fromJSON', () => { @@ -222,7 +277,6 @@ suite('Map', () => { assert.equal(key, 'ck'); assert.equal(value, 'cv'); } - i++; }); }); @@ -237,7 +291,7 @@ suite('Map', () => { map.delete('1'); assert.equal(map.get('1'), undefined); assert.equal(map.size, 0); - assert.equal(map.keys().length, 0); + assert.equal([...map.keys()].length, 0); }); test('LinkedMap - delete Head', function () { @@ -251,8 +305,8 @@ suite('Map', () => { map.delete('1'); assert.equal(map.get('2'), 2); assert.equal(map.size, 1); - assert.equal(map.keys().length, 1); - assert.equal(map.keys()[0], 2); + assert.equal([...map.keys()].length, 1); + assert.equal([...map.keys()][0], 2); }); test('LinkedMap - delete Tail', function () { @@ -266,8 +320,8 @@ suite('Map', () => { map.delete('2'); assert.equal(map.get('1'), 1); assert.equal(map.size, 1); - assert.equal(map.keys().length, 1); - assert.equal(map.keys()[0], 1); + assert.equal([...map.keys()].length, 1); + assert.equal([...map.keys()][0], 1); }); @@ -317,7 +371,8 @@ suite('Map', () => { iter.reset(URI.parse('file:///usr/bin/file.txt')); assert.equal(iter.value(), 'file'); - assert.equal(iter.cmp('FILE'), 0); + // assert.equal(iter.cmp('FILE'), 0); + assert.equal(iter.cmp('file'), 0); assert.equal(iter.hasNext(), true); iter.next(); @@ -337,7 +392,8 @@ suite('Map', () => { // scheme assert.equal(iter.value(), 'file'); - assert.equal(iter.cmp('FILE'), 0); + // assert.equal(iter.cmp('FILE'), 0); + assert.equal(iter.cmp('file'), 0); assert.equal(iter.hasNext(), true); iter.next(); 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 7776bee61e..8653b8e7aa 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 Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { dirname, basename, distinctParents, joinPath, isEqual, isEqualOrParent, hasToIgnoreCase, normalizePath, isAbsolutePath, relativePath, removeTrailingPathSeparator, hasTrailingPathSeparator, resolvePath, addTrailingPathSeparator, getComparisonKey } 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, hasToIgnoreCase(expected), 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); } } @@ -254,14 +257,14 @@ suite('Resources', () => { assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/bar/goo'), 'bar/goo'); assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/foo/bar/goo'), 'foo/bar/goo'); assertRelativePath(URI.parse('foo://a/foo/xoo'), URI.parse('foo://a/foo/bar'), '../bar'); - assertRelativePath(URI.parse('foo://a/foo/xoo/yoo'), URI.parse('foo://a'), '../../..'); + assertRelativePath(URI.parse('foo://a/foo/xoo/yoo'), URI.parse('foo://a'), '../../..', true); assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo/'), ''); assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo'), ''); assertRelativePath(URI.parse('foo://a/foo/'), URI.parse('foo://a/foo/'), ''); assertRelativePath(URI.parse('foo://a/foo'), URI.parse('foo://a/foo'), ''); - assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), ''); + assertRelativePath(URI.parse('foo://a'), URI.parse('foo://a'), '', true); assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a/'), ''); - assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), ''); + assertRelativePath(URI.parse('foo://a/'), URI.parse('foo://a'), '', true); assertRelativePath(URI.parse('foo://a/foo?q'), URI.parse('foo://a/foo/bar#h'), 'bar', true); assertRelativePath(URI.parse('foo://'), URI.parse('foo://a/b'), undefined); assertRelativePath(URI.parse('foo://a2/b'), URI.parse('foo://a/b'), undefined); @@ -346,10 +349,17 @@ suite('Resources', () => { }); - function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean, expected: boolean) { - assert.equal(isEqual(u1, u2, ignoreCase), expected, `${u1.toString()}${expected ? '===' : '!=='}${u2.toString()}`); - 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()}`); + function assertIsEqual(u1: URI, u2: URI, ignoreCase: boolean | undefined, expected: boolean) { + + 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); + } } @@ -358,7 +368,7 @@ suite('Resources', () => { let fileURI2 = isWindows ? URI.file('C:\\foo\\Bar') : URI.file('/foo/Bar'); assertIsEqual(fileURI, fileURI, true, true); assertIsEqual(fileURI, fileURI, false, true); - assertIsEqual(fileURI, fileURI, hasToIgnoreCase(fileURI), true); + assertIsEqual(fileURI, fileURI, undefined, true); assertIsEqual(fileURI, fileURI2, true, true); assertIsEqual(fileURI, fileURI2, false, false); @@ -366,13 +376,15 @@ suite('Resources', () => { let fileURI4 = URI.parse('foo://server:453/foo/Bar'); assertIsEqual(fileURI3, fileURI3, true, true); assertIsEqual(fileURI3, fileURI3, false, true); - assertIsEqual(fileURI3, fileURI3, hasToIgnoreCase(fileURI3), true); + assertIsEqual(fileURI3, fileURI3, undefined, true); assertIsEqual(fileURI3, fileURI4, true, true); assertIsEqual(fileURI3, fileURI4, false, false); assertIsEqual(fileURI, fileURI3, true, false); - assertIsEqual(URI.parse('foo://server'), URI.parse('foo://server/'), true, true); + assertIsEqual(URI.parse('file://server'), URI.parse('file://server/'), true, true); + assertIsEqual(URI.parse('http://server'), URI.parse('http://server/'), true, true); + assertIsEqual(URI.parse('foo://server'), URI.parse('foo://server/'), true, false); // only selected scheme have / as the default path assertIsEqual(URI.parse('foo://server/foo'), URI.parse('foo://server/foo/'), true, false); assertIsEqual(URI.parse('foo://server/foo'), URI.parse('foo://server/foo?'), true, true); @@ -383,38 +395,39 @@ suite('Resources', () => { assertIsEqual(fileURI5, fileURI3, true, false); assertIsEqual(fileURI6, fileURI6, true, true); assertIsEqual(fileURI6, fileURI5, true, false); - assertIsEqual(fileURI6, fileURI3, true, true); + assertIsEqual(fileURI6, fileURI3, true, false); }); 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 0000000000..9e579095fd --- /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 Source EULA. 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 1645bac22b..87e1f1f78e 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`, '@angular/core': `${window.location.origin}/static/remote/web/node_modules/@angular/core/bundles/core.umd.js`, diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 3deb653f0a..c2be002f6b 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`, '@angular/core': `${window.location.origin}/static/node_modules/@angular/core/bundles/core.umd.js`, diff --git a/src/vs/code/electron-browser/issue/issueReporterMain.ts b/src/vs/code/electron-browser/issue/issueReporterMain.ts index d73e0da1c2..ab2b4c2cf7 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 Source EULA. 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 208e998a39..101b556e61 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/media/issueReporter.css b/src/vs/code/electron-browser/issue/media/issueReporter.css index 3276b242d9..78c3b1d2ca 100644 --- a/src/vs/code/electron-browser/issue/media/issueReporter.css +++ b/src/vs/code/electron-browser/issue/media/issueReporter.css @@ -95,25 +95,25 @@ textarea, input, select { } html { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; color: #CCCCCC; height: 100%; } html:lang(zh-Hans) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } html:lang(zh-Hant) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } html:lang(ja) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } html:lang(ko) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } body { 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 9a645ae18c..d25195a9be 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/media/processExplorer.css b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css index 2d6476c18d..575fcbf64d 100644 --- a/src/vs/code/electron-browser/processExplorer/media/processExplorer.css +++ b/src/vs/code/electron-browser/processExplorer/media/processExplorer.css @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ html { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif; font-size: 13px; } html:lang(zh-Hans) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } html:lang(zh-Hant) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Microsoft Jhenghei", "PingFang TC", "Source Han Sans TC", "Source Han Sans", "Source Han Sans TW", sans-serif; } html:lang(ja) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Yu Gothic UI", "Meiryo UI", "Hiragino Kaku Gothic Pro", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", "Sazanami Gothic", "IPA Gothic", sans-serif; } html:lang(ko) { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Noto Sans", "Malgun Gothic", "Nanum Gothic", "Dotom", "Apple SD Gothic Neo", "AppleGothic", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } body { diff --git a/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts b/src/vs/code/electron-browser/processExplorer/processExplorerMain.ts index 94c6974f94..95198acde6 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/proxy/auth.html b/src/vs/code/electron-browser/proxy/auth.html index 5ef195878c..f0fc7231e3 100644 --- a/src/vs/code/electron-browser/proxy/auth.html +++ b/src/vs/code/electron-browser/proxy/auth.html @@ -5,7 +5,7 @@ + content="default-src 'none'; img-src 'self' https: data:; media-src 'none'; child-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' https:; font-src 'self' https:;"> diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index a6090e54fc..f56310f1d0 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 = 'adsworkbench'; // {{ SQL CARBON EDIT }} 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-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index 4268647aab..bf03da6108 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -5,7 +5,7 @@ - + diff --git a/src/vs/code/electron-browser/workbench/workbench.js b/src/vs/code/electron-browser/workbench/workbench.js index 1db1f45ec4..6d7dbafdfc 100644 --- a/src/vs/code/electron-browser/workbench/workbench.js +++ b/src/vs/code/electron-browser/workbench/workbench.js @@ -50,6 +50,7 @@ bootstrapWindow.load([ * @param {{ * partsSplashPath?: string, * highContrast?: boolean, + * defaultThemeType?: string, * extensionDevelopmentPath?: string[], * folderUri?: object, * workspace?: object @@ -78,13 +79,27 @@ function showPartsSplash(configuration) { } // minimal color configuration (works with or without persisted data) - const baseTheme = data ? data.baseTheme : configuration.highContrast ? 'hc-black' : 'vs-dark'; - const shellBackground = data ? data.colorInfo.editorBackground : configuration.highContrast ? '#000000' : '#1E1E1E'; - const shellForeground = data ? data.colorInfo.foreground : configuration.highContrast ? '#FFFFFF' : '#CCCCCC'; + let baseTheme, shellBackground, shellForeground; + if (data) { + baseTheme = data.baseTheme; + shellBackground = data.colorInfo.editorBackground; + shellForeground = data.colorInfo.foreground; + } else if (configuration.highContrast || configuration.defaultThemeType === 'hc') { + baseTheme = 'hc-black'; + shellBackground = '#000000'; + shellForeground = '#FFFFFF'; + } else if (configuration.defaultThemeType === 'vs') { + baseTheme = 'vs'; + shellBackground = '#FFFFFF'; + shellForeground = '#000000'; + } else { + baseTheme = 'vs-dark'; + shellBackground = '#1E1E1E'; + shellForeground = '#CCCCCC'; + } const style = document.createElement('style'); style.className = 'initialShellColors'; document.head.appendChild(style); - document.body.className = baseTheme; style.innerHTML = `body { background-color: ${shellBackground}; color: ${shellForeground}; margin: 0; padding: 0; }`; if (data && data.layoutInfo) { @@ -92,6 +107,7 @@ function showPartsSplash(configuration) { const { id, layoutInfo, colorInfo } = data; const splash = document.createElement('div'); splash.id = id; + splash.className = baseTheme; if (layoutInfo.windowBorder) { splash.style.position = 'relative'; diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index daaf11cb57..4d6174d30c 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; @@ -125,7 +124,7 @@ export class CodeApplication extends Disposable { // Mac only event: open new window when we get activated if (!hasVisibleWindows && this.windowsMainService) { - this.windowsMainService.openEmptyWindow(OpenContext.DOCK); + this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }); } }); @@ -258,7 +257,7 @@ export class CodeApplication extends Disposable { app.on('new-window-for-tab', () => { if (this.windowsMainService) { - this.windowsMainService.openEmptyWindow(OpenContext.DESKTOP); //macOS native tab "+" button + this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP }); //macOS native tab "+" button } }); @@ -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 0795c701e0..625e135390 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 be54b9bf0c..b7baef7e21 100644 --- a/src/vs/code/electron-main/main.ts +++ b/src/vs/code/electron-main/main.ts @@ -5,7 +5,7 @@ import 'vs/platform/update/common/update.config.contribution'; import { app, dialog } from 'electron'; -import { isWindows, IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; +import { isWindows, IProcessEnvironment } from 'vs/base/common/platform'; import product from 'vs/platform/product/common/product'; import { parseMainProcessArgv, addArg } from 'vs/platform/environment/node/argvHelper'; import { createWaitMarkerFile } from 'vs/platform/environment/node/waitMarkerFile'; @@ -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'; @@ -226,11 +226,6 @@ class CodeMain { throw error; } - // Since we are the second instance, we do not want to show the dock - if (isMacintosh) { - app.dock.hide(); - } - // there's a running instance, let's connect to it let client: Client; try { @@ -330,11 +325,6 @@ class CodeMain { throw new ExpectedError('Terminating...'); } - // dock might be hidden at this case due to a retry - if (isMacintosh) { - app.dock.show(); - } - // Set the VSCODE_PID variable here when we are sure we are the first // instance to startup. Otherwise we would wrongly overwrite the PID process.env['VSCODE_PID'] = String(process.pid); diff --git a/src/vs/code/electron-main/sharedProcess.ts b/src/vs/code/electron-main/sharedProcess.ts index 59df84ce4b..bb4d02390e 100644 --- a/src/vs/code/electron-main/sharedProcess.ts +++ b/src/vs/code/electron-main/sharedProcess.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. 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 83bb2cfe75..bf01c63f4a 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 2428e9deb4..bbb56c517d 100644 --- a/src/vs/code/node/cliProcessMain.ts +++ b/src/vs/code/node/cliProcessMain.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from 'vs/nls'; +import { raceTimeout } from 'vs/base/common/async'; import product from 'vs/platform/product/common/product'; import * as path from 'vs/base/common/path'; import * as semver from 'semver-umd'; @@ -85,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']) { @@ -125,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) { @@ -134,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); } @@ -149,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); @@ -157,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 => { @@ -204,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; })); } @@ -226,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)) { @@ -361,8 +362,10 @@ export async function main(argv: ParsedArgs): Promise { try { await main.run(argv); + // Flush the remaining data in AI adapter. - await combinedAppender(...appenders).flush(); + // If it does not complete in 1 second, exit the process. + await raceTimeout(combinedAppender(...appenders).flush(), 1000); } finally { disposables.dispose(); } diff --git a/src/vs/code/node/paths.ts b/src/vs/code/node/paths.ts index 10eefb07d9..c54ef2f496 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 ea00a071b5..e5e31e6817 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'; @@ -26,20 +26,21 @@ import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; 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) { + const viewModel = editor._getViewModel(); + if (!viewModel) { // the editor has no view => has no cursors return; } - this.runCoreEditorCommand(cursors, args || {}); + this.runCoreEditorCommand(viewModel, args || {}); } - public abstract runCoreEditorCommand(cursors: ICursors, args: any): void; + public abstract runCoreEditorCommand(viewModel: IViewModel, args: any): void; } export namespace EditorScroll_ { @@ -286,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); } } @@ -312,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; } @@ -338,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.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); } }); @@ -364,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); } }); @@ -383,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); } }); @@ -397,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); } } @@ -435,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); } } @@ -473,23 +478,49 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { const parsed = CursorMove_.parse(args); if (!parsed) { // illegal arguments return; } - this._runCursorMove(cursors, args.source, parsed); + this._runCursorMove(viewModel, args.source, parsed); } - _runCursorMove(cursors: ICursors, source: string, 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, - CursorMoveCommands.move(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(viewModel: IViewModel, cursors: CursorState[], args: CursorMove_.ParsedArguments): PartialCursorState[] | null { + const inSelectionMode = args.select; + const value = args.value; + + switch (args.direction) { + case CursorMove_.Direction.Left: + case CursorMove_.Direction.Right: + case CursorMove_.Direction.Up: + case CursorMove_.Direction.Down: + case CursorMove_.Direction.WrappedLineStart: + case CursorMove_.Direction.WrappedLineFirstNonWhitespaceCharacter: + case CursorMove_.Direction.WrappedLineColumnCenter: + case CursorMove_.Direction.WrappedLineEnd: + case CursorMove_.Direction.WrappedLineLastNonWhitespaceCharacter: + 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, cursors, args.direction, inSelectionMode, value); + } + + return null; } } @@ -501,14 +532,14 @@ export namespace CoreNavigationCommands { class CursorMoveBasedCommand extends CoreEditorCommand { - private readonly _staticArgs: CursorMove_.ParsedArguments; + private readonly _staticArgs: CursorMove_.SimpleMoveArguments; - constructor(opts: ICommandOptions & { args: CursorMove_.ParsedArguments }) { + constructor(opts: ICommandOptions & { args: CursorMove_.SimpleMoveArguments }) { super(opts); 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 @@ -516,10 +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 }; } - CursorMove._runCursorMove(cursors, dynamicArgs.source, args); + + viewModel.model.pushStackElement(); + viewModel.setCursorStates( + dynamicArgs.source, + CursorChangeReason.Explicit, + CursorMoveCommands.simpleMove(viewModel, viewModel.getCursorStates(), args.direction, args.select, args.value, args.unit) + ); + viewModel.revealPrimaryCursor(dynamicArgs.source, true); } } @@ -733,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) { @@ -764,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 @@ -777,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 @@ -794,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 @@ -821,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); } } @@ -865,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]; @@ -919,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); } } @@ -963,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; @@ -1018,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); } } @@ -1062,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); } } @@ -1106,39 +1140,40 @@ export namespace CoreNavigationCommands { }); } - public runCoreEditorCommand(cursors: ICursors, args: any): void { + public runCoreEditorCommand(viewModel: IViewModel, args: any): void { const parsed = EditorScroll_.parse(args); if (!parsed) { // illegal arguments return; } - this._runEditorScroll(cursors, args.source, parsed); + this._runEditorScroll(viewModel, args.source, parsed); } - _runEditorScroll(cursors: ICursors, source: string, args: EditorScroll_.ParsedArguments): void { + _runEditorScroll(viewModel: IViewModel, source: string | null | undefined, args: EditorScroll_.ParsedArguments): void { - const desiredScrollTop = this._computeDesiredScrollTop(cursors.context, args); + const desiredScrollTop = this._computeDesiredScrollTop(viewModel, args); if (args.revealCursor) { // must ensure cursor is in new visible range - const desiredVisibleViewRange = cursors.context.getCompletelyVisibleViewRangeAtScrollTop(desiredScrollTop); - cursors.setStates( + const desiredVisibleViewRange = viewModel.getCompletelyVisibleViewRangeAtScrollTop(desiredScrollTop); + viewModel.setCursorStates( source, CursorChangeReason.Explicit, [ - CursorMoveCommands.findPositionInViewportIfOutside(cursors.context, cursors.getPrimaryCursor(), desiredVisibleViewRange, args.select) + CursorMoveCommands.findPositionInViewportIfOutside(viewModel, viewModel.getPrimaryCursorState(), desiredVisibleViewRange, args.select) ] ); } - cursors.scrollTo(desiredScrollTop); + viewModel.setScrollTop(desiredScrollTop, ScrollType.Smooth); } - private _computeDesiredScrollTop(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 visibleModelRange = context.getCompletelyVisibleModelRange(); + const visibleViewRange = viewModel.getCompletelyVisibleViewRange(); + const visibleModelRange = viewModel.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); let desiredTopModelLineNumber: number; if (args.direction === EditorScroll_.Direction.Up) { @@ -1146,23 +1181,23 @@ 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); } - const desiredTopViewPosition = context.convertModelPositionToViewPosition(new Position(desiredTopModelLineNumber, 1)); - return context.getVerticalOffsetForViewLine(desiredTopViewPosition.lineNumber); + 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 context.getCurrentScrollTop() + deltaLines * context.config.lineHeight; + return viewModel.getScrollTop() + deltaLines * viewModel.cursorConfig.lineHeight; } } @@ -1182,8 +1217,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.WrappedLine, value: 1, @@ -1208,8 +1243,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Up, unit: EditorScroll_.Unit.Page, value: 1, @@ -1233,8 +1268,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.WrappedLine, value: 1, @@ -1259,8 +1294,8 @@ export namespace CoreNavigationCommands { }); } - runCoreEditorCommand(cursors: ICursors, args: any): void { - EditorScroll._runEditorScroll(cursors, args.source, { + runCoreEditorCommand(viewModel: IViewModel, args: any): void { + EditorScroll._runEditorScroll(viewModel, args.source, { direction: EditorScroll_.Direction.Down, unit: EditorScroll_.Unit.Page, value: 1, @@ -1279,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); } } @@ -1312,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 @@ -1339,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); } } @@ -1372,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 @@ -1413,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); } }); @@ -1439,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); } }); @@ -1466,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); } }); @@ -1488,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; @@ -1521,9 +1554,9 @@ export namespace CoreNavigationCommands { } } - const viewRange = cursors.context.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); } }); @@ -1535,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()) ] ); } @@ -1555,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, [ @@ -1688,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 { @@ -1713,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))); } }); @@ -1735,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(); } }); @@ -1758,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(); } }); @@ -1780,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); } }); @@ -1804,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 5b2e5182a4..46bf16af38 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 4b438f6e1a..b4222a8a18 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 0cef7357a7..4aa7a1a723 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -30,6 +30,7 @@ import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; export interface ITextAreaHandlerHelper { visibleRangeForPositionRelativeToEditor(lineNumber: number, column: number): HorizontalPosition | null; @@ -117,7 +118,7 @@ export class TextAreaHandler extends ViewPart { // Text Area (The focus will always be in the textarea when the cursor is blinking) this.textArea = createFastDomNode(document.createElement('textarea')); PartFingerprints.write(this.textArea, PartFingerprint.TextArea); - this.textArea.setClassName('inputarea'); + this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); this.textArea.setAttribute('wrap', 'off'); this.textArea.setAttribute('autocorrect', 'off'); this.textArea.setAttribute('autocapitalize', 'off'); @@ -234,37 +235,36 @@ export class TextAreaHandler extends ViewPart { multicursorText = (typeof e.metadata.multicursorText !== 'undefined' ? e.metadata.multicursorText : null); mode = e.metadata.mode; } - this._viewController.paste('keyboard', e.text, pasteOnNewLine, multicursorText, mode); + this._viewController.paste(e.text, pasteOnNewLine, multicursorText, mode); })); this._register(this._textAreaInput.onCut(() => { - this._viewController.cut('keyboard'); + this._viewController.cut(); })); this._register(this._textAreaInput.onType((e: ITypeData) => { if (e.replaceCharCnt) { - this._viewController.replacePreviousChar('keyboard', e.text, e.replaceCharCnt); + this._viewController.replacePreviousChar(e.text, e.replaceCharCnt); } else { - this._viewController.type('keyboard', e.text); + this._viewController.type(e.text); } })); this._register(this._textAreaInput.onSelectionChangeRequest((modelSelection: Selection) => { - this._viewController.setSelection('keyboard', modelSelection); + this._viewController.setSelection(modelSelection); })); this._register(this._textAreaInput.onCompositionStart((e) => { 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); @@ -279,9 +279,9 @@ export class TextAreaHandler extends ViewPart { } // Show the textarea - this.textArea.setClassName('inputarea ime-input'); + this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`); - this._viewController.compositionStart('keyboard'); + this._viewController.compositionStart(); })); this._register(this._textAreaInput.onCompositionUpdate((e: ICompositionData) => { @@ -301,16 +301,16 @@ export class TextAreaHandler extends ViewPart { this._visibleTextArea = null; this._render(); - this.textArea.setClassName('inputarea'); - this._viewController.compositionEnd('keyboard'); + this.textArea.setClassName(`inputarea ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); + this._viewController.compositionEnd(); })); 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 ad7530b57c..ded06675f1 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'; @@ -19,6 +18,7 @@ import { OverviewRulerZone } from 'vs/editor/common/view/overviewZoneManager'; import { IEditorWhitespace } from 'vs/editor/common/viewLayout/linesLayout'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IDiffComputationResult } from 'vs/editor/common/services/editorWorkerService'; +import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; /** * A view zone is a full horizontal rectangle that 'pushes' text down. @@ -616,15 +616,15 @@ export interface ICodeEditor extends editorCommon.IEditor { /** * Change the scrollLeft of the editor's viewport. */ - setScrollLeft(newScrollLeft: number): void; + setScrollLeft(newScrollLeft: number, scrollType?: editorCommon.ScrollType): void; /** * Change the scrollTop of the editor's viewport. */ - setScrollTop(newScrollTop: number): void; + setScrollTop(newScrollTop: number, scrollType?: editorCommon.ScrollType): void; /** * Change the scroll position of the editor's viewport. */ - setScrollPosition(position: editorCommon.INewScrollPosition): void; + setScrollPosition(position: editorCommon.INewScrollPosition, scrollType?: editorCommon.ScrollType): void; /** * Get an action that is a contribution to this editor. @@ -639,7 +639,7 @@ export interface ICodeEditor extends editorCommon.IEditor { * @param source The source of the call. * @param command The command to execute */ - executeCommand(source: string, command: editorCommon.ICommand): void; + executeCommand(source: string | null | undefined, command: editorCommon.ICommand): void; /** * Push an "undo stop" in the undo-redo stack. @@ -653,19 +653,19 @@ export interface ICodeEditor extends editorCommon.IEditor { * @param edits The edits to execute. * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; + executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; /** * Execute multiple (concomitant) commands on the editor. * @param source The source of the call. * @param command The commands to execute */ - executeCommands(source: string, commands: (editorCommon.ICommand | null)[]): void; + executeCommands(source: string | null | undefined, commands: (editorCommon.ICommand | null)[]): void; /** * @internal */ - _getCursors(): ICursors | null; + _getViewModel(): IViewModel | null; /** * Get all the decorations on a line (filtering out decorations from other editors). @@ -858,7 +858,7 @@ export interface IActiveCodeEditor extends ICodeEditor { /** * @internal */ - _getCursors(): ICursors; + _getViewModel(): IViewModel; /** * Get all the decorations on a line (filtering out decorations from other editors). diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index 6b890c6323..193805b210 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -217,7 +217,9 @@ function renderLine(lineContent: string, initialVisibleColumn: number, tabSize: break; case CharCode.UTF8_BOM: - case CharCode.LINE_SEPARATOR_2028: + case CharCode.LINE_SEPARATOR: + case CharCode.PARAGRAPH_SEPARATOR: + case CharCode.NEXT_LINE: sb.write1(0xFFFD); break; diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 8600c09421..f064cf963d 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'; @@ -37,30 +37,30 @@ export interface IMouseDispatchData { export interface ICommandDelegate { executeEditorCommand(editorCommand: CoreEditorCommand, args: any): void; - paste(source: string, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void; - type(source: string, text: string): void; - replacePreviousChar(source: string, text: string, replaceCharCnt: number): void; - compositionStart(source: string): void; - compositionEnd(source: string): void; - cut(source: string): void; + paste(text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void; + type(text: string): void; + replacePreviousChar(text: string, replaceCharCnt: number): void; + startComposition(): void; + endComposition(): void; + cut(): void; } 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; } @@ -69,33 +69,33 @@ export class ViewController { this.commandDelegate.executeEditorCommand(editorCommand, args); } - public paste(source: string, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void { - this.commandDelegate.paste(source, text, pasteOnNewLine, multicursorText, mode); + public paste(text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null): void { + this.commandDelegate.paste(text, pasteOnNewLine, multicursorText, mode); } - public type(source: string, text: string): void { - this.commandDelegate.type(source, text); + public type(text: string): void { + this.commandDelegate.type(text); } - public replacePreviousChar(source: string, text: string, replaceCharCnt: number): void { - this.commandDelegate.replacePreviousChar(source, text, replaceCharCnt); + public replacePreviousChar(text: string, replaceCharCnt: number): void { + this.commandDelegate.replacePreviousChar(text, replaceCharCnt); } - public compositionStart(source: string): void { - this.commandDelegate.compositionStart(source); + public compositionStart(): void { + this.commandDelegate.startComposition(); } - public compositionEnd(source: string): void { - this.commandDelegate.compositionEnd(source); + public compositionEnd(): void { + this.commandDelegate.endComposition(); } - public cut(source: string): void { - this.commandDelegate.cut(source); + public cut(): void { + this.commandDelegate.cut(); } - public setSelection(source: string, modelSelection: Selection): void { + public setSelection(modelSelection: Selection): void { this.commandDelegate.executeEditorCommand(CoreNavigationCommands.SetSelection, { - source: source, + source: 'keyboard', selection: modelSelection }); } @@ -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 9252fcde0f..c73ed3daa3 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; +import { Selection } from 'vs/editor/common/core/selection'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { onUnexpectedError } from 'vs/base/common/errors'; @@ -13,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'; @@ -36,13 +37,11 @@ import { ScrollDecorationViewPart } from 'vs/editor/browser/viewParts/scrollDeco import { SelectionsOverlay } from 'vs/editor/browser/viewParts/selections/selections'; import { ViewCursors } from 'vs/editor/browser/viewParts/viewCursors/viewCursors'; import { ViewZones } from 'vs/editor/browser/viewParts/viewZones/viewZones'; -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 { IConfiguration } from 'vs/editor/common/editorCommon'; +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 readonly _cursor: Cursor; + 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,78 +93,73 @@ export class View extends ViewEventHandler { configuration: IConfiguration, themeService: IThemeService, model: IViewModel, - cursor: Cursor, - outgoingEvents: ViewOutgoingEvents + userInputEvents: ViewUserInputEvents ) { super(); - this._cursor = cursor; + 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)); @@ -177,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 @@ -205,82 +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.addEventListener((events: viewEvents.ViewEvent[]) => { - this.eventDispatcher.emitMany(events); - })); - - this._register(this._cursor.addEventListener((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)); } }; } @@ -292,45 +274,38 @@ 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 handleEvents(events: viewEvents.ViewEvent[]): void { + super.handleEvents(events); + this._scheduleRender(); + } public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); + this.domNode.setClassName(this._getEditorClassName()); this._applyLayout(); return false; } - public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { - this.outgoingEvents.emitContentSizeChange(e); + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { + this._selections = e.selections; 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; } @@ -342,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); @@ -379,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; } @@ -395,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; } @@ -404,26 +371,26 @@ export class View extends ViewEventHandler { this._context.model.setViewport(partialViewportData.startLineNumber, partialViewportData.endLineNumber, partialViewportData.centeredLineNumber); const viewportData = new ViewportData( - this._cursor.getViewSelections(), + this._selections, partialViewportData, this._context.viewLayout.getWhitespaceViewportData(), 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++) { @@ -445,11 +412,11 @@ export class View extends ViewEventHandler { } public restoreState(scrollPosition: { scrollLeft: number; scrollTop: number; }): void { - this._context.viewLayout.setScrollPositionNow({ scrollTop: scrollPosition.scrollTop }); + this._context.model.setScrollPosition({ scrollTop: scrollPosition.scrollTop }, ScrollType.Immediate); this._context.model.tokenizeViewport(); this._renderNow(); - this.viewLines.updateLineWidths(); - this._context.viewLayout.setScrollPositionNow({ scrollLeft: scrollPosition.scrollLeft }); + this._viewLines.updateLineWidths(); + this._context.model.setScrollPosition({ scrollLeft: scrollPosition.scrollLeft }, ScrollType.Immediate); } public getOffsetForColumn(modelLineNumber: number, modelColumn: number): number { @@ -459,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; } @@ -467,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(); } } @@ -522,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(); } @@ -536,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 f78eb51160..d2e2ea874a 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 261e63e8b9..ca1c064238 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -9,7 +9,7 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent'; import { IOverviewRulerLayoutInfo, SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement'; import { ScrollableElementChangeOptions, ScrollableElementCreationOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; -import { INewScrollPosition } from 'vs/editor/common/editorCommon'; +import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; @@ -88,7 +88,7 @@ export class EditorScrollbar extends ViewPart { } } - this._context.viewLayout.setScrollPositionNow(newScrollPosition); + 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 b2111aaaf1..fa5950e7e0 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.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index 47e4e96019..634d266205 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -23,7 +23,6 @@ } .monaco-editor .view-lines { - cursor: text; white-space: nowrap; } diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index b241ee9ad1..d37a33cd61 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -22,6 +22,7 @@ import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData' import { Viewport } from 'vs/editor/common/viewModel/viewModel'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { Constants } from 'vs/base/common/uint'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; class LastRenderedData { @@ -134,7 +135,7 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, this._viewLineOptions = new ViewLineOptions(conf, this._context.theme.type); PartFingerprints.write(this.domNode, PartFingerprint.ViewLines); - this.domNode.setClassName('view-lines'); + this.domNode.setClassName(`view-lines ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); Configuration.applyFontInfo(this.domNode, fontInfo); // --- width & height @@ -280,11 +281,8 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, } const scrollTopDelta = Math.abs(this._context.viewLayout.getCurrentScrollTop() - newScrollPosition.scrollTop); - if (e.scrollType === ScrollType.Smooth && scrollTopDelta > this._lineHeight) { - this._context.viewLayout.setScrollPositionSmooth(newScrollPosition); - } else { - this._context.viewLayout.setScrollPositionNow(newScrollPosition); - } + const scrollType = (scrollTopDelta <= this._lineHeight ? ScrollType.Immediate : e.scrollType); + this._context.model.setScrollPosition(newScrollPosition, scrollType); return true; } @@ -309,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 { @@ -589,15 +587,9 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, this._ensureMaxLineWidth(newScrollLeft.maxHorizontalOffset); } // set `scrollLeft` - if (horizontalRevealRequest.scrollType === ScrollType.Smooth) { - this._context.viewLayout.setScrollPositionSmooth({ - scrollLeft: newScrollLeft.scrollLeft - }); - } else { - this._context.viewLayout.setScrollPositionNow({ - scrollLeft: newScrollLeft.scrollLeft - }); - } + this._context.model.setScrollPosition({ + scrollLeft: newScrollLeft.scrollLeft + }, horizontalRevealRequest.scrollType); } } } @@ -634,11 +626,11 @@ 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); } } - private _computeScrollTopToRevealRange(viewport: Viewport, source: string, range: Range | null, selections: Selection[] | null, verticalType: viewEvents.VerticalRevealType): number { + private _computeScrollTopToRevealRange(viewport: Viewport, source: string | null | undefined, range: Range | null, selections: Selection[] | null, verticalType: viewEvents.VerticalRevealType): number { const viewportStartY = viewport.top; const viewportHeight = viewport.height; const viewportEndY = viewportStartY + viewportHeight; diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index bc5aebe196..cab70be309 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1004,27 +1004,26 @@ 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.setScrollPositionNow({ + this._context.model.setScrollPosition({ scrollTop: scrollTop - }); + }, ScrollType.Immediate); } //#endregion diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index faae9d3ad3..3154918d80 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/selections/selections.ts b/src/vs/editor/browser/viewParts/selections/selections.ts index a451d44f59..435c7ae805 100644 --- a/src/vs/editor/browser/viewParts/selections/selections.ts +++ b/src/vs/editor/browser/viewParts/selections/selections.ts @@ -419,7 +419,7 @@ registerThemingParticipant((theme, collector) => { collector.addRule(`.monaco-editor .selected-text { background-color: ${editorInactiveSelectionColor}; }`); } const editorSelectionForegroundColor = theme.getColor(editorSelectionForeground); - if (editorSelectionForegroundColor) { + if (editorSelectionForegroundColor && !editorSelectionForegroundColor.isTransparent()) { collector.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${editorSelectionForegroundColor}; }`); } }); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts index fc95177975..8686697c42 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursor.ts @@ -13,6 +13,7 @@ import { Range } from 'vs/editor/common/core/range'; import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; export interface IViewCursorRenderData { domNode: HTMLElement; @@ -63,7 +64,7 @@ export class ViewCursor { // Create the dom node this._domNode = createFastDomNode(document.createElement('div')); - this._domNode.setClassName('cursor'); + this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`); this._domNode.setHeight(this._lineHeight); this._domNode.setTop(0); this._domNode.setLeft(0); @@ -200,7 +201,7 @@ export class ViewCursor { this._domNode.domNode.textContent = this._lastRenderedContent; } - this._domNode.setClassName('cursor ' + this._renderData.textContentClassName); + this._domNode.setClassName(`cursor ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`); this._domNode.setDisplay('block'); this._domNode.setTop(this._renderData.top); diff --git a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css index 77d16cb7bb..f22aa42d48 100644 --- a/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css +++ b/src/vs/editor/browser/viewParts/viewCursors/viewCursors.css @@ -9,7 +9,6 @@ .monaco-editor .cursors-layer > .cursor { position: absolute; - cursor: text; overflow: hidden; } diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 76664fd367..70b95678f8 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 29b864c080..da581ce364 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'; @@ -53,6 +53,8 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; 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; @@ -79,15 +81,13 @@ export interface ICodeEditorWidgetOptions { class ModelData { public readonly model: ITextModel; public readonly viewModel: ViewModel; - public readonly cursor: Cursor; public readonly view: View; public readonly hasRealView: boolean; public readonly listenersToRemove: IDisposable[]; - constructor(model: ITextModel, viewModel: ViewModel, cursor: Cursor, view: View, hasRealView: boolean, listenersToRemove: IDisposable[]) { + constructor(model: ITextModel, viewModel: ViewModel, view: View, hasRealView: boolean, listenersToRemove: IDisposable[]) { this.model = model; this.viewModel = viewModel; - this.cursor = cursor; this.view = view; this.hasRealView = hasRealView; this.listenersToRemove = listenersToRemove; @@ -99,7 +99,6 @@ class ModelData { if (this.hasRealView) { this.view.dispose(); } - this.cursor.dispose(); this.viewModel.dispose(); } } @@ -529,7 +528,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return null; } - return this._modelData.cursor.getPosition(); + return this._modelData.viewModel.getPosition(); } public setPosition(position: IPosition): void { @@ -539,7 +538,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!Position.isIPosition(position)) { throw new Error('Invalid arguments'); } - this._modelData.cursor.setSelections('api', [{ + this._modelData.viewModel.setSelections('api', [{ selectionStartLineNumber: position.lineNumber, selectionStartColumn: position.column, positionLineNumber: position.lineNumber, @@ -557,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.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 { @@ -642,14 +641,14 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData) { return null; } - return this._modelData.cursor.getSelection(); + return this._modelData.viewModel.getSelection(); } public getSelections(): Selection[] | null { if (!this._modelData) { return null; } - return this._modelData.cursor.getSelections(); + return this._modelData.viewModel.getSelections(); } public setSelection(range: IRange): void; @@ -683,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.cursor.setSelections('api', [selection]); + this._modelData.viewModel.setSelections('api', [selection]); } public revealLines(startLineNumber: number, endLineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { @@ -814,7 +813,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE throw new Error('Invalid arguments'); } } - this._modelData.cursor.setSelections(source, ranges); + this._modelData.viewModel.setSelections(source, ranges); } public getContentWidth(): number { @@ -857,33 +856,33 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._modelData.viewModel.viewLayout.getCurrentScrollTop(); } - public setScrollLeft(newScrollLeft: number): void { + public setScrollLeft(newScrollLeft: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Immediate): void { if (!this._modelData) { return; } if (typeof newScrollLeft !== 'number') { throw new Error('Invalid arguments'); } - this._modelData.viewModel.viewLayout.setScrollPositionNow({ + this._modelData.viewModel.setScrollPosition({ scrollLeft: newScrollLeft - }); + }, scrollType); } - public setScrollTop(newScrollTop: number): void { + public setScrollTop(newScrollTop: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Immediate): void { if (!this._modelData) { return; } if (typeof newScrollTop !== 'number') { throw new Error('Invalid arguments'); } - this._modelData.viewModel.viewLayout.setScrollPositionNow({ + this._modelData.viewModel.setScrollPosition({ scrollTop: newScrollTop - }); + }, scrollType); } - public setScrollPosition(position: editorCommon.INewScrollPosition): void { + public setScrollPosition(position: editorCommon.INewScrollPosition, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Immediate): void { if (!this._modelData) { return; } - this._modelData.viewModel.viewLayout.setScrollPositionNow(position); + this._modelData.viewModel.setScrollPosition(position, scrollType); } public saveViewState(): editorCommon.ICodeEditorViewState | null { @@ -900,7 +899,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE } } - const cursorState = this._modelData.cursor.saveState(); + const cursorState = this._modelData.viewModel.saveCursorState(); const viewState = this._modelData.viewModel.saveState(); return { cursorState: cursorState, @@ -917,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.cursor.restoreState(cursorState); + this._modelData.viewModel.restoreCursorState(cursorState); } else { // Backwards compatibility - this._modelData.cursor.restoreState([cursorState]); + this._modelData.viewModel.restoreCursorState([cursorState]); } const contributionsState = codeEditorState.contributionsState || {}; @@ -975,43 +974,34 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return this._actions[id] || null; } - public trigger(source: string, handlerId: string, payload: any): void { + 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.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.cursor.getSelection().getStartPosition(); - this._modelData.cursor.trigger(source, handlerId, payload); - const endPosition = this._modelData.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); @@ -1027,18 +1017,67 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (this._triggerEditorCommand(source, handlerId, payload)) { return; } + } - this._modelData.cursor.trigger(source, handlerId, payload); - - if (handlerId === editorCommon.Handler.CompositionStart) { - this._onDidCompositionStart.fire(); + private _startComposition(): void { + if (!this._modelData) { + return; } - if (handlerId === editorCommon.Handler.CompositionEnd) { - this._onDidCompositionEnd.fire(); + this._modelData.viewModel.startComposition(); + this._onDidCompositionStart.fire(); + } + + private _endComposition(source: string | null | undefined): void { + if (!this._modelData) { + return; + } + 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 _triggerEditorCommand(source: string, handlerId: string, payload: any): boolean { + 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 { const command = EditorExtensionsRegistry.getEditorCommand(handlerId); if (command) { payload = payload || {}; @@ -1052,11 +1091,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return false; } - public _getCursors(): ICursors | null { + public _getViewModel(): IViewModel | null { if (!this._modelData) { return null; } - return this._modelData.cursor; + return this._modelData.viewModel; } public pushUndoStop(): boolean { @@ -1071,7 +1110,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE return true; } - public executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean { + public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean { if (!this._modelData) { return false; } @@ -1089,22 +1128,22 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE cursorStateComputer = endCursorState; } - this._modelData.cursor.executeEdits(source, edits, cursorStateComputer); + this._modelData.viewModel.executeEdits(source, edits, cursorStateComputer); return true; } - public executeCommand(source: string, command: editorCommon.ICommand): void { + public executeCommand(source: string | null | undefined, command: editorCommon.ICommand): void { if (!this._modelData) { return; } - this._modelData.cursor.trigger(source, editorCommon.Handler.ExecuteCommand, command); + this._modelData.viewModel.executeCommand(command, source); } - public executeCommands(source: string, commands: editorCommon.ICommand[]): void { + public executeCommands(source: string | null | undefined, commands: editorCommon.ICommand[]): void { if (!this._modelData) { return; } - this._modelData.cursor.trigger(source, editorCommon.Handler.ExecuteCommands, commands); + this._modelData.viewModel.executeCommands(commands, source); } public changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { @@ -1346,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 { @@ -1437,43 +1473,59 @@ 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))); - const cursor = new Cursor(this._configuration, model, viewModel); + 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(cursor.onDidReachMaxCursorCount(() => { - this._notificationService.warn(nls.localize('cursors.maximum', "The number of cursors has been limited to {0}.", Cursor.MAX_CURSOR_COUNT)); - })); + const positions: Position[] = []; + for (let i = 0, len = e.selections.length; i < len; i++) { + positions[i] = e.selections[i].getPosition(); + } - listenersToRemove.push(cursor.onDidAttemptReadOnlyEdit(() => { - this._onDidAttemptReadOnlyEdit.fire(undefined); - })); + 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(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, cursor); + const [view, hasRealView] = this._createView(viewModel); if (hasRealView) { this._domElement.appendChild(view.domNode.domNode); @@ -1493,101 +1545,82 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE view.domNode.domNode.setAttribute('data-uri', model.uri.toString()); } - this._modelData = new ModelData(model, viewModel, cursor, view, hasRealView, listenersToRemove); + this._modelData = new ModelData(model, viewModel, view, hasRealView, listenersToRemove); } - protected _createView(viewModel: ViewModel, cursor: Cursor): [View, boolean] { + protected _createView(viewModel: ViewModel): [View, boolean] { let commandDelegate: ICommandDelegate; if (this.isSimpleWidget) { commandDelegate = { executeEditorCommand: (editorCommand: CoreEditorCommand, args: any): void => { - editorCommand.runCoreEditorCommand(cursor, args); + editorCommand.runCoreEditorCommand(viewModel, args); }, - paste: (source: string, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { - this.trigger(source, editorCommon.Handler.Paste, { text, pasteOnNewLine, multicursorText, mode }); + paste: (text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { + this._paste('keyboard', text, pasteOnNewLine, multicursorText, mode); }, - type: (source: string, text: string) => { - this.trigger(source, editorCommon.Handler.Type, { text }); + type: (text: string) => { + this._type('keyboard', text); }, - replacePreviousChar: (source: string, text: string, replaceCharCnt: number) => { - this.trigger(source, editorCommon.Handler.ReplacePreviousChar, { text, replaceCharCnt }); + replacePreviousChar: (text: string, replaceCharCnt: number) => { + this._replacePreviousChar('keyboard', text, replaceCharCnt); }, - compositionStart: (source: string) => { - this.trigger(source, editorCommon.Handler.CompositionStart, undefined); + startComposition: () => { + this._startComposition(); }, - compositionEnd: (source: string) => { - this.trigger(source, editorCommon.Handler.CompositionEnd, undefined); + endComposition: () => { + this._endComposition('keyboard'); }, - cut: (source: string) => { - this.trigger(source, editorCommon.Handler.Cut, undefined); + cut: () => { + this._cut('keyboard'); } }; } else { commandDelegate = { executeEditorCommand: (editorCommand: CoreEditorCommand, args: any): void => { - editorCommand.runCoreEditorCommand(cursor, args); + editorCommand.runCoreEditorCommand(viewModel, args); }, - paste: (source: string, text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { - this._commandService.executeCommand(editorCommon.Handler.Paste, { - text: text, - pasteOnNewLine: pasteOnNewLine, - multicursorText: multicursorText, - mode - }); + paste: (text: string, pasteOnNewLine: boolean, multicursorText: string[] | null, mode: string | null) => { + const payload: editorCommon.PastePayload = { text, pasteOnNewLine, multicursorText, mode }; + this._commandService.executeCommand(editorCommon.Handler.Paste, payload); }, - type: (source: string, text: string) => { - this._commandService.executeCommand(editorCommon.Handler.Type, { - text: text - }); + type: (text: string) => { + const payload: editorCommon.TypePayload = { text }; + this._commandService.executeCommand(editorCommon.Handler.Type, payload); }, - replacePreviousChar: (source: string, text: string, replaceCharCnt: number) => { - this._commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, { - text: text, - replaceCharCnt: replaceCharCnt - }); + replacePreviousChar: (text: string, replaceCharCnt: number) => { + const payload: editorCommon.ReplacePreviousCharPayload = { text, replaceCharCnt }; + this._commandService.executeCommand(editorCommon.Handler.ReplacePreviousChar, payload); }, - compositionStart: (source: string) => { + startComposition: () => { this._commandService.executeCommand(editorCommon.Handler.CompositionStart, {}); }, - compositionEnd: (source: string) => { + endComposition: () => { this._commandService.executeCommand(editorCommon.Handler.CompositionEnd, {}); }, - cut: (source: string) => { + cut: () => { this._commandService.executeCommand(editorCommon.Handler.Cut, {}); } }; } - const onDidChangeTextFocus = (textFocus: boolean) => { - if (this._modelData) { - this._modelData.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, - cursor, - viewOutgoingEvents + viewUserInputEvents ); return [view, true]; diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index ecd626e0f0..6eabc4ddba 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -7,7 +7,7 @@ import 'vs/css!./media/diffEditor'; import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; -import { ISashEvent, IVerticalSashLayoutProvider, Sash, SashState } from 'vs/base/browser/ui/sash/sash'; +import { ISashEvent, IVerticalSashLayoutProvider, Sash, SashState, Orientation } from 'vs/base/browser/ui/sash/sash'; import { RunOnceScheduler } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; import { Emitter, Event } from 'vs/base/common/event'; @@ -50,6 +50,7 @@ import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/co import { ElementSizeObserver } from 'vs/editor/browser/config/elementSizeObserver'; import { reverseLineChanges } from 'sql/editor/browser/diffEditorHelper'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; interface IEditorDiffDecorations { decorations: IModelDeltaDecoration[]; @@ -892,7 +893,7 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this._cleanViewZonesAndDecorations(); } - public trigger(source: string, handlerId: string, payload: any): void { + public trigger(source: string | null | undefined, handlerId: string, payload: any): void { this.modifiedEditor.trigger(source, handlerId, payload); } @@ -1675,7 +1676,7 @@ class DiffEditorWidgetSideBySide extends DiffEditorWidgetStyle implements IDiffE this._sashRatio = null; this._sashPosition = null; this._startSashPosition = null; - this._sash = this._register(new Sash(this._dataSource.getContainerDomNode(), this)); + this._sash = this._register(new Sash(this._dataSource.getContainerDomNode(), this, { orientation: Orientation.VERTICAL })); if (this._disableSash) { this._sash.state = SashState.Disabled; @@ -2113,7 +2114,7 @@ class InlineViewZonesComputer extends ViewZonesComputer { maxCharsPerLine += this.modifiedEditorOptions.get(EditorOption.scrollBeyondLastColumn); let domNode = document.createElement('div'); - domNode.className = 'view-lines line-delete'; + domNode.className = `view-lines line-delete ${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`; domNode.innerHTML = sb.build(); Configuration.applyFontInfoSlow(domNode, fontInfo); diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index adc509761e..0f5df6e756 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 f652d90aa5..248f6e9996 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 4713d29f70..62589c406c 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -93,6 +93,11 @@ export interface IEditorOptions { * Defaults to true. */ renderFinalNewline?: boolean; + /** + * Remove unusual line terminators like LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL). + * Defaults to true. + */ + removeUnusualLineTerminators?: boolean; /** * Should the corresponding line be selected when clicking on the line number? * Defaults to true. @@ -2878,8 +2883,8 @@ class EditorScrollbar extends BaseEditorOption = 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: IViewModel; + private readonly _viewModel: ICursorSimpleModel; + private readonly _coordinatesConverter: ICoordinatesConverter; public context: CursorContext; private _cursors: CursorCollection; @@ -194,13 +138,13 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { private _autoClosedActions: AutoClosedAction[]; private _prevEditOperationType: EditOperationType; - constructor(configuration: editorCommon.IConfiguration, model: ITextModel, viewModel: IViewModel) { + 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.context = new CursorContext(this._configuration, this._model, this._viewModel); + this._coordinatesConverter = coordinatesConverter; + this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig); this._cursors = new CursorCollection(this.context); this._hasFocus = false; @@ -210,53 +154,6 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this._columnSelectData = null; this._autoClosedActions = []; this._prevEditOperationType = EditOperationType.Other; - - this._register(this._model.onDidChangeRawContent((e) => { - this._knownModelVersionId = e.versionId; - if (this._isHandling) { - return; - } - - this._onModelContentChanged(e); - })); - - this._register(viewModel.addEventListener((events: viewEvents.ViewEvent[]) => { - if (!containsLineMappingChanged(events)) { - return; - } - - if (this._knownModelVersionId !== this._model.getVersionId()) { - // There are model change events that I didn't yet receive. - // - // This can happen when editing the model, and the view model receives the change events first, - // and the view model emits line mapping changed events, all before the cursor gets a chance to - // recover from markers. - // - // The model change listener above will be called soon and we'll ensure a valid cursor state there. - return; - } - // Ensure valid state - this.setStates('viewModel', CursorChangeReason.NotSet, this.getAll()); - })); - - const updateCursorContext = () => { - this.context = new CursorContext(this._configuration, this._model, this._viewModel); - this._cursors.updateContext(this.context); - }; - this._register(this._model.onDidChangeLanguage((e) => { - updateCursorContext(); - })); - this._register(this._model.onDidChangeLanguageConfiguration(() => { - updateCursorContext(); - })); - this._register(this._model.onDidChangeOptions(() => { - updateCursorContext(); - })); - this._register(this._configuration.onDidChange((e) => { - if (CursorConfiguration.shouldRecreate(e)) { - updateCursorContext(); - } - })); } public dispose(): void { @@ -265,6 +162,26 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { super.dispose(); } + public updateConfiguration(cursorConfig: CursorConfiguration): void { + this.context = new CursorContext(this._model, this._coordinatesConverter, cursorConfig); + this._cursors.updateContext(this.context); + } + + public onLineMappingChanged(eventsCollector: ViewModelEventsCollector): void { + if (this._knownModelVersionId !== this._model.getVersionId()) { + // There are model change events that I didn't yet receive. + // + // This can happen when editing the model, and the view model receives the change events first, + // and the view model emits line mapping changed events, all before the cursor gets a chance to + // recover from markers. + // + // The model change listener above will be called soon and we'll ensure a valid cursor state there. + return; + } + // Ensure valid state + this.setStates(eventsCollector, 'viewModel', CursorChangeReason.NotSet, this.getCursorStates()); + } + public setHasFocus(hasFocus: boolean): void { this._hasFocus = hasFocus; } @@ -285,7 +202,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ------ some getters/setters - public getPrimaryCursor(): CursorState { + public getPrimaryCursorState(): CursorState { return this._cursors.getPrimaryCursor(); } @@ -293,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, 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); @@ -311,25 +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, 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, 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); + } } - public scrollTo(desiredScrollTop: number): void { - this._viewModel.viewLayout.setScrollPositionSmooth({ - scrollTop: desiredScrollTop - }); + 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[] { @@ -356,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[] = []; @@ -393,11 +324,16 @@ 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); } - private _onModelContentChanged(e: ModelRawContentChangedEvent): void { + public onModelContentChanged(eventsCollector: ViewModelEventsCollector, e: ModelRawContentChangedEvent): void { + + this._knownModelVersionId = e.versionId; + if (this._isHandling) { + return; + } const hadFlushEvent = e.containsEvent(RawContentChangedType.Flush); this._prevEditOperationType = EditOperationType.Other; @@ -407,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)); } } } @@ -425,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; } @@ -435,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), }; } @@ -445,16 +389,12 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return this._cursors.getSelections(); } - public getViewSelections(): Selection[] { - return this._cursors.getViewSelections(); - } - public getPosition(): Position { return this._cursors.getPrimaryCursor().modelState.position; } - public setSelections(source: string, 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 { @@ -545,7 +485,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { // ----------------------------------------------------------------------------------------------------------- // ----- emitting events - private _emitStateChangedIfNecessary(source: string, 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; @@ -555,12 +495,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { const viewSelections = this._cursors.getViewSelections(); // Let the view get the event first. - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewCursorStateChangedEvent(viewSelections, selections)); - } finally { - this._endEmit(); - } + eventsCollector.emitViewEvent(new ViewCursorStateChangedEvent(viewSelections, selections)); // Only after the view has been notified, let the rest of the world know... if (!oldState @@ -569,49 +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, 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, viewRange: Range | null, viewSelections: Selection[] | null, verticalType: viewEvents.VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) { - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, viewSelections, verticalType, revealHorizontal, scrollType)); - } finally { - this._endEmit(); - } - } - // ----------------------------------------------------------------------------------------------------------- // ----- handlers beyond this point @@ -633,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; } @@ -651,7 +549,7 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { return indices; } - public executeEdits(source: string, 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); @@ -686,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, 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) { - 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, 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 7054f0f832..d8d93531af 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 df52c1bf20..ba9bbefd59 100644 --- a/src/vs/editor/common/controller/cursorCommon.ts +++ b/src/vs/editor/common/controller/cursorCommon.ts @@ -7,18 +7,16 @@ 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 { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; import { Constants } from 'vs/base/common/uint'; export interface IColumnSelectData { @@ -46,25 +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, reason: CursorChangeReason, states: PartialCursorState[] | null): void; - reveal(source: string, horizontal: boolean, target: RevealTarget, scrollType: ScrollType): void; - revealRange(source: string, revealHorizontal: boolean, viewRange: Range, verticalType: VerticalRevealType, scrollType: ScrollType): void; - - scrollTo(desiredScrollTop: number): void; - - getPrevEditOperationType(): EditOperationType; - setPrevEditOperationType(type: EditOperationType): void; -} - export interface CharacterMap { [char: string]: string; } @@ -357,62 +336,13 @@ export class CursorContext { _cursorContextBrand: void; public readonly model: ITextModel; - public readonly viewModel: IViewModel; - public readonly config: CursorConfiguration; + public readonly coordinatesConverter: ICoordinatesConverter; + public readonly cursorConfig: CursorConfiguration; - constructor(configuration: IConfiguration, model: ITextModel, viewModel: IViewModel) { + constructor(model: ITextModel, coordinatesConverter: ICoordinatesConverter, cursorConfig: CursorConfiguration) { this.model = model; - this.viewModel = viewModel; - this.config = new CursorConfiguration( - this.model.getLanguageIdentifier(), - this.model.getOptions(), - configuration - ); - } - - public validateViewPosition(viewPosition: Position, modelPosition: Position): Position { - return this.viewModel.coordinatesConverter.validateViewPosition(viewPosition, modelPosition); - } - - public validateViewRange(viewRange: Range, expectedModelRange: Range): Range { - return this.viewModel.coordinatesConverter.validateViewRange(viewRange, expectedModelRange); - } - - public convertViewRangeToModelRange(viewRange: Range): Range { - return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange); - } - - public convertViewPositionToModelPosition(lineNumber: number, column: number): Position { - return this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(new Position(lineNumber, column)); - } - - public convertModelPositionToViewPosition(modelPosition: Position): Position { - return this.viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); - } - - public convertModelRangeToViewRange(modelRange: Range): Range { - return this.viewModel.coordinatesConverter.convertModelRangeToViewRange(modelRange); - } - - public getCurrentScrollTop(): number { - return this.viewModel.viewLayout.getCurrentScrollTop(); - } - - public getCompletelyVisibleViewRange(): Range { - return this.viewModel.getCompletelyVisibleViewRange(); - } - - public getCompletelyVisibleModelRange(): Range { - const viewRange = this.viewModel.getCompletelyVisibleViewRange(); - return this.viewModel.coordinatesConverter.convertViewRangeToModelRange(viewRange); - } - - public getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range { - return this.viewModel.getCompletelyVisibleViewRangeAtScrollTop(scrollTop); - } - - public getVerticalOffsetForViewLine(viewLineNumber: number): number { - return this.viewModel.viewLayout.getVerticalOffsetForLineNumber(viewLineNumber); + this.coordinatesConverter = coordinatesConverter; + this.cursorConfig = cursorConfig; } } diff --git a/src/vs/editor/common/controller/cursorMoveCommands.ts b/src/vs/editor/common/controller/cursorMoveCommands.ts index c38aeb7567..84feaa9955 100644 --- a/src/vs/editor/common/controller/cursorMoveCommands.ts +++ b/src/vs/editor/common/controller/cursorMoveCommands.ts @@ -4,131 +4,132 @@ *--------------------------------------------------------------------------------------------*/ 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'; import { Range } from 'vs/editor/common/core/range'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; +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; @@ -142,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, @@ -170,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.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position) - : context.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( @@ -206,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( @@ -229,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); } @@ -248,108 +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.validateViewPosition(new Position(_viewPosition.lineNumber, _viewPosition.column), position) - : context.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 move(context: CursorContext, cursors: CursorState[], args: CursorMove.ParsedArguments): PartialCursorState[] | null { - const inSelectionMode = args.select; - const value = args.value; - - switch (args.direction) { + 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 (args.unit === CursorMove.Unit.HalfLine) { + 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 (args.unit === CursorMove.Unit.HalfLine) { + 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 (args.unit === CursorMove.Unit.WrappedLine) { + 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 (args.unit === CursorMove.Unit.WrappedLine) { + 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, cursors: CursorState[], direction: CursorMove.ViewportDirection, inSelectionMode: boolean, value: number): PartialCursorState[] | null { + const visibleViewRange = viewModel.getCompletelyVisibleViewRange(); + 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 cursor = cursors[0]; - const visibleModelRange = context.getCompletelyVisibleModelRange(); - const modelLineNumber = this._firstLineNumberInRange(context.model, visibleModelRange, value); - const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber); - return [this._moveToModelPosition(context, cursor, 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 cursor = cursors[0]; - const visibleModelRange = context.getCompletelyVisibleModelRange(); - const modelLineNumber = this._lastLineNumberInRange(context.model, visibleModelRange, value); - const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber); - return [this._moveToModelPosition(context, cursor, 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 cursor = cursors[0]; - const visibleModelRange = context.getCompletelyVisibleModelRange(); const modelLineNumber = Math.round((visibleModelRange.startLineNumber + visibleModelRange.endLineNumber) / 2); - const modelColumn = context.model.getLineFirstNonWhitespaceColumn(modelLineNumber); - return [this._moveToModelPosition(context, cursor, 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 - const visibleViewRange = context.getCompletelyVisibleViewRange(); 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,8 +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) { @@ -373,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); } } @@ -404,19 +403,19 @@ 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[] { let result: PartialCursorState[] = []; for (let i = 0, len = cursors.length; i < len; i++) { const cursor = cursors[i]; - let newViewState = MoveOperations.moveLeft(context.config, context.viewModel, cursor.viewState, inSelectionMode, noOfColumns); + let newViewState = MoveOperations.moveLeft(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); if (!cursor.viewState.hasSelection() && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { // moved over to the previous view line - const newViewModelPosition = context.viewModel.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); } } @@ -425,29 +424,29 @@ 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[] { 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); + let newViewState = MoveOperations.moveRight(viewModel.cursorConfig, viewModel, cursor.viewState, inSelectionMode, noOfColumns); if (!cursor.viewState.hasSelection() && noOfColumns === 1 && newViewState.position.lineNumber !== cursor.viewState.position.lineNumber) { // moved over to the next view line - const newViewModelPosition = context.viewModel.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); } } @@ -456,112 +455,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; } @@ -767,6 +766,13 @@ export namespace CursorMove { value: number; } + export interface SimpleMoveArguments { + direction: SimpleMoveDirection; + unit: Unit; + select: boolean; + value: number; + } + export const enum Direction { Left, Right, @@ -786,6 +792,25 @@ export namespace CursorMove { ViewPortIfOutside, } + export type SimpleMoveDirection = ( + Direction.Left + | Direction.Right + | Direction.Up + | Direction.Down + | Direction.WrappedLineStart + | Direction.WrappedLineFirstNonWhitespaceCharacter + | Direction.WrappedLineColumnCenter + | Direction.WrappedLineEnd + | Direction.WrappedLineLastNonWhitespaceCharacter + ); + + export type ViewportDirection = ( + Direction.ViewPortTop + | Direction.ViewPortCenter + | Direction.ViewPortBottom + | Direction.ViewPortIfOutside + ); + export const enum Unit { None, Line, diff --git a/src/vs/editor/common/controller/oneCursor.ts b/src/vs/editor/common/controller/oneCursor.ts index 7412617806..600b237242 100644 --- a/src/vs/editor/common/controller/oneCursor.ts +++ b/src/vs/editor/common/controller/oneCursor.ts @@ -81,11 +81,11 @@ export class OneCursor { } // We only have the view state => compute the model state const selectionStart = context.model.validateRange( - context.convertViewRangeToModelRange(viewState.selectionStart) + context.coordinatesConverter.convertViewRangeToModelRange(viewState.selectionStart) ); const position = context.model.validatePosition( - context.convertViewPositionToModelPosition(viewState.position.lineNumber, viewState.position.column) + context.coordinatesConverter.convertViewPositionToModelPosition(viewState.position) ); modelState = new SingleCursorState(selectionStart, viewState.selectionStartLeftoverVisibleColumns, position, viewState.leftoverVisibleColumns); @@ -104,15 +104,15 @@ export class OneCursor { if (!viewState) { // We only have the model state => compute the view state - const viewSelectionStart1 = context.convertModelPositionToViewPosition(new Position(modelState.selectionStart.startLineNumber, modelState.selectionStart.startColumn)); - const viewSelectionStart2 = context.convertModelPositionToViewPosition(new Position(modelState.selectionStart.endLineNumber, modelState.selectionStart.endColumn)); + const viewSelectionStart1 = context.coordinatesConverter.convertModelPositionToViewPosition(new Position(modelState.selectionStart.startLineNumber, modelState.selectionStart.startColumn)); + const viewSelectionStart2 = context.coordinatesConverter.convertModelPositionToViewPosition(new Position(modelState.selectionStart.endLineNumber, modelState.selectionStart.endColumn)); const viewSelectionStart = new Range(viewSelectionStart1.lineNumber, viewSelectionStart1.column, viewSelectionStart2.lineNumber, viewSelectionStart2.column); - const viewPosition = context.convertModelPositionToViewPosition(modelState.position); + const viewPosition = context.coordinatesConverter.convertModelPositionToViewPosition(modelState.position); viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns); } else { // Validate new view state - const viewSelectionStart = context.validateViewRange(viewState.selectionStart, modelState.selectionStart); - const viewPosition = context.validateViewPosition(viewState.position, modelState.position); + const viewSelectionStart = context.coordinatesConverter.validateViewRange(viewState.selectionStart, modelState.selectionStart); + const viewPosition = context.coordinatesConverter.validateViewPosition(viewState.position, modelState.position); viewState = new SingleCursorState(viewSelectionStart, modelState.selectionStartLeftoverVisibleColumns, viewPosition, modelState.leftoverVisibleColumns); } diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index f040506be0..43c8c3eb66 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; @@ -480,7 +481,7 @@ export interface IEditor { * @param handlerId The id of the handler or the id of a contribution. * @param payload Extra data to be sent to the handler. */ - trigger(source: string, handlerId: string, payload: any): void; + trigger(source: string | null | undefined, handlerId: string, payload: any): void; /** * Gets the current model attached to this editor. @@ -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/model.ts b/src/vs/editor/common/model.ts index c52d33a751..228dbdae4a 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -551,6 +551,18 @@ export interface ITextModel { */ mightContainRTL(): boolean; + /** + * If true, the text model might contain LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL). + * If false, the text model definitely does not contain these. + * @internal + */ + mightContainUnusualLineTerminators(): boolean; + + /** + * @internal + */ + removeUnusualLineTerminators(selections?: Selection[]): void; + /** * If true, the text model might contain non basic ASCII. * If false, the text model **contains only** basic ASCII. @@ -1281,6 +1293,8 @@ export interface IReadonlyTextBuffer { onDidChangeContent: Event; equals(other: ITextBuffer): boolean; mightContainRTL(): boolean; + mightContainUnusualLineTerminators(): boolean; + resetMightContainUnusualLineTerminators(): void; mightContainNonBasicASCII(): boolean; getBOM(): string; getEOL(): string; diff --git a/src/vs/editor/common/model/editStack.ts b/src/vs/editor/common/model/editStack.ts index 63eef8f45e..2a145c649d 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; @@ -224,6 +229,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 +247,27 @@ 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 setModel(model: ITextModel | URI): void { diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts index 4de4ee88a0..4de657dcf5 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBuffer.ts @@ -36,15 +36,17 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { private readonly _pieceTree: PieceTreeBase; private readonly _BOM: string; private _mightContainRTL: boolean; + private _mightContainUnusualLineTerminators: boolean; private _mightContainNonBasicASCII: boolean; private readonly _onDidChangeContent: Emitter = new Emitter(); public readonly onDidChangeContent: Event = this._onDidChangeContent.event; - constructor(chunks: StringBuffer[], BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, isBasicASCII: boolean, eolNormalized: boolean) { + constructor(chunks: StringBuffer[], BOM: string, eol: '\r\n' | '\n', containsRTL: boolean, containsUnusualLineTerminators: boolean, isBasicASCII: boolean, eolNormalized: boolean) { this._BOM = BOM; this._mightContainNonBasicASCII = !isBasicASCII; this._mightContainRTL = containsRTL; + this._mightContainUnusualLineTerminators = containsUnusualLineTerminators; this._pieceTree = new PieceTreeBase(chunks, eol, eolNormalized); } dispose(): void { @@ -67,6 +69,12 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { public mightContainRTL(): boolean { return this._mightContainRTL; } + public mightContainUnusualLineTerminators(): boolean { + return this._mightContainUnusualLineTerminators; + } + public resetMightContainUnusualLineTerminators(): void { + this._mightContainUnusualLineTerminators = false; + } public mightContainNonBasicASCII(): boolean { return this._mightContainNonBasicASCII; } @@ -216,6 +224,7 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { public applyEdits(rawOperations: ValidAnnotatedEditOperation[], recordTrimAutoWhitespace: boolean, computeUndoEdits: boolean): ApplyEditsResult { let mightContainRTL = this._mightContainRTL; + let mightContainUnusualLineTerminators = this._mightContainUnusualLineTerminators; let mightContainNonBasicASCII = this._mightContainNonBasicASCII; let canReduceOperations = true; @@ -226,12 +235,20 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { canReduceOperations = false; } let validatedRange = op.range; - if (!mightContainRTL && op.text) { - // check if the new inserted text contains RTL - mightContainRTL = strings.containsRTL(op.text); - } - if (!mightContainNonBasicASCII && op.text) { - mightContainNonBasicASCII = !strings.isBasicASCII(op.text); + if (op.text) { + let textMightContainNonBasicASCII = true; + if (!mightContainNonBasicASCII) { + textMightContainNonBasicASCII = !strings.isBasicASCII(op.text); + mightContainNonBasicASCII = textMightContainNonBasicASCII; + } + if (!mightContainRTL && textMightContainNonBasicASCII) { + // check if the new inserted text contains RTL + mightContainRTL = strings.containsRTL(op.text); + } + if (!mightContainUnusualLineTerminators && textMightContainNonBasicASCII) { + // check if the new inserted text contains unusual line terminators + mightContainUnusualLineTerminators = strings.containsUnusualLineTerminators(op.text); + } } let validText = ''; @@ -340,6 +357,7 @@ export class PieceTreeTextBuffer implements ITextBuffer, IDisposable { this._mightContainRTL = mightContainRTL; + this._mightContainUnusualLineTerminators = mightContainUnusualLineTerminators; this._mightContainNonBasicASCII = mightContainNonBasicASCII; const contentChanges = this._doApplyEdits(operations); diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts index 3654c267d3..36677ef131 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeTextBufferBuilder.ts @@ -18,6 +18,7 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { private readonly _lf: number, private readonly _crlf: number, private readonly _containsRTL: boolean, + private readonly _containsUnusualLineTerminators: boolean, private readonly _isBasicASCII: boolean, private readonly _normalizeEOL: boolean ) { } @@ -53,7 +54,7 @@ export class PieceTreeTextBufferFactory implements ITextBufferFactory { } } - return new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._isBasicASCII, this._normalizeEOL); + return new PieceTreeTextBuffer(chunks, this._bom, eol, this._containsRTL, this._containsUnusualLineTerminators, this._isBasicASCII, this._normalizeEOL); } public getFirstLineText(lengthLimit: number): string { @@ -73,6 +74,7 @@ export class PieceTreeTextBufferBuilder implements ITextBufferBuilder { private lf: number; private crlf: number; private containsRTL: boolean; + private containsUnusualLineTerminators: boolean; private isBasicASCII: boolean; constructor() { @@ -87,6 +89,7 @@ export class PieceTreeTextBufferBuilder implements ITextBufferBuilder { this.lf = 0; this.crlf = 0; this.containsRTL = false; + this.containsUnusualLineTerminators = false; this.isBasicASCII = true; } @@ -140,9 +143,13 @@ export class PieceTreeTextBufferBuilder implements ITextBufferBuilder { this.isBasicASCII = lineStarts.isBasicASCII; } if (!this.isBasicASCII && !this.containsRTL) { - // No need to check if is basic ASCII + // No need to check if it is basic ASCII this.containsRTL = strings.containsRTL(chunk); } + if (!this.isBasicASCII && !this.containsUnusualLineTerminators) { + // No need to check if it is basic ASCII + this.containsUnusualLineTerminators = strings.containsUnusualLineTerminators(chunk); + } } public finish(normalizeEOL: boolean = true): PieceTreeTextBufferFactory { @@ -154,6 +161,7 @@ export class PieceTreeTextBufferBuilder implements ITextBufferBuilder { this.lf, this.crlf, this.containsRTL, + this.containsUnusualLineTerminators, this.isBasicASCII, normalizeEOL ); diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index 2578b2a599..63c2aef309 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -37,6 +37,7 @@ import { Color } from 'vs/base/common/color'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo'; import { TextChange } from 'vs/editor/common/model/textChange'; +import { Constants } from 'vs/base/common/uint'; function createTextBufferBuilder() { return new PieceTreeTextBufferBuilder(); @@ -699,6 +700,17 @@ export class TextModel extends Disposable implements model.ITextModel { return this._buffer.mightContainRTL(); } + public mightContainUnusualLineTerminators(): boolean { + return this._buffer.mightContainUnusualLineTerminators(); + } + + public removeUnusualLineTerminators(selections: Selection[] | null = null): void { + const matches = this.findMatches(strings.UNUSUAL_LINE_TERMINATORS.source, false, true, false, null, false, Constants.MAX_SAFE_SMALL_INTEGER); + const eol = this.getEOL(); + this._buffer.resetMightContainUnusualLineTerminators(); + this.pushEditOperations(selections, matches.map(m => ({ range: m.range, text: eol })), () => null); + } + public mightContainNonBasicASCII(): boolean { return this._buffer.mightContainNonBasicASCII(); } @@ -1097,7 +1109,7 @@ export class TextModel extends Disposable implements model.ITextModel { return this._buffer.findMatchesLineByLine(searchRange, searchData, captureMatches, limitResultCount); } - public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] { + public findMatches(searchString: string, rawSearchScope: any, isRegex: boolean, matchCase: boolean, wordSeparators: string | null, captureMatches: boolean, limitResultCount: number = LIMIT_FIND_COUNT): model.FindMatch[] { this._assertNotDisposed(); let searchRange: Range; diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 87f82d1eed..6aa68a7cff 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1411,11 +1411,12 @@ export interface RenameProvider { */ export interface AuthenticationSession { id: string; - getAccessToken(): Thenable; + accessToken: string; account: { displayName: string; id: string; } + scopes: string[]; } /** @@ -1602,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/modes/textToHtmlTokenizer.ts b/src/vs/editor/common/modes/textToHtmlTokenizer.ts index d45497ec20..d8d47a6456 100644 --- a/src/vs/editor/common/modes/textToHtmlTokenizer.ts +++ b/src/vs/editor/common/modes/textToHtmlTokenizer.ts @@ -68,7 +68,9 @@ export function tokenizeLineToHTML(text: string, viewLineTokens: IViewLineTokens break; case CharCode.UTF8_BOM: - case CharCode.LINE_SEPARATOR_2028: + case CharCode.LINE_SEPARATOR: + case CharCode.PARAGRAPH_SEPARATOR: + case CharCode.NEXT_LINE: partContent += '\ufffd'; break; diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index b9156a6fb9..7ca2239276 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -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)); } diff --git a/src/vs/editor/common/services/modelUndoRedoParticipant.ts b/src/vs/editor/common/services/modelUndoRedoParticipant.ts new file mode 100644 index 0000000000..2001857bf0 --- /dev/null +++ b/src/vs/editor/common/services/modelUndoRedoParticipant.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 ba7e097210..70e59939aa 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 c482ea526d..f4a876f38c 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -240,49 +240,50 @@ export enum EditorOption { quickSuggestions = 70, quickSuggestionsDelay = 71, readOnly = 72, - 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, - useTabStops = 102, - wordSeparators = 103, - wordWrap = 104, - wordWrapBreakAfterCharacters = 105, - wordWrapBreakBeforeCharacters = 106, - wordWrapColumn = 107, - wordWrapMinified = 108, - wrappingIndent = 109, - wrappingStrategy = 110, - editorClassName = 111, - pixelRatio = 112, - tabFocusMode = 113, - layoutInfo = 114, - wrappingInfo = 115 + 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, + useTabStops = 103, + wordSeparators = 104, + wordWrap = 105, + wordWrapBreakAfterCharacters = 106, + wordWrapBreakBeforeCharacters = 107, + wordWrapColumn = 108, + wordWrapMinified = 109, + wrappingIndent = 110, + wrappingStrategy = 111, + editorClassName = 112, + pixelRatio = 113, + tabFocusMode = 114, + layoutInfo = 115, + wrappingInfo = 116 } /** diff --git a/src/vs/editor/common/view/viewContext.ts b/src/vs/editor/common/view/viewContext.ts index a9d970f222..9e5a5bbad8 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 a77287e675..0000000000 --- a/src/vs/editor/common/view/viewEventDispatcher.ts +++ /dev/null @@ -1,92 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. 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 5378c2db92..43d3490808 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -3,33 +3,30 @@ * Licensed under the Source EULA. 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; @@ -224,9 +202,9 @@ export class ViewRevealRangeRequestEvent { /** * Source of the call that caused the event. */ - readonly source: string; + readonly source: string | null | undefined; - constructor(source: string, range: Range | null, selections: Selection[] | null, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: ScrollType) { + constructor(source: string | null | undefined, range: Range | null, selections: Selection[] | null, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: ScrollType) { this.source = source; this.range = range; this.selections = selections; @@ -308,7 +286,6 @@ export class ViewZonesChangedEvent { export type ViewEvent = ( ViewConfigurationChangedEvent - | ViewContentSizeChangedEvent | ViewCursorStateChangedEvent | ViewDecorationsChangedEvent | ViewFlushedEvent @@ -325,94 +302,3 @@ export type ViewEvent = ( | ViewTokensColorsChangedEvent | ViewZonesChangedEvent ); - -export interface IViewEventListener { - (events: ViewEvent[]): void; -} - -export class ViewEventEmitter extends Disposable { - 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 _beginEmit(): ViewEventsCollector { - this._collectorCnt++; - if (this._collectorCnt === 1) { - this._collector = new ViewEventsCollector(); - } - return this._collector!; - } - - protected _endEmit(): void { - this._collectorCnt--; - if (this._collectorCnt === 0) { - const events = this._collector!.finalize(); - this._collector = null; - if (events.length > 0) { - this._emit(events); - } - } - } - - 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 addEventListener(listener: (events: ViewEvent[]) => void): 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 d0df762169..d2500d3c27 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 b42a0bfa17..ad4f624bac 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 } 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); @@ -424,12 +426,12 @@ export class ViewLayout extends Disposable implements IViewLayout { return this._scrollable.validateScrollPosition(scrollPosition); } - public setScrollPositionNow(position: INewScrollPosition): void { - this._scrollable.setScrollPositionNow(position); - } - - public setScrollPositionSmooth(position: INewScrollPosition): void { - this._scrollable.setScrollPositionSmooth(position); + public setScrollPosition(position: INewScrollPosition, type: ScrollType): void { + if (type === ScrollType.Immediate) { + this._scrollable.setScrollPositionNow(position); + } else { + this._scrollable.setScrollPositionSmooth(position); + } } public deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 7815a55f46..a7402fb3bd 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -934,7 +934,9 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render break; case CharCode.UTF8_BOM: - case CharCode.LINE_SEPARATOR_2028: + case CharCode.LINE_SEPARATOR: + case CharCode.PARAGRAPH_SEPARATOR: + case CharCode.NEXT_LINE: sb.write1(0xFFFD); break; diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index 741d9ecb78..0dfe991305 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 edd17ac3e8..010ead1328 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -3,18 +3,20 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; import { IScrollPosition, Scrollable } from 'vs/base/common/scrollable'; import * as strings from 'vs/base/common/strings'; 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 } from 'vs/editor/common/editorCommon'; -import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { IViewEventListener } from 'vs/editor/common/view/viewEvents'; +import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon'; +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, 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,9 +55,6 @@ export interface IViewLayout { getFutureViewport(): Viewport; validateScrollPosition(scrollPosition: INewScrollPosition): IScrollPosition; - setScrollPositionNow(position: INewScrollPosition): void; - setScrollPositionSmooth(position: INewScrollPosition): void; - deltaScrollNow(deltaScrollLeft: number, deltaScrollTop: number): void; getLinesViewportData(): IPartialViewLinesViewportData; getLinesViewportDataAtScrollTop(scrollTop: number): IPartialViewLinesViewportData; @@ -68,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 { @@ -95,20 +84,26 @@ export interface ICoordinatesConverter { modelPositionIsVisible(modelPosition: Position): boolean; } -export interface IViewModel { +export interface IViewModel extends ICursorSimpleModel { - addEventListener(listener: IViewEventListener): IDisposable; + 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; @@ -117,7 +112,7 @@ export interface IViewModel { getCompletelyVisibleViewRange(): Range; getCompletelyVisibleViewRangeAtScrollTop(scrollTop: number): Range; - getOptions(): TextModelResolvedOptions; + getTextModelOptions(): TextModelResolvedOptions; getLineCount(): number; getLineContent(lineNumber: number): string; getLineLength(lineNumber: number): number; @@ -140,6 +135,38 @@ export interface IViewModel { 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 0000000000..20dd21dd3e --- /dev/null +++ b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 b6a5be0209..c5dede786a 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 } 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'; @@ -24,24 +26,34 @@ import { ViewModelDecorations } from 'vs/editor/common/viewModel/viewModelDecora 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; - private readonly decorations: ViewModelDecorations; + private readonly _cursor: Cursor; + private readonly _decorations: ViewModelDecorations; constructor( editorId: number, @@ -53,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, @@ -86,51 +101,42 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - this.coordinatesConverter = this.lines.createCoordinatesConverter(); + this.coordinatesConverter = this._lines.createCoordinatesConverter(); - this.viewLayout = this._register(new ViewLayout(this.configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); + this._cursor = this._register(new Cursor(model, this, this.coordinatesConverter, this.cursorConfig)); + + this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); this._register(this.viewLayout.onDidScroll((e) => { if (e.scrollTopChanged) { this._tokenizeViewportSoon.schedule(); } - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewScrollChangedEvent(e)); - } finally { - this._endEmit(); - } + 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) => { - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewContentSizeChangedEvent(e)); - } finally { - this._endEmit(); - } + 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._beginEmit(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); this._onConfigurationChanged(eventsCollector, e); } finally { - this._endEmit(); + this._eventDispatcher.endEmitViewEvents(); } })); this._register(MinimapTokensColorTracker.getInstance().onDidChange(() => { - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewTokensColorsChangedEvent()); - } finally { - this._endEmit(); - } + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensColorsChangedEvent()); })); this._updateConfigurationViewLineCountNow(); @@ -140,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 { @@ -158,30 +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.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,17 +217,22 @@ 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.setScrollPositionNow({ scrollTop: viewPositionTop + this.viewportStartLineDelta }); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStartLineDelta }, ScrollType.Immediate); + } + + if (CursorConfiguration.shouldRecreate(e)) { + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); } } @@ -212,7 +240,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this._register(this.model.onDidChangeRawContentFast((e) => { try { - const eventsCollector = this._beginEmit(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); let hadOtherModelChange = false; let hadModelLineChangeThatChangedLineMapping = false; @@ -221,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: { @@ -243,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; @@ -263,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; @@ -275,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; @@ -296,32 +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.decorations.onLineMappingChanged(); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); } } finally { - this._endEmit(); + 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.setScrollPositionNow({ scrollTop: viewPositionTop + this.viewportStartLineDelta }); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStartLineDelta }, ScrollType.Immediate); } } + + try { + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + this._cursor.onModelContentChanged(eventsCollector, e); + } finally { + this._eventDispatcher.endEmitViewEvents(); + } })); this._register(this.model.onDidChangeTokens((e) => { @@ -335,12 +371,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel toLineNumber: viewEndLineNumber }; } - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewTokensChangedEvent(viewRanges)); - } finally { - this._endEmit(); - } + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensChangedEvent(viewRanges)); if (e.tokenizationSupportChanged) { this._tokenizeViewportSoon.schedule(); @@ -348,63 +379,65 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel })); this._register(this.model.onDidChangeLanguageConfiguration((e) => { - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewLanguageConfigurationEvent()); - } finally { - this._endEmit(); - } + 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.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.decorations.onLineMappingChanged(); - this.viewLayout.onFlushed(this.getLineCount()); + if (this._lines.setTabSize(this.model.getOptions().tabSize)) { try { - const eventsCollector = this._beginEmit(); - 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._endEmit(); + this._eventDispatcher.endEmitViewEvents(); } this._updateConfigurationViewLineCount.schedule(); } + + 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(); - try { - const eventsCollector = this._beginEmit(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(e)); - } finally { - this._endEmit(); - } + this._decorations.onModelDecorationsChanged(); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); })); } public setHiddenAreas(ranges: Range[]): void { try { - const eventsCollector = this._beginEmit(); - 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.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._endEmit(); + 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); @@ -423,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]; @@ -528,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 { @@ -589,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( @@ -615,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 @@ -627,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 { @@ -769,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}`; @@ -827,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 12af004c29..cddbeb2a0a 100644 --- a/src/vs/platform/files/node/files.ts +++ b/src/vs/editor/contrib/anchorSelect/anchorSelect.css @@ -3,5 +3,7 @@ * Licensed under the Source EULA. 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 0000000000..614b4c0335 --- /dev/null +++ b/src/vs/editor/contrib/anchorSelect/anchorSelect.ts @@ -0,0 +1,178 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 68616df131..153793cc21 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/colorPicker/colorPickerModel.ts b/src/vs/editor/contrib/colorPicker/colorPickerModel.ts index 781be66050..91fdc627e2 100644 --- a/src/vs/editor/contrib/colorPicker/colorPickerModel.ts +++ b/src/vs/editor/contrib/colorPicker/colorPickerModel.ts @@ -64,7 +64,7 @@ export class ColorPickerModel { guessColorPresentation(color: Color, originalText: string): void { for (let i = 0; i < this.colorPresentations.length; i++) { - if (originalText === this.colorPresentations[i].label) { + if (originalText.toLowerCase() === this.colorPresentations[i].label) { this.presentationIndex = i; this._onDidChangePresentation.fire(this.presentation); break; diff --git a/src/vs/editor/contrib/documentSymbols/outlineTree.ts b/src/vs/editor/contrib/documentSymbols/outlineTree.ts index 094f08dfec..62dfacb26f 100644 --- a/src/vs/editor/contrib/documentSymbols/outlineTree.ts +++ b/src/vs/editor/contrib/documentSymbols/outlineTree.ts @@ -335,11 +335,11 @@ export class OutlineItemComparator implements ITreeSorter { } else if (a instanceof OutlineElement && b instanceof OutlineElement) { if (this.type === OutlineSortOrder.ByKind) { - return a.symbol.kind - b.symbol.kind || this._collator.getValue().compare(a.symbol.name, b.symbol.name); + return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name); } else if (this.type === OutlineSortOrder.ByName) { - return this._collator.getValue().compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); + return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range); } else if (this.type === OutlineSortOrder.ByPosition) { - return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.getValue().compare(a.symbol.name, b.symbol.name); + return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name); } } return 0; diff --git a/src/vs/editor/contrib/find/findController.ts b/src/vs/editor/contrib/find/findController.ts index 5d51dab98e..3c08f4fdec 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/findWidget.css b/src/vs/editor/contrib/find/findWidget.css index ed533e2c15..35aef59279 100644 --- a/src/vs/editor/contrib/find/findWidget.css +++ b/src/vs/editor/contrib/find/findWidget.css @@ -126,10 +126,6 @@ justify-content: center; } -.monaco-editor .find-widget .button:not(.disabled):hover { - background-color: rgba(0, 0, 0, 0.1); -} - .monaco-editor .find-widget .button.left { margin-left: 0; margin-right: 3px; @@ -207,13 +203,7 @@ } .monaco-editor .find-widget .monaco-sash { - width: 2px !important; - margin-left: -4px; -} - -.monaco-editor.hc-black .find-widget .button:not(.disabled):hover, -.monaco-editor.vs-dark .find-widget .button:not(.disabled):hover { - background-color: rgba(255, 255, 255, 0.1); + left: 0 !important; } .monaco-editor.hc-black .find-widget .button:before { diff --git a/src/vs/editor/contrib/find/findWidget.ts b/src/vs/editor/contrib/find/findWidget.ts index c2d31236b0..5e46b7f9c0 100644 --- a/src/vs/editor/contrib/find/findWidget.ts +++ b/src/vs/editor/contrib/find/findWidget.ts @@ -14,7 +14,7 @@ import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox'; import { FindInput, IFindInputStyles } from 'vs/base/browser/ui/findinput/findInput'; import { IMessage as InputBoxMessage } from 'vs/base/browser/ui/inputbox/inputBox'; import { ReplaceInput } from 'vs/base/browser/ui/findinput/replaceInput'; -import { IHorizontalSashLayoutProvider, ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; +import { IVerticalSashLayoutProvider, ISashEvent, Orientation, Sash } from 'vs/base/browser/ui/sash/sash'; import { Widget } from 'vs/base/browser/ui/widget'; import { Delayer } from 'vs/base/common/async'; import { Color } from 'vs/base/common/color'; @@ -113,7 +113,7 @@ function stopPropagationForMultiLineDownwards(event: IKeyboardEvent, value: stri } } -export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSashLayoutProvider { +export class FindWidget extends Widget implements IOverlayWidget, IVerticalSashLayoutProvider { private static readonly ID = 'editor.contrib.findWidget'; private readonly _codeEditor: ICodeEditor; private readonly _state: FindReplaceState; @@ -903,16 +903,9 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas } // ----- sash - public getHorizontalSashTop(_sash: Sash): number { + public getVerticalSashLeft(_sash: Sash): number { return 0; } - public getHorizontalSashLeft?(_sash: Sash): number { - return 0; - } - public getHorizontalSashWidth?(_sash: Sash): number { - return 500; - } - // ----- initialization private _keybindingLabelFor(actionId: string): string { @@ -1173,7 +1166,7 @@ export class FindWidget extends Widget implements IOverlayWidget, IHorizontalSas this._domNode.appendChild(findPart); this._domNode.appendChild(replacePart); - this._resizeSash = new Sash(this._domNode, this, { orientation: Orientation.VERTICAL }); + this._resizeSash = new Sash(this._domNode, this, { orientation: Orientation.VERTICAL, size: 2 }); this._resized = false; let originalWidth = FIND_WIDGET_INITIAL_WIDTH; @@ -1365,11 +1358,11 @@ registerThemingParticipant((theme, collector) => { const resizeBorderBackground = theme.getColor(editorWidgetResizeBorder); if (resizeBorderBackground) { - collector.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${resizeBorderBackground}; width: 3px !important; margin-left: -4px;}`); + collector.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${resizeBorderBackground}; }`); } else { const border = theme.getColor(editorWidgetBorder); if (border) { - collector.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${border}; width: 3px !important; margin-left: -4px;}`); + collector.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${border}; }`); } } diff --git a/src/vs/editor/contrib/find/test/find.test.ts b/src/vs/editor/contrib/find/test/find.test.ts index 4884dbdc1f..3cf1791d29 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 8d93d1a824..55c8b5c899 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 5336d9da3a..b95370c37f 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/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 2618f9a3e6..55aca375b4 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -15,7 +15,7 @@ import { ITextModel } from 'vs/editor/common/model'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, registerInstantiatedEditorAction } from 'vs/editor/browser/editorExtensions'; import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateForType, toggleCollapseState, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel'; -import { FoldingDecorationProvider } from './foldingDecorations'; +import { FoldingDecorationProvider, foldingCollapsedIcon, foldingExpandedIcon } from './foldingDecorations'; import { FoldingRegions, FoldingRegion } from './foldingRanges'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; @@ -909,8 +909,8 @@ registerThemingParticipant((theme, collector) => { const editorFoldColor = theme.getColor(editorFoldForeground); if (editorFoldColor) { collector.addRule(` - .monaco-editor .cldr.codicon-chevron-right, - .monaco-editor .cldr.codicon-chevron-down { + .monaco-editor .cldr${foldingExpandedIcon.cssSelector}, + .monaco-editor .cldr${foldingCollapsedIcon.cssSelector} { color: ${editorFoldColor} !important; } `); diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index dce66628f4..f7d7c5055a 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -9,8 +9,8 @@ import { IDecorationProvider } from 'vs/editor/contrib/folding/foldingModel'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; -const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown); -const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight); +export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown); +export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight); export class FoldingDecorationProvider implements IDecorationProvider { diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 28b9071669..1eb0bfff18 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -4,12 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; -import { Emitter } from 'vs/base/common/event'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IMarker, IMarkerService, MarkerSeverity } from 'vs/platform/markers/common/markers'; +import { IMarker } from 'vs/platform/markers/common/markers'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; @@ -18,194 +17,32 @@ import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { MarkerNavigationWidget } from './gotoErrorWidget'; -import { compare } from 'vs/base/common/strings'; -import { binarySearch, find } from 'vs/base/common/arrays'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { isEqual } from 'vs/base/common/resources'; +import { MenuId } from 'vs/platform/actions/common/actions'; import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; import { Codicon, registerIcon } from 'vs/base/common/codicons'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; - -class MarkerModel { - - private readonly _editor: ICodeEditor; - private _markers: IMarker[]; - private _nextIdx: number; - private readonly _toUnbind = new DisposableStore(); - private _ignoreSelectionChange: boolean; - private readonly _onCurrentMarkerChanged: Emitter; - private readonly _onMarkerSetChanged: Emitter; - - constructor(editor: ICodeEditor, markers: IMarker[]) { - this._editor = editor; - this._markers = []; - this._nextIdx = -1; - this._ignoreSelectionChange = false; - this._onCurrentMarkerChanged = new Emitter(); - this._onMarkerSetChanged = new Emitter(); - this.setMarkers(markers); - - // listen on editor - this._toUnbind.add(this._editor.onDidDispose(() => this.dispose())); - this._toUnbind.add(this._editor.onDidChangeCursorPosition(() => { - if (this._ignoreSelectionChange) { - return; - } - if (this.currentMarker && this._editor.getPosition() && Range.containsPosition(this.currentMarker, this._editor.getPosition()!)) { - return; - } - this._nextIdx = -1; - })); - } - - public get onCurrentMarkerChanged() { - return this._onCurrentMarkerChanged.event; - } - - public get onMarkerSetChanged() { - return this._onMarkerSetChanged.event; - } - - public setMarkers(markers: IMarker[]): void { - - let oldMarker = this._nextIdx >= 0 ? this._markers[this._nextIdx] : undefined; - this._markers = markers || []; - this._markers.sort(MarkerNavigationAction.compareMarker); - if (!oldMarker) { - this._nextIdx = -1; - } else { - this._nextIdx = Math.max(-1, binarySearch(this._markers, oldMarker, MarkerNavigationAction.compareMarker)); - } - this._onMarkerSetChanged.fire(this); - } - - public withoutWatchingEditorPosition(callback: () => void): void { - this._ignoreSelectionChange = true; - try { - callback(); - } finally { - this._ignoreSelectionChange = false; - } - } - - private _initIdx(fwd: boolean): void { - let found = false; - const position = this._editor.getPosition(); - for (let i = 0; i < this._markers.length; i++) { - let range = Range.lift(this._markers[i]); - - if (range.isEmpty() && this._editor.getModel()) { - const word = this._editor.getModel()!.getWordAtPosition(range.getStartPosition()); - if (word) { - range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn); - } - } - - if (position && (range.containsPosition(position) || position.isBeforeOrEqual(range.getStartPosition()))) { - this._nextIdx = i; - found = true; - break; - } - } - if (!found) { - // after the last change - this._nextIdx = fwd ? 0 : this._markers.length - 1; - } - if (this._nextIdx < 0) { - this._nextIdx = this._markers.length - 1; - } - } - - get currentMarker(): IMarker | undefined { - return this.canNavigate() ? this._markers[this._nextIdx] : undefined; - } - - set currentMarker(marker: IMarker | undefined) { - const idx = this._nextIdx; - this._nextIdx = -1; - if (marker) { - this._nextIdx = this.indexOf(marker); - } - if (this._nextIdx !== idx) { - this._onCurrentMarkerChanged.fire(marker); - } - } - - public move(fwd: boolean, inCircles: boolean): boolean { - if (!this.canNavigate()) { - this._onCurrentMarkerChanged.fire(undefined); - return !inCircles; - } - - let oldIdx = this._nextIdx; - let atEdge = false; - - if (this._nextIdx === -1) { - this._initIdx(fwd); - - } else if (fwd) { - if (inCircles || this._nextIdx + 1 < this._markers.length) { - this._nextIdx = (this._nextIdx + 1) % this._markers.length; - } else { - atEdge = true; - } - - } else if (!fwd) { - if (inCircles || this._nextIdx > 0) { - this._nextIdx = (this._nextIdx - 1 + this._markers.length) % this._markers.length; - } else { - atEdge = true; - } - } - - if (oldIdx !== this._nextIdx) { - const marker = this._markers[this._nextIdx]; - this._onCurrentMarkerChanged.fire(marker); - } - - return atEdge; - } - - public canNavigate(): boolean { - return this._markers.length > 0; - } - - public findMarkerAtPosition(pos: Position): IMarker | undefined { - return find(this._markers, marker => Range.containsPosition(marker, pos)); - } - - public get total() { - return this._markers.length; - } - - public indexOf(marker: IMarker): number { - return 1 + this._markers.indexOf(marker); - } - - public dispose(): void { - this._toUnbind.dispose(); - } -} +import { IMarkerNavigationService, MarkerList } from 'vs/editor/contrib/gotoError/markerNavigationService'; export class MarkerController implements IEditorContribution { - public static readonly ID = 'editor.contrib.markerController'; + static readonly ID = 'editor.contrib.markerController'; - public static get(editor: ICodeEditor): MarkerController { + static get(editor: ICodeEditor): MarkerController { return editor.getContribution(MarkerController.ID); } private readonly _editor: ICodeEditor; - private _model: MarkerModel | null = null; - private _widget: MarkerNavigationWidget | null = null; + private readonly _widgetVisible: IContextKey; - private readonly _disposeOnClose = new DisposableStore(); + private readonly _sessionDispoables = new DisposableStore(); + + private _model?: MarkerList; + private _widget?: MarkerNavigationWidget; constructor( editor: ICodeEditor, - @IMarkerService private readonly _markerService: IMarkerService, + @IMarkerNavigationService private readonly _markerNavigationService: IMarkerNavigationService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @ICodeEditorService private readonly _editorService: ICodeEditorService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -214,195 +51,134 @@ export class MarkerController implements IEditorContribution { this._widgetVisible = CONTEXT_MARKERS_NAVIGATION_VISIBLE.bindTo(this._contextKeyService); } - public dispose(): void { + dispose(): void { this._cleanUp(); - this._disposeOnClose.dispose(); + this._sessionDispoables.dispose(); } private _cleanUp(): void { this._widgetVisible.reset(); - this._disposeOnClose.clear(); - this._widget = null; - this._model = null; + this._sessionDispoables.clear(); + this._widget = undefined; + this._model = undefined; } - public getOrCreateModel(): MarkerModel { + private _getOrCreateModel(uri: URI | undefined): MarkerList { - if (this._model) { + if (this._model && this._model.matches(uri)) { return this._model; } + let reusePosition = false; + if (this._model) { + reusePosition = true; + this._cleanUp(); + } - const markers = this._getMarkers(); - this._model = new MarkerModel(this._editor, markers); - this._markerService.onMarkerChanged(this._onMarkerChanged, this, this._disposeOnClose); + this._model = this._markerNavigationService.getMarkerList(uri); + if (reusePosition) { + this._model.move(true, this._editor.getModel()!, this._editor.getPosition()!); + } this._widget = this._instantiationService.createInstance(MarkerNavigationWidget, this._editor); + this._widget.onDidClose(() => this.close(), this, this._sessionDispoables); this._widgetVisible.set(true); - this._widget.onDidClose(() => this.closeMarkersNavigation(), this, this._disposeOnClose); - this._disposeOnClose.add(this._model); - this._disposeOnClose.add(this._widget); + this._sessionDispoables.add(this._model); + this._sessionDispoables.add(this._widget); - this._disposeOnClose.add(this._widget.onDidSelectRelatedInformation(related => { - this._editorService.openCodeEditor({ - resource: related.resource, - options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() } - }, this._editor).then(undefined, onUnexpectedError); - this.closeMarkersNavigation(false); - })); - this._disposeOnClose.add(this._editor.onDidChangeModel(() => this._cleanUp())); - - this._disposeOnClose.add(this._model.onCurrentMarkerChanged(marker => { - if (!marker || !this._model) { - this._cleanUp(); - } else { - this._model.withoutWatchingEditorPosition(() => { - if (!this._widget || !this._model) { - return; - } - this._widget.showAtMarker(marker, this._model.indexOf(marker), this._model.total); - }); + // follow cursor + this._sessionDispoables.add(this._editor.onDidChangeCursorPosition(e => { + if (!this._model?.selected || !Range.containsPosition(this._model?.selected.marker, e.position)) { + this._model?.resetIndex(); } })); - this._disposeOnClose.add(this._model.onMarkerSetChanged(() => { + + // update markers + this._sessionDispoables.add(this._model.onDidChange(() => { if (!this._widget || !this._widget.position || !this._model) { return; } - - const marker = this._model.findMarkerAtPosition(this._widget.position); - if (marker) { - this._widget.updateMarker(marker); + const info = this._model.find(this._editor.getModel()!.uri, this._widget!.position!); + if (info) { + this._widget.updateMarker(info.marker); } else { this._widget.showStale(); } })); + // open related + this._sessionDispoables.add(this._widget.onDidSelectRelatedInformation(related => { + this._editorService.openCodeEditor({ + resource: related.resource, + options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() } + }, this._editor); + this.close(false); + })); + this._sessionDispoables.add(this._editor.onDidChangeModel(() => this._cleanUp())); + return this._model; } - public closeMarkersNavigation(focusEditor: boolean = true): void { + close(focusEditor: boolean = true): void { this._cleanUp(); if (focusEditor) { this._editor.focus(); } } - public show(marker: IMarker): void { - const model = this.getOrCreateModel(); - model.currentMarker = marker; + showAtMarker(marker: IMarker): void { + if (this._editor.hasModel()) { + const model = this._getOrCreateModel(this._editor.getModel().uri); + model.resetIndex(); + model.move(true, this._editor.getModel(), new Position(marker.startLineNumber, marker.startColumn)); + if (model.selected) { + this._widget!.showAtMarker(model.selected.marker, model.selected.index, model.selected.total); + } + } } - private _onMarkerChanged(changedResources: readonly URI[]): void { - const editorModel = this._editor.getModel(); - if (!editorModel) { - return; - } + async nagivate(next: boolean, multiFile: boolean) { + if (this._editor.hasModel()) { + const model = this._getOrCreateModel(multiFile ? undefined : this._editor.getModel().uri); + model.move(next, this._editor.getModel(), this._editor.getPosition()); + if (!model.selected) { + return; + } + if (model.selected.marker.resource.toString() !== this._editor.getModel().uri.toString()) { + // show in different editor + this._cleanUp(); + const otherEditor = await this._editorService.openCodeEditor({ + resource: model.selected.marker.resource, + options: { pinned: false, revealIfOpened: true, selectionRevealType: TextEditorSelectionRevealType.NearTop, selection: model.selected.marker } + }, this._editor); - if (!this._model) { - return; - } + if (otherEditor) { + MarkerController.get(otherEditor).close(); + MarkerController.get(otherEditor).nagivate(next, multiFile); + } - if (!changedResources.some(r => isEqual(editorModel.uri, r))) { - return; + } else { + // show in this editor + this._widget!.showAtMarker(model.selected.marker, model.selected.index, model.selected.total); + } } - this._model.setMarkers(this._getMarkers()); - } - - private _getMarkers(): IMarker[] { - let model = this._editor.getModel(); - if (!model) { - return []; - } - - return this._markerService.read({ - resource: model.uri, - severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info - }); } } class MarkerNavigationAction extends EditorAction { - private readonly _isNext: boolean; - - private readonly _multiFile: boolean; - - constructor(next: boolean, multiFile: boolean, opts: IActionOptions) { + constructor( + private readonly _next: boolean, + private readonly _multiFile: boolean, + opts: IActionOptions + ) { super(opts); - this._isNext = next; - this._multiFile = multiFile; } - public run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - - const markerService = accessor.get(IMarkerService); - const editorService = accessor.get(ICodeEditorService); - const controller = MarkerController.get(editor); - if (!controller) { - return Promise.resolve(undefined); + async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise { + if (editor.hasModel()) { + MarkerController.get(editor).nagivate(this._next, this._multiFile); } - - const model = controller.getOrCreateModel(); - const atEdge = model.move(this._isNext, !this._multiFile); - if (!atEdge || !this._multiFile) { - return Promise.resolve(undefined); - } - - // try with the next/prev file - let markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info }).sort(MarkerNavigationAction.compareMarker); - if (markers.length === 0) { - return Promise.resolve(undefined); - } - - const editorModel = editor.getModel(); - if (!editorModel) { - return Promise.resolve(undefined); - } - - let oldMarker = model.currentMarker || { resource: editorModel!.uri, severity: MarkerSeverity.Error, startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }; - let idx = binarySearch(markers, oldMarker, MarkerNavigationAction.compareMarker); - if (idx < 0) { - // find best match... - idx = ~idx; - idx %= markers.length; - } else if (this._isNext) { - idx = (idx + 1) % markers.length; - } else { - idx = (idx + markers.length - 1) % markers.length; - } - - let newMarker = markers[idx]; - if (isEqual(newMarker.resource, editorModel.uri)) { - // the next `resource` is this resource which - // means we cycle within this file - model.move(this._isNext, true); - return Promise.resolve(undefined); - } - - // close the widget for this editor-instance, open the resource - // for the next marker and re-start marker navigation in there - controller.closeMarkersNavigation(); - - return editorService.openCodeEditor({ - resource: newMarker.resource, - options: { pinned: false, revealIfOpened: true, selectionRevealType: TextEditorSelectionRevealType.NearTop, selection: newMarker } - }, editor).then(editor => { - if (!editor) { - return undefined; - } - return editor.getAction(this.id).run(); - }); - } - - static compareMarker(a: IMarker, b: IMarker): number { - let res = compare(a.resource.toString(), b.resource.toString()); - if (res === 0) { - res = MarkerSeverity.compare(a.severity, b.severity); - } - if (res === 0) { - res = Range.compareRangesUsingStarts(a, b); - } - return res; } } @@ -467,6 +243,12 @@ class NextMarkerInFilesAction extends MarkerNavigationAction { kbExpr: EditorContextKeys.focus, primary: KeyCode.F8, weight: KeybindingWeight.EditorContrib + }, + menuOpts: { + menuId: MenuId.MenubarGoMenu, + title: nls.localize({ key: 'miGotoNextProblem', comment: ['&& denotes a mnemonic'] }, "Next &&Problem"), + group: '6_problem_nav', + order: 1 } }); } @@ -483,6 +265,12 @@ class PrevMarkerInFilesAction extends MarkerNavigationAction { kbExpr: EditorContextKeys.focus, primary: KeyMod.Shift | KeyCode.F8, weight: KeybindingWeight.EditorContrib + }, + menuOpts: { + menuId: MenuId.MenubarGoMenu, + title: nls.localize({ key: 'miGotoPreviousProblem', comment: ['&& denotes a mnemonic'] }, "Previous &&Problem"), + group: '6_problem_nav', + order: 2 } }); } @@ -501,7 +289,7 @@ const MarkerCommand = EditorCommand.bindToContribution(MarkerC registerEditorCommand(new MarkerCommand({ id: 'closeMarkersNavigation', precondition: CONTEXT_MARKERS_NAVIGATION_VISIBLE, - handler: x => x.closeMarkersNavigation(), + handler: x => x.close(), kbOpts: { weight: KeybindingWeight.EditorContrib + 50, kbExpr: EditorContextKeys.focus, @@ -509,22 +297,3 @@ registerEditorCommand(new MarkerCommand({ secondary: [KeyMod.Shift | KeyCode.Escape] } })); - -// Go to menu -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '6_problem_nav', - command: { - id: 'editor.action.marker.nextInFiles', - title: nls.localize({ key: 'miGotoNextProblem', comment: ['&& denotes a mnemonic'] }, "Next &&Problem") - }, - order: 1 -}); - -MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, { - group: '6_problem_nav', - command: { - id: 'editor.action.marker.prevInFiles', - title: nls.localize({ key: 'miGotoPreviousProblem', comment: ['&& denotes a mnemonic'] }, "Previous &&Problem") - }, - order: 2 -}); diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 3910aaebaa..d0fe8db126 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -8,7 +8,6 @@ import * as nls from 'vs/nls'; import * as dom from 'vs/base/browser/dom'; import { dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers'; -import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { registerColor, oneOf, textLinkForeground, editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoForeground, editorInfoBorder } from 'vs/platform/theme/common/colorRegistry'; @@ -296,6 +295,9 @@ export class MarkerNavigationWidget extends PeekViewWidget { protected _fillHead(container: HTMLElement): void { super._fillHead(container); + + this._disposables.add(this._actionbarWidget!.actionRunner.onDidBeforeRun(e => this.editor.focus())); + const actions: IAction[] = []; const menu = this._menuService.createMenu(MarkerNavigationWidget.TitleMenu, this._contextKeyService); createAndFillInActionBarActions(menu, undefined, actions); @@ -327,7 +329,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._disposables.add(this._message); } - show(where: Position, heightInLines: number): void { + show(): void { throw new Error('call showAtMarker'); } diff --git a/src/vs/editor/contrib/gotoError/markerNavigationService.ts b/src/vs/editor/contrib/gotoError/markerNavigationService.ts new file mode 100644 index 0000000000..53d54b2ed4 --- /dev/null +++ b/src/vs/editor/contrib/gotoError/markerNavigationService.ts @@ -0,0 +1,217 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IMarkerService, MarkerSeverity, IMarker } from 'vs/platform/markers/common/markers'; +import { URI } from 'vs/base/common/uri'; +import { Emitter, Event } from 'vs/base/common/event'; +import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { compare } from 'vs/base/common/strings'; +import { binarySearch } from 'vs/base/common/arrays'; +import { ITextModel } from 'vs/editor/common/model'; +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { LinkedList } from 'vs/base/common/linkedList'; + +export class MarkerCoordinate { + constructor( + readonly marker: IMarker, + readonly index: number, + readonly total: number + ) { } +} + +export class MarkerList { + + private readonly _onDidChange = new Emitter(); + readonly onDidChange: Event = this._onDidChange.event; + + private readonly _resourceFilter?: (uri: URI) => boolean; + private readonly _dispoables = new DisposableStore(); + + private _markers: IMarker[] = []; + private _nextIdx: number = -1; + + constructor( + resourceFilter: URI | ((uri: URI) => boolean) | undefined, + @IMarkerService private readonly _markerService: IMarkerService, + ) { + if (URI.isUri(resourceFilter)) { + this._resourceFilter = uri => uri.toString() === resourceFilter.toString(); + } else if (resourceFilter) { + this._resourceFilter = resourceFilter; + } + + const updateMarker = () => { + this._markers = this._markerService.read({ + resource: URI.isUri(resourceFilter) ? resourceFilter : undefined, + severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info + }); + if (typeof resourceFilter === 'function') { + this._markers = this._markers.filter(m => this._resourceFilter!(m.resource)); + } + this._markers.sort(MarkerList._compareMarker); + }; + + updateMarker(); + + this._dispoables.add(_markerService.onMarkerChanged(uris => { + if (!this._resourceFilter || uris.some(uri => this._resourceFilter!(uri))) { + updateMarker(); + this._nextIdx = -1; + this._onDidChange.fire(); + } + })); + } + + dispose(): void { + this._dispoables.dispose(); + this._onDidChange.dispose(); + } + + matches(uri: URI | undefined) { + if (!this._resourceFilter && !uri) { + return true; + } + if (!this._resourceFilter || !uri) { + return false; + } + return this._resourceFilter(uri); + } + + get selected(): MarkerCoordinate | undefined { + const marker = this._markers[this._nextIdx]; + return marker && new MarkerCoordinate(marker, this._nextIdx + 1, this._markers.length); + } + + private _initIdx(model: ITextModel, position: Position, fwd: boolean): void { + let found = false; + + let idx = this._markers.findIndex(marker => marker.resource.toString() === model.uri.toString()); + if (idx < 0) { + idx = binarySearch(this._markers, { resource: model.uri }, (a, b) => compare(a.resource.toString(), b.resource.toString())); + if (idx < 0) { + idx = ~idx; + } + } + + for (let i = idx; i < this._markers.length; i++) { + let range = Range.lift(this._markers[i]); + + if (range.isEmpty()) { + const word = model.getWordAtPosition(range.getStartPosition()); + if (word) { + range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn); + } + } + + if (position && (range.containsPosition(position) || position.isBeforeOrEqual(range.getStartPosition()))) { + this._nextIdx = i; + found = true; + break; + } + + if (this._markers[i].resource.toString() !== model.uri.toString()) { + break; + } + } + + if (!found) { + // after the last change + this._nextIdx = fwd ? 0 : this._markers.length - 1; + } + if (this._nextIdx < 0) { + this._nextIdx = this._markers.length - 1; + } + } + + resetIndex() { + this._nextIdx = -1; + } + + move(fwd: boolean, model: ITextModel, position: Position): boolean { + if (this._markers.length === 0) { + return false; + } + + let oldIdx = this._nextIdx; + if (this._nextIdx === -1) { + this._initIdx(model, position, fwd); + } else if (fwd) { + this._nextIdx = (this._nextIdx + 1) % this._markers.length; + } else if (!fwd) { + this._nextIdx = (this._nextIdx - 1 + this._markers.length) % this._markers.length; + } + + if (oldIdx !== this._nextIdx) { + return true; + } + return false; + } + + find(uri: URI, position: Position): MarkerCoordinate | undefined { + let idx = this._markers.findIndex(marker => marker.resource.toString() === uri.toString()); + if (idx < 0) { + return undefined; + } + for (; idx < this._markers.length; idx++) { + if (Range.containsPosition(this._markers[idx], position)) { + return new MarkerCoordinate(this._markers[idx], idx + 1, this._markers.length); + } + } + return undefined; + } + + private static _compareMarker(a: IMarker, b: IMarker): number { + let res = compare(a.resource.toString(), b.resource.toString()); + if (res === 0) { + res = MarkerSeverity.compare(a.severity, b.severity); + } + if (res === 0) { + res = Range.compareRangesUsingStarts(a, b); + } + return res; + } +} + +export const IMarkerNavigationService = createDecorator('IMarkerNavigationService'); + +export interface IMarkerNavigationService { + readonly _serviceBrand: undefined; + registerProvider(provider: IMarkerListProvider): IDisposable; + getMarkerList(resource: URI | undefined): MarkerList; +} + +export interface IMarkerListProvider { + getMarkerList(resource: URI | undefined): MarkerList | undefined; +} + +class MarkerNavigationService implements IMarkerNavigationService, IMarkerListProvider { + + readonly _serviceBrand: undefined; + + private readonly _provider = new LinkedList(); + + constructor(@IMarkerService private readonly _markerService: IMarkerService) { } + + registerProvider(provider: IMarkerListProvider): IDisposable { + const remove = this._provider.unshift(provider); + return toDisposable(() => remove()); + } + + getMarkerList(resource: URI | undefined): MarkerList { + for (let provider of this._provider) { + const result = provider.getMarkerList(resource); + if (result) { + return result; + } + } + // default + return new MarkerList(resource, this._markerService); + } +} + +registerSingleton(IMarkerNavigationService, MarkerNavigationService, true); diff --git a/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css b/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css index 0b68c48a91..f78e4d548a 100644 --- a/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css +++ b/src/vs/editor/contrib/gotoError/media/gotoErrorWidget.css @@ -32,7 +32,7 @@ user-select: text; -webkit-user-select: text; -ms-user-select: text; - padding: 8px 12px 0px 20px; + padding: 8px 12px 0 20px; } .monaco-editor .marker-widget .descriptioncontainer .message { diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts index abf3b12ec3..c37726638d 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesTree.ts @@ -36,7 +36,7 @@ export class DataSource implements IAsyncDataSource 1) { + if (len > 1) { this.badge.setTitleFormat(localize('referencesCount', "{0} references", len)); } else { this.badge.setTitleFormat(localize('referenceCount', "{0} reference", len)); @@ -174,8 +171,7 @@ class OneReferenceTemplate { } set(element: OneReference, score?: FuzzyScore): void { - const filePreview = element.parent.preview; - const preview = filePreview && filePreview.preview(element.range); + const preview = element.parent.getPreview(element)?.preview(element.range); if (!preview) { // this means we FAILED to resolve the document... this.label.set(`${basename(element.uri)}:${element.range.startLineNumber + 1}:${element.range.startColumn + 1}`); diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesWidget.ts index 3f0ba69d27..5fd133809b 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 ce83d9b8e8..49cfa043d4 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 } 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'; @@ -16,6 +16,8 @@ import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/r import { Position } from 'vs/editor/common/core/position'; import { IMatch } from 'vs/base/common/filters'; import { Constants } from 'vs/base/common/uint'; +import { ResourceMap } from 'vs/base/common/map'; +import { onUnexpectedError } from 'vs/base/common/errors'; export class OneReference { @@ -24,14 +26,11 @@ export class OneReference { constructor( readonly isProviderFirst: boolean, readonly parent: FileReferences, + readonly uri: URI, private _range: IRange, private _rangeCallback: (ref: OneReference) => void ) { } - get uri(): URI { - return this.parent.uri; - } - get range(): IRange { return this._range; } @@ -86,9 +85,7 @@ export class FileReferences implements IDisposable { readonly children: OneReference[] = []; - private _preview?: FilePreview; - private _resolved?: boolean; - private _loadFailure?: any; + private _previews = new ResourceMap(); constructor( readonly parent: ReferencesModel, @@ -96,16 +93,12 @@ export class FileReferences implements IDisposable { ) { } dispose(): void { - dispose(this._preview); - this._preview = undefined; + dispose(this._previews.values()); + this._previews.clear(); } - get preview(): FilePreview | undefined { - return this._preview; - } - - get failure(): any { - return this._loadFailure; + getPreview(child: OneReference): FilePreview | undefined { + return this._previews.get(child.uri); } get ariaMessage(): string { @@ -117,31 +110,22 @@ export class FileReferences implements IDisposable { } } - resolve(textModelResolverService: ITextModelService): Promise { - - if (this._resolved) { - return Promise.resolve(this); + async resolve(textModelResolverService: ITextModelService): Promise { + if (this._previews.size !== 0) { + return this; } - - return Promise.resolve(textModelResolverService.createModelReference(this.uri).then(modelReference => { - const model = modelReference.object; - - if (!model) { - modelReference.dispose(); - throw new Error(); + for (let child of this.children) { + if (this._previews.has(child.uri)) { + continue; } - - this._preview = new FilePreview(modelReference); - this._resolved = true; - return this; - - }, err => { - // something wrong here - this.children.length = 0; - this._resolved = true; - this._loadFailure = err; - return this; - })); + try { + const ref = await textModelResolverService.createModelReference(child.uri); + this._previews.set(child.uri, new FilePreview(ref)); + } catch (err) { + onUnexpectedError(err); + } + } + return this; } } @@ -167,17 +151,20 @@ export class ReferencesModel implements IDisposable { let current: FileReferences | undefined; for (let link of links) { - if (!current || current.uri.toString() !== link.uri.toString()) { + if (!current || !extUri.isEqual(current.uri, link.uri, true)) { // new group current = new FileReferences(this, link.uri); this.groups.push(current); } // append, check for equality first! - if (current.children.length === 0 || !Range.equalsRange(link.range, current.children[current.children.length - 1].range)) { + if (current.children.length === 0 || ReferencesModel._compareReferences(link, current.children[current.children.length - 1]) !== 0) { const oneRef = new OneReference( - providersFirst === link, current, link.targetSelectionRange || link.range, + providersFirst === link, + current, + link.uri, + link.targetSelectionRange || link.range, ref => this._onDidChangeReferenceRange.fire(ref) ); this.references.push(oneRef); @@ -294,6 +281,6 @@ export class ReferencesModel implements IDisposable { } private static _compareReferences(a: Location, b: Location): number { - return strings.compare(a.uri.toString(), b.uri.toString()) || 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 5642e4c6d5..4e0299a26b 100644 --- a/src/vs/editor/contrib/hover/hover.ts +++ b/src/vs/editor/contrib/hover/hover.ts @@ -3,7 +3,6 @@ * Licensed under the Source EULA. 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'; @@ -205,7 +204,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._markerDecorationsService, this._keybindingService, this._themeService, this._modeService, this._openerService); this._glyphWidget.value = new ModesGlyphHoverWidget(this._editor, this._modeService, this._openerService); } @@ -312,29 +311,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 390901ba4b..ff21e2cb3e 100644 --- a/src/vs/editor/contrib/hover/hoverWidgets.ts +++ b/src/vs/editor/contrib/hover/hoverWidgets.ts @@ -3,27 +3,27 @@ * Licensed under the Source EULA. 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'; 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 +34,23 @@ 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 _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,7 +76,7 @@ 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 { @@ -97,7 +91,7 @@ export class ContentHoverWidget extends Widget implements IContentWidget { this._editor.render(); this._stoleFocus = focus; if (focus) { - this._containerDomNode.focus(); + this._hover.containerDomNode.focus(); } } @@ -134,31 +128,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 +173,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 +194,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 0c8bf3d5e3..c6cb4b38fd 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -213,12 +213,12 @@ export class ModesContentHoverWidget extends ContentHoverWidget { constructor( editor: ICodeEditor, 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, keybindingService); this._messages = []; this._lastRange = null; @@ -249,7 +249,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 +461,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,12 +562,12 @@ 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: () => { this.hide(); - MarkerController.get(this._editor).show(markerHover.marker); + MarkerController.get(this._editor).showAtMarker(markerHover.marker); this._editor.focus(); } })); @@ -600,7 +600,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 +633,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,7 +664,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 03dee6085a..22d61d037d 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 41b7b0eab9..7b3d711b5e 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 7ccaa55a7f..23ac1387bf 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 4ee15a6de6..6981244846 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 76deca95df..f5f96aadc4 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 cab63c6f06..70b396a8c1 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; @@ -296,10 +297,10 @@ export class ParameterHintsWidget extends Disposable implements IContentWidget { private getParameterLabel(signature: modes.SignatureInformation, paramIdx: number): string { const param = signature.parameters[paramIdx]; - if (typeof param.label === 'string') { - return param.label; - } else { + if (Array.isArray(param.label)) { return signature.label.substring(param.label[0], param.label[1]); + } else { + return param.label; } } diff --git a/src/vs/editor/contrib/rename/onTypeRename.ts b/src/vs/editor/contrib/rename/onTypeRename.ts index 25da817b2c..0b588a6903 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 483bcbea77..cff426fffa 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 @@ -182,7 +202,7 @@ class RenameController implements IEditorContribution { } const supportPreview = this._bulkEditService.hasPreviewHandler() && this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); - const inputFieldResult = await this._renameInputField.getValue().getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, this._cts.token); + const inputFieldResult = await this._renameInputField.value.getInput(loc.range, loc.text, selectionStart, selectionEnd, supportPreview, this._cts.token); // no result, only hint to focus the editor or not if (typeof inputFieldResult === 'boolean') { @@ -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; @@ -230,11 +250,11 @@ class RenameController implements IEditorContribution { } acceptRenameInput(wantsPreview: boolean): void { - this._renameInputField.getValue().acceptInput(wantsPreview); + this._renameInputField.value.acceptInput(wantsPreview); } cancelRenameInput(): void { - this._renameInputField.getValue().cancelInput(true); + this._renameInputField.value.cancelInput(true); } } diff --git a/src/vs/editor/contrib/rename/renameInputField.ts b/src/vs/editor/contrib/rename/renameInputField.ts index 33b4d0bc49..0275ad5e34 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 2892fd0d5a..9159160e5e 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 591c292a3f..aedf4866b0 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/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index cbbdeb904b..d6f5cbd568 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -94,35 +94,40 @@ export class CompletionItem { this.isInvalid = this.isInvalid || Range.spansMultipleLines(completion.range.insert) || Range.spansMultipleLines(completion.range.replace) || completion.range.insert.startLineNumber !== position.lineNumber || completion.range.replace.startLineNumber !== position.lineNumber - || Range.compareRangesUsingStarts(completion.range.insert, completion.range.replace) !== 0; + || completion.range.insert.startColumn !== completion.range.replace.startColumn; } // create the suggestion resolver if (typeof provider.resolveCompletionItem !== 'function') { this._resolveCache = Promise.resolve(); + this._isResolved = true; } } // resolving get isResolved() { - return Boolean(this._resolveCache); + return Boolean(this._isResolved); } private _resolveCache?: Promise; + private _isResolved?: boolean; async resolve(token: CancellationToken) { if (!this._resolveCache) { const sub = token.onCancellationRequested(() => { this._resolveCache = undefined; + this._isResolved = false; }); this._resolveCache = Promise.resolve(this.provider.resolveCompletionItem!(this.completion, token)).then(value => { Object.assign(this.completion, value); + this._isResolved = true; sub.dispose(); }, err => { if (isPromiseCanceledError(err)) { // the IPC queue will reject the request with the // cancellation error -> reset cached this._resolveCache = undefined; + this._isResolved = false; } }); } @@ -179,7 +184,7 @@ export async function provideSuggestionItems( if (!container) { return; } - for (let suggestion of container.suggestions || []) { + for (let suggestion of container.suggestions) { if (!options.kindFilter.has(suggestion.kind)) { // fill in default range when missing if (!suggestion.range) { @@ -199,18 +204,16 @@ export async function provideSuggestionItems( // ask for snippets in parallel to asking "real" providers. Only do something if configured to // do so - no snippet filter, no special-providers-only request - const snippetCompletions = new Promise((resolve, reject) => { + const snippetCompletions = (async () => { if (!_snippetSuggestSupport || options.kindFilter.has(modes.CompletionItemKind.Snippet)) { - resolve(); + return; } if (options.providerFilter.size > 0 && !options.providerFilter.has(_snippetSuggestSupport)) { - resolve(); + return; } - Promise.resolve(_snippetSuggestSupport.provideCompletionItems(model, position, context, token)).then(list => { - onCompletionList(_snippetSuggestSupport, list); - resolve(); - }, reject); - }); + const list = await _snippetSuggestSupport.provideCompletionItems(model, position, context, token); + onCompletionList(_snippetSuggestSupport, list); + })(); // add suggestions from contributed providers - providers are ordered in groups of // equal score and once a group produces a result the process stops diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 0da204fcb4..d732e81a3d 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -39,6 +39,9 @@ import { TrackedRangeStickiness, ITextModel } from 'vs/editor/common/model'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; import * as platform from 'vs/base/common/platform'; import { MenuRegistry } from 'vs/platform/actions/common/actions'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { ILogService } from 'vs/platform/log/common/log'; +import { StopWatch } from 'vs/base/common/stopwatch'; // sticky suggest widget which doesn't disappear on focus out and such let _sticky = false; @@ -116,6 +119,7 @@ export class SuggestController implements IEditorContribution { @ICommandService private readonly _commandService: ICommandService, @IContextKeyService private readonly _contextKeyService: IContextKeyService, @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ILogService private readonly _logService: ILogService, ) { this.editor = editor; this.model = new SuggestModel(this.editor, editorWorker); @@ -204,18 +208,18 @@ export class SuggestController implements IEditorContribution { this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor)); this._toDispose.add(this.model.onDidTrigger(e => { - this.widget.getValue().showTriggered(e.auto, e.shy ? 250 : 50); + this.widget.value.showTriggered(e.auto, e.shy ? 250 : 50); this._lineSuffix.value = new LineSuffix(this.editor.getModel()!, e.position); })); this._toDispose.add(this.model.onDidSuggest(e => { if (!e.shy) { let index = this._memoryService.select(this.editor.getModel()!, this.editor.getPosition()!, e.completionModel.items); - this.widget.getValue().showSuggestions(e.completionModel, index, e.isFrozen, e.auto); + this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.auto); } })); this._toDispose.add(this.model.onDidCancel(e => { if (!e.retrigger) { - this.widget.getValue().hideWidget(); + this.widget.value.hideWidget(); } })); this._toDispose.add(this.editor.onDidBlurEditorWidget(() => { @@ -248,7 +252,7 @@ export class SuggestController implements IEditorContribution { flags: InsertFlags ): void { if (!event || !event.item) { - this._alternatives.getValue().reset(); + this._alternatives.value.reset(); this.model.cancel(); this.model.clear(); return; @@ -260,7 +264,10 @@ export class SuggestController implements IEditorContribution { const model = this.editor.getModel(); const modelVersionNow = model.getAlternativeVersionId(); const { item } = event; - const { completion: suggestion } = item; + + // + const tasks: Promise[] = []; + const cts = new CancellationTokenSource(); // pushing undo stops *before* additional text edits and // *after* the main edit @@ -274,14 +281,75 @@ export class SuggestController implements IEditorContribution { // keep item in memory this._memoryService.memorize(model, this.editor.getPosition(), item); - const scrollState = StableEditorScrollState.capture(this.editor); - if (Array.isArray(suggestion.additionalTextEdits)) { - this.editor.executeEdits('suggestController.additionalTextEdits', suggestion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text))); + if (Array.isArray(item.completion.additionalTextEdits)) { + // sync additional edits + const scrollState = StableEditorScrollState.capture(this.editor); + this.editor.executeEdits( + 'suggestController.additionalTextEdits.sync', + item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)) + ); + scrollState.restoreRelativeVerticalPositionOfCursor(this.editor); + + } else if (!item.isResolved) { + // async additional edits + const sw = new StopWatch(true); + let position: IPosition | undefined; + + const docListener = model.onDidChangeContent(e => { + if (e.isFlush) { + cts.cancel(); + docListener.dispose(); + return; + } + for (let change of e.changes) { + const thisPosition = Range.getEndPosition(change.range); + if (!position || Position.isBefore(thisPosition, position)) { + position = thisPosition; + } + } + }); + + let oldFlags = flags; + flags |= InsertFlags.NoAfterUndoStop; + let didType = false; + let typeListener = this.editor.onWillType(() => { + typeListener.dispose(); + didType = true; + if (!(oldFlags & InsertFlags.NoAfterUndoStop)) { + this.editor.pushUndoStop(); + } + }); + + tasks.push(item.resolve(cts.token).then(() => { + if (!item.completion.additionalTextEdits || cts.token.isCancellationRequested) { + return false; + } + if (position && item.completion.additionalTextEdits.some(edit => Position.isBefore(position!, Range.getStartPosition(edit.range)))) { + return false; + } + if (didType) { + this.editor.pushUndoStop(); + } + const scrollState = StableEditorScrollState.capture(this.editor); + this.editor.executeEdits( + 'suggestController.additionalTextEdits.async', + item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)) + ); + scrollState.restoreRelativeVerticalPositionOfCursor(this.editor); + if (didType || !(oldFlags & InsertFlags.NoAfterUndoStop)) { + this.editor.pushUndoStop(); + } + return true; + }).then(applied => { + this._logService.trace('[suggest] async resolving of edits DONE (ms, applied?)', sw.elapsed(), applied); + docListener.dispose(); + typeListener.dispose(); + })); } - let { insertText } = suggestion; - if (!(suggestion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) { + let { insertText } = item.completion; + if (!(item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet)) { insertText = SnippetParser.escape(insertText); } @@ -290,34 +358,33 @@ export class SuggestController implements IEditorContribution { overwriteAfter: info.overwriteAfter, undoStopBefore: false, undoStopAfter: false, - adjustWhitespace: !(suggestion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace) + adjustWhitespace: !(item.completion.insertTextRules! & CompletionItemInsertTextRule.KeepWhitespace) }); - scrollState.restoreRelativeVerticalPositionOfCursor(this.editor); - if (!(flags & InsertFlags.NoAfterUndoStop)) { this.editor.pushUndoStop(); } - if (!suggestion.command) { + if (!item.completion.command) { // done this.model.cancel(); - this.model.clear(); - } else if (suggestion.command.id === TriggerSuggestAction.id) { + } else if (item.completion.command.id === TriggerSuggestAction.id) { // retigger this.model.trigger({ auto: true, shy: false }, true); } else { // exec command, done - this._commandService.executeCommand(suggestion.command.id, ...(suggestion.command.arguments ? [...suggestion.command.arguments] : [])) - .catch(onUnexpectedError) - .finally(() => this.model.clear()); // <- clear only now, keep commands alive + tasks.push(this._commandService.executeCommand(item.completion.command.id, ...(item.completion.command.arguments ? [...item.completion.command.arguments] : [])).catch(onUnexpectedError)); this.model.cancel(); } if (flags & InsertFlags.KeepAlternativeSuggestions) { - this._alternatives.getValue().set(event, next => { + this._alternatives.value.set(event, next => { + + // cancel resolving of additional edits + cts.cancel(); + // this is not so pretty. when inserting the 'next' // suggestion we undo until we are at the state at // which we were before inserting the previous suggestion... @@ -334,7 +401,13 @@ export class SuggestController implements IEditorContribution { }); } - this._alertCompletionItem(event.item); + this._alertCompletionItem(item); + + // clear only now - after all tasks are done + Promise.all(tasks).finally(() => { + this.model.clear(); + cts.dispose(); + }); } getOverwriteInfo(item: CompletionItem, toggleMode: boolean): { overwriteBefore: number, overwriteAfter: number } { @@ -440,7 +513,7 @@ export class SuggestController implements IEditorContribution { } acceptSelectedSuggestion(keepAlternativeSuggestions: boolean, alternativeOverwriteConfig: boolean): void { - const item = this.widget.getValue().getFocusedItem(); + const item = this.widget.value.getFocusedItem(); let flags = 0; if (keepAlternativeSuggestions) { flags |= InsertFlags.KeepAlternativeSuggestions; @@ -451,53 +524,53 @@ export class SuggestController implements IEditorContribution { this._insertSuggestion(item, flags); } acceptNextSuggestion() { - this._alternatives.getValue().next(); + this._alternatives.value.next(); } acceptPrevSuggestion() { - this._alternatives.getValue().prev(); + this._alternatives.value.prev(); } cancelSuggestWidget(): void { this.model.cancel(); this.model.clear(); - this.widget.getValue().hideWidget(); + this.widget.value.hideWidget(); } selectNextSuggestion(): void { - this.widget.getValue().selectNext(); + this.widget.value.selectNext(); } selectNextPageSuggestion(): void { - this.widget.getValue().selectNextPage(); + this.widget.value.selectNextPage(); } selectLastSuggestion(): void { - this.widget.getValue().selectLast(); + this.widget.value.selectLast(); } selectPrevSuggestion(): void { - this.widget.getValue().selectPrevious(); + this.widget.value.selectPrevious(); } selectPrevPageSuggestion(): void { - this.widget.getValue().selectPreviousPage(); + this.widget.value.selectPreviousPage(); } selectFirstSuggestion(): void { - this.widget.getValue().selectFirst(); + this.widget.value.selectFirst(); } toggleSuggestionDetails(): void { - this.widget.getValue().toggleDetails(); + this.widget.value.toggleDetails(); } toggleExplainMode(): void { - this.widget.getValue().toggleExplainMode(); + this.widget.value.toggleExplainMode(); } toggleSuggestionFocus(): void { - this.widget.getValue().toggleDetailsFocus(); + this.widget.value.toggleDetailsFocus(); } } diff --git a/src/vs/editor/contrib/suggest/suggestMemory.ts b/src/vs/editor/contrib/suggest/suggestMemory.ts index b554190071..ad3bd43cea 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/suggest.test.ts b/src/vs/editor/contrib/suggest/test/suggest.test.ts index 1037cb3176..129702abbe 100644 --- a/src/vs/editor/contrib/suggest/test/suggest.test.ts +++ b/src/vs/editor/contrib/suggest/test/suggest.test.ts @@ -106,4 +106,47 @@ suite('Suggest', function () { assert.ok(items[0].provider === foo); }); }); + + test('Ctrl+space completions stopped working with the latest Insiders, #97650', async function () { + + + const foo = new class implements CompletionItemProvider { + + triggerCharacters = []; + + provideCompletionItems() { + return { + suggestions: [{ + label: 'one', + kind: CompletionItemKind.Class, + insertText: 'one', + range: { + insert: new Range(0, 0, 0, 0), + replace: new Range(0, 0, 0, 10) + } + }, { + label: 'two', + kind: CompletionItemKind.Class, + insertText: 'two', + range: { + insert: new Range(0, 0, 0, 0), + replace: new Range(0, 1, 0, 10) + } + }] + }; + } + }; + + const registration = CompletionProviderRegistry.register({ pattern: 'bar/path', scheme: 'foo' }, foo); + const items = await provideSuggestionItems(model, new Position(0, 0), new CompletionOptions(undefined, undefined, new Set().add(foo))); + registration.dispose(); + + assert.equal(items.length, 2); + const [a, b] = items; + + assert.equal(a.completion.label, 'one'); + assert.equal(a.isInvalid, false); + assert.equal(b.completion.label, 'two'); + assert.equal(b.isInvalid, true); + }); }); diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts index 2e6293eaaf..f18c9e58f1 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'; @@ -24,13 +24,16 @@ import { Event } from 'vs/base/common/event'; import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; 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 () { @@ -38,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() { @@ -102,4 +106,312 @@ suite('SuggestController', function () { assert.equal(editor.getValue(), ' let name = foo'); }); + + test('use additionalTextEdits sync when possible', async function () { + + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hello', + range: Range.fromPositions(pos), + additionalTextEdits: [{ + text: 'I came sync', + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } + }] + }] + }; + }, + async resolveCompletionItem(item) { + return item; + } + })); + + editor.setValue('hello\nhallo'); + editor.setSelection(new Selection(2, 6, 2, 6)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(false, false); + await p2; + + // insertText happens sync! + assert.equal(editor.getValue(), 'I came synchello\nhallohello'); + }); + + test('resolve additionalTextEdits async when needed', async function () { + + let resolveCallCount = 0; + + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hello', + range: Range.fromPositions(pos) + }] + }; + }, + async resolveCompletionItem(item) { + resolveCallCount += 1; + await timeout(10); + item.additionalTextEdits = [{ + text: 'I came late', + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } + }]; + return item; + } + })); + + editor.setValue('hello\nhallo'); + editor.setSelection(new Selection(2, 6, 2, 6)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(false, false); + await p2; + + // insertText happens sync! + assert.equal(editor.getValue(), 'hello\nhallohello'); + assert.equal(resolveCallCount, 1); + + // additional edits happened after a litte wait + await timeout(20); + assert.equal(editor.getValue(), 'I came latehello\nhallohello'); + + // single undo stop + editor.getModel()?.undo(); + assert.equal(editor.getValue(), 'hello\nhallo'); + }); + + test('resolve additionalTextEdits async when needed (typing)', async function () { + + let resolveCallCount = 0; + let resolve: Function = () => { }; + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hello', + range: Range.fromPositions(pos) + }] + }; + }, + async resolveCompletionItem(item) { + resolveCallCount += 1; + await new Promise(_resolve => resolve = _resolve); + item.additionalTextEdits = [{ + text: 'I came late', + range: { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 } + }]; + return item; + } + })); + + editor.setValue('hello\nhallo'); + editor.setSelection(new Selection(2, 6, 2, 6)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(false, false); + await p2; + + // insertText happens sync! + assert.equal(editor.getValue(), 'hello\nhallohello'); + assert.equal(resolveCallCount, 1); + + // additional edits happened after a litte wait + assert.ok(editor.getSelection()?.equalsSelection(new Selection(2, 11, 2, 11))); + editor.trigger('test', 'type', { text: 'TYPING' }); + + assert.equal(editor.getValue(), 'hello\nhallohelloTYPING'); + + resolve(); + await timeout(10); + assert.equal(editor.getValue(), 'I came latehello\nhallohelloTYPING'); + assert.ok(editor.getSelection()?.equalsSelection(new Selection(2, 17, 2, 17))); + }); + + // additional edit come late and are AFTER the selection -> cancel + test('resolve additionalTextEdits async when needed (simple conflict)', async function () { + + let resolveCallCount = 0; + let resolve: Function = () => { }; + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hello', + range: Range.fromPositions(pos) + }] + }; + }, + async resolveCompletionItem(item) { + resolveCallCount += 1; + await new Promise(_resolve => resolve = _resolve); + item.additionalTextEdits = [{ + text: 'I came late', + range: { startLineNumber: 1, startColumn: 6, endLineNumber: 1, endColumn: 6 } + }]; + return item; + } + })); + + editor.setValue(''); + editor.setSelection(new Selection(1, 1, 1, 1)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(false, false); + await p2; + + // insertText happens sync! + assert.equal(editor.getValue(), 'hello'); + assert.equal(resolveCallCount, 1); + + resolve(); + await timeout(10); + assert.equal(editor.getValue(), 'hello'); + }); + + // additional edit come late and are AFTER the position at which the user typed -> cancelled + test('resolve additionalTextEdits async when needed (conflict)', async function () { + + let resolveCallCount = 0; + let resolve: Function = () => { }; + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hello', + range: Range.fromPositions(pos) + }] + }; + }, + async resolveCompletionItem(item) { + resolveCallCount += 1; + await new Promise(_resolve => resolve = _resolve); + item.additionalTextEdits = [{ + text: 'I came late', + range: { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 } + }]; + return item; + } + })); + + editor.setValue('hello\nhallo'); + editor.setSelection(new Selection(2, 6, 2, 6)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(false, false); + await p2; + + // insertText happens sync! + assert.equal(editor.getValue(), 'hello\nhallohello'); + assert.equal(resolveCallCount, 1); + + // additional edits happened after a litte wait + editor.setSelection(new Selection(1, 1, 1, 1)); + editor.trigger('test', 'type', { text: 'TYPING' }); + + assert.equal(editor.getValue(), 'TYPINGhello\nhallohello'); + + resolve(); + await timeout(10); + assert.equal(editor.getValue(), 'TYPINGhello\nhallohello'); + assert.ok(editor.getSelection()?.equalsSelection(new Selection(1, 7, 1, 7))); + }); + + test('resolve additionalTextEdits async when needed (cancel)', async function () { + + let resolve: Function[] = []; + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hello', + range: Range.fromPositions(pos) + }, { + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'hallo', + range: Range.fromPositions(pos) + }] + }; + }, + async resolveCompletionItem(item) { + await new Promise(_resolve => resolve.push(_resolve)); + item.additionalTextEdits = [{ + text: 'additionalTextEdits', + range: { startLineNumber: 1, startColumn: 2, endLineNumber: 1, endColumn: 2 } + }]; + return item; + } + })); + + editor.setValue('abc'); + editor.setSelection(new Selection(1, 1, 1, 1)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(true, false); + await p2; + + // insertText happens sync! + assert.equal(editor.getValue(), 'helloabc'); + + // next + controller.acceptNextSuggestion(); + + // resolve additional edits (MUST be cancelled) + resolve.forEach(fn => fn); + resolve.length = 0; + await timeout(10); + + // next suggestion used + assert.equal(editor.getValue(), 'halloabc'); + }); }); diff --git a/src/vs/editor/contrib/suggest/test/suggestModel.test.ts b/src/vs/editor/contrib/suggest/test/suggestModel.test.ts index ae4df814ef..f29c705a98 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 new file mode 100644 index 0000000000..0fe9929b63 --- /dev/null +++ b/src/vs/editor/contrib/unusualLineTerminators/unusualLineTerminators.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { ITextModel } from 'vs/editor/common/model'; +import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; + +const ignoreUnusualLineTerminators = 'ignoreUnusualLineTerminators'; + +function writeIgnoreState(codeEditorService: ICodeEditorService, model: ITextModel, state: boolean): void { + codeEditorService.setTransientModelProperty(model, ignoreUnusualLineTerminators, state); +} + +function readIgnoreState(codeEditorService: ICodeEditorService, model: ITextModel): boolean | undefined { + return codeEditorService.getTransientModelProperty(model, ignoreUnusualLineTerminators); +} + +class UnusualLineTerminatorsDetector extends Disposable implements IEditorContribution { + + public static readonly ID = 'editor.contrib.unusualLineTerminatorsDetector'; + + private _enabled: boolean; + + constructor( + private readonly _editor: ICodeEditor, + @IDialogService private readonly _dialogService: IDialogService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService + ) { + super(); + + this._enabled = this._editor.getOption(EditorOption.removeUnusualLineTerminators); + this._register(this._editor.onDidChangeConfiguration((e) => { + if (e.hasChanged(EditorOption.removeUnusualLineTerminators)) { + this._enabled = this._editor.getOption(EditorOption.removeUnusualLineTerminators); + this._checkForUnusualLineTerminators(); + } + })); + + this._register(this._editor.onDidChangeModel(() => { + this._checkForUnusualLineTerminators(); + })); + + this._register(this._editor.onDidChangeModelContent(() => { + this._checkForUnusualLineTerminators(); + })); + } + + private async _checkForUnusualLineTerminators(): Promise { + if (!this._enabled) { + return; + } + if (!this._editor.hasModel()) { + return; + } + const model = this._editor.getModel(); + if (!model.mightContainUnusualLineTerminators()) { + return; + } + const ignoreState = readIgnoreState(this._codeEditorService, model); + if (ignoreState === true) { + // this model should be ignored + return; + } + if (this._editor.getOption(EditorOption.readOnly)) { + // read only editor => sorry! + 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`."), + primaryButton: nls.localize('unusualLineTerminators.fix', "Fix this file"), + secondaryButton: nls.localize('unusualLineTerminators.ignore', "Ignore problem for this file") + }); + + if (!result.confirmed) { + // this model should be ignored + writeIgnoreState(this._codeEditorService, model, true); + return; + } + + model.removeUnusualLineTerminators(this._editor.getSelections()); + } +} + +registerEditorContribution(UnusualLineTerminatorsDetector.ID, UnusualLineTerminatorsDetector); diff --git a/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts b/src/vs/editor/contrib/wordOperations/test/wordOperations.test.ts index a390f78cfd..c1aabc58c0 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 422e699be0..0111a0b7d8 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 d226aba210..963309342a 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 7301afe999..9e395e489d 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'; @@ -40,6 +41,7 @@ import 'vs/editor/contrib/snippet/snippetController2'; import 'vs/editor/contrib/suggest/suggestController'; import 'vs/editor/contrib/tokenization/tokenization'; import 'vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode'; +import 'vs/editor/contrib/unusualLineTerminators/unusualLineTerminators'; import 'vs/editor/contrib/viewportSemanticTokens/viewportSemanticTokens'; import 'vs/editor/contrib/wordHighlighter/wordHighlighter'; import 'vs/editor/contrib/wordOperations/wordOperations'; diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index 9cd119512b..84dd91dd46 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; } @@ -548,6 +547,7 @@ export class StandaloneTelemetryService implements ITelemetryService { _serviceBrand: undefined; public isOptedIn = false; + public sendErrorTelemetry = false; public setEnabled(value: boolean): void { } diff --git a/src/vs/editor/standalone/browser/standalone-tokens.css b/src/vs/editor/standalone/browser/standalone-tokens.css index 71a10570f7..c4188df11e 100644 --- a/src/vs/editor/standalone/browser/standalone-tokens.css +++ b/src/vs/editor/standalone/browser/standalone-tokens.css @@ -6,7 +6,7 @@ /* Default standalone editor font */ .monaco-editor { - font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif; } .monaco-menu .monaco-action-bar.vertical .action-item .action-menu-item:focus .action-label { @@ -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 16a17ac2e6..78506d9087 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/test/browser/standaloneLanguages.test.ts b/src/vs/editor/standalone/test/browser/standaloneLanguages.test.ts index 83e87eef96..9049b95975 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 616ef310dd..ec2352477e 100644 --- a/src/vs/editor/test/browser/commands/sideEditing.test.ts +++ b/src/vs/editor/test/browser/commands/sideEditing.test.ts @@ -4,29 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { Cursor } 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 { IIdentifiedSingleEditOperation } from 'vs/editor/common/model'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; -import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; -import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; -import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; 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())); }); @@ -199,25 +194,16 @@ suite('SideEditing', () => { ]; function _runTest(selection: Selection, editRange: Range, editText: string, editForceMoveMarkers: boolean, expected: Selection, msg: string): void { - const model = createTextModel(LINES.join('\n')); - const config = new TestConfiguration({}); - const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(config.options); - const viewModel = new ViewModel(0, config, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); - const cursor = new Cursor(config, model, viewModel); - - cursor.setSelections('tests', [selection]); - model.applyEdits([{ - range: editRange, - text: editText, - forceMoveMarkers: editForceMoveMarkers - }]); - const actual = cursor.getSelection(); - assert.deepEqual(actual.toString(), expected.toString(), msg); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); + withTestCodeEditor(LINES.join('\n'), {}, (editor, viewModel) => { + viewModel.setSelections('tests', [selection]); + editor.getModel().applyEdits([{ + range: editRange, + text: editText, + forceMoveMarkers: editForceMoveMarkers + }]); + const actual = viewModel.getSelection(); + assert.deepEqual(actual.toString(), expected.toString(), msg); + }); } function runTest(selection: Range, editRange: Range, editText: string, expected: Selection[][]): void { diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 090afc5200..a275c2b2a1 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -6,117 +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, IConfiguration } 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 { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -import { withTestCodeEditor } 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 { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; -import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; - -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)]; @@ -125,17 +112,12 @@ 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); } -function createViewModel(configuration: IConfiguration, model: ITextModel): ViewModel { - const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(configuration.options); - return new ViewModel(0, configuration, model, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); -} - suite('Editor Controller - Cursor', () => { const LINE1 = ' \tMy First Line\t '; const LINE2 = '\tMy Second Line'; @@ -143,581 +125,724 @@ suite('Editor Controller - Cursor', () => { const LINE4 = ''; const LINE5 = '1'; - let thisModel: TextModel; - let thisConfiguration: TestConfiguration; - let thisViewModel: ViewModel; - let thisCursor: Cursor; + const TEXT = + LINE1 + '\r\n' + + LINE2 + '\n' + + LINE3 + '\n' + + LINE4 + '\r\n' + + LINE5; - setup(() => { - let text = - LINE1 + '\r\n' + - LINE2 + '\n' + - LINE3 + '\n' + - LINE4 + '\r\n' + - LINE5; + // let thisModel: TextModel; + // let thisConfiguration: TestConfiguration; + // let thisViewModel: ViewModel; + // let cursor: Cursor; - thisModel = createTextModel(text); - thisConfiguration = new TestConfiguration({}); - thisViewModel = createViewModel(thisConfiguration, thisModel); + // setup(() => { + // let text = + // LINE1 + '\r\n' + + // LINE2 + '\n' + + // LINE3 + '\n' + + // LINE4 + '\r\n' + + // LINE5; - thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel); - }); + // thisModel = createTextModel(text); + // thisConfiguration = new TestConfiguration({}); + // thisViewModel = createViewModel(thisConfiguration, thisModel); - teardown(() => { - thisCursor.dispose(); - thisViewModel.dispose(); - thisModel.dispose(); - thisConfiguration.dispose(); - }); + // cursor = new Cursor(thisConfiguration, thisModel, thisViewModel); + // }); + + // teardown(() => { + // cursor.dispose(); + // thisViewModel.dispose(); + // thisModel.dispose(); + // thisConfiguration.dispose(); + // }); + + function runTest(callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { + callback(editor, viewModel); + }); + } test('cursor initialized', () => { - assertCursor(thisCursor, new Position(1, 1)); + runTest((editor, viewModel) => { + assertCursor(viewModel, new Position(1, 1)); + }); }); // --------- absolute move test('no move', () => { - moveTo(thisCursor, 1, 1); - assertCursor(thisCursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 1); + assertCursor(viewModel, new Position(1, 1)); + }); }); test('move', () => { - moveTo(thisCursor, 1, 2); - assertCursor(thisCursor, new Position(1, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 1, 2); + assertCursor(viewModel, new Position(1, 2)); + }); }); test('move in selection mode', () => { - moveTo(thisCursor, 1, 2, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 25); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 4, 20); - assertCursor(thisCursor, new Position(4, 1)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 4, 20); + assertCursor(viewModel, new Position(4, 1)); + }); }); test('move one char line', () => { - moveTo(thisCursor, 5, 20); - assertCursor(thisCursor, new Position(5, 2)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 5, 20); + assertCursor(viewModel, new Position(5, 2)); + }); }); test('selection down', () => { - moveTo(thisCursor, 2, 1, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 2, 3); - assertCursor(thisCursor, new Position(2, 3)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 3); + assertCursor(viewModel, new Position(2, 3)); - moveTo(thisCursor, 2, 15, true); - assertCursor(thisCursor, new Selection(2, 3, 2, 15)); + moveTo(editor, viewModel, 2, 15, true); + assertCursor(viewModel, new Selection(2, 3, 2, 15)); - moveTo(thisCursor, 1, 2, true); - assertCursor(thisCursor, 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', () => { - moveLeft(thisCursor); - assertCursor(thisCursor, new Position(1, 1)); + runTest((editor, viewModel) => { + moveLeft(editor, viewModel); + assertCursor(viewModel, new Position(1, 1)); + }); }); test('move left', () => { - moveTo(thisCursor, 1, 3); - assertCursor(thisCursor, new Position(1, 3)); - moveLeft(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 17); - assertCursor(thisCursor, new Position(3, 17)); - moveLeft(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 2, 1); - assertCursor(thisCursor, new Position(2, 1)); - moveLeft(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 2, 1); - assertCursor(thisCursor, new Position(2, 1)); - moveLeft(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 5, 2); - assertCursor(thisCursor, new Position(5, 2)); - moveRight(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 3); - assertCursor(thisCursor, new Position(1, 3)); - moveRight(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 15); - assertCursor(thisCursor, new Position(3, 15)); - moveRight(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 21); - assertCursor(thisCursor, new Position(1, 21)); - moveRight(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 21); - assertCursor(thisCursor, new Position(1, 21)); - moveRight(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveDown(thisCursor); - assertCursor(thisCursor, new Position(2, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(3, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(4, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(5, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, 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', () => { - moveDown(thisCursor, true); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); - moveDown(thisCursor, true); - assertCursor(thisCursor, new Selection(1, 1, 3, 1)); - moveDown(thisCursor, true); - assertCursor(thisCursor, new Selection(1, 1, 4, 1)); - moveDown(thisCursor, true); - assertCursor(thisCursor, new Selection(1, 1, 5, 1)); - moveDown(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 5); - assertCursor(thisCursor, new Position(1, 5)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(2, 2)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(3, 5)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(4, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 5); - assertCursor(thisCursor, new Position(3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 5); + assertCursor(viewModel, new Position(3, 5)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(2, 2)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(2, 2)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(1, 5)); + moveUp(editor, viewModel); + assertCursor(viewModel, new Position(1, 5)); + }); }); test('move up with selection', () => { - moveTo(thisCursor, 3, 5); - assertCursor(thisCursor, new Position(3, 5)); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 3, 5); + assertCursor(viewModel, new Position(3, 5)); - moveUp(thisCursor, true); - assertCursor(thisCursor, new Selection(3, 5, 2, 2)); + moveUp(editor, viewModel, true); + assertCursor(viewModel, new Selection(3, 5, 2, 2)); - moveUp(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 5); - assertCursor(thisCursor, new Position(1, 5)); - moveDown(thisCursor); - moveDown(thisCursor); - moveDown(thisCursor); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(5, 2)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(4, 1)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(3, 5)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(2, 2)); - moveUp(thisCursor); - assertCursor(thisCursor, 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', () => { - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, LINE1.length + 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(2, LINE2.length + 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(3, LINE3.length + 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(4, LINE4.length + 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(5, LINE5.length + 1)); - moveUp(thisCursor); - moveUp(thisCursor); - moveUp(thisCursor); - moveUp(thisCursor); - assertCursor(thisCursor, 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', () => { - thisCursor.setSelections('test', [new Selection(1, 5, 1, 5)]); - // going once up on the first line remembers the offset visual columns - moveUp(thisCursor); - assertCursor(thisCursor, new Position(1, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(2, 2)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(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(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(thisCursor); - assertCursor(thisCursor, new Position(1, 1)); - moveUp(thisCursor); - assertCursor(thisCursor, new Position(1, 1)); - moveDown(thisCursor); - assertCursor(thisCursor, new Position(2, 1)); + // going twice up on the first line discards the offset visual columns + 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', () => { - moveToBeginningOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, 6)); - moveToBeginningOfLine(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - moveToBeginningOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, 6)); - moveToBeginningOfLine(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 2); - moveToBeginningOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, 6)); - moveToBeginningOfLine(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - moveToBeginningOfLine(thisCursor, true); - assertCursor(thisCursor, new Selection(1, 8, 1, 6)); - moveToBeginningOfLine(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - moveTo(thisCursor, 3, 9, true); - moveToBeginningOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 9); - moveTo(thisCursor, 1, 8, true); - moveToBeginningOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 2); - moveTo(thisCursor, 3, 9, true); - moveToBeginningOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 9); - moveTo(thisCursor, 3, 2, true); - moveToBeginningOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - moveTo(thisCursor, 3, 9, true); - moveToBeginningOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - moveTo(thisCursor, 3, 9, true); - moveToBeginningOfLine(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 6); - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 20); - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, new Position(1, LINE1.length + 1)); - moveToEndOfLine(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 6); - moveToEndOfLine(thisCursor, true); - assertCursor(thisCursor, new Selection(1, 6, 1, LINE1.length + 1)); - moveToEndOfLine(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 1); - moveTo(thisCursor, 3, 9, true); - moveToEndOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 9); - moveTo(thisCursor, 1, 1, true); - moveToEndOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 1); - moveTo(thisCursor, 3, 9, true); - moveToEndOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 9); - moveTo(thisCursor, 3, 1, true); - moveToEndOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 1); - moveTo(thisCursor, 3, 9, true); - moveToEndOfLine(thisCursor, false); - assertCursor(thisCursor, 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', () => { - moveToBeginningOfBuffer(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 3); - moveToBeginningOfBuffer(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 3); - moveToBeginningOfBuffer(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 1, 3); - moveToBeginningOfBuffer(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 3); - moveToBeginningOfBuffer(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveToEndOfBuffer(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 5, 1); - moveToEndOfBuffer(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 3); - moveToEndOfBuffer(thisCursor); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 5, 1); - moveToEndOfBuffer(thisCursor, true); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 3, 3); - moveToEndOfBuffer(thisCursor, true); - assertCursor(thisCursor, 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', () => { - CoreNavigationCommands.SelectAll.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, 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', () => { - // 0 1 2 - // 01234 56789012345678 0 - // let LINE1 = ' \tMy First Line\t '; - moveTo(thisCursor, 1, 1); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); + runTest((editor, viewModel) => { + // 0 1 2 + // 01234 56789012345678 0 + // let LINE1 = ' \tMy First Line\t '; + moveTo(editor, viewModel, 1, 1); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(thisCursor, 1, 2); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 2); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(thisCursor, 1, 5); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 5); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(thisCursor, 1, 19); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 19); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(thisCursor, 1, 20); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); + moveTo(editor, viewModel, 1, 20); + CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, new Selection(1, 1, 2, 1)); - moveTo(thisCursor, 1, 21); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 2, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 3, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 4, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 5, 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, new Selection(1, 1, 5, LINE5.length + 1)); - CoreNavigationCommands.ExpandLineSelection.runCoreEditorCommand(thisCursor, {}); - assertCursor(thisCursor, 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', () => { - thisCursor.onDidChange((e) => { - assert.ok(false, 'was not expecting event'); + runTest((editor, viewModel) => { + viewModel.onEvent((e) => { + assert.ok(false, 'was not expecting event'); + }); + moveTo(editor, viewModel, 1, 1); }); - moveTo(thisCursor, 1, 1); }); test('move eventing', () => { - let events = 0; - thisCursor.onDidChange((e: CursorStateChangedEvent) => { - events++; - assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + runTest((editor, viewModel) => { + let events = 0; + viewModel.onEvent((e) => { + if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { + events++; + assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + } + }); + moveTo(editor, viewModel, 1, 2); + assert.equal(events, 1, 'receives 1 event'); }); - moveTo(thisCursor, 1, 2); - assert.equal(events, 1, 'receives 1 event'); }); test('move in selection mode eventing', () => { - let events = 0; - thisCursor.onDidChange((e: CursorStateChangedEvent) => { - events++; - assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + runTest((editor, viewModel) => { + let events = 0; + viewModel.onEvent((e) => { + if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { + events++; + assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + } + }); + moveTo(editor, viewModel, 1, 2, true); + assert.equal(events, 1, 'receives 1 event'); }); - moveTo(thisCursor, 1, 2, true); - assert.equal(events, 1, 'receives 1 event'); }); // --------- state save & restore test('saveState & restoreState', () => { - moveTo(thisCursor, 2, 1, true); - assertCursor(thisCursor, 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(thisCursor.saveState()); + let savedState = JSON.stringify(viewModel.saveCursorState()); - moveTo(thisCursor, 1, 1, false); - assertCursor(thisCursor, new Position(1, 1)); + moveTo(editor, viewModel, 1, 1, false); + assertCursor(viewModel, new Position(1, 1)); - thisCursor.restoreState(JSON.parse(savedState)); - assertCursor(thisCursor, 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', () => { - moveTo(thisCursor, 2, 16, true); + runTest((editor, viewModel) => { + moveTo(editor, viewModel, 2, 16, true); - thisModel.applyEdits([EditOperation.delete(new Range(2, 1, 2, 2))]); - assertCursor(thisCursor, 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)); + }); }); test('column select 1', () => { @@ -727,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, @@ -746,7 +871,7 @@ suite('Editor Controller - Cursor', () => { new Selection(4, 4, 4, 4), ]; - assertCursor(cursor, expectedSelections); + assertCursor(viewModel, expectedSelections); }); }); @@ -757,41 +882,41 @@ 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)); }); }); test('issue #4905 - column select is biased to the right', () => { - const model = createTextModel([ + withTestCodeEditor([ 'var gulp = require("gulp");', 'var path = require("path");', 'var rimraf = require("rimraf");', @@ -799,36 +924,28 @@ suite('Editor Controller - Cursor', () => { 'var merge = require("merge-stream");', 'var concat = require("gulp-concat");', 'var newer = require("gulp-newer");', - ].join('\n')); - const config = new TestConfiguration({}); - const viewModel = createViewModel(config, model); - const cursor = new Cursor(config, model, viewModel); + ].join('\n'), {}, (editor, viewModel) => { + moveTo(editor, viewModel, 1, 4, false); + assertCursor(viewModel, new Position(1, 4)); - moveTo(cursor, 1, 4, false); - assertCursor(cursor, new Position(1, 4)); + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { + position: new Position(4, 1), + viewPosition: new Position(4, 1), + mouseColumn: 1, + doColumnSelect: true + }); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { - position: new Position(4, 1), - viewPosition: new Position(4, 1), - mouseColumn: 1, - doColumnSelect: true + assertCursor(viewModel, [ + new Selection(1, 4, 1, 1), + new Selection(2, 4, 2, 1), + new Selection(3, 4, 3, 1), + new Selection(4, 4, 4, 1), + ]); }); - - assertCursor(cursor, [ - new Selection(1, 4, 1, 1), - new Selection(2, 4, 2, 1), - new Selection(3, 4, 3, 1), - new Selection(4, 4, 4, 1), - ]); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); }); test('issue #20087: column select with mouse', () => { - const model = createTextModel([ + withTestCodeEditor([ '', '', '', @@ -839,60 +956,54 @@ suite('Editor Controller - Cursor', () => { '', '', '', - ].join('\n')); - const config = new TestConfiguration({}); - const viewModel = createViewModel(config, model); - const cursor = new Cursor(config, model, viewModel); + ].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(viewModel, { + position: new Position(1, 1), + viewPosition: new Position(1, 1), + mouseColumn: 1, + doColumnSelect: true + }); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 1), + new Selection(9, 10, 9, 1), + new Selection(8, 10, 8, 1), + new Selection(7, 10, 7, 1), + new Selection(6, 10, 6, 1), + new Selection(5, 10, 5, 1), + new Selection(4, 10, 4, 1), + new Selection(3, 10, 3, 1), + new Selection(2, 10, 2, 1), + new Selection(1, 10, 1, 1), + ]); + + CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(viewModel, { + position: new Position(1, 1), + viewPosition: new Position(1, 1), + mouseColumn: 1, + doColumnSelect: true + }); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 1), + new Selection(9, 10, 9, 1), + new Selection(8, 10, 8, 1), + new Selection(7, 10, 7, 1), + new Selection(6, 10, 6, 1), + new Selection(5, 10, 5, 1), + new Selection(4, 10, 4, 1), + new Selection(3, 10, 3, 1), + new Selection(2, 10, 2, 1), + new Selection(1, 10, 1, 1), + ]); - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { - position: new Position(1, 1), - viewPosition: new Position(1, 1), - mouseColumn: 1, - doColumnSelect: true }); - assertCursor(cursor, [ - new Selection(10, 10, 10, 1), - new Selection(9, 10, 9, 1), - new Selection(8, 10, 8, 1), - new Selection(7, 10, 7, 1), - new Selection(6, 10, 6, 1), - new Selection(5, 10, 5, 1), - new Selection(4, 10, 4, 1), - new Selection(3, 10, 3, 1), - new Selection(2, 10, 2, 1), - new Selection(1, 10, 1, 1), - ]); - - CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(cursor, { - position: new Position(1, 1), - viewPosition: new Position(1, 1), - mouseColumn: 1, - doColumnSelect: true - }); - assertCursor(cursor, [ - new Selection(10, 10, 10, 1), - new Selection(9, 10, 9, 1), - new Selection(8, 10, 8, 1), - new Selection(7, 10, 7, 1), - new Selection(6, 10, 6, 1), - new Selection(5, 10, 5, 1), - new Selection(4, 10, 4, 1), - new Selection(3, 10, 3, 1), - new Selection(2, 10, 2, 1), - new Selection(1, 10, 1, 1), - ]); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); }); test('issue #20087: column select with keyboard', () => { - const model = createTextModel([ + withTestCodeEditor([ '', '', '', @@ -903,48 +1014,41 @@ suite('Editor Controller - Cursor', () => { '', '', '', - ].join('\n')); - const config = new TestConfiguration({}); - const viewModel = createViewModel(config, model); - const cursor = new Cursor(config, model, viewModel); + ].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, [ - new Selection(10, 10, 10, 9) - ]); + CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 9) + ]); - CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(10, 10, 10, 8) - ]); + CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 8) + ]); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(10, 10, 10, 9) - ]); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 9) + ]); - CoreNavigationCommands.CursorColumnSelectUp.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(10, 10, 10, 9), - new Selection(9, 10, 9, 9), - ]); + CoreNavigationCommands.CursorColumnSelectUp.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 9), + new Selection(9, 10, 9, 9), + ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(10, 10, 10, 9) - ]); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(10, 10, 10, 9) + ]); + }); }); test('column select with keyboard', () => { - const model = createTextModel([ + withTestCodeEditor([ 'var gulp = require("gulp");', 'var path = require("path");', 'var rimraf = require("rimraf");', @@ -952,209 +1056,202 @@ suite('Editor Controller - Cursor', () => { 'var merge = require("merge-stream");', 'var concat = require("gulp-concat");', 'var newer = require("gulp-newer");', - ].join('\n')); - const config = new TestConfiguration({}); - const viewModel = createViewModel(config, model); - const cursor = new Cursor(config, model, viewModel); + ].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, [ - new Selection(1, 4, 1, 5) - ]); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(1, 4, 1, 5) + ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 5), - new Selection(2, 4, 2, 5) - ]); + CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(1, 4, 1, 5), + new Selection(2, 4, 2, 5) + ]); - CoreNavigationCommands.CursorColumnSelectDown.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 5), - new Selection(2, 4, 2, 5), - new Selection(3, 4, 3, 5), - ]); + 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, [ - new Selection(1, 4, 1, 5), - new Selection(2, 4, 2, 5), - new Selection(3, 4, 3, 5), - new Selection(4, 4, 4, 5), - new Selection(5, 4, 5, 5), - new Selection(6, 4, 6, 5), - new Selection(7, 4, 7, 5), - ]); + 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), + new Selection(4, 4, 4, 5), + new Selection(5, 4, 5, 5), + new Selection(6, 4, 6, 5), + new Selection(7, 4, 7, 5), + ]); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 6), - new Selection(2, 4, 2, 6), - new Selection(3, 4, 3, 6), - new Selection(4, 4, 4, 6), - new Selection(5, 4, 5, 6), - new Selection(6, 4, 6, 6), - new Selection(7, 4, 7, 6), - ]); + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(1, 4, 1, 6), + new Selection(2, 4, 2, 6), + new Selection(3, 4, 3, 6), + new Selection(4, 4, 4, 6), + new Selection(5, 4, 5, 6), + new Selection(6, 4, 6, 6), + new Selection(7, 4, 7, 6), + ]); - // 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, [ - new Selection(1, 4, 1, 16), - new Selection(2, 4, 2, 16), - new Selection(3, 4, 3, 16), - new Selection(4, 4, 4, 16), - new Selection(5, 4, 5, 16), - new Selection(6, 4, 6, 16), - new Selection(7, 4, 7, 16), - ]); + // 10 times + 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), + new Selection(4, 4, 4, 16), + new Selection(5, 4, 5, 16), + new Selection(6, 4, 6, 16), + new Selection(7, 4, 7, 16), + ]); - // 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, [ - new Selection(1, 4, 1, 26), - new Selection(2, 4, 2, 26), - new Selection(3, 4, 3, 26), - new Selection(4, 4, 4, 26), - new Selection(5, 4, 5, 26), - new Selection(6, 4, 6, 26), - new Selection(7, 4, 7, 26), - ]); + // 10 times + 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), + new Selection(4, 4, 4, 26), + new Selection(5, 4, 5, 26), + new Selection(6, 4, 6, 26), + new Selection(7, 4, 7, 26), + ]); - // 2 times => reaching the ending of lines 1 and 2 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 28), - new Selection(4, 4, 4, 28), - new Selection(5, 4, 5, 28), - new Selection(6, 4, 6, 28), - new Selection(7, 4, 7, 28), - ]); + // 2 times => reaching the ending of lines 1 and 2 + 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), + new Selection(4, 4, 4, 28), + new Selection(5, 4, 5, 28), + new Selection(6, 4, 6, 28), + new Selection(7, 4, 7, 28), + ]); - // 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, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 32), - new Selection(5, 4, 5, 32), - new Selection(6, 4, 6, 32), - new Selection(7, 4, 7, 32), - ]); + // 4 times => reaching the ending of line 3 + 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), + new Selection(4, 4, 4, 32), + new Selection(5, 4, 5, 32), + new Selection(6, 4, 6, 32), + new Selection(7, 4, 7, 32), + ]); - // 2 times => reaching the ending of line 4 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 34), - new Selection(5, 4, 5, 34), - new Selection(6, 4, 6, 34), - new Selection(7, 4, 7, 34), - ]); + // 2 times => reaching the ending of line 4 + 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), + new Selection(4, 4, 4, 34), + new Selection(5, 4, 5, 34), + new Selection(6, 4, 6, 34), + new Selection(7, 4, 7, 34), + ]); - // 1 time => reaching the ending of line 7 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 34), - new Selection(5, 4, 5, 35), - new Selection(6, 4, 6, 35), - new Selection(7, 4, 7, 35), - ]); + // 1 time => reaching the ending of line 7 + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(1, 4, 1, 28), + new Selection(2, 4, 2, 28), + new Selection(3, 4, 3, 32), + new Selection(4, 4, 4, 34), + new Selection(5, 4, 5, 35), + new Selection(6, 4, 6, 35), + new Selection(7, 4, 7, 35), + ]); - // 3 times => reaching the ending of lines 5 & 6 - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 34), - new Selection(5, 4, 5, 37), - new Selection(6, 4, 6, 37), - new Selection(7, 4, 7, 35), - ]); + // 3 times => reaching the ending of lines 5 & 6 + 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), + new Selection(4, 4, 4, 34), + new Selection(5, 4, 5, 37), + new Selection(6, 4, 6, 37), + new Selection(7, 4, 7, 35), + ]); - // cannot go anywhere anymore - CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 34), - new Selection(5, 4, 5, 37), - new Selection(6, 4, 6, 37), - new Selection(7, 4, 7, 35), - ]); + // cannot go anywhere anymore + CoreNavigationCommands.CursorColumnSelectRight.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(1, 4, 1, 28), + new Selection(2, 4, 2, 28), + new Selection(3, 4, 3, 32), + new Selection(4, 4, 4, 34), + new Selection(5, 4, 5, 37), + new Selection(6, 4, 6, 37), + new Selection(7, 4, 7, 35), + ]); - // 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, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 34), - new Selection(5, 4, 5, 37), - new Selection(6, 4, 6, 37), - new Selection(7, 4, 7, 35), - ]); + // cannot go anywhere anymore even if we insist + 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), + new Selection(4, 4, 4, 34), + new Selection(5, 4, 5, 37), + new Selection(6, 4, 6, 37), + new Selection(7, 4, 7, 35), + ]); - // can easily go back - CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(cursor, {}); - assertCursor(cursor, [ - new Selection(1, 4, 1, 28), - new Selection(2, 4, 2, 28), - new Selection(3, 4, 3, 32), - new Selection(4, 4, 4, 34), - new Selection(5, 4, 5, 36), - new Selection(6, 4, 6, 36), - new Selection(7, 4, 7, 35), - ]); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); + // can easily go back + CoreNavigationCommands.CursorColumnSelectLeft.runCoreEditorCommand(viewModel, {}); + assertCursor(viewModel, [ + new Selection(1, 4, 1, 28), + new Selection(2, 4, 2, 28), + new Selection(3, 4, 3, 32), + new Selection(4, 4, 4, 34), + new Selection(5, 4, 5, 36), + new Selection(6, 4, 6, 36), + new Selection(7, 4, 7, 35), + ]); + }); }); }); @@ -1209,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(); @@ -1231,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); @@ -1285,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'); @@ -1314,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); @@ -1331,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(); @@ -1396,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(); @@ -1416,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(); @@ -1438,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(); @@ -1466,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(); @@ -1484,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'); @@ -1500,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), ''); }); @@ -1523,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(); }); @@ -1547,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(;'); }); @@ -1568,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'); @@ -1587,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'); @@ -1606,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', @@ -1636,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', @@ -1679,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', @@ -1709,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', @@ -1733,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', @@ -1759,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'); } }); @@ -1809,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', @@ -1833,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'); }); @@ -1853,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'); }); @@ -1869,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 = { @@ -1880,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); @@ -1949,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(); @@ -1967,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)); }); }); @@ -2012,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); }); }); @@ -2038,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)); }); }); @@ -2054,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)); }); }); @@ -2077,66 +2174,55 @@ 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 #41573 - delete across multiple lines does not shrink the selection when word wraps', () => { - const model = createTextModel([ + withTestCodeEditor([ 'Authorization: \'Bearer pHKRfCTFSnGxs6akKlb9ddIXcca0sIUSZJutPHYqz7vEeHdMTMh0SGN0IGU3a0n59DXjTLRsj5EJ2u33qLNIFi9fk5XF8pK39PndLYUZhPt4QvHGLScgSkK0L4gwzkzMloTQPpKhqiikiIOvyNNSpd2o8j29NnOmdTUOKi9DVt74PD2ohKxyOrWZ6oZprTkb3eKajcpnS0LABKfaw2rmv4\',' - ].join('\n')); - const config = new TestConfiguration({ - wordWrap: 'wordWrapColumn', - wordWrapColumn: 100 + ].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)); + + editor.getModel().applyEdits([{ + range: new Range(1, 1, 1, 43), + text: '' + }]); + + assertCursor(viewModel, new Selection(1, 1, 1, 105)); }); - const viewModel = createViewModel(config, model); - const cursor = new Cursor(config, model, viewModel); - - moveTo(cursor, 1, 43, false); - moveTo(cursor, 1, 147, true); - assertCursor(cursor, new Selection(1, 43, 1, 147)); - - model.applyEdits([{ - range: new Range(1, 1, 1, 43), - text: '' - }]); - - assertCursor(cursor, new Selection(1, 1, 1, 105)); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); }); test('issue #22717: Moving text cursor cause an incorrect position in Chinese', () => { @@ -2146,20 +2232,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)); }); }); @@ -2170,7 +2256,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!' @@ -2221,7 +2307,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), @@ -2231,14 +2317,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 = \'\''); @@ -2254,7 +2340,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) ]); @@ -2264,17 +2350,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), ]); }); @@ -2289,7 +2375,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) ]); @@ -2298,12 +2384,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), ]); @@ -2311,7 +2397,7 @@ suite('Editor Controller - Regression tests', () => { range: new Range(1, 1, 1, 2), text: '' }]); - assertCursor(cursor, [ + assertCursor(viewModel, [ new Selection(1, 1, 1, 1), ]); }); @@ -2327,17 +2413,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(); @@ -2350,7 +2436,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) ]); @@ -2389,9 +2475,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), ' '); }); @@ -2412,58 +2498,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'); }); @@ -2478,11 +2564,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(); @@ -2495,11 +2581,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(); @@ -2512,11 +2598,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(); @@ -2530,16 +2616,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), ' '); @@ -2551,13 +2637,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), ' '); @@ -2571,10 +2657,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), '}'); @@ -2585,7 +2671,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 { @@ -2594,7 +2680,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), '}'); @@ -2613,9 +2699,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), ' '); @@ -2623,7 +2709,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), ' '); @@ -2631,8 +2717,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), ''); @@ -2650,16 +2736,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), ' '); @@ -2671,15 +2757,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), ''); @@ -2687,9 +2773,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), ' '); @@ -2710,10 +2796,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() {', @@ -2722,9 +2808,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', @@ -2733,7 +2819,7 @@ suite('Editor Controller - Cursor Configuration', () => { '', ' }', ].join('\n')); - assertCursor(cursor, new Position(5, 1)); + assertCursor(viewModel, new Position(5, 1)); }); model.dispose(); @@ -2750,10 +2836,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() {', @@ -2763,7 +2849,7 @@ suite('Editor Controller - Cursor Configuration', () => { ' return 3;', ' }', ].join('\n')); - assertCursor(cursor, new Position(5, 10)); + assertCursor(viewModel, new Position(5, 10)); }); model.dispose(); @@ -2778,9 +2864,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 '); }); @@ -2797,14 +2883,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 '); @@ -2817,12 +2903,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'); @@ -2836,7 +2922,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), ''); @@ -2845,8 +2931,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 '); }); @@ -2864,23 +2950,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); @@ -2930,10 +3016,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(); @@ -2965,18 +3051,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)); }); }); @@ -2988,12 +3075,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'); }); }); @@ -3007,12 +3094,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)); }); }); @@ -3027,18 +3114,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)); }); }); @@ -3054,16 +3142,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(); @@ -3079,12 +3168,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'); }); }); @@ -3099,13 +3188,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'); }); }); @@ -3118,16 +3207,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)); }); }); @@ -3138,20 +3227,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)); }); }); @@ -3162,19 +3251,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)); }); }); @@ -3186,19 +3276,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)); }); }); @@ -3215,13 +3306,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)); }); }); @@ -3235,12 +3326,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'); }); }); @@ -3255,12 +3346,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'); }); }); @@ -3274,12 +3365,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'); }); }); @@ -3294,19 +3385,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'); }); }); @@ -3321,19 +3412,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'); }); }); @@ -3347,17 +3438,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'); }); }); @@ -3380,17 +3471,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'); }); }); @@ -3406,13 +3497,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'); }); }); @@ -3429,14 +3520,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)); }); }); @@ -3452,14 +3543,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)); }); }); @@ -3474,13 +3565,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}'); }); }); @@ -3493,12 +3584,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'); }); }); @@ -3519,9 +3610,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'); @@ -3547,9 +3638,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'); @@ -3575,9 +3666,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'); @@ -3602,9 +3693,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'); @@ -3629,9 +3720,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'); @@ -3654,9 +3745,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), ' '); @@ -3684,11 +3775,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'); }); @@ -3706,12 +3797,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'); }); }); @@ -3727,12 +3818,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'); }); }); @@ -3746,12 +3837,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), '}'); }); }); @@ -3794,11 +3885,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 {', @@ -3812,7 +3903,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(); @@ -3858,9 +3949,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(), @@ -3876,7 +3967,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(); @@ -3921,30 +4012,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), ' ]'); }); @@ -3960,19 +4051,13 @@ interface ICursorOpts { editorOpts?: IEditorOptions; } -function usingCursor(opts: ICursorOpts, callback: (model: TextModel, cursor: Cursor) => void): void { - let model = createTextModel(opts.text.join('\n'), opts.modelOpts, opts.languageIdentifier); - model.forceTokenization(model.getLineCount()); - let config = new TestConfiguration(opts.editorOpts || {}); - let viewModel = createViewModel(config, model); - let cursor = new Cursor(config, model, viewModel); - - callback(model, cursor); - - cursor.dispose(); - viewModel.dispose(); - config.dispose(); - model.dispose(); +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, viewModel) => { + callback(editor, model, viewModel); + }); } class ElectricCharMode extends MockMode { @@ -4003,9 +4088,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(); @@ -4019,9 +4104,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(); @@ -4035,9 +4120,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(); @@ -4053,9 +4138,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(); @@ -4071,9 +4156,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(); @@ -4087,9 +4172,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(); @@ -4103,9 +4188,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(); @@ -4119,9 +4204,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(); @@ -4136,9 +4221,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(); @@ -4151,13 +4236,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, ')'); }); @@ -4173,9 +4258,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(); @@ -4189,9 +4274,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 +4290,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(); @@ -4221,10 +4306,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(); @@ -4296,11 +4381,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(); } @@ -4319,7 +4404,7 @@ suite('autoClosingPairs', () => { 'var h = { a: \'value\' };', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var| a| |=| [|]|;|', @@ -4338,9 +4423,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})`); } } } @@ -4365,7 +4450,7 @@ suite('autoClosingPairs', () => { editorOpts: { autoClosingBrackets: 'beforeWhitespace' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var| a| =| [|];|', @@ -4384,9 +4469,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})`); } } } @@ -4405,7 +4490,7 @@ suite('autoClosingPairs', () => { autoClosingBrackets: 'beforeWhitespace', autoClosingQuotes: 'never' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var| a| =| [|];|', @@ -4417,11 +4502,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})`); } } }); @@ -4435,7 +4520,7 @@ suite('autoClosingPairs', () => { autoClosingBrackets: 'never', autoClosingQuotes: 'beforeWhitespace' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var b =| [|];|', @@ -4447,11 +4532,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})`); } } }); @@ -4476,7 +4561,7 @@ suite('autoClosingPairs', () => { editorOpts: { autoClosingBrackets: 'languageDefined' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'v|ar |a = [|];|', @@ -4495,9 +4580,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})`); } } } @@ -4523,7 +4608,7 @@ suite('autoClosingPairs', () => { autoClosingBrackets: 'never', autoClosingQuotes: 'never' } - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var a = [];', @@ -4542,11 +4627,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})`); } } } @@ -4561,20 +4646,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)`'); }); @@ -4587,14 +4672,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'); }); @@ -4607,18 +4692,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'); }); @@ -4630,18 +4715,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(); @@ -4661,7 +4746,7 @@ suite('autoClosingPairs', () => { 'var h = { a: \'value\' };', ], languageIdentifier: mode.getLanguageIdentifier() - }, (model, cursor) => { + }, (editor, model, viewModel) => { let autoClosePositions = [ 'var a |=| [|]|;|', @@ -4680,11 +4765,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})`); } } } @@ -4699,16 +4784,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(); @@ -4744,17 +4829,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(); }); @@ -4766,8 +4851,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(); }); @@ -4779,60 +4864,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(); @@ -4846,22 +4931,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=());'); }); @@ -4876,14 +4961,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(); @@ -4897,17 +4982,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(); @@ -4921,19 +5006,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(); @@ -4946,22 +5031,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(); @@ -5006,8 +5091,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(); }); @@ -5019,16 +5104,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(); @@ -5071,14 +5156,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(), 'è'); }); @@ -5092,15 +5177,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\''); }); @@ -5114,20 +5199,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(); @@ -5140,23 +5225,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(); @@ -5170,20 +5255,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(); }); @@ -5195,60 +5280,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\''); }); @@ -5262,14 +5347,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(); @@ -5282,15 +5367,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`'); }); @@ -5314,19 +5399,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'); }); @@ -5344,8 +5429,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), ]); @@ -5373,16 +5458,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)); }); }); }); @@ -5397,24 +5482,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)); }); }); @@ -5426,24 +5511,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)); }); }); @@ -5455,8 +5540,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); @@ -5465,19 +5550,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)); }); }); @@ -5489,8 +5574,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); @@ -5499,7 +5584,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); @@ -5507,15 +5592,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)); }); }); @@ -5527,26 +5612,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)); }); }); @@ -5558,14 +5643,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); @@ -5574,15 +5659,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)); }); }); @@ -5594,23 +5679,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)); }); }); @@ -5622,19 +5707,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)); }); }); @@ -5646,12 +5731,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 73b08d8cee..7cb092094a 100644 --- a/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts +++ b/src/vs/editor/test/browser/controller/cursorMoveCommand.test.ts @@ -5,474 +5,482 @@ 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 { TextModel } from 'vs/editor/common/model/textModel'; +import { withTestCodeEditor, ITestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; -import { TestConfiguration } from 'vs/editor/test/common/mocks/testConfiguration'; -import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/monospaceLineBreaksComputer'; -import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; suite('Cursor move command test', () => { - let thisModel: TextModel; - let thisConfiguration: TestConfiguration; - let thisViewModel: ViewModel; - let thisCursor: Cursor; + const TEXT = [ + ' \tMy First Line\t ', + '\tMy Second Line', + ' Third Line🐶', + '', + '1' + ].join('\n'); - setup(() => { - let text = [ - ' \tMy First Line\t ', - '\tMy Second Line', - ' Third Line🐶', - '', - '1' - ].join('\n'); - - thisModel = createTextModel(text); - thisConfiguration = new TestConfiguration({}); - const monospaceLineBreaksComputerFactory = MonospaceLineBreaksComputerFactory.create(thisConfiguration.options); - thisViewModel = new ViewModel(0, thisConfiguration, thisModel, monospaceLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, null!); - thisCursor = new Cursor(thisConfiguration, thisModel, thisViewModel); - }); - - teardown(() => { - thisCursor.dispose(); - thisViewModel.dispose(); - thisModel.dispose(); - thisConfiguration.dispose(); - }); + function executeTest(callback: (editor: ITestCodeEditor, viewModel: ViewModel) => void): void { + withTestCodeEditor(TEXT, {}, (editor, viewModel) => { + callback(editor, viewModel); + }); + } test('move left should move to left character', () => { - moveTo(thisCursor, 1, 8); - - moveLeft(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - - moveLeft(thisCursor, 3); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - - moveLeft(thisCursor, 1, CursorMove.RawUnit.HalfLine); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 2, 3); - - moveLeft(thisCursor, 10); - - cursorEqual(thisCursor, 1, 21); + executeTest((editor, viewModel) => { + moveTo(viewModel, 2, 3); + moveLeft(viewModel, 10); + cursorEqual(viewModel, 1, 21); + }); }); test('move right should move to right character', () => { - moveTo(thisCursor, 1, 5); - - moveRight(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 2); - - moveRight(thisCursor, 6); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 4); - - moveRight(thisCursor, 1, CursorMove.RawUnit.HalfLine); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - - moveRight(thisCursor, 100); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - moveToLineStart(thisCursor); - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 6); - - moveToLineStart(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 1); - - moveToLineStart(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - - moveToLineFirstNonWhitespaceCharacter(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 6); - - moveToLineFirstNonWhitespaceCharacter(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 1); - - moveToLineFirstNonWhitespaceCharacter(thisCursor); - - cursorEqual(thisCursor, 1, 6); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 1); + moveToLineFirstNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 6); + }); }); test('move to end of line from middle', () => { - moveTo(thisCursor, 1, 8); - - moveToLineEnd(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 19); - - moveToLineEnd(thisCursor); - - cursorEqual(thisCursor, 1, 21); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 19); + moveToLineEnd(viewModel); + cursorEqual(viewModel, 1, 21); + }); }); test('move to end of line from line end', () => { - moveTo(thisCursor, 1, 21); - - moveToLineEnd(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 8); - - moveToLineLastNonWhitespaceCharacter(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 19); - - moveToLineLastNonWhitespaceCharacter(thisCursor); - - cursorEqual(thisCursor, 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', () => { - moveTo(thisCursor, 1, 21); - - moveToLineLastNonWhitespaceCharacter(thisCursor); - - cursorEqual(thisCursor, 1, 19); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 21); + moveToLineLastNonWhitespaceCharacter(viewModel); + cursorEqual(viewModel, 1, 19); + }); }); test('move to center of line not from center', () => { - moveTo(thisCursor, 1, 8); - - moveToLineCenter(thisCursor); - - cursorEqual(thisCursor, 1, 11); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 8); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); + }); }); test('move to center of line from center', () => { - moveTo(thisCursor, 1, 11); - - moveToLineCenter(thisCursor); - - cursorEqual(thisCursor, 1, 11); + executeTest((editor, viewModel) => { + moveTo(viewModel, 1, 11); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); + }); }); test('move to center of line from start', () => { - moveToLineStart(thisCursor); - - moveToLineCenter(thisCursor); - - cursorEqual(thisCursor, 1, 11); + executeTest((editor, viewModel) => { + moveToLineStart(viewModel); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); + }); }); test('move to center of line from end', () => { - moveToLineEnd(thisCursor); - - moveToLineCenter(thisCursor); - - cursorEqual(thisCursor, 1, 11); + executeTest((editor, viewModel) => { + moveToLineEnd(viewModel); + moveToLineCenter(viewModel); + cursorEqual(viewModel, 1, 11); + }); }); test('move up by cursor move command', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveTo(thisCursor, 3, 5); - cursorEqual(thisCursor, 3, 5); + moveUp(viewModel, 2); + cursorEqual(viewModel, 1, 5); - moveUp(thisCursor, 2); - cursorEqual(thisCursor, 1, 5); - - moveUp(thisCursor, 1); - cursorEqual(thisCursor, 1, 1); + moveUp(viewModel, 1); + cursorEqual(viewModel, 1, 1); + }); }); test('move up by model line cursor move command', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveTo(thisCursor, 3, 5); - cursorEqual(thisCursor, 3, 5); + moveUpByModelLine(viewModel, 2); + cursorEqual(viewModel, 1, 5); - moveUpByModelLine(thisCursor, 2); - cursorEqual(thisCursor, 1, 5); - - moveUpByModelLine(thisCursor, 1); - cursorEqual(thisCursor, 1, 1); + moveUpByModelLine(viewModel, 1); + cursorEqual(viewModel, 1, 1); + }); }); test('move down by model line cursor move command', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveTo(thisCursor, 3, 5); - cursorEqual(thisCursor, 3, 5); + moveDownByModelLine(viewModel, 2); + cursorEqual(viewModel, 5, 2); - moveDownByModelLine(thisCursor, 2); - cursorEqual(thisCursor, 5, 2); - - moveDownByModelLine(thisCursor, 1); - cursorEqual(thisCursor, 5, 2); + moveDownByModelLine(viewModel, 1); + cursorEqual(viewModel, 5, 2); + }); }); test('move up with selection by cursor move command', () => { + executeTest((editor, viewModel) => { + moveTo(viewModel, 3, 5); + cursorEqual(viewModel, 3, 5); - moveTo(thisCursor, 3, 5); - cursorEqual(thisCursor, 3, 5); + moveUp(viewModel, 1, true); + cursorEqual(viewModel, 2, 2, 3, 5); - moveUp(thisCursor, 1, true); - cursorEqual(thisCursor, 2, 2, 3, 5); - - moveUp(thisCursor, 1, true); - cursorEqual(thisCursor, 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) => { + moveTo(viewModel, 1, 5); + cursorEqual(viewModel, 1, 5); - moveTo(thisCursor, 1, 5); - cursorEqual(thisCursor, 1, 5); + moveDown(viewModel, 4); + cursorEqual(viewModel, 5, 2); - moveDown(thisCursor, 4); - cursorEqual(thisCursor, 5, 2); + moveUp(viewModel, 1); + cursorEqual(viewModel, 4, 1); - moveUp(thisCursor, 1); - cursorEqual(thisCursor, 4, 1); + moveUp(viewModel, 1); + cursorEqual(viewModel, 3, 5); - moveUp(thisCursor, 1); - cursorEqual(thisCursor, 3, 5); + moveUp(viewModel, 1); + cursorEqual(viewModel, 2, 2); - moveUp(thisCursor, 1); - cursorEqual(thisCursor, 2, 2); - - moveUp(thisCursor, 1); - cursorEqual(thisCursor, 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) => { + moveToEndOfLine(viewModel); + cursorEqual(viewModel, 1, 21); - moveToEndOfLine(thisCursor); - cursorEqual(thisCursor, 1, 21); + moveToEndOfLine(viewModel); + cursorEqual(viewModel, 1, 21); - moveToEndOfLine(thisCursor); - cursorEqual(thisCursor, 1, 21); + moveDown(viewModel, 2); + cursorEqual(viewModel, 3, 17); - moveDown(thisCursor, 2); - cursorEqual(thisCursor, 3, 17); + moveDown(viewModel, 1); + cursorEqual(viewModel, 4, 1); - moveDown(thisCursor, 1); - cursorEqual(thisCursor, 4, 1); + moveDown(viewModel, 1); + cursorEqual(viewModel, 5, 2); - moveDown(thisCursor, 1); - cursorEqual(thisCursor, 5, 2); - - moveUp(thisCursor, 4); - cursorEqual(thisCursor, 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', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 10, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 10, 1); - moveTo(thisCursor, 2, 2); - moveToTop(thisCursor); + moveTo(viewModel, 2, 2); + moveToTop(viewModel); - cursorEqual(thisCursor, 1, 6); + cursorEqual(viewModel, 1, 6); + }); }); test('move to view top line moves to top visible line when first line is not visible', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 10, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 10, 1); - moveTo(thisCursor, 4, 1); - moveToTop(thisCursor); + moveTo(viewModel, 4, 1); + moveToTop(viewModel); - cursorEqual(thisCursor, 2, 2); + cursorEqual(viewModel, 2, 2); + }); }); test('move to view top line moves to nth line from top', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 10, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 10, 1); - moveTo(thisCursor, 4, 1); - moveToTop(thisCursor, 3); + moveTo(viewModel, 4, 1); + moveToTop(viewModel, 3); - cursorEqual(thisCursor, 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', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 3, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 3, 1); - moveTo(thisCursor, 2, 2); - moveToTop(thisCursor, 4); + moveTo(viewModel, 2, 2); + moveToTop(viewModel, 4); - cursorEqual(thisCursor, 3, 5); + cursorEqual(viewModel, 3, 5); + }); }); test('move to view center line moves to the center line', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(3, 1, 3, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(3, 1, 3, 1); - moveTo(thisCursor, 2, 2); - moveToCenter(thisCursor); + moveTo(viewModel, 2, 2); + moveToCenter(viewModel); - cursorEqual(thisCursor, 3, 5); + cursorEqual(viewModel, 3, 5); + }); }); test('move to view bottom line moves to last visible line if it is last line', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 5, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 5, 1); - moveTo(thisCursor, 2, 2); - moveToBottom(thisCursor); + moveTo(viewModel, 2, 2); + moveToBottom(viewModel); - cursorEqual(thisCursor, 5, 1); + cursorEqual(viewModel, 5, 1); + }); }); test('move to view bottom line moves to last visible line when last line is not visible', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 3, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 3, 1); - moveTo(thisCursor, 2, 2); - moveToBottom(thisCursor); + moveTo(viewModel, 2, 2); + moveToBottom(viewModel); - cursorEqual(thisCursor, 3, 5); + cursorEqual(viewModel, 3, 5); + }); }); test('move to view bottom line moves to nth line from bottom', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 5, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(1, 1, 5, 1); - moveTo(thisCursor, 4, 1); - moveToBottom(thisCursor, 3); + moveTo(viewModel, 4, 1); + moveToBottom(viewModel, 3); - cursorEqual(thisCursor, 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', () => { - thisViewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 5, 1); + executeTest((editor, viewModel) => { + viewModel.getCompletelyVisibleViewRange = () => new Range(2, 1, 5, 1); - moveTo(thisCursor, 4, 1); - moveToBottom(thisCursor, 5); + moveTo(viewModel, 4, 1); + moveToBottom(viewModel, 5); - cursorEqual(thisCursor, 2, 2); + cursorEqual(viewModel, 2, 2); + }); }); }); // Move command -function move(cursor: Cursor, args: any) { - CoreNavigationCommands.CursorMove.runCoreEditorCommand(cursor, args); +function move(viewModel: ViewModel, args: any) { + CoreNavigationCommands.CursorMove.runCoreEditorCommand(viewModel, args); } -function moveToLineStart(cursor: Cursor) { - move(cursor, { to: CursorMove.RawDirection.WrappedLineStart }); +function moveToLineStart(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineStart }); } -function moveToLineFirstNonWhitespaceCharacter(cursor: Cursor) { - move(cursor, { to: CursorMove.RawDirection.WrappedLineFirstNonWhitespaceCharacter }); +function moveToLineFirstNonWhitespaceCharacter(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineFirstNonWhitespaceCharacter }); } -function moveToLineCenter(cursor: Cursor) { - move(cursor, { to: CursorMove.RawDirection.WrappedLineColumnCenter }); +function moveToLineCenter(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineColumnCenter }); } -function moveToLineEnd(cursor: Cursor) { - move(cursor, { to: CursorMove.RawDirection.WrappedLineEnd }); +function moveToLineEnd(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineEnd }); } -function moveToLineLastNonWhitespaceCharacter(cursor: Cursor) { - move(cursor, { to: CursorMove.RawDirection.WrappedLineLastNonWhitespaceCharacter }); +function moveToLineLastNonWhitespaceCharacter(viewModel: ViewModel) { + move(viewModel, { to: CursorMove.RawDirection.WrappedLineLastNonWhitespaceCharacter }); } -function moveLeft(cursor: Cursor, value?: number, by?: string, select?: boolean) { - move(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(cursor: Cursor, value?: number, by?: string, select?: boolean) { - move(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(cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(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(cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(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(cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(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(cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(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(cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(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(cursor: Cursor, select?: boolean) { - move(cursor, { to: CursorMove.RawDirection.ViewPortCenter, select: select }); +function moveToCenter(viewModel: ViewModel, select?: boolean) { + move(viewModel, { to: CursorMove.RawDirection.ViewPortCenter, select: select }); } -function moveToBottom(cursor: Cursor, noOfLines: number = 1, select?: boolean) { - move(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) { @@ -493,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 9c45df36f9..a04b8ecaac 100644 --- a/src/vs/editor/test/browser/testCodeEditor.ts +++ b/src/vs/editor/test/browser/testCodeEditor.ts @@ -3,13 +3,12 @@ * Licensed under the Source EULA. 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,21 +26,26 @@ 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 { return new TestConfiguration(options); } - protected _createView(viewModel: ViewModel, cursor: Cursor): [View, boolean] { + protected _createView(viewModel: ViewModel): [View, boolean] { // Never create a view return [null! as View, false]; } //#endregion //#region Testing utils - public getCursor(): Cursor | undefined { - return this._modelData ? this._modelData.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 03957abbba..e40b17ce4c 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 354b131f3b..422277010e 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/viewLayout/viewLineRenderer.test.ts b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts index 3ca10a0119..f464ca8cec 100644 --- a/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts +++ b/src/vs/editor/test/common/viewLayout/viewLineRenderer.test.ts @@ -703,6 +703,36 @@ suite('viewLineRenderer.renderLine', () => { assert.equal(actual.containsRTL, true); }); + test('issue #95685: Uses unicode replacement character for Paragraph Separator', () => { + const lineText = 'var ftext = [\u2029"Und", "dann", "eines"];'; + const lineParts = createViewLineTokens([createPart(lineText.length, 1)]); + const expectedOutput = [ + 'var\u00a0ftext\u00a0=\u00a0[\uFFFD"Und",\u00a0"dann",\u00a0"eines"];' + ]; + const actual = renderViewLine(new RenderLineInput( + false, + true, + lineText, + false, + false, + false, + 0, + lineParts, + [], + 4, + 0, + 10, + 10, + 10, + -1, + 'none', + false, + false, + null + )); + assert.equal(actual.html, '' + expectedOutput.join('') + ''); + }); + test('issue #19673: Monokai Theme bad-highlighting in line wrap', () => { let lineText = ' MongoCallback): void {'; diff --git a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts index f2f57e34fa..8c9b4c59a3 100644 --- a/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelDecorations.test.ts @@ -77,7 +77,7 @@ suite('ViewModelDecorations', () => { new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) ).map((dec) => { return dec.options.className; - }); + }).filter(Boolean); assert.deepEqual(actualDecorations, [ 'dec1', @@ -292,7 +292,7 @@ suite('ViewModelDecorations', () => { let decorations = viewModel.getDecorationsInViewport( new Range(2, viewModel.getLineMinColumn(2), 3, viewModel.getLineMaxColumn(3)) - ); + ).filter(x => Boolean(x.options.beforeContentClassName)); assert.deepEqual(decorations, []); let inlineDecorations1 = viewModel.getViewLineRenderingData( diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 3a879f6fb0..e14395a056 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,14 +65,16 @@ suite('ViewModel', () => { let viewLineCount: number[] = []; viewLineCount.push(viewModel.getLineCount()); - viewModel.addEventListener((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()); - assert.deepEqual(viewLineCount, [4, 1, 1, 1]); + assert.deepEqual(viewLineCount, [4, 1, 1, 1, 1]); }); }); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index f9258fce82..330b7bffe1 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -2328,7 +2328,7 @@ declare namespace monaco.editor { * @param handlerId The id of the handler or the id of a contribution. * @param payload Extra data to be sent to the handler. */ - trigger(source: string, handlerId: string, payload: any): void; + trigger(source: string | null | undefined, handlerId: string, payload: any): void; /** * Gets the current model attached to this editor. */ @@ -2628,6 +2628,11 @@ declare namespace monaco.editor { * Defaults to true. */ renderFinalNewline?: boolean; + /** + * Remove unusual line terminators like LINE SEPARATOR (LS), PARAGRAPH SEPARATOR (PS), NEXT LINE (NEL). + * Defaults to true. + */ + removeUnusualLineTerminators?: boolean; /** * Should the corresponding line be selected when clicking on the line number? * Defaults to true. @@ -3905,49 +3910,50 @@ declare namespace monaco.editor { quickSuggestions = 70, quickSuggestionsDelay = 71, readOnly = 72, - 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, - useTabStops = 102, - wordSeparators = 103, - wordWrap = 104, - wordWrapBreakAfterCharacters = 105, - wordWrapBreakBeforeCharacters = 106, - wordWrapColumn = 107, - wordWrapMinified = 108, - wrappingIndent = 109, - wrappingStrategy = 110, - editorClassName = 111, - pixelRatio = 112, - tabFocusMode = 113, - layoutInfo = 114, - wrappingInfo = 115 + 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, + useTabStops = 103, + wordSeparators = 104, + wordWrap = 105, + wordWrapBreakAfterCharacters = 106, + wordWrapBreakBeforeCharacters = 107, + wordWrapColumn = 108, + wordWrapMinified = 109, + wrappingIndent = 110, + wrappingStrategy = 111, + editorClassName = 112, + pixelRatio = 113, + tabFocusMode = 114, + layoutInfo = 115, + wrappingInfo = 116 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4023,6 +4029,7 @@ declare namespace monaco.editor { quickSuggestions: IEditorOption; quickSuggestionsDelay: IEditorOption; readOnly: IEditorOption; + removeUnusualLineTerminators: IEditorOption; renameOnType: IEditorOption; renderControlCharacters: IEditorOption; renderIndentGuides: IEditorOption; @@ -4595,15 +4602,15 @@ declare namespace monaco.editor { /** * Change the scrollLeft of the editor's viewport. */ - setScrollLeft(newScrollLeft: number): void; + setScrollLeft(newScrollLeft: number, scrollType?: ScrollType): void; /** * Change the scrollTop of the editor's viewport. */ - setScrollTop(newScrollTop: number): void; + setScrollTop(newScrollTop: number, scrollType?: ScrollType): void; /** * Change the scroll position of the editor's viewport. */ - setScrollPosition(position: INewScrollPosition): void; + setScrollPosition(position: INewScrollPosition, scrollType?: ScrollType): void; /** * Get an action that is a contribution to this editor. * @id Unique identifier of the contribution. @@ -4616,7 +4623,7 @@ declare namespace monaco.editor { * @param source The source of the call. * @param command The command to execute */ - executeCommand(source: string, command: ICommand): void; + executeCommand(source: string | null | undefined, command: ICommand): void; /** * Push an "undo stop" in the undo-redo stack. */ @@ -4628,13 +4635,13 @@ declare namespace monaco.editor { * @param edits The edits to execute. * @param endCursorState Cursor state after the edits were applied. */ - executeEdits(source: string, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; + executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean; /** * Execute multiple (concomitant) commands on the editor. * @param source The source of the call. * @param command The commands to execute */ - executeCommands(source: string, commands: (ICommand | null)[]): void; + executeCommands(source: string | null | undefined, commands: (ICommand | null)[]): void; /** * Get all the decorations on a line (filtering out decorations from other editors). */ diff --git a/src/vs/platform/accessibility/common/accessibility.ts b/src/vs/platform/accessibility/common/accessibility.ts index b52e39a360..56c9b5fa1e 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 0847dbbbb8..538a3a9d97 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'); @@ -483,7 +484,8 @@ export function registerAction2(ctor: { new(): Action2 }): IDisposable { disposables.add(MenuRegistry.appendMenuItem(menu.id, { command, ...menu })); } if (f1) { - disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command })); + disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, { command, when: command.precondition })); + disposables.add(MenuRegistry.addCommand(command)); } // keybinding diff --git a/src/vs/platform/authentication/common/authentication.ts b/src/vs/platform/authentication/common/authentication.ts index f0ed100364..7e1ad64f64 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 a7362c16a7..e15e5ae7d9 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/backup/electron-main/backupMainService.ts b/src/vs/platform/backup/electron-main/backupMainService.ts index fb5fe23d51..f28c47767b 100644 --- a/src/vs/platform/backup/electron-main/backupMainService.ts +++ b/src/vs/platform/backup/electron-main/backupMainService.ts @@ -17,7 +17,7 @@ import { IFilesConfiguration, HotExitConfiguration } from 'vs/platform/files/com import { ILogService } from 'vs/platform/log/common/log'; import { IWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { URI } from 'vs/base/common/uri'; -import { isEqual as areResourcesEquals, getComparisonKey, hasToIgnoreCase } from 'vs/base/common/resources'; +import { isEqual as areResourcesEquals, getComparisonKey } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; import { Schemas } from 'vs/base/common/network'; @@ -486,7 +486,7 @@ export class BackupMainService implements IBackupMainService { // for backward compatibility, use the fspath as key key = platform.isLinux ? folderUri.fsPath : folderUri.fsPath.toLowerCase(); } else { - key = hasToIgnoreCase(folderUri) ? folderUri.toString().toLowerCase() : folderUri.toString(); + key = folderUri.toString().toLowerCase(); } return crypto.createHash('md5').update(key).digest('hex'); diff --git a/src/vs/platform/clipboard/browser/clipboardService.ts b/src/vs/platform/clipboard/browser/clipboardService.ts index 15e56ac94e..aca3a4a0c0 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,25 +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 { - return undefined; // {{SQL CARBON EDIT}} strict-null-checks + /** @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 d0882e3b2c..c9052ad1e6 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 58a2027bf7..8879e7c597 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 2bfd8ac730..d2b63d5f03 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 64981ed584..38ef10b7db 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 e62f0ab7fd..6259dc64fb 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/credentials/node/credentialsService.ts b/src/vs/platform/credentials/node/credentialsService.ts index e994c983b5..c6720c71d2 100644 --- a/src/vs/platform/credentials/node/credentialsService.ts +++ b/src/vs/platform/credentials/node/credentialsService.ts @@ -14,27 +14,27 @@ export class KeytarCredentialsService implements ICredentialsService { private readonly _keytar = new IdleValue>(() => import('keytar')); async getPassword(service: string, account: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.getPassword(service, account); } async setPassword(service: string, account: string, password: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.setPassword(service, account, password); } async deletePassword(service: string, account: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.deletePassword(service, account); } async findPassword(service: string): Promise { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.findPassword(service); } async findCredentials(service: string): Promise> { - const keytar = await this._keytar.getValue(); + const keytar = await this._keytar.value; return keytar.findCredentials(service); } } diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index f165fc6fc9..1d84be6a7f 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 39f7c0d959..e6a28d81c5 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 761b2e0554..ac57c89d9e 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 5dd4f5fcd0..1660b18ab7 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 eb77f1ed42..490657cc9b 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, @@ -114,7 +120,10 @@ export class ElectronMainService implements IElectronMainService { } private async doOpenEmptyWindow(windowId: number | undefined, options?: IOpenEmptyWindowOptions): Promise { - this.windowsMainService.openEmptyWindow(OpenContext.API, options); + this.windowsMainService.openEmptyWindow({ + context: OpenContext.API, + contextWindowId: windowId + }, options); } async toggleFullScreen(windowId: number | undefined): Promise { @@ -290,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 0000000000..884eadb726 --- /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 Source EULA. 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 56e6d3306e..bf3f73aa69 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'; @@ -196,6 +197,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 09c68a312e..f23df051b3 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 69fae4e056..772836f344 100644 --- a/src/vs/platform/extensionManagement/common/extensionGalleryService.ts +++ b/src/vs/platform/extensionManagement/common/extensionGalleryService.ts @@ -415,10 +415,9 @@ export class ExtensionGalleryService implements IExtensionGalleryService { } const { id, uuid } = arg1; // {{SQL CARBON EDIT}} @anthonydresser remove extension ? extension.identifier 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); @@ -479,8 +478,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" @@ -641,6 +639,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.')); } @@ -768,10 +771,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 c73e3b74c3..59afd22f12 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagement.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagement.ts @@ -96,7 +96,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; } @@ -191,6 +193,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; @@ -200,10 +208,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 583c3d56bb..a9d656bb69 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 59077e4336..fb7013139a 100644 --- a/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts +++ b/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts @@ -69,12 +69,11 @@ 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, - // {{SQL CARBON EDIT}} - extensionVersion: extension.manifest.version + extensionVersion: extension.manifest.version // {{SQL CARBON EDIT}} }; } @@ -120,4 +119,4 @@ export function getMaliciousExtensionsSet(report: IReportedExtension[]): Set result.push(recommendationByRemote.get(remote)!)); + const domains = getDomainsOfRemotes(content.value.toString(), keys(recommendationByRemote)); + for (const domain of domains) { + const remote = recommendationByRemote.get(domain); + if (remote) { + result.push(remote); + } + } } catch (error) { /* Ignore */ } } return result; diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 9d60e842b9..f6276ceb17 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -8,8 +8,7 @@ import * as path from 'vs/base/common/path'; import * as pfs from 'vs/base/node/pfs'; import { assign } from 'vs/base/common/objects'; import { toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { flatten/*, isNonEmptyArray*/ } from 'vs/base/common/arrays'; -import { extract, ExtractError, zip, IFile } from 'vs/base/node/zip'; +import { zip, IFile } from 'vs/base/node/zip'; import { IExtensionManagementService, IExtensionGalleryService, ILocalExtension, IGalleryExtension, IGalleryMetadata, @@ -19,21 +18,20 @@ import { IReportedExtension, InstallOperation, INSTALL_ERROR_MALICIOUS, - INSTALL_ERROR_INCOMPATIBLE + INSTALL_ERROR_INCOMPATIBLE, + ExtensionManagementError } from 'vs/platform/extensionManagement/common/extensionManagement'; -import { areSameExtensions, getGalleryExtensionId, groupByExtension, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; -import { localizeManifest } from '../common/extensionNls'; +import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService'; -import { Limiter, createCancelablePromise, CancelablePromise, Queue } from 'vs/base/common/async'; +import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; import * as semver from 'semver-umd'; import { URI } from 'vs/base/common/uri'; import product from 'vs/platform/product/common/product'; -import { isMacintosh, isWindows } from 'vs/base/common/platform'; +import { isMacintosh } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; -import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; @@ -43,79 +41,35 @@ import { IDownloadService } from 'vs/platform/download/common/download'; import { optional, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Schemas } from 'vs/base/common/network'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil'; import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader'; +import { ExtensionsScanner, IMetadata } from 'vs/platform/extensionManagement/node/extensionsScanner'; +import { ExtensionsLifecycle } from 'vs/platform/extensionManagement/node/extensionLifecycle'; -const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem'; -const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser'; const INSTALL_ERROR_UNSET_UNINSTALLED = 'unsetUninstalled'; const INSTALL_ERROR_DOWNLOADING = 'downloading'; const INSTALL_ERROR_VALIDATING = 'validating'; const INSTALL_ERROR_LOCAL = 'local'; -const INSTALL_ERROR_EXTRACTING = 'extracting'; -const INSTALL_ERROR_RENAMING = 'renaming'; -const INSTALL_ERROR_DELETING = 'deleting'; const ERROR_UNKNOWN = 'unknown'; -export class ExtensionManagementError extends Error { - constructor(message: string, readonly code: string) { - super(message); - } -} - -function parseManifest(raw: string): Promise<{ manifest: IExtensionManifest; metadata: IGalleryMetadata; }> { - 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 +84,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 +92,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 +102,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 +114,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 +151,7 @@ export class ExtensionManagementService extends Disposable implements IExtension } - install(vsix: URI, type: ExtensionType = ExtensionType.User): Promise { + install(vsix: URI, isMachineScoped?: boolean): Promise { // {{SQL CARBON EDIT}} let startTime = new Date().getTime(); @@ -220,10 +173,10 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(installedExtensions => { const existing = installedExtensions.filter(i => areSameExtensions(identifier, i.identifier))[0]; if (existing) { - // operation = InstallOperation.Update; {{SQL CARBON EDIT}} comment out for no unused + isMachineScoped = isMachineScoped || existing.isMachineScoped; + // operation = InstallOperation.Update; if (identifierWithVersion.equals(new ExtensionIdentifierWithVersion(existing.identifier, existing.manifest.version))) { - // {{SQL CARBON EDIT}} - Update VS Code product name - return this.removeExtension(existing, 'existing').then(null, e => Promise.reject(new Error(nls.localize('restartCode', "Please restart Azure Data Studio 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 Azure Data Studio before reinstalling {0}.", manifest.displayName || manifest.name)))); } else if (semver.gt(existing.manifest.version, manifest.version)) { return this.uninstall(existing, true); } @@ -233,7 +186,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 Azure Data Studio 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 Azure Data Studio before reinstalling {0}.", manifest.displayName || manifest.name)))); } return undefined; }); @@ -245,7 +198,7 @@ export class ExtensionManagementService extends Disposable implements IExtension this._onInstallExtension.fire({ identifier, zipPath }); // {{SQL CARBON EDIT}} // Until there's a gallery for SQL Ops Studio, skip retrieving the metadata from the gallery - return this.installExtension({ zipPath, identifierWithVersion, metadata: null }, type, token) + return this.installExtension({ zipPath, identifierWithVersion, metadata: { isMachineScoped } }, token) .then( local => { this.reportTelemetry(this.getTelemetryEvent(InstallOperation.Install), getLocalExtensionTelemetryData(local), new Date().getTime() - startTime, void 0); @@ -282,9 +235,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 { {{SQL CARBON EDIT}} comment out for no unused - 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 { {{SQL CARBON EDIT}} + return this.toNonCancellablePromise(this.installExtension({ zipPath, identifierWithVersion, metadata }, token) + .then(local => this.installDependenciesAndPackExtensions(local, undefined) .then( () => local, error => { @@ -302,7 +255,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"))); } @@ -344,14 +297,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( @@ -403,7 +359,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)))))); @@ -421,7 +377,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, @@ -436,21 +392,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) { @@ -477,63 +433,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) { @@ -587,31 +497,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 { {{SQL CARBON EDIT}} comment out function for no unused + /*private getGalleryMetadata(extensionName: string): Promise { {{SQL CARBON EDIT}} 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 { @@ -724,7 +619,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); @@ -748,142 +643,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 { @@ -891,7 +655,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()]) { @@ -904,34 +668,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 { @@ -958,18 +699,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 0000000000..b905ce5b9f --- /dev/null +++ b/src/vs/platform/extensionManagement/node/extensionsScanner.ts @@ -0,0 +1,351 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 5d17a9e920..25b4c9a0b5 100644 --- a/src/vs/platform/extensions/common/extensions.ts +++ b/src/vs/platform/extensions/common/extensions.ts @@ -157,6 +157,7 @@ export interface IExtensionManifest { readonly forceReload?: boolean; // {{ SQL CARBON EDIT }} add field 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 442e7bda03..187fd740f0 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, isEqual, ExtUri } 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'; @@ -674,15 +674,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 = new ExtUri(_ => !isPathCaseSensitive); 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))); } } @@ -700,7 +701,8 @@ export class FileService extends Disposable implements IFileService { // 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 = new ExtUri(_ => !isPathCaseSensitive); + 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))); } } diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index d0d6644bd3..94441d9945 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -824,7 +824,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 +837,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 63436280e9..3da323fe96 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 Source EULA. 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/test/electron-browser/diskFileService.test.ts b/src/vs/platform/files/test/electron-browser/diskFileService.test.ts index 43478f0f47..3429b2f520 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'; diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index 21e05f2d5e..4e470cc1ba 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -219,7 +219,7 @@ export class InstantiationService implements IInstantiationService { if (key in target) { return target[key]; } - let obj = idle.getValue(); + let obj = idle.value; let prop = obj[key]; if (typeof prop !== 'function') { return prop; @@ -229,7 +229,7 @@ export class InstantiationService implements IInstantiationService { return prop; }, set(_target: T, p: PropertyKey, value: any): boolean { - idle.getValue()[p] = value; + idle.value[p] = value; return true; } }); 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 643af5f74f..0e914631a9 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 Source EULA. 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 aa0478656d..3dfa3f79ce 100644 --- a/src/vs/platform/issue/node/issue.ts +++ b/src/vs/platform/issue/common/issue.ts @@ -3,10 +3,6 @@ * Licensed under the Source EULA. 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 6279251e43..53547af445 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 0000000000..3ea9a63de3 --- /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 Source EULA. 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/launch/electron-main/launchMainService.ts b/src/vs/platform/launch/electron-main/launchMainService.ts index 289f02f9c4..11e0d3bfc5 100644 --- a/src/vs/platform/launch/electron-main/launchMainService.ts +++ b/src/vs/platform/launch/electron-main/launchMainService.ts @@ -6,7 +6,6 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURLService } from 'vs/platform/url/common/url'; import { IProcessEnvironment, isMacintosh } from 'vs/base/common/platform'; -import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ParsedArgs } from 'vs/platform/environment/node/argv'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IWindowSettings } from 'vs/platform/windows/common/windows'; @@ -56,7 +55,6 @@ export interface ILaunchMainService { start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise; getMainProcessId(): Promise; getMainProcessInfo(): Promise; - getLogsPath(): Promise; getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]>; } @@ -69,7 +67,6 @@ export class LaunchMainService implements ILaunchMainService { @IWindowsMainService private readonly windowsMainService: IWindowsMainService, @IURLService private readonly urlService: IURLService, @IWorkspacesMainService private readonly workspacesMainService: IWorkspacesMainService, - @IEnvironmentService private readonly environmentService: IEnvironmentService, @IConfigurationService private readonly configurationService: IConfigurationService ) { } @@ -84,7 +81,7 @@ export class LaunchMainService implements ILaunchMainService { // Create a window if there is none if (this.windowsMainService.getWindowCount() === 0) { - const window = this.windowsMainService.openEmptyWindow(OpenContext.DESKTOP)[0]; + const window = this.windowsMainService.openEmptyWindow({ context: OpenContext.DESKTOP })[0]; whenWindowReady = window.ready(); } @@ -234,12 +231,6 @@ export class LaunchMainService implements ILaunchMainService { }); } - getLogsPath(): Promise { - this.logService.trace('Received request for logs path from other instance.'); - - return Promise.resolve(this.environmentService.logsPath); - } - getRemoteDiagnostics(options: IRemoteDiagnosticOptions): Promise<(IRemoteDiagnosticInfo | IRemoteDiagnosticError)[]> { const windows = this.windowsMainService.getWindows(); const promises: Promise[] = windows.map(window => { diff --git a/src/vs/platform/list/browser/listService.ts b/src/vs/platform/list/browser/listService.ts index b7a42fb0bb..17ec57e0d4 100644 --- a/src/vs/platform/list/browser/listService.ts +++ b/src/vs/platform/list/browser/listService.ts @@ -129,6 +129,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'); @@ -658,6 +659,7 @@ abstract class ResourceNavigator extends Disposable { onDidChangeFocus: Event<{ browserEvent?: UIEvent }>, onDidChangeSelection: Event<{ browserEvent?: UIEvent }>, onDidOpen: Event<{ browserEvent?: UIEvent }>, + readonly openOnSingleClick?: boolean }, options?: IResourceNavigatorOptions ) { @@ -711,7 +713,7 @@ abstract class ResourceNavigator extends Disposable { !!(browserEvent).preserveFocus : !isDoubleClick; - if (this.options.openOnSingleClick || isDoubleClick || isKeyboardEvent) { + if (this.options.openOnSingleClick || this.treeOrList.openOnSingleClick || isDoubleClick || isKeyboardEvent) { const sideBySide = browserEvent instanceof MouseEvent && (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey); this.open(preserveFocus, isDoubleClick || isMiddleClick, sideBySide, browserEvent); } @@ -738,8 +740,8 @@ export class ListResourceNavigator extends ResourceNavigator { } export class TreeResourceNavigator extends ResourceNavigator { - constructor(tree: WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options?: IResourceNavigatorOptions) { - super(tree, { openOnSingleClick: tree.openOnSingleClick, ...(options || {}) }); + constructor(tree: WorkbenchObjectTree | WorkbenchCompressibleObjectTree | WorkbenchDataTree | WorkbenchAsyncDataTree | WorkbenchCompressibleAsyncDataTree, options: IResourceNavigatorOptions = {}) { + super(tree, options); } } @@ -1003,6 +1005,7 @@ function workbenchTreeDataPreamble(treeIndentKey), renderIndentGuides: configurationService.getValue(treeRenderIndentGuidesKey), + smoothScrolling: configurationService.getValue(listSmoothScrolling), automaticKeyboardNavigation: getAutomaticKeyboardNavigation(), simpleKeyboardNavigation: keyboardNavigation === 'simple', filterOnType: keyboardNavigation === 'filter', @@ -1078,25 +1081,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 => { @@ -1181,6 +1192,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 36e3d6b617..e3e39e6f90 100644 --- a/src/vs/platform/menubar/node/menubar.ts +++ b/src/vs/platform/menubar/common/menubar.ts @@ -3,14 +3,9 @@ * Licensed under the Source EULA. 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 91f877b441..c80c939de7 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'; @@ -61,7 +61,7 @@ export class Menubar { private keybindings: { [commandId: string]: IMenubarKeybinding }; - private fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void } = {}; + private readonly fallbackMenuHandlers: { [id: string]: (menuItem: MenuItem, browserWindow: BrowserWindow, event: Event) => void } = Object.create(null); constructor( @IUpdateService private readonly updateService: IUpdateService, @@ -113,8 +113,8 @@ export class Menubar { private addFallbackHandlers(): void { // File Menu Items - this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = () => this.windowsMainService.openEmptyWindow(OpenContext.MENU); - this.fallbackMenuHandlers['workbench.action.newWindow'] = () => this.windowsMainService.openEmptyWindow(OpenContext.MENU); + this.fallbackMenuHandlers['workbench.action.files.newUntitledFile'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win.id }); + this.fallbackMenuHandlers['workbench.action.newWindow'] = (menuItem, win, event) => this.windowsMainService.openEmptyWindow({ context: OpenContext.MENU, contextWindowId: win.id }); this.fallbackMenuHandlers['workbench.action.files.openFileFolder'] = (menuItem, win, event) => this.electronMainService.pickFileFolderAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); this.fallbackMenuHandlers['workbench.action.openWorkspace'] = (menuItem, win, event) => this.electronMainService.pickWorkspaceAndOpen(undefined, { forceNewWindow: this.isOptionClick(event), telemetryExtraData: { from: telemetryFrom } }); @@ -267,7 +267,7 @@ export class Menubar { this.appMenuInstalled = true; const dockMenu = new Menu(); - dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow(OpenContext.DOCK) })); + dockMenu.append(new MenuItem({ label: this.mnemonicLabel(nls.localize({ key: 'miNewWindow', comment: ['&& denotes a mnemonic'] }, "New &&Window")), click: () => this.windowsMainService.openEmptyWindow({ context: OpenContext.DOCK }) })); app.dock.setMenu(dockMenu); } diff --git a/src/vs/platform/menubar/electron-main/menubarMainService.ts b/src/vs/platform/menubar/electron-main/menubarMainService.ts index 5d9f12803e..393fe1525f 100644 --- a/src/vs/platform/menubar/electron-main/menubarMainService.ts +++ b/src/vs/platform/menubar/electron-main/menubarMainService.ts @@ -3,38 +3,42 @@ * Licensed under the Source EULA. 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; - private _menubar: Menubar | undefined; + private menubar: Promise; constructor( @IInstantiationService private readonly instantiationService: IInstantiationService, @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, @ILogService private readonly logService: ILogService ) { - // Install Menu - this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { - this._menubar = this.instantiationService.createInstance(Menubar); - }); + this.menubar = this.installMenuBarAfterWindowOpen(); } - updateMenubar(windowId: number, menus: IMenubarData): Promise { - return this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen).then(() => { - this.logService.trace('menubarService#updateMenubar', windowId); + private async installMenuBarAfterWindowOpen(): Promise { + await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); - if (this._menubar) { - this._menubar.updateMenu(menus, windowId); - } + return this.instantiationService.createInstance(Menubar); + } - return undefined; - }); + async updateMenubar(windowId: number, menus: IMenubarData): Promise { + this.logService.trace('menubarService#updateMenubar', windowId); + + const menubar = await this.menubar; + menubar.updateMenu(menus, windowId); } } 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 0000000000..d18766b8d6 --- /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 Source EULA. 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 c16eef09cc..61c31d799d 100644 --- a/src/vs/platform/product/common/product.ts +++ b/src/vs/platform/product/common/product.ts @@ -21,7 +21,7 @@ if (isWeb) { if (Object.keys(product).length === 0) { Object.assign(product, { version: '1.17.0-dev', - vscodeVersion: '1.45.0-dev', + vscodeVersion: '1.46.0-dev', nameLong: 'Azure Data Studio Web Dev', nameShort: 'Azure Data Studio Web Dev', urlProtocol: 'azuredatastudio-oss' diff --git a/src/vs/platform/quickinput/common/quickAccess.ts b/src/vs/platform/quickinput/common/quickAccess.ts index 088ea15901..d6f0051814 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/telemetry/common/telemetry.ts b/src/vs/platform/telemetry/common/telemetry.ts index 2cad870139..a1cb8139b7 100644 --- a/src/vs/platform/telemetry/common/telemetry.ts +++ b/src/vs/platform/telemetry/common/telemetry.ts @@ -23,6 +23,11 @@ export interface ITelemetryData { export interface ITelemetryService { + /** + * Whether error telemetry will get sent. If false, `publicLogError` will no-op. + */ + readonly sendErrorTelemetry: boolean; + _serviceBrand: undefined; /** diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 815230c12d..b57fc86147 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -35,7 +35,7 @@ export class TelemetryService implements ITelemetryService { private _piiPaths: string[]; private _userOptIn: boolean; private _enabled: boolean; - private _sendErrorTelemetry: boolean; + public readonly sendErrorTelemetry: boolean; private readonly _disposables = new DisposableStore(); private _cleanupPatterns: RegExp[] = []; @@ -49,7 +49,7 @@ export class TelemetryService implements ITelemetryService { this._piiPaths = config.piiPaths || []; this._userOptIn = true; this._enabled = true; - this._sendErrorTelemetry = !!config.sendErrorTelemetry; + this.sendErrorTelemetry = !!config.sendErrorTelemetry; // static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information` this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi]; @@ -148,7 +148,7 @@ export class TelemetryService implements ITelemetryService { } publicLogError(errorEventName: string, data?: ITelemetryData): Promise { - if (!this._sendErrorTelemetry) { + if (!this.sendErrorTelemetry) { return Promise.resolve(undefined); } diff --git a/src/vs/platform/telemetry/common/telemetryUtils.ts b/src/vs/platform/telemetry/common/telemetryUtils.ts index 27c5bf747d..5a655fb2bd 100644 --- a/src/vs/platform/telemetry/common/telemetryUtils.ts +++ b/src/vs/platform/telemetry/common/telemetryUtils.ts @@ -13,6 +13,8 @@ import { isObject } from 'vs/base/common/types'; export const NullTelemetryService = new class implements ITelemetryService { _serviceBrand: undefined; + readonly sendErrorTelemetry = false; + publicLog(eventName: string, data?: ITelemetryData) { return Promise.resolve(undefined); } diff --git a/src/vs/platform/theme/common/themeService.ts b/src/vs/platform/theme/common/themeService.ts index a339736341..c79c22ab2d 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/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index 121f5197bd..41596cd489 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -515,7 +515,7 @@ function createDefaultTokenClassificationRegistry(): TokenClassificationRegistry registerTokenType('namespace', nls.localize('namespace', "Style for namespaces."), [['entity.name.namespace']]); registerTokenType('type', nls.localize('type', "Style for types."), [['entity.name.type'], ['support.type']]); - registerTokenType('struct', nls.localize('struct', "Style for structs."), [['storage.type.struct']]); + registerTokenType('struct', nls.localize('struct', "Style for structs."), [['entity.name.type.struct']]); registerTokenType('class', nls.localize('class', "Style for classes."), [['entity.name.type.class'], ['support.class']]); registerTokenType('interface', nls.localize('interface', "Style for interfaces."), [['entity.name.type.interface']]); registerTokenType('enum', nls.localize('enum', "Style for enums."), [['entity.name.type.enum']]); diff --git a/src/vs/platform/theme/test/common/testThemeService.ts b/src/vs/platform/theme/test/common/testThemeService.ts index 7cd68987a8..c17e1e1df1 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 9defb41ff5..22fc6ee0bc 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,18 @@ 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; + /** * Add a new element to the `undo` stack. * This will destroy the `redo` stack. diff --git a/src/vs/platform/undoRedo/common/undoRedoService.ts b/src/vs/platform/undoRedo/common/undoRedoService.ts index db23a2d5fa..cbb19c0c5e 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,179 @@ 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); + } + } + } + + 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 +328,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; + } + } + } + }; + } + + private _uriGetComparisonKey(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._uriGetComparisonKey(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._uriGetComparisonKey(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._uriGetComparisonKey(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 +425,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._uriGetComparisonKey(_element.resource); + const element = new ResourceStackElement(_element, resourceLabel, strResource); individualMap.set(element.strResource, element); } @@ -234,18 +436,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 +444,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._uriGetComparisonKey(_element.resource); + const element = new ResourceStackElement(_element, resourceLabel, strResource); individualMap.set(element.strResource, element); } @@ -262,94 +455,52 @@ 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._uriGetComparisonKey(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); + const strResource = this._uriGetComparisonKey(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.setElementsIsValid(isValid); } } // resource public hasElements(resource: URI): boolean { - const strResource = uriGetComparisonKey(resource); + const strResource = this._uriGetComparisonKey(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._uriGetComparisonKey(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._uriGetComparisonKey(resource); if (this._editStacks.has(strResource)) { const editStack = this._editStacks.get(strResource)!; - return (editStack.past.length > 0); + return editStack.hasPastElements(); } return false; } @@ -357,200 +508,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('ok', "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._uriGetComparisonKey(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._uriGetComparisonKey(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._uriGetComparisonKey(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 0000000000..8369a3a2e1 --- /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 Source EULA. 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 8764d827dd..317af38379 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -94,8 +94,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 a69a37e0d8..8cdaded61f 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 fdfe1e4d5c..0000000000 --- a/src/vs/platform/url/node/urlService.ts +++ /dev/null @@ -1,21 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. 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 886bfa2635..6b2f256336 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -7,10 +7,10 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IFileService, IFileContent, FileChangesEvent, FileOperationResult, FileOperationError } from 'vs/platform/files/common/files'; import { VSBuffer } from 'vs/base/common/buffer'; 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 } from 'vs/platform/userDataSync/common/userDataSync'; +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 { @@ -108,7 +143,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected isEnabled(): boolean { return this.userDataSyncEnablementService.isResourceEnabled(this.resource); } - async sync(ref?: string): Promise { + async sync(manifest: IUserDataManifest | null): Promise { if (!this.isEnabled()) { if (this.status !== SyncStatus.Idle) { await this.stop(); @@ -129,7 +164,7 @@ export abstract class AbstractSynchroniser extends Disposable { this.setStatus(SyncStatus.Syncing); const lastSyncUserData = await this.getLastSyncUserData(); - const remoteUserData = ref && lastSyncUserData && lastSyncUserData.ref === ref ? lastSyncUserData : await this.getRemoteUserData(lastSyncUserData); + const remoteUserData = await this.getLatestRemoteUserData(manifest, lastSyncUserData); let status: SyncStatus = SyncStatus.Idle; try { @@ -144,6 +179,51 @@ export abstract class AbstractSynchroniser extends Disposable { } } + async replace(uri: URI): Promise { + const content = await this.resolveContent(uri); + if (!content) { + return false; + } + + const syncData = this.parseSyncData(content); + if (!syncData) { + return false; + } + + await this.stop(); + + try { + this.logService.trace(`${this.syncResourceLogLabel}: Started resetting ${this.resource.toLowerCase()}...`); + this.setStatus(SyncStatus.Syncing); + const lastSyncUserData = await this.getLastSyncUserData(); + const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData); + await this.performReplace(syncData, remoteUserData, lastSyncUserData); + this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`); + } finally { + this.setStatus(SyncStatus.Idle); + } + + return true; + } + + private async getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise { + if (lastSyncUserData) { + + const latestRef = manifest && manifest.latest ? manifest.latest[this.resource] : undefined; + + // Last time synced resource and latest resource on server are same + if (lastSyncUserData.ref === latestRef) { + return lastSyncUserData; + } + + // There is no resource on server and last time it was synced with no resource + if (latestRef === undefined && lastSyncUserData.syncData === null) { + return lastSyncUserData; + } + } + return this.getRemoteUserData(lastSyncUserData); + } + async getSyncPreview(): Promise { if (!this.isEnabled()) { return { hasLocalChanged: false, hasRemoteChanged: false }; @@ -158,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); @@ -166,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...`); @@ -207,6 +287,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))) { @@ -225,18 +317,21 @@ export abstract class AbstractSynchroniser extends Disposable { } catch (e) { /* ignore */ } } - protected async getLastSyncUserData(): Promise { + async getLastSyncUserData(): Promise { try { const content = await this.fileService.readFile(this.lastSyncResource); const parsed = JSON.parse(content.value.toString()); - let syncData: ISyncData = JSON.parse(parsed.content); + const userData: IUserData = parsed as IUserData; + if (userData.content === null) { + return { ref: parsed.ref, syncData: null } as T; + } + const syncData: ISyncData = JSON.parse(userData.content); - // Migration from old content to sync data - if (!isSyncData(syncData)) { - syncData = { version: this.version, content: parsed.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 @@ -247,11 +342,11 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async updateLastSyncUserData(lastSyncRemoteUserData: IRemoteUserData, additionalProps: IStringDictionary = {}): Promise { - const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: JSON.stringify(lastSyncRemoteUserData.syncData), ...additionalProps }; + const lastSyncUserData: IUserData = { ref: lastSyncRemoteUserData.ref, content: lastSyncRemoteUserData.syncData ? JSON.stringify(lastSyncRemoteUserData.syncData) : null, ...additionalProps }; await this.fileService.writeFile(this.lastSyncResource, VSBuffer.fromString(JSON.stringify(lastSyncUserData))); } - protected async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { + async getRemoteUserData(lastSyncData: IRemoteUserData | null): Promise { const { ref, content } = await this.getUserData(lastSyncData); let syncData: ISyncData | null = null; if (content !== null) { @@ -260,20 +355,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 { @@ -287,7 +378,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 }; } @@ -301,6 +393,7 @@ export abstract class AbstractSynchroniser extends Disposable { protected abstract readonly version: number; protected abstract performSync(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; + protected abstract performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise; } @@ -321,6 +414,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, @@ -328,7 +422,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))); } @@ -403,7 +497,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser { // Otherwise fire change event else { - this._onDidChangeLocal.fire(); + this.triggerLocalChange(); } } @@ -426,6 +520,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, @@ -434,7 +529,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 3cff5faacd..0a3db0dee8 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[]; @@ -21,16 +25,15 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync const updated: ISyncExtension[] = []; if (!remoteExtensions) { + const remote = localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())); return { added, removed, updated, - remote: localExtensions.filter(({ identifier }) => ignoredExtensions.every(id => id.toLowerCase() !== identifier.id.toLowerCase())) + remote: remote.length > 0 ? remote : null }; } - // 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; @@ -53,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) => { @@ -62,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))); } @@ -153,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()); @@ -169,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 bc95e82c16..2c16206340 100644 --- a/src/vs/platform/userDataSync/common/extensionsSync.ts +++ b/src/vs/platform/userDataSync/common/extensionsSync.ts @@ -3,22 +3,24 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; +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 { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge'; import { isNonEmptyArray } from 'vs/base/common/arrays'; 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'; -import { joinPath, dirname, basename } from 'vs/base/common/resources'; +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[]; @@ -35,14 +37,20 @@ interface ILastSyncUserData extends IRemoteUserData { skippedExtensions: ISyncExtension[] | undefined; } + export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { - protected readonly version: number = 2; + private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` }); + /* + 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, @@ -53,14 +61,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 { @@ -79,9 +87,11 @@ 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, @@ -112,8 +122,10 @@ 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({ @@ -132,35 +144,58 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse async stop(): Promise { } async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - return [{ resource: joinPath(uri, 'extensions.json') }]; + return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }]; } async resolveContent(uri: URI): Promise { + if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) { + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); + return this.format(localExtensions); + } + let content = await super.resolveContent(uri); if (content) { return content; } + content = await super.resolveContent(dirname(uri)); if (content) { const syncData = this.parseSyncData(content); if (syncData) { switch (basename(uri)) { case 'extensions.json': - const edits = format(syncData.content, undefined, {}); - return applyEdits(syncData.content, edits); + return this.format(this.parseExtensions(syncData)); } } } + return null; } + private format(extensions: ISyncExtension[]): string { + extensions.sort((e1, e2) => { + if (!e1.identifier.uuid && e2.identifier.uuid) { + return -1; + } + if (e1.identifier.uuid && !e2.identifier.uuid) { + return 1; + } + return compare(e1.identifier.id, e2.identifier.id); + }); + const content = JSON.stringify(extensions); + const edits = format(content, undefined, {}); + return applyEdits(content, edits); + } + async acceptConflict(conflict: URI, content: string): Promise { throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } async hasLocalData(): Promise { try { - const localExtensions = await this.getLocalExtensions(); + const installedExtensions = await this.extensionManagementService.getInstalled(); + const localExtensions = this.getLocalExtensions(installedExtensions); if (isNonEmptyArray(localExtensions)) { return true; } @@ -176,12 +211,28 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return SyncStatus.Idle; } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + 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 + }); + } + 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 lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; - 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...`); @@ -189,7 +240,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, @@ -205,10 +256,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse }; } - 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) { @@ -216,9 +263,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } if (hasLocalChanged) { - // back up all disabled or market place extensions - const backUpExtensions = localExtensions.filter(e => e.disabled || !!e.identifier.uuid); - await this.backupLocal(JSON.stringify(backUpExtensions)); + await this.backupLocal(JSON.stringify(localExtensions)); skippedExtensions = await this.updateLocalExtensions(added, removed, updated, skippedExtensions); } @@ -317,31 +362,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 79c649f2cd..53f2a8cd86 100644 --- a/src/vs/platform/userDataSync/common/globalStateMerge.ts +++ b/src/vs/platform/userDataSync/common/globalStateMerge.ts @@ -18,7 +18,7 @@ export interface IMergeResult { export function merge(localStorage: IStringDictionary, remoteStorage: IStringDictionary | null, baseStorage: IStringDictionary | null, storageKeys: ReadonlyArray, previouslySkipped: string[], logService: ILogService): IMergeResult { if (!remoteStorage) { - return { remote: localStorage, local: { added: {}, removed: [], updated: {} }, skipped: [] }; + return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] }; } const localToRemote = compare(localStorage, remoteStorage); @@ -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 ba7cacd231..813d4a28ea 100644 --- a/src/vs/platform/userDataSync/common/globalStateSync.ts +++ b/src/vs/platform/userDataSync/common/globalStateSync.ts @@ -3,17 +3,17 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; +import { SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreviewResult, USER_DATA_SYNC_SCHEME } from 'vs/platform/userDataSync/common/userDataSync'; import { VSBuffer } from 'vs/base/common/buffer'; import { Event } from 'vs/base/common/event'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; -import { dirname, joinPath, basename } from 'vs/base/common/resources'; +import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; import { IStringDictionary } from 'vs/base/common/collections'; import { edit } from 'vs/platform/userDataSync/common/content'; import { merge } from 'vs/platform/userDataSync/common/globalStateMerge'; import { parse } from 'vs/base/common/json'; -import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { URI } from 'vs/base/common/uri'; @@ -41,6 +41,7 @@ interface ILastSyncUserData extends IRemoteUserData { export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser { + private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` }); protected readonly version: number = 1; constructor( @@ -55,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( @@ -65,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())) ); } @@ -139,28 +140,44 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs async stop(): Promise { } async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> { - return [{ resource: joinPath(uri, 'globalState.json') }]; + return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }]; } async resolveContent(uri: URI): Promise { + if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) { + const localGlobalState = await this.getLocalGlobalState(); + return this.format(localGlobalState); + } + let content = await super.resolveContent(uri); if (content) { return content; } + content = await super.resolveContent(dirname(uri)); if (content) { const syncData = this.parseSyncData(content); if (syncData) { switch (basename(uri)) { case 'globalState.json': - const edits = format(syncData.content, undefined, {}); - return applyEdits(syncData.content, edits); + return this.format(JSON.parse(syncData.content)); } } } + return null; } + private format(globalState: IGlobalState): string { + const storageKeys = Object.keys(globalState.storage).sort(); + const storage: IStringDictionary = {}; + storageKeys.forEach(key => storage[key] = globalState.storage[key]); + globalState.storage = storage; + const content = JSON.stringify(globalState); + const edits = format(content, undefined, {}); + return applyEdits(content, edits); + } + async acceptConflict(conflict: URI, content: string): Promise { throw new Error(`${this.syncResourceLogLabel}: Conflicts should not occur`); } @@ -183,6 +200,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs return SyncStatus.Idle; } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { + const localUserData = await this.getLocalGlobalState(); + const syncGlobalState: IGlobalState = JSON.parse(syncData.content); + const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService); + await this.apply({ + 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 + }); + } + 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; diff --git a/src/vs/platform/userDataSync/common/keybindingsSync.ts b/src/vs/platform/userDataSync/common/keybindingsSync.ts index fb20e6a9a3..1ded20757b 100644 --- a/src/vs/platform/userDataSync/common/keybindingsSync.ts +++ b/src/vs/platform/userDataSync/common/keybindingsSync.ts @@ -16,10 +16,11 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { OS, OperatingSystem } from 'vs/base/common/platform'; import { isUndefined } from 'vs/base/common/types'; import { isNonEmptyArray } from 'vs/base/common/arrays'; -import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; +import { IStorageService } from 'vs/platform/storage/common/storage'; 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 { @@ -212,6 +214,24 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem } } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const content = this.getKeybindingsContentFromSyncContent(syncData.content); + + if (content !== null) { + const fileContent = await this.getLocalFileContent(); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + remoteUserData, + lastSyncUserData, + content, + hasConflicts: false, + hasLocalChanged: true, + hasRemoteChanged: true, + })); + await this.apply(); + } + } + private async apply(forcePush?: boolean): Promise { if (!this.syncPreviewResultPromise) { return; @@ -248,10 +268,10 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`); } - if (lastSyncUserData?.ref !== remoteUserData.ref && (content !== null || fileContent !== null)) { + if (lastSyncUserData?.ref !== remoteUserData.ref) { this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`); - const lastSyncContent = this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null); - await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: { version: remoteUserData.syncData!.version, content: lastSyncContent } }); + 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, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null }); this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`); } @@ -315,7 +335,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts }; } - private getKeybindingsContentFromSyncContent(syncContent: string): string | null { + getKeybindingsContentFromSyncContent(syncContent: string): string | null { try { const parsed = JSON.parse(syncContent); if (!this.configurationService.getValue('sync.keybindingsPerPlatform')) { diff --git a/src/vs/platform/userDataSync/common/settingsMerge.ts b/src/vs/platform/userDataSync/common/settingsMerge.ts index c8b837e36d..22a427aaf7 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 2a246f8a03..a10800aac5 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -14,11 +14,14 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { CancellationToken } from 'vs/base/common/cancellation'; import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge'; import { edit } from 'vs/platform/userDataSync/common/content'; -import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; 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 { @@ -257,6 +261,27 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { } } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + if (settingsSyncContent) { + const fileContent = await this.getLocalFileContent(); + const formatUtils = await this.getFormattingOptions(); + const ignoredSettings = await this.getIgnoredSettings(); + const content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils); + this.syncPreviewResultPromise = createCancelablePromise(() => Promise.resolve({ + fileContent, + remoteUserData, + lastSyncUserData, + content, + hasLocalChanged: true, + hasRemoteChanged: true, + hasConflicts: false, + })); + + await this.apply(); + } + } + private async apply(forcePush?: boolean): Promise { if (!this.syncPreviewResultPromise) { return; @@ -357,7 +382,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { return remoteUserData.syncData ? this.parseSettingsSyncContent(remoteUserData.syncData.content) : null; } - private parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { + parseSettingsSyncContent(syncContent: string): ISettingsSyncContent | null { try { const parsed = JSON.parse(syncContent); return isSettingsSyncContent(parsed) ? parsed : /* migrate */ { settings: syncContent }; @@ -391,4 +416,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/snippetsMerge.ts b/src/vs/platform/userDataSync/common/snippetsMerge.ts index 0d955316ae..162891f811 100644 --- a/src/vs/platform/userDataSync/common/snippetsMerge.ts +++ b/src/vs/platform/userDataSync/common/snippetsMerge.ts @@ -26,7 +26,7 @@ export function merge(local: IStringDictionary, remote: IStringDictionar removed: values(removed), updated, conflicts: [], - remote: local + remote: Object.keys(local).length > 0 ? local : null }; } diff --git a/src/vs/platform/userDataSync/common/snippetsSync.ts b/src/vs/platform/userDataSync/common/snippetsSync.ts index 5835afd0e4..01992c2cbd 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(); } } @@ -256,6 +258,19 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD } } + protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { + const local = await this.getSnippetsFileContents(); + const localSnippets = this.toSnippetsContents(local); + const snippets = this.parseSnippets(syncData); + const { added, updated, removed } = merge(localSnippets, snippets, localSnippets); + 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 + })); + await this.apply(); + } + protected getPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { if (!this.syncPreviewResultPromise) { this.syncPreviewResultPromise = createCancelablePromise(token => this.generatePreview(remoteUserData, lastSyncUserData, token)); @@ -285,7 +300,7 @@ 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 ? this.parseSnippets(lastSyncUserData.syncData!) : null; + const lastSyncSnippets: IStringDictionary | null = lastSyncUserData && lastSyncUserData.syncData ? this.parseSnippets(lastSyncUserData.syncData) : null; if (remoteSnippets) { this.logService.trace(`${this.syncResourceLogLabel}: Merging remote snippets with local snippets...`); diff --git a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts index 38bdfb5382..1486538df2 100644 --- a/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataAutoSyncService.ts @@ -3,24 +3,29 @@ * Licensed under the Source EULA. 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 c1dd8e61b3..be8538d292 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,20 @@ export interface IUserDataSyncBackupStoreService { // #region User Data Sync Error export enum UserDataSyncErrorCode { - // Server Errors - Unauthorized = 'Unauthorized', - Forbidden = 'Forbidden', + // Client Errors (>= 400 ) + Unauthorized = 'Unauthorized', /* 401 */ + 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 +226,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 +240,7 @@ export interface ISyncExtension { identifier: IExtensionIdentifier; version?: string; disabled?: boolean; + installed?: boolean; } export interface IStorageValue { @@ -274,7 +282,8 @@ export interface IUserDataSynchroniser { pull(): Promise; push(): Promise; - sync(ref?: string): Promise; + sync(manifest: IUserDataManifest | null): Promise; + replace(uri: URI): Promise; stop(): Promise; getSyncPreview(): Promise @@ -288,6 +297,7 @@ export interface IUserDataSynchroniser { getRemoteSyncResourceHandles(): Promise; getLocalSyncResourceHandles(): Promise; getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>; + getMachineId(syncResourceHandle: ISyncResourceHandle): Promise; } //#endregion @@ -330,6 +340,7 @@ export interface IUserDataSyncService { pull(): Promise; sync(): Promise; stop(): Promise; + replace(uri: URI): Promise; reset(): Promise; resetLocal(): Promise; @@ -340,6 +351,7 @@ export interface IUserDataSyncService { 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'); @@ -369,9 +381,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) { diff --git a/src/vs/platform/userDataSync/common/userDataSyncIpc.ts b/src/vs/platform/userDataSync/common/userDataSyncIpc.ts index 809a73b39e..61b05d560d 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,12 +29,23 @@ 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(); case 'sync': return this.service.sync(); case 'stop': this.service.stop(); return Promise.resolve(); + 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(); @@ -41,6 +54,7 @@ export class UserDataSyncChannel implements IServerChannel { 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'); } @@ -151,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 0000000000..0253f726fb --- /dev/null +++ b/src/vs/platform/userDataSync/common/userDataSyncMachines.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 b4d3f8ef64..eb1de4249b 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. 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 && manifest.latest ? manifest.latest[synchroniser.resource] : undefined); + 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,15 +198,34 @@ 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); } } + async replace(uri: URI): Promise { + await this.checkEnablement(); + for (const synchroniser of this.synchronisers) { + if (await synchroniser.replace(uri)) { + return; + } + } + } + async stop(): Promise { await this.checkEnablement(); if (this.status === SyncStatus.Idle) { @@ -209,6 +273,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle); } + getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise { + return this.getSynchroniser(resource).getMachineId(syncResourceHandle); + } + async isFirstTimeSyncWithMerge(): Promise { await this.checkEnablement(); if (!await this.userDataSyncStoreService.manifest()) { @@ -232,16 +300,19 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ 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); @@ -322,13 +393,16 @@ 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.UpgradeRequired: + case UserDataSyncErrorCode.Incompatible: + throw e; } - throw e; } this.logService.error(e); this.logService.error(`${source}: ${toErrorMessage(e)}`); @@ -339,6 +413,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 57f56820fa..a5a32b09b3 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'; @@ -15,9 +15,15 @@ import { IProductService } from 'vs/platform/product/common/productService'; 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 { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { assign } from 'vs/base/common/objects'; +import { generateUuid } from 'vs/base/common/uuid'; +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 { @@ -25,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, @@ -34,21 +41,28 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn @IUserDataSyncLogService private readonly logService: IUserDataSyncLogService, @IEnvironmentService environmentService: IEnvironmentService, @IFileService fileService: IFileService, - @IStorageService storageService: IStorageService, + @IStorageService private readonly storageService: IStorageService, ) { super(); this.userDataSyncStore = getUserDataSyncStore(productService, configurationService); this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService) .then(uuid => { const headers: IHeaders = { - 'X-Sync-Client-Id': productService.version, + 'X-Client-Name': `${productService.applicationName}${isWeb ? '-web' : ''}`, + 'X-Client-Version': productService.version, + 'X-Machine-Id': uuid }; - headers['X-Sync-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.'); } @@ -56,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.'); } @@ -75,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.'); } @@ -93,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.'); } @@ -113,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. @@ -121,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.'); } @@ -143,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; } @@ -164,12 +178,30 @@ 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); } - return asJson(context); + const manifest = await asJson(context); + const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL); + + if (currentSessionId && manifest && currentSessionId !== manifest.session) { + // Server session is different from client session so clear cached session. + this.clearSession(); + } + + if (manifest === null && currentSessionId) { + // server session is cleared so clear cached session. + this.clearSession(); + } + + if (manifest) { + // update session + this.storageService.store(USER_SESSION_ID_KEY, manifest.session, StorageScope.GLOBAL); + } + + return manifest; } async clear(): Promise { @@ -180,17 +212,25 @@ 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); } + + // clear cached session. + this.clearSession(); } - private async request(options: IRequestOptions, source: SyncResource | undefined, token: CancellationToken): Promise { - const authToken = await this.authTokenService.getToken(); + private clearSession(): void { + this.storageService.remove(USER_SESSION_ID_KEY, StorageScope.GLOBAL); + this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL); + } + + 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; @@ -199,34 +239,95 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn 'authorization': `Bearer ${authToken.token}`, }); + // Add session headers + this.addSessionHeaders(options.headers); + this.logService.trace('Sending request to server', { url: options.url, type: options.type, headers: { ...options.headers, ...{ authorization: undefined } } }); 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); - } - - if (context.res.statusCode === 403) { - throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' is Forbidden (403).`, UserDataSyncErrorCode.Forbidden, source); + throw new UserDataSyncStoreError(`Request '${options.url?.toString()}' failed because of Unauthorized (401).`, UserDataSyncErrorCode.Unauthorized); } 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; } + private addSessionHeaders(headers: IHeaders): void { + let machineSessionId = this.storageService.get(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL); + if (machineSessionId === undefined) { + machineSessionId = generateUuid(); + this.storageService.store(MACHINE_SESSION_ID_KEY, machineSessionId, StorageScope.GLOBAL); + } + headers['X-Machine-Session-Id'] = machineSessionId; + + const userSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL); + if (userSessionId !== undefined) { + headers['X-User-Session-Id'] = userSessionId; + } + } + +} + +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 8dac6aa191..f92e6cb503 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 21d1c1ef58..40010f21bd 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/globalStateSync.test.ts b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts index bc41e15487..b268f02418 100644 --- a/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/globalStateSync.test.ts @@ -44,11 +44,58 @@ suite('GlobalStateSync', () => { teardown(() => disposableStore.clear()); + test('when global state does not exist', async () => { + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when global state is created after first sync', async () => { + await testObject.sync(await testClient.manifest()); + updateStorage('a', 'value1', testClient); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepEqual(JSON.parse(lastSyncUserData!.syncData!.content).storage, { 'a': { version: 1, value: 'value1' } }); + }); + test('first time sync - outgoing to server (no state)', async () => { updateStorage('a', 'value1', testClient); await updateLocale(testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -63,7 +110,7 @@ suite('GlobalStateSync', () => { await updateLocale(client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -76,7 +123,7 @@ suite('GlobalStateSync', () => { await client2.sync(); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -94,7 +141,7 @@ suite('GlobalStateSync', () => { await client2.sync(); updateStorage('a', 'value2', client2); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -109,10 +156,10 @@ suite('GlobalStateSync', () => { test('sync adding a storage value', async () => { updateStorage('a', 'value1', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -127,10 +174,10 @@ suite('GlobalStateSync', () => { test('sync updating a storage value', async () => { updateStorage('a', 'value1', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); updateStorage('a', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -145,10 +192,10 @@ suite('GlobalStateSync', () => { test('sync removing a storage value', async () => { updateStorage('a', 'value1', testClient); updateStorage('b', 'value2', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); removeStorage('b', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); diff --git a/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts new file mode 100644 index 0000000000..70c1526b2b --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/keybindingsSync.test.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource } from 'vs/platform/userDataSync/common/userDataSync'; +import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService'; +import { IFileService } from 'vs/platform/files/common/files'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { KeybindingsSynchroniser } from 'vs/platform/userDataSync/common/keybindingsSync'; +import { VSBuffer } from 'vs/base/common/buffer'; + +suite('KeybindingsSync', () => { + + const disposableStore = new DisposableStore(); + const server = new UserDataSyncTestServer(); + let client: UserDataSyncClient; + + let testObject: KeybindingsSynchroniser; + + setup(async () => { + client = disposableStore.add(new UserDataSyncClient(server)); + await client.setUp(true); + testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Keybindings) as KeybindingsSynchroniser; + disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); + }); + + teardown(() => disposableStore.clear()); + + test('when keybindings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(keybindingsResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when keybindings file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource; + await testObject.sync(await client.manifest()); + await fileService.createFile(keybindingsResource, VSBuffer.fromString('[]')); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]'); + }); + +}); diff --git a/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts b/src/vs/platform/userDataSync/test/common/settingsMerge.test.ts index 4ed45d829e..05cee7c114 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 955306edb9..070d17e27a 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', () => { @@ -43,13 +44,67 @@ suite('SettingsSync', () => { setup(async () => { client = disposableStore.add(new UserDataSyncClient(server)); - await client.setUp(); + await client.setUp(true); testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser; disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear())); }); teardown(() => disposableStore.clear()); + test('when settings file does not exist', async () => { + const fileService = client.instantiationService.get(IFileService); + const settingResource = client.instantiationService.get(IEnvironmentService).settingsResource; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(settingResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when settings file is created after first sync', async () => { + const fileService = client.instantiationService.get(IFileService); + + const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource; + await testObject.sync(await client.manifest()); + await fileService.createFile(settingsResource, VSBuffer.fromString('{}')); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await client.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}'); + }); + test('sync for first time to the server', async () => { const expected = `{ @@ -75,7 +130,7 @@ suite('SettingsSync', () => { }`; await updateSettings(expected); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -99,7 +154,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -130,7 +185,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -161,7 +216,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -185,7 +240,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -203,7 +258,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -213,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 = `{ @@ -237,7 +310,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -285,7 +358,7 @@ suite('SettingsSync', () => { }`; await updateSettings(settingsContent); - await testObject.sync(); + await testObject.sync(await client.manifest()); const { content } = await client.read(testObject.resource); assert.ok(content !== null); @@ -333,7 +406,7 @@ suite('SettingsSync', () => { await updateSettings(expected); try { - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.fail('should fail with invalid content error'); } catch (e) { assert.ok(e instanceof UserDataSyncError); diff --git a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts index 976537bff1..628435fa8d 100644 --- a/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts +++ b/src/vs/platform/userDataSync/test/common/snippetsSync.test.ts @@ -167,11 +167,62 @@ suite('SnippetsSync', () => { teardown(() => disposableStore.clear()); + test('when snippets does not exist', async () => { + const fileService = testClient.instantiationService.get(IFileService); + const snippetsResource = testClient.instantiationService.get(IEnvironmentService).snippetsHome; + + assert.deepEqual(await testObject.getLastSyncUserData(), null); + let manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'GET', url: `${server.url}/v1/resource/${testObject.resource}/latest`, headers: {} }, + ]); + assert.ok(!await fileService.exists(snippetsResource)); + + const lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.equal(lastSyncUserData!.syncData, null); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + + manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + assert.deepEqual(server.requests, []); + }); + + test('when snippet is created after first sync', async () => { + await testObject.sync(await testClient.manifest()); + await updateSnippet('html.json', htmlSnippet1, testClient); + + let lastSyncUserData = await testObject.getLastSyncUserData(); + const manifest = await testClient.manifest(); + server.reset(); + await testObject.sync(manifest); + + assert.deepEqual(server.requests, [ + { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': lastSyncUserData?.ref } }, + ]); + + lastSyncUserData = await testObject.getLastSyncUserData(); + const remoteUserData = await testObject.getRemoteUserData(null); + assert.deepEqual(lastSyncUserData!.ref, remoteUserData.ref); + assert.deepEqual(lastSyncUserData!.syncData, remoteUserData.syncData); + assert.deepEqual(lastSyncUserData!.syncData!.content, JSON.stringify({ 'html.json': htmlSnippet1 })); + }); + test('first time sync - outgoing to server (no snippets)', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -186,7 +237,7 @@ suite('SnippetsSync', () => { await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -201,7 +252,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -221,7 +272,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -234,7 +285,7 @@ suite('SnippetsSync', () => { await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); const conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet1); @@ -259,7 +310,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -278,7 +329,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); let conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); @@ -299,7 +350,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet2, testClient); await updateSnippet('typescript.json', tsSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); const conflicts = testObject.conflicts; await testObject.acceptConflict(conflicts[0].local, htmlSnippet2); @@ -324,10 +375,10 @@ suite('SnippetsSync', () => { test('sync adding a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -345,12 +396,12 @@ suite('SnippetsSync', () => { test('sync adding a snippet - accept', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -362,10 +413,10 @@ suite('SnippetsSync', () => { test('sync updating a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -381,12 +432,12 @@ suite('SnippetsSync', () => { test('sync updating a snippet - accept', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -397,13 +448,13 @@ suite('SnippetsSync', () => { test('sync updating a snippet - conflict', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet3, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); const local = joinPath(environmentService.userDataSyncHome, testObject.resource, PREVIEW_DIR_NAME, 'html.json'); @@ -413,13 +464,13 @@ suite('SnippetsSync', () => { test('sync updating a snippet - resolve conflict', async () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet3, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet2); assert.equal(testObject.status, SyncStatus.Idle); @@ -437,10 +488,10 @@ suite('SnippetsSync', () => { test('sync removing a snippet', async () => { await updateSnippet('html.json', htmlSnippet1, testClient); await updateSnippet('typescript.json', tsSnippet1, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -459,12 +510,12 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -478,13 +529,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await updateSnippet('html.json', htmlSnippet2, client2); await client2.sync(); await removeSnippet('html.json', testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -499,13 +550,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.HasConflicts); const environmentService = testClient.instantiationService.get(IEnvironmentService); @@ -517,13 +568,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, htmlSnippet3); assert.equal(testObject.status, SyncStatus.Idle); @@ -544,13 +595,13 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await removeSnippet('html.json', client2); await client2.sync(); await updateSnippet('html.json', htmlSnippet2, testClient); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); await testObject.acceptConflict(testObject.conflicts[0].local, ''); assert.equal(testObject.status, SyncStatus.Idle); @@ -601,7 +652,7 @@ suite('SnippetsSync', () => { await updateSnippet('html.json', htmlSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); @@ -622,7 +673,7 @@ suite('SnippetsSync', () => { await updateSnippet('typescript.json', tsSnippet1, client2); await client2.sync(); - await testObject.sync(); + await testObject.sync(await testClient.manifest()); assert.equal(testObject.status, SyncStatus.Idle); assert.deepEqual(testObject.conflicts, []); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 38ff99b72d..6f50cc78de 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -7,7 +7,7 @@ import * as assert from 'assert'; import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncEnablementService, ISyncPreviewResult } from 'vs/platform/userDataSync/common/userDataSync'; import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient'; import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; -import { AbstractSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; +import { AbstractSynchroniser, IRemoteUserData, ISyncData } from 'vs/platform/userDataSync/common/abstractSynchronizer'; import { Barrier } from 'vs/base/common/async'; import { Emitter, Event } from 'vs/base/common/event'; @@ -39,9 +39,11 @@ class TestSynchroniser extends AbstractSynchroniser { return this.syncResult.status || SyncStatus.Idle; } + 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 { @@ -49,6 +51,16 @@ 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 }; } @@ -79,7 +91,7 @@ suite('TestSynchronizer', () => { const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(); + testObject.sync(await client.manifest()); await promise; assert.deepEqual(actual, [SyncStatus.Syncing]); @@ -94,7 +106,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); assert.deepEqual(testObject.status, SyncStatus.Idle); @@ -107,7 +119,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.HasConflicts]); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); @@ -122,7 +134,7 @@ suite('TestSynchronizer', () => { disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); try { - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.fail('Should fail'); } catch (e) { assert.deepEqual(actual, [SyncStatus.Syncing, SyncStatus.Idle]); @@ -134,12 +146,12 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); const promise = Event.toPromise(testObject.onDoSyncCall.event); - testObject.sync(); + testObject.sync(await client.manifest()); await promise; const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.Syncing); @@ -154,7 +166,7 @@ suite('TestSynchronizer', () => { const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.Idle); @@ -164,11 +176,11 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); testObject.syncResult = { status: SyncStatus.HasConflicts }; testObject.syncBarrier.open(); - await testObject.sync(); + await testObject.sync(await client.manifest()); const actual: SyncStatus[] = []; disposableStore.add(testObject.onDidChangeStatus(status => actual.push(status))); - await testObject.sync(); + await testObject.sync(await client.manifest()); assert.deepEqual(actual, []); assert.deepEqual(testObject.status, SyncStatus.HasConflicts); @@ -178,7 +190,7 @@ suite('TestSynchronizer', () => { const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings); // Sync once testObject.syncBarrier.open(); - await testObject.sync(); + await testObject.sync(await client.manifest()); testObject.syncBarrier = new Barrier(); // update remote data before syncing so that 412 is thrown by server @@ -190,8 +202,9 @@ suite('TestSynchronizer', () => { }); // Start sycing - const { ref } = await userDataSyncStoreService.read(testObject.resource, null); - await testObject.sync(ref); + const manifest = await client.manifest(); + const ref = manifest!.latest![testObject.resource]; + await testObject.sync(await client.manifest()); assert.deepEqual(server.requests, [ { type: 'POST', url: `${server.url}/v1/resource/${testObject.resource}`, headers: { 'If-Match': ref } }, @@ -200,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 0000000000..88c2111b5b --- /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 Source EULA. 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 17e8883e4a..a20dea2248 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)); @@ -124,22 +126,31 @@ export class UserDataSyncClient extends Disposable { return this.instantiationService.get(IUserDataSyncStoreService).read(resource, null); } + manifest(): Promise { + return this.instantiationService.get(IUserDataSyncStoreService).manifest(); + } + } +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; } + private _requestsWithAllHeaders: { url: string, type: string, headers?: IHeaders }[] = []; + get requestsWithAllHeaders(): { url: string, type: string, headers?: IHeaders }[] { return this._requestsWithAllHeaders; } + private _responses: { status: number }[] = []; get responses(): { status: number }[] { return this._responses; } - reset(): void { this._requests = []; this._responses = []; } + reset(): void { this._requests = []; this._responses = []; this._requestsWithAllHeaders = []; } async resolveProxy(url: string): Promise { return url; } @@ -154,6 +165,7 @@ export class UserDataSyncTestServer implements IRequestService { } } this._requests.push({ url: options.url!, type: options.type!, headers }); + this._requestsWithAllHeaders.push({ url: options.url!, type: options.type!, headers: options.headers }); const requestContext = await this.doRequest(options); this._responses.push({ status: requestContext.res.statusCode! }); return requestContext; @@ -180,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)); @@ -189,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) { @@ -207,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')) { @@ -220,7 +232,7 @@ export class UserDataSyncTestServer implements IRequestService { return this.toResponse(204); } - private async clear(headers?: IHeaders): Promise { + async clear(headers?: IHeaders): Promise { this.data.clear(); this.session = null; return this.toResponse(204); diff --git a/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts b/src/vs/platform/userDataSync/test/common/userDataSyncService.test.ts index e63afc6fdb..930a344c9e 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.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te 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' } }, @@ -45,9 +47,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } }, ]); }); @@ -65,21 +68,22 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te 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 { type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} }, // Snippets { type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/snippets`, headers: { 'If-Match': '0' } }, // Global state { type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // Manifest { type: 'GET', url: `${target.url}/v1/manifest`, headers: {} }, + // Machines + { type: 'POST', url: `${target.url}/v1/resource/machines`, headers: { 'If-Match': '0' } }, ]); }); @@ -183,11 +187,13 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { 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' } }, ]); }); @@ -223,6 +229,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te /* 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: {} }, @@ -231,6 +238,7 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { 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' } }, ]); }); @@ -370,6 +378,8 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te 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' } }, @@ -384,9 +394,10 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te { type: 'POST', url: `${target.url}/v1/resource/globalState`, headers: { 'If-Match': '0' } }, // Extensions { type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} }, - { type: 'POST', url: `${target.url}/v1/resource/extensions`, headers: { 'If-Match': '0' } }, // 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 new file mode 100644 index 0000000000..0b2697a70b --- /dev/null +++ b/src/vs/platform/userDataSync/test/common/userDataSyncStoreService.test.ts @@ -0,0 +1,371 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +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', () => { + + const disposableStore = new DisposableStore(); + + teardown(() => disposableStore.clear()); + + test('test read manifest for the first time', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + const productService = client.instantiationService.get(IProductService); + + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Client-Name'], `${productService.applicationName}${isWeb ? '-web' : ''}`); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Client-Version'], productService.version); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Id'], undefined); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test read manifest for the second time when session is not yet created', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test session id header is not set in the first manifest request after session is created', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + await testObject.write(SyncResource.Settings, 'some content', null); + + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test session id header is set from the second manifest request after session is created', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test headers are send for write request', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + await testObject.manifest(); + + target.reset(); + await testObject.write(SyncResource.Settings, 'some content', null); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test headers are send for read request', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + await testObject.manifest(); + + target.reset(); + await testObject.read(SyncResource.Settings, null); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test headers are reset after session is cleared ', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + await testObject.manifest(); + await testObject.clear(); + + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test old headers are sent after session is changed on server ', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id']; + await target.clear(); + + // client 2 + const client2 = disposableStore.add(new UserDataSyncClient(target)); + await client2.setUp(); + const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); + await testObject2.write(SyncResource.Settings, 'some content', null); + + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId); + }); + + test('test old headers are reset from second request after session is changed on server ', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id']; + await target.clear(); + + // client 2 + const client2 = disposableStore.add(new UserDataSyncClient(target)); + await client2.setUp(); + const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); + await testObject2.write(SyncResource.Settings, 'some content', null); + + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId); + }); + + test('test old headers are sent after session is cleared from another server ', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id']; + + // client 2 + const client2 = disposableStore.add(new UserDataSyncClient(target)); + await client2.setUp(); + const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); + await testObject2.clear(); + + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId); + }); + + test('test headers are reset after session is cleared from another server ', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + + // client 2 + const client2 = disposableStore.add(new UserDataSyncClient(target)); + await client2.setUp(); + const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); + await testObject2.clear(); + + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.equal(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], undefined); + }); + + test('test headers are reset after session is cleared from another server - started syncing again', async () => { + // Setup the client + const target = new UserDataSyncTestServer(); + const client = disposableStore.add(new UserDataSyncClient(target)); + await client.setUp(); + const testObject = client.instantiationService.get(IUserDataSyncStoreService); + + await testObject.manifest(); + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + const machineSessionId = target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id']; + const userSessionId = target.requestsWithAllHeaders[0].headers!['X-User-Session-Id']; + + // client 2 + const client2 = disposableStore.add(new UserDataSyncClient(target)); + await client2.setUp(); + const testObject2 = client2.instantiationService.get(IUserDataSyncStoreService); + await testObject2.clear(); + + await testObject.manifest(); + await testObject.write(SyncResource.Settings, 'some content', null); + await testObject.manifest(); + target.reset(); + await testObject.manifest(); + + assert.equal(target.requestsWithAllHeaders.length, 1); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], undefined); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-Machine-Session-Id'], machineSessionId); + assert.notEqual(target.requestsWithAllHeaders[0].headers!['X-User-Session-Id'], userSessionId); + 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 7139af71ee..8109c0f828 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 0000000000..91f2624cf2 --- /dev/null +++ b/src/vs/platform/webview/common/resourceLoader.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 0000000000..e289b41ba1 --- /dev/null +++ b/src/vs/platform/webview/common/webviewManagerService.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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 0000000000..2820b6ece9 --- /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 Source EULA. 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 0000000000..fd3a0d567e --- /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 Source EULA. 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 b0051d80c5..70c9386b7c 100644 --- a/src/vs/platform/windows/common/windows.ts +++ b/src/vs/platform/windows/common/windows.ts @@ -7,6 +7,8 @@ import { isMacintosh, isLinux, isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ThemeType } from 'vs/platform/theme/common/themeService'; +import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; export interface IBaseOpenWindowsOptions { forceReuseWindow?: boolean; @@ -17,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 { @@ -157,6 +179,7 @@ export interface IWindowConfiguration { remoteAuthority?: string; highContrast?: boolean; + defaultThemeType?: ThemeType; filesToOpenOrCreate?: IPath[]; filesToDiff?: IPath[]; 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 2f928cae44..cbf1bd7633 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/windows.ts b/src/vs/platform/windows/electron-main/windows.ts index de11c1fae1..fd7743bdaa 100644 --- a/src/vs/platform/windows/electron-main/windows.ts +++ b/src/vs/platform/windows/electron-main/windows.ts @@ -105,7 +105,7 @@ export interface IWindowsMainService { readonly onWindowsCountChanged: Event; open(openConfig: IOpenConfiguration): ICodeWindow[]; - openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[]; + openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[]; openExtensionDevelopmentHostWindow(extensionDevelopmentPath: string[], openConfig: IOpenConfiguration): ICodeWindow[]; sendToFocused(channel: string, ...args: any[]): void; @@ -118,9 +118,12 @@ export interface IWindowsMainService { getWindowCount(): number; } -export interface IOpenConfiguration { +export interface IBaseOpenConfiguration { readonly context: OpenContext; readonly contextWindowId?: number; +} + +export interface IOpenConfiguration extends IBaseOpenConfiguration { readonly cli: ParsedArgs; readonly userEnv?: IProcessEnvironment; readonly urisToOpen?: IWindowOpenable[]; @@ -136,3 +139,5 @@ export interface IOpenConfiguration { readonly initialStartup?: boolean; readonly noRecentEntry?: boolean; } + +export interface IOpenEmptyConfiguration extends IBaseOpenConfiguration { } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 22110927ce..2ae8f5a5e3 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -16,15 +16,14 @@ 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 } from 'vs/platform/windows/electron-main/windows'; +import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode, IOpenEmptyConfiguration } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; import { IProcessEnvironment, isMacintosh, isWindows } from 'vs/base/common/platform'; import { IWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, hasWorkspaceFileExtension, IRecent } from 'vs/platform/workspaces/common/workspaces'; @@ -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 { @@ -393,7 +392,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic }; } - openEmptyWindow(context: OpenContext, options?: IOpenEmptyWindowOptions): ICodeWindow[] { + openEmptyWindow(openConfig: IOpenEmptyConfiguration, options?: IOpenEmptyWindowOptions): ICodeWindow[] { let cli = this.environmentService.args; const remote = options?.remoteAuthority; if (cli && (cli.remote !== remote)) { @@ -403,7 +402,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const forceReuseWindow = options?.forceReuseWindow; const forceNewWindow = !forceReuseWindow; - return this.open({ context, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); + return this.open({ ...openConfig, cli, forceEmpty: true, forceNewWindow, forceReuseWindow }); } open(openConfig: IOpenConfiguration): ICodeWindow[] { @@ -474,7 +473,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Make sure to pass focus to the most relevant of the windows if we open multiple if (usedWindows.length > 1) { - const focusLastActive = this.windowsState.lastActiveWindow && !openConfig.forceEmpty && openConfig.cli._.length && !openConfig.cli['file-uri'] && !openConfig.cli['folder-uri'] && !(openConfig.urisToOpen && openConfig.urisToOpen.length); let focusLastOpened = true; let focusLastWindow = true; @@ -753,15 +751,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic const remoteAuthority = fileInputs ? fileInputs.remoteAuthority : (openConfig.cli && openConfig.cli.remote || undefined); for (let i = 0; i < emptyToOpen; i++) { - usedWindows.push(this.openInBrowserWindow({ - userEnv: openConfig.userEnv, - cli: openConfig.cli, - initialStartup: openConfig.initialStartup, - remoteAuthority, - forceNewWindow: openFolderInNewWindow, - forceNewTabbedWindow: openConfig.forceNewTabbedWindow, - fileInputs - })); + usedWindows.push(this.doOpenEmpty(openConfig, openFolderInNewWindow, remoteAuthority, fileInputs)); // Reset these because we handled them fileInputs = undefined; @@ -801,12 +791,29 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic return window; } + private doOpenEmpty(openConfig: IOpenConfiguration, forceNewWindow: boolean, remoteAuthority: string | undefined, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow { + if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { + windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/microsoft/vscode/issues/97172 + } + + return this.openInBrowserWindow({ + userEnv: openConfig.userEnv, + cli: openConfig.cli, + initialStartup: openConfig.initialStartup, + remoteAuthority, + forceNewWindow, + forceNewTabbedWindow: openConfig.forceNewTabbedWindow, + fileInputs, + windowToUse + }); + } + private doOpenFolderOrWorkspace(openConfig: IOpenConfiguration, folderOrWorkspace: IPathToOpen, forceNewWindow: boolean, fileInputs: IFileInputs | undefined, windowToUse?: ICodeWindow): ICodeWindow { if (!forceNewWindow && !windowToUse && typeof openConfig.contextWindowId === 'number') { windowToUse = this.getWindowById(openConfig.contextWindowId); // fix for https://github.com/Microsoft/vscode/issues/49587 } - const browserWindow = this.openInBrowserWindow({ + return this.openInBrowserWindow({ userEnv: openConfig.userEnv, cli: openConfig.cli, initialStartup: openConfig.initialStartup, @@ -818,8 +825,6 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic forceNewTabbedWindow: openConfig.forceNewTabbedWindow, windowToUse }); - - return browserWindow; } private getPathsToOpen(openConfig: IOpenConfiguration): IPathToOpen[] { diff --git a/src/vs/platform/windows/node/window.ts b/src/vs/platform/windows/node/window.ts index 24dc70f54f..a51a3d3e9e 100644 --- a/src/vs/platform/windows/node/window.ts +++ b/src/vs/platform/windows/node/window.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. 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/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 481febcdac..f2ea278146 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 df39033372..e7690c0b23 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 { /** @@ -2270,7 +2270,7 @@ declare module 'vscode' { * A code lens provider adds [commands](#Command) to source text. The commands will be shown * as dedicated horizontal lines in between the source text. */ - export interface CodeLensProvider { + export interface CodeLensProvider { /** * An optional event to signal that the code lenses from this provider have changed. @@ -2287,17 +2287,17 @@ declare module 'vscode' { * @return An array of code lenses or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ - provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult; + provideCodeLenses(document: TextDocument, token: CancellationToken): ProviderResult; /** * This function will be called for each visible code lens, usually when scrolling and after * calls to [compute](#CodeLensProvider.provideCodeLenses)-lenses. * - * @param codeLens code lens that must be resolved. + * @param codeLens Code lens that must be resolved. * @param token A cancellation token. * @return The given, resolved code lens or thenable that resolves to such. */ - resolveCodeLens?(codeLens: CodeLens, token: CancellationToken): ProviderResult; + resolveCodeLens?(codeLens: T, token: CancellationToken): ProviderResult; } /** @@ -2645,7 +2645,6 @@ declare module 'vscode' { TypeParameter = 25 } - /** * Symbol tags are extra annotations that tweak the rendering of a symbol. */ @@ -2798,7 +2797,7 @@ declare module 'vscode' { * The workspace symbol provider interface defines the contract between extensions and * the [symbol search](https://code.visualstudio.com/docs/editor/editingevolved#_open-symbol-by-name)-feature. */ - export interface WorkspaceSymbolProvider { + export interface WorkspaceSymbolProvider { /** * Project-wide search for a symbol matching the given query string. @@ -2817,7 +2816,7 @@ declare module 'vscode' { * @return An array of document highlights or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined`, `null`, or an empty array. */ - provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult; + provideWorkspaceSymbols(query: string, token: CancellationToken): ProviderResult; /** * Given a symbol fill in its [location](#SymbolInformation.location). This method is called whenever a symbol @@ -2831,7 +2830,7 @@ declare module 'vscode' { * @return The resolved symbol or a thenable that resolves to that. When no result is returned, * the given `symbol` is used. */ - resolveWorkspaceSymbol?(symbol: SymbolInformation, token: CancellationToken): ProviderResult; + resolveWorkspaceSymbol?(symbol: T, token: CancellationToken): ProviderResult; } /** @@ -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. * @@ -3858,7 +3856,7 @@ declare module 'vscode' { * Represents a collection of [completion items](#CompletionItem) to be presented * in the editor. */ - export class CompletionList { + export class CompletionList { /** * This list is not complete. Further typing should result in recomputing @@ -3869,7 +3867,7 @@ declare module 'vscode' { /** * The completion items. */ - items: CompletionItem[]; + items: T[]; /** * Creates a new completion list. @@ -3877,7 +3875,7 @@ declare module 'vscode' { * @param items The completion items. * @param isIncomplete The list is not complete. */ - constructor(items?: CompletionItem[], isIncomplete?: boolean); + constructor(items?: T[], isIncomplete?: boolean); } /** @@ -3931,7 +3929,7 @@ declare module 'vscode' { * Providers are asked for completions either explicitly by a user gesture or -depending on the configuration- * implicitly when typing words or trigger characters. */ - export interface CompletionItemProvider { + export interface CompletionItemProvider { /** * Provide completion items for the given position and document. @@ -3944,7 +3942,7 @@ declare module 'vscode' { * @return An array of completions, a [completion list](#CompletionList), or a thenable that resolves to either. * The lack of a result can be signaled by returning `undefined`, `null`, or an empty array. */ - provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult; + provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken, context: CompletionContext): ProviderResult>; /** * Given a completion item fill in more data, like [doc-comment](#CompletionItem.documentation) @@ -3952,19 +3950,22 @@ 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. * @return The resolved completion item or a thenable that resolves to of such. It is OK to return the given * `item`. When no result is returned, the given `item` will be used. */ - resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; + 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. @@ -4003,7 +4004,7 @@ declare module 'vscode' { * The document link provider defines the contract between extensions and feature of showing * links in the editor. */ - export interface DocumentLinkProvider { + export interface DocumentLinkProvider { /** * Provide links for the given document. Note that the editor ships with a default provider that detects @@ -4014,7 +4015,7 @@ declare module 'vscode' { * @return An array of [document links](#DocumentLink) or a thenable that resolves to such. The lack of a result * can be signaled by returning `undefined`, `null`, or an empty array. */ - provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult; + provideDocumentLinks(document: TextDocument, token: CancellationToken): ProviderResult; /** * Given a link fill in its [target](#DocumentLink.target). This method is called when an incomplete @@ -4025,7 +4026,7 @@ declare module 'vscode' { * @param link The link that is to be resolved. * @param token A cancellation token. */ - resolveDocumentLink?(link: DocumentLink, token: CancellationToken): ProviderResult; + resolveDocumentLink?(link: T, token: CancellationToken): ProviderResult; } /** @@ -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. @@ -6036,13 +6036,13 @@ declare module 'vscode' { * A task provider allows to add tasks to the task service. * A task provider is registered via #tasks.registerTaskProvider. */ - export interface TaskProvider { + export interface TaskProvider { /** * Provides tasks. * @param token A cancellation token. * @return an array of tasks */ - provideTasks(token?: CancellationToken): ProviderResult; + provideTasks(token?: CancellationToken): ProviderResult; /** * Resolves a task that has no [`execution`](#Task.execution) set. Tasks are @@ -6057,7 +6057,7 @@ declare module 'vscode' { * @param token A cancellation token. * @return The resolved task */ - resolveTask(task: Task, token?: CancellationToken): ProviderResult; + resolveTask(task: T, token?: CancellationToken): ProviderResult; } /** @@ -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. * @@ -7889,7 +7889,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 +8957,6 @@ declare module 'vscode' { readonly files: ReadonlyArray<{ oldUri: Uri, newUri: Uri }>; } - /** * An event describing a change to the set of [workspace folders](#workspace.workspaceFolders). */ @@ -9274,9 +9273,11 @@ declare module 'vscode' { * An event that is emitted when a [text document](#TextDocument) is disposed or when the language id * of a text document [has been changed](#languages.setTextDocumentLanguage). * - * To add an event listener when a visible text document is closed, use the [TextEditor](#TextEditor) events in the - * [window](#window) namespace. Note that this event is not emitted when a [TextEditor](#TextEditor) is closed - * but the document remains open in another [visible text editor](#window.visibleTextEditors). + * *Note 1:* There is no guarantee that this event fires when an editor tab is closed, use the + * [`onDidChangeVisibleTextEditors`](#window.onDidChangeVisibleTextEditors)-event to know when editors change. + * + * *Note 2:* A document can be open but not shown in an editor which means this event can fire + * for a document that has not been shown in an editor. */ export const onDidCloseTextDocument: Event; @@ -9592,7 +9593,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; @@ -9743,8 +9744,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. @@ -10639,7 +10640,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 ff2e947766..17631a4aa4 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,6 +78,22 @@ declare module 'vscode' { readonly removed: string[]; } + /** + * Options to be used when getting a session from an [AuthenticationProvider](#AuthenticationProvider). + */ + export interface AuthenticationGetSessionOptions { + /** + * Whether login should be performed if there is no matching session. Defaults to false. + */ + createIfNone?: boolean; + + /** + * Whether the existing user session preference should be cleared. Set to allow the user to switch accounts. + * Defaults to false. + */ + clearSessionPreference?: boolean; + } + /** * An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed. */ @@ -75,8 +126,17 @@ declare module 'vscode' { * another provider with the same id will fail. */ readonly id: string; + + /** + * The human-readable name of the provider. + */ readonly displayName: string; + /** + * Whether it is possible to be signed into multiple accounts at once with this provider + */ + readonly supportsMultipleAccounts: boolean; + /** * An [event](#Event) which fires when the array of sessions has changed, or data * within a session has changed. @@ -86,16 +146,30 @@ 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. + * @param sessionId The session id to log out of + */ logout(sessionId: string): Thenable; } export namespace authentication { + /** + * Register an authentication provider. + * + * There can only be one provider per id and an error is being thrown when an id + * has already been used by another provider. + * + * @param provider The authentication provider provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; /** @@ -103,12 +177,54 @@ 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. */ export const providerIds: string[]; /** + * Returns whether a provider has any sessions matching the requested scopes. This request + * is transparent to the user, not UI is shown. Rejects if a provider with providerId is not + * registered. + * @param providerId The id of the provider + * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication + * provider + * @returns A thenable that resolve to whether the provider has sessions with the requested scopes. + */ + export function hasSessions(providerId: string, scopes: string[]): 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 + */ + 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 * Get existing authentication sessions. Rejects if a provider with providerId is not * registered, or if the user does not consent to sharing authentication information with * the extension. @@ -116,9 +232,10 @@ declare module 'vscode' { * @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication * provider */ - export function getSessions(providerId: string, scopes: string[]): Thenable; + export function getSessions(providerId: string, scopes: string[]): Thenable>; /** + * @deprecated * Prompt a user to login to create a new authenticaiton session. Rejects if a provider with * providerId is not registered, or if the user does not consent to sharing authentication * information with the extension. @@ -129,6 +246,7 @@ declare module 'vscode' { export function login(providerId: string, scopes: string[]): Thenable; /** + * @deprecated * Logout of a specific session. * @param providerId The id of the provider to use * @param sessionId The session id to remove @@ -899,8 +1017,6 @@ declare module 'vscode' { //#endregion - - //#region Terminal link handlers https://github.com/microsoft/vscode/issues/91606 export namespace window { @@ -972,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) @@ -1033,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. */ @@ -1405,7 +1531,6 @@ declare module 'vscode' { export interface CustomTextEditorProvider { - /** * Handle when the underlying resource for a custom editor is renamed. * @@ -1423,7 +1548,6 @@ declare module 'vscode' { //#endregion - //#region allow QuickPicks to skip sorting: https://github.com/microsoft/vscode/issues/73904 export interface QuickPick extends QuickInput { @@ -1524,6 +1648,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. */ @@ -1538,9 +1674,25 @@ declare module 'vscode' { * The cell's current run state */ runState?: NotebookCellRunState; + + /** + * If the cell is running, the time at which the cell started running + */ + runStartTime?: number; + + /** + * The total duration of the cell's last run + */ + lastRunDuration?: number; + + /** + * Additional attributes of a cell metadata. + */ + custom?: { [key: string]: any }; } export interface NotebookCell { + readonly notebook: NotebookDocument; readonly uri: Uri; readonly cellKind: CellKind; readonly document: TextDocument; @@ -1577,12 +1729,17 @@ 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[]; + + /** + * Additional attributes of the document metadata. + */ + custom?: { [key: string]: any }; } export interface NotebookDocument { @@ -1623,7 +1780,27 @@ declare module 'vscode' { * The primary selected cell on this notebook editor. */ 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. */ @@ -1637,6 +1814,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; } @@ -1655,17 +1837,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 { @@ -1682,18 +1895,31 @@ declare module 'vscode' { readonly metadata: NotebookDocumentMetadata; } + interface NotebookDocumentEditEvent { + + /** + * The document that the edit is for. + */ + readonly document: NotebookDocument; + } + export interface NotebookContentProvider { openNotebook(uri: Uri): NotebookData | Promise; saveNotebook(document: NotebookDocument, cancellation: CancellationToken): Promise; saveNotebookAs(targetResource: Uri, document: NotebookDocument, cancellation: CancellationToken): Promise; - readonly onDidChangeNotebook: Event; + readonly onDidChangeNotebook: Event; + // 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 { + label: string; + preloads?: Uri[]; + executeCell(document: NotebookDocument, cell: NotebookCell, token: CancellationToken): Promise; + executeAllCells(document: NotebookDocument, token: CancellationToken): Promise; } export namespace notebook { @@ -1702,23 +1928,31 @@ declare module 'vscode' { provider: NotebookContentProvider ): Disposable; + export function registerNotebookKernel( + id: string, + selectors: GlobPattern[], + kernel: NotebookKernel + ): Disposable; + export function registerNotebookOutputRenderer( - type: string, + id: string, outputSelector: NotebookOutputSelector, renderer: NotebookOutputRenderer ): Disposable; 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. @@ -1801,7 +2035,6 @@ declare module 'vscode' { //#endregion - //#region @eamodio - timeline: https://github.com/microsoft/vscode/issues/84297 export class TimelineItem { @@ -1862,6 +2095,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 @@ -2009,4 +2247,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 a4a5440565..227cb84ee5 100644 --- a/src/vs/workbench/api/browser/mainThreadAuthentication.ts +++ b/src/vs/workbench/api/browser/mainThreadAuthentication.ts @@ -7,7 +7,7 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import * as modes from 'vs/editor/common/modes'; import * as nls from 'vs/nls'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService'; +import { IAuthenticationService, AllowedExtension, readAllowedExtensions } from 'vs/workbench/services/authentication/browser/authenticationService'; import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; @@ -18,43 +18,56 @@ import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; - -interface AllowedExtension { - id: string; - name: string; -} - -const accountUsages = new Map(); +import { fromNow } from 'vs/base/common/date'; const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git']; -function addAccountUsage(providerId: string, accountName: string, extensionOrFeatureName: string) { - const providerAccountUsage = accountUsages.get(providerId); - if (!providerAccountUsage) { - accountUsages.set(providerId, { [accountName]: [extensionOrFeatureName] }); - } else { - if (providerAccountUsage[accountName]) { - if (!providerAccountUsage[accountName].includes(extensionOrFeatureName)) { - providerAccountUsage[accountName].push(extensionOrFeatureName); - } - } else { - providerAccountUsage[accountName] = [extensionOrFeatureName]; - } - - accountUsages.set(providerId, providerAccountUsage); - } +interface IAccountUsage { + extensionId: string; + extensionName: string; + lastUsed: number; } -function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] { - let trustedExtensions: AllowedExtension[] = []; - try { - const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.GLOBAL); - if (trustedExtensionSrc) { - trustedExtensions = JSON.parse(trustedExtensionSrc); +function readAccountUsages(storageService: IStorageService, providerId: string, accountName: string,): IAccountUsage[] { + const accountKey = `${providerId}-${accountName}-usages`; + const storedUsages = storageService.get(accountKey, StorageScope.GLOBAL); + let usages: IAccountUsage[] = []; + if (storedUsages) { + try { + usages = JSON.parse(storedUsages); + } catch (e) { + // ignore } - } catch (err) { } + } - return trustedExtensions; + 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); + + const existingUsageIndex = usages.findIndex(usage => usage.extensionId === extensionId); + if (existingUsageIndex > -1) { + usages.splice(existingUsageIndex, 1, { + extensionId, + extensionName, + lastUsed: Date.now() + }); + } else { + usages.push({ + extensionId, + extensionName, + lastUsed: Date.now() + }); + } + + storageService.store(accountKey, JSON.stringify(usages), StorageScope.GLOBAL); } export class MainThreadAuthenticationProvider extends Disposable { @@ -66,8 +79,10 @@ 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 storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, + private readonly storageService: IStorageService ) { super(); } @@ -81,12 +96,17 @@ export class MainThreadAuthenticationProvider extends Disposable { } private manageTrustedExtensions(quickInputService: IQuickInputService, storageService: IStorageService, accountName: string) { - const quickPick = quickInputService.createQuickPick<{ label: string, extension: AllowedExtension }>(); + const quickPick = quickInputService.createQuickPick<{ label: string, description: string, extension: AllowedExtension }>(); quickPick.canSelectMany = true; const allowedExtensions = readAllowedExtensions(storageService, this.id, accountName); + const usages = readAccountUsages(storageService, this.id, accountName); const items = allowedExtensions.map(extension => { + const usage = usages.find(usage => extension.id === usage.extensionId); return { label: extension.name, + description: usage + ? nls.localize('accountLastUsedDate', "Last used this account {0}", fromNow(usage.lastUsed, true)) + : nls.localize('notUsed', "Has not used this account"), extension }; }); @@ -110,24 +130,6 @@ export class MainThreadAuthenticationProvider extends Disposable { quickPick.show(); } - private showUsage(quickInputService: IQuickInputService, accountName: string) { - const quickPick = quickInputService.createQuickPick(); - const providerUsage = accountUsages.get(this.id); - const accountUsage = (providerUsage || {})[accountName] || []; - - quickPick.items = accountUsage.map(extensionOrFeature => { - return { - label: extensionOrFeature - }; - }); - - quickPick.onDidHide(() => { - quickPick.dispose(); - }); - - quickPick.show(); - } - private async registerCommandsAndContextMenuItems(): Promise { const sessions = await this._proxy.$getSessions(this.id); sessions.forEach(session => this.registerSession(session)); @@ -163,10 +165,9 @@ export class MainThreadAuthenticationProvider extends Disposable { const dialogService = accessor.get(IDialogService); const quickPick = quickInputService.createQuickPick(); - const showUsage = nls.localize('showUsage', "Show Extensions and Features Using This Account"); const manage = nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"); const signOut = nls.localize('signOut', "Sign Out"); - const items = ([{ label: showUsage }, { label: manage }, { label: signOut }]); + const items = ([{ label: manage }, { label: signOut }]); quickPick.items = items; @@ -180,10 +181,6 @@ export class MainThreadAuthenticationProvider extends Disposable { this.manageTrustedExtensions(quickInputService, storageService, session.account.displayName); } - if (selected.label === showUsage) { - this.showUsage(quickInputService, session.account.displayName); - } - quickPick.dispose(); }); @@ -199,39 +196,24 @@ export class MainThreadAuthenticationProvider extends Disposable { } async signOut(dialogService: IDialogService, session: modes.AuthenticationSession): Promise { - const providerUsage = accountUsages.get(this.id); - const accountUsage = (providerUsage || {})[session.account.displayName] || []; + 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 (!accountUsage.length) { - accountUsages.set(this.id, { [session.account.displayName]: [] }); - 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 currently used by: \n\n{1}\n\n Sign out of these features?", session.account.displayName, accountUsage.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) { - accountUsages.set(this.id, { [session.account.displayName]: [] }); 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, - getAccessToken: () => { - addAccountUsage(this.id, session.account.displayName, 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 { @@ -262,13 +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, - getAccessToken: () => this._proxy.$getSessionAccessToken(this.id, session.id) - }; - }); + return this._proxy.$login(this.id, scopes); } async logout(sessionId: string): Promise { @@ -294,14 +270,31 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu @IStorageService private readonly storageService: IStorageService, @INotificationService private readonly notificationService: INotificationService, @IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService, - @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService + @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { 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); + $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); } @@ -310,21 +303,142 @@ 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); } - async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { - addAccountUsage(providerId, accountName, extensionName); + $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, 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'); + } + + if (clearSessionPreference) { + this.storageService.remove(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + } else { + const existingSessionPreference = this.storageService.get(`${extensionName}-${providerId}`, StorageScope.GLOBAL); + if (existingSessionPreference) { + const matchingSession = potentialSessions.find(session => session.id === existingSessionPreference); + if (matchingSession) { + const allowed = await this.$getSessionsPrompt(providerId, matchingSession.account.displayName, providerName, extensionId, extensionName); + if (allowed) { + return matchingSession; + } + } + } + } + + return new Promise((resolve, reject) => { + const quickPick = this.quickInputService.createQuickPick<{ label: string, session?: modes.AuthenticationSession }>(); + quickPick.ignoreFocusOut = true; + const items: { label: string, session?: modes.AuthenticationSession }[] = potentialSessions.map(session => { + return { + label: session.account.displayName, + session + }; + }); + + items.push({ + label: nls.localize('useOtherAccount', "Sign in to another account") + }); + + quickPick.items = items; + quickPick.title = nls.localize('selectAccount', "The extension '{0}' wants to access a {1} account", extensionName, providerName); + quickPick.placeholder = nls.localize('getSessionPlateholder', "Select an account for '{0}' to use or Esc to cancel", extensionName); + + quickPick.onDidAccept(async _ => { + const selected = quickPick.selectedItems[0]; + + const session = selected.session ?? await this.authenticationService.login(providerId, scopes); + + const accountName = session.account.displayName; + + const allowList = readAllowedExtensions(this.storageService, providerId, accountName); + if (!allowList.find(allowed => allowed.id === extensionId)) { + allowList.push({ id: extensionId, name: extensionName }); + this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL); + } + + this.storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL); + + quickPick.dispose(); + resolve(session); + }); + + quickPick.onDidHide(_ => { + if (!quickPick.selectedItems[0]) { + reject('User did not consent to account access'); + } + + quickPick.dispose(); + }); + + quickPick.show(); + }); + } + + async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise { const allowList = readAllowedExtensions(this.storageService, providerId, accountName); const extensionData = allowList.find(extension => extension.id === extensionId); if (extensionData) { + addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); return true; } const remoteConnection = this.remoteAgentService.getConnection(); if (remoteConnection && remoteConnection.remoteAuthority && remoteConnection.remoteAuthority.startsWith('vsonline') && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) { + addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); return true; } @@ -339,6 +453,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu const allow = choice === 0; if (allow) { + addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName); allowList.push({ id: extensionId, name: extensionName }); this.storageService.store(`${providerId}-${accountName}`, JSON.stringify(allowList), StorageScope.GLOBAL); } diff --git a/src/vs/workbench/api/browser/mainThreadCodeInsets.ts b/src/vs/workbench/api/browser/mainThreadCodeInsets.ts index cbd470a170..2b66450d8f 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 9c548e3dcf..eafa55b3cb 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 f7c6466a37..3cfc371bb9 100644 --- a/src/vs/workbench/api/browser/mainThreadDocuments.ts +++ b/src/vs/workbench/api/browser/mainThreadDocuments.ts @@ -10,17 +10,19 @@ 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, isEqualOrParent, extUri } 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( @@ -34,10 +36,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 (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 +58,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 +79,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 = new BoundModelReferenceCollection(); constructor( documentsAndEditors: MainThreadDocumentsAndEditors, @@ -83,13 +94,16 @@ 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._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocuments); @@ -109,6 +123,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 +183,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 +221,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 1e5cc5b82c..10056c6e22 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 fe8999cee8..0170d46620 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 628da41389..5b9e904ab0 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 fc4fbf4fcd..fbd496e2d1 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -4,17 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 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 } 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'; import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; +import { IRelativePattern } from 'vs/base/common/glob'; +import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; export class MainThreadNotebookDocument extends Disposable { private _textModel: NotebookTextModel; @@ -27,37 +29,103 @@ 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; - this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange }); + this._proxy.$acceptEditorPropertiesChanged(uri, { selections: selectionsChange, metadata: null }); })); } - 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 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 + }; + } + // const documentDelta = delta.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; + + return { + addedEditors: addedAPIEditors, + removedEditors: removedAPIEditors, + newActiveEditor: newActiveEditor + }; + } + + constructor( + readonly documents: Set, + readonly textEditors: Map, + readonly activeEditor: string | null | undefined, + ) { + // + } +} + @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, @@ -83,12 +151,30 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo } registerListeners() { + this._notebookService.listNotebookEditors().forEach((e) => { + this._addNotebookEditor(e); + }); + this._register(this._notebookService.onDidChangeActiveEditor(e => { this._proxy.$acceptDocumentAndEditorsDelta({ - newActiveEditor: e.uri + newActiveEditor: e }); })); + this._register(this._notebookService.onDidChangeVisibleEditors(e => { + this._proxy.$acceptDocumentAndEditorsDelta({ + visibleEditors: e + }); + })); + + this._register(this._notebookService.onNotebookEditorAdd(editor => { + this._addNotebookEditor(editor); + })); + + this._register(this._notebookService.onNotebookEditorRemove(editor => { + this._removeNotebookEditor(editor); + })); + const updateOrder = () => { let userOrder = this.configurationService.getValue('notebook.displayOrder'); this._proxy.$acceptDisplayOrder({ @@ -108,29 +194,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) { + await this._proxy.$acceptDocumentAndEditorsDelta({ + addedDocuments: [data] + }); } - 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(e: IEditor) { + const sub = this._toDisposeOnEditorRemove.get(e.getId()); + if (sub) { + this._toDisposeOnEditorRemove.delete(e.getId()); + sub.dispose(); + this._updateState(); + } + } + + private async _updateState(focusedNotebookEditor?: IEditor) { + const documents = new Set(); + this._notebookService.listNotebookDocuments().forEach(document => { + documents.add(document.uri); + }); + + const editors = new Map(); + let activeEditor: string | null = null; + + for (const editor of this._notebookService.listNotebookEditors()) { + if (editor.hasModel()) { + editors.set(editor.getId(), editor); + if (editor.hasFocus()) { + activeEditor = editor.getId(); + } + } + } + + if (!activeEditor && focusedNotebookEditor) { + 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); + 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._proxy.$acceptDocumentAndEditorsDelta(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); return; } + async $registerNotebookKernel(extension: NotebookExtensionDescription, id: string, label: string, selectors: (string | IRelativePattern)[], preloads: UriComponents[]): Promise { + const kernel = new MainThreadNotebookKernel(this._proxy, id, label, selectors, extension.id, URI.revive(extension.location), preloads.map(preload => URI.revive(preload))); + this._notebookKernels.set(id, kernel); + this._notebookService.registerNotebookKernel(kernel); + return; + } + + async $unregisterNotebookKernel(id: string): Promise { + this._notebookKernels.delete(id); + this._notebookService.unregisterNotebookKernel(id); + return; + } + async $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise { let controller = this._notebookProviders.get(viewType); @@ -157,18 +334,18 @@ 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 { const activeEditorPane = this.editorService.activeEditorPane as any | undefined; if (activeEditorPane?.isNotebookEditor) { - const notebookEditor = (activeEditorPane as INotebookEditor); + const notebookEditor = (activeEditorPane.getControl() as INotebookEditor); if (notebookEditor.viewModel?.handle === handle) { notebookEditor.postMessage(value); @@ -187,11 +364,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) { @@ -203,7 +383,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 } ]); @@ -211,10 +391,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; } @@ -226,7 +439,36 @@ export class MainThreadNotebookController implements IMainNotebookController { document.textModel.languages = data.languages; document.textModel.metadata = data.metadata; - document.textModel.initialize(data!.cells); + + if (data.cells.length) { + document.textModel.initialize(data!.cells); + } else { + const mainCell = document.textModel.createCellTextModel([''], document.textModel.languages.length ? document.textModel.languages[0] : '', CellKind.Code, [], undefined); + 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; } @@ -235,37 +477,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 { - 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 - }] - }); + onDidReceiveMessage(editorId: string, message: any): void { + this._proxy.$onDidReceiveMessage(editorId, message); } async removeNotebookDocument(notebook: INotebookTextModel): Promise { @@ -282,6 +510,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); @@ -297,13 +530,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 { @@ -315,3 +543,41 @@ export class MainThreadNotebookController implements IMainNotebookController { } } + +export class MainThreadNotebookKernel implements INotebookKernelInfo { + constructor( + private readonly _proxy: ExtHostNotebookShape, + readonly id: string, + readonly label: string, + readonly selectors: (string | IRelativePattern)[], + readonly extension: ExtensionIdentifier, + readonly extensionLocation: URI, + readonly preloads: URI[] + ) { + } + + async executeNotebook(viewType: string, uri: URI, handle: number | undefined, token: CancellationToken): Promise { + 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 868e56c45c..d705f50a9d 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/mainThreadTheming.ts b/src/vs/workbench/api/browser/mainThreadTheming.ts index 5d193cd241..8d8021baa7 100644 --- a/src/vs/workbench/api/browser/mainThreadTheming.ts +++ b/src/vs/workbench/api/browser/mainThreadTheming.ts @@ -25,6 +25,7 @@ export class MainThreadTheming implements MainThreadThemingShape { this._themeChangeListener = this._themeService.onDidColorThemeChange(e => { this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type); }); + this._proxy.$onColorThemeChange(this._themeService.getColorTheme().type); } dispose(): void { diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index 454974641f..7b8126fe31 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -325,13 +325,14 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma throw new Error(`Provider for ${viewType} already registered`); } - this._customEditorService.registerCustomEditorCapabilities(viewType, { - supportsMultipleEditorsPerDocument - }); - const extension = reviveWebviewExtension(extensionData); const disposables = new DisposableStore(); + + disposables.add(this._customEditorService.registerCustomEditorCapabilities(viewType, { + supportsMultipleEditorsPerDocument + })); + disposables.add(this._webviewWorkbenchService.registerResolver({ canResolve: (webviewInput) => { return webviewInput instanceof CustomEditorInput && webviewInput.viewType === viewType; @@ -360,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(); }); @@ -649,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() { @@ -710,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/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index f5cf39b33c..defac45631 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -80,6 +80,9 @@ interface IUserFriendlyViewDescriptor { name: string; when?: string; + icon?: string; + contextualTitle?: string; + // From 'remoteViewDescriptor' type group?: string; remoteName?: string | string[]; @@ -100,6 +103,14 @@ const viewDescriptor: IJSONSchema = { description: localize('vscode.extension.contributes.view.when', 'Condition which must be true to show this view'), type: 'string' }, + icon: { + description: localize('vscode.extension.contributes.view.icon', "Path to the view icon. View icons are displayed when the name of the view cannot be shown. It is recommended that icons be in SVG, though any image file type is accepted."), + type: 'string' + }, + contextualTitle: { + description: localize('vscode.extension.contributes.view.contextualTitle', "Human-readable context for when the view is moved out of its original location. By default, the view's container name will be used. Will be shown"), + type: 'string' + }, } }; @@ -406,12 +417,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution { ? container.viewOrderDelegate.getOrder(item.group) : undefined; + const icon = item.icon ? resources.joinPath(extension.description.extensionLocation, item.icon) : undefined; const viewDescriptor = { id: item.id, name: item.name, ctorDescriptor: new SyncDescriptor(TreeViewPane), when: ContextKeyExpr.deserialize(item.when), - containerIcon: viewContainer?.icon, + containerIcon: icon || viewContainer?.icon, + containerTitle: item.contextualTitle || viewContainer?.name, canToggleVisibility: true, canMoveView: true, treeView: this.instantiationService.createInstance(CustomTreeView, item.id, item.name), @@ -468,6 +481,14 @@ class ViewsExtensionHandler implements IWorkbenchContribution { collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'when')); return false; } + if (descriptor.icon && typeof descriptor.icon !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'icon')); + return false; + } + if (descriptor.contextualTitle && typeof descriptor.contextualTitle !== 'string') { + collector.error(localize('optstring', "property `{0}` can be omitted or must be of type `string`", 'contextualTitle')); + return false; + } } return true; diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index bbac2862db..b77f284849 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 9fdfd2fc92..e70d1ff8ec 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -135,7 +135,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)); @@ -196,9 +196,18 @@ 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) { + return extHostAuthentication.getSession(extension, providerId, scopes, options as any); + }, getSessions(providerId: string, scopes: string[]): Thenable { return extHostAuthentication.getSessions(extension, providerId, scopes); }, @@ -265,7 +274,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get sessionId() { return initData.telemetryInfo.sessionId; }, get language() { return initData.environment.appLanguage; }, get appName() { return initData.environment.appName; }, - get appRoot() { return initData.environment.appRoot!.fsPath; }, + get appRoot() { return initData.environment.appRoot?.fsPath ?? ''; }, get uriScheme() { return initData.environment.appUriScheme; }, get logLevel() { checkProposedApiEnabled(extension); @@ -428,6 +437,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); } }; @@ -528,12 +541,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); @@ -541,7 +556,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); @@ -703,8 +718,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; }); }); }, @@ -931,10 +946,20 @@ 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); }, + registerNotebookKernel: (id: string, selector: vscode.GlobPattern[], kernel: vscode.NotebookKernel) => { + checkProposedApiEnabled(extension); + return extHostNotebook.registerNotebookKernel(extension, id, selector, kernel); + }, registerNotebookOutputRenderer: (type: string, outputFilter: vscode.NotebookOutputSelector, renderer: vscode.NotebookOutputRenderer) => { checkProposedApiEnabled(extension); return extHostNotebook.registerNotebookOutputRenderer(type, extension, outputFilter, renderer); @@ -947,9 +972,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); @@ -1050,6 +1087,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, @@ -1086,7 +1124,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 8d8db3c9f0..e054614824 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'; // {{SQL CARBON EDIT}} import { ITreeItem as sqlITreeItem } from 'sql/workbench/common/views'; @@ -161,12 +162,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; + $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 { @@ -215,7 +224,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; } @@ -394,6 +403,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 { @@ -547,7 +557,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; } @@ -674,7 +684,7 @@ export interface ICellDto { source: string[]; language: string; cellKind: CellKind; - outputs: IOutput[]; + outputs: IProcessedOutput[]; metadata?: NotebookCellMetadata; } @@ -687,14 +697,17 @@ 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; $updateNotebookLanguages(viewType: string, resource: UriComponents, languages: string[]): Promise; $updateNotebookMetadata(viewType: string, resource: UriComponents, metadata: NotebookDocumentMetadata): Promise; @@ -1006,6 +1019,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 { @@ -1539,30 +1554,44 @@ export interface INotebookSelectionChangeEvent { export interface INotebookEditorPropertiesChangeData { selections: INotebookSelectionChangeEvent | null; + metadata: NotebookDocumentMetadata | null; } 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 e9b61940ed..c5295fd209 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,16 +37,74 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { return ids; } - async getSessions(requestingExtension: IExtensionDescription, 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(); } + return sessions; + } + + 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) { + 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); + + if (sessions.length) { + if (!provider.supportsMultipleAccounts) { + const session = sessions[0]; + const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.displayName, provider.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._proxy.$selectSession(providerId, provider.displayName, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference); + return sessions.find(session => session.id === selected.id); + } else { + if (options.createIfNone) { + const isAllowed = await this._proxy.$loginPrompt(provider.displayName, extensionName); + if (!isAllowed) { + throw new Error('User did not consent to login.'); + } + + const session = await provider.login(scopes); + await this._proxy.$setTrustedExtension(providerId, session.account.displayName, extensionId, extensionName); + return session; + } else { + await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName); + return undefined; + } + } + } + + async getSessions(requestingExtension: IExtensionDescription, providerId: string, scopes: string[]): Promise { 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 { @@ -51,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); @@ -61,7 +124,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error('User did not consent to token access.'); } - return session.getAccessToken(); + return session.accessToken; } }; }); @@ -97,7 +160,7 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape { throw new Error('User did not consent to token access.'); } - return session.getAccessToken(); + return session.accessToken; } }; } @@ -105,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); @@ -119,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] }); }); } @@ -167,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}`); @@ -175,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 fb9e982ad2..74e9967239 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 326639fafc..c93cde332b 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/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index ea73fbb1bb..811d2c1d79 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 cbc0c83627..c7fca6e1d7 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 1f01961b11..9fde81213a 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -10,14 +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; @@ -40,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[]; @@ -59,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, @@ -133,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) { @@ -217,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 @@ -238,6 +252,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo } dispose() { + this._disposed = true; super.dispose(); this._cellDisposableMapping.forEach(cell => cell.dispose()); } @@ -246,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) { @@ -258,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) { @@ -297,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) { @@ -448,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, @@ -489,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( @@ -497,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 ) { @@ -584,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 { @@ -620,16 +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; @@ -653,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({ @@ -683,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 undefined; // {{SQL CARBON EDIT}} strict-null-checks + } + + 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 undefined; // {{SQL CARBON EDIT}} strict-null-checks + } + + 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) { @@ -713,50 +810,70 @@ 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); }); } - async $resolveNotebookData(viewType: string, uri: UriComponents): Promise { - let provider = this._notebookContentProviders.get(viewType); - let document = this._documents.get(URI.revive(uri).toString()); + registerNotebookKernel(extension: IExtensionDescription, id: string, selectors: vscode.GlobPattern[], kernel: vscode.NotebookKernel): vscode.Disposable { + if (this._notebookKernels.has(id)) { + throw new Error(`Notebook kernel for '${id}' already registered`); + } + + this._notebookKernels.set(id, { kernel, extension }); + 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 extHostTypes.Disposable(() => { + this._notebookKernels.delete(id); + this._proxy.$unregisterNotebookKernel(id); + }); + } + + async $resolveNotebookData(viewType: string, uri: UriComponents): Promise { + 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; @@ -765,58 +882,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN return undefined; // {{SQL CARBON EDIT}} } - 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) { @@ -824,9 +890,38 @@ 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); + } + } + } + } + + async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined, token: CancellationToken): Promise { + let document = this._documents.get(URI.revive(uri).toString()); + + if (!document || document.viewType !== viewType) { + return; + } + + let kernelInfo = this._notebookKernels.get(kernelId); + + if (!kernelInfo) { + return; + } + + let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined; + + if (cell) { + return kernelInfo.kernel.executeCell(document, cell, token); + } else { + return kernelInfo.kernel.executeAllCells(document, token); } } @@ -872,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); @@ -881,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; @@ -910,61 +1013,196 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN editor.editor.selection = undefined; } } + + if (data.metadata) { + editor.editor.document.metadata = { + ...notebookDocumentMetadataDefaults, + ...data.metadata + }; + } + } + + 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); - this._documents.set(revivedUri.toString(), document); + 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, + ...modelData.metadata + }; + } + + 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; + } else { + this._activeNotebookEditor = undefined; + this._activeNotebookDocument = undefined; + } + + this._onDidChangeActiveNotebookEditor.fire(this._activeNotebookEditor); + } + + [...this._editors.values()].forEach((e) => { + if (e.editor !== this.activeNotebookEditor) { + e.editor._acceptActive(false); + } + }); } } diff --git a/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts b/src/vs/workbench/api/common/extHostNotebookConcatDocument.ts index 09257f049c..00a9a08cee 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 74a4e53a85..8e7f92763e 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 47f1b98917..d92766c7ad 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 953f206402..4575c4e1e3 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 220a9d0e83..a27acd285d 100644 --- a/src/vs/workbench/api/common/extHostTreeViews.ts +++ b/src/vs/workbench/api/common/extHostTreeViews.ts @@ -498,7 +498,7 @@ export class ExtHostTreeView extends Disposable { return node; } - protected createTreeNode(element: T, extensionTreeItem: azdata.TreeItem, parent: TreeNode | Root): TreeNode { // {{SQL CARBON EDIT}} change to protected, change to azdata.TreeItem + protected createTreeNode(element: T, extensionTreeItem: azdata.TreeItem2, parent: TreeNode | Root): TreeNode { // {{SQL CARBON EDIT}} change to protected, change to azdata.TreeItem const disposable = new DisposableStore(); const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); @@ -515,10 +515,10 @@ export class ExtHostTreeView extends Disposable { iconDark: this.getDarkIconPath(extensionTreeItem) || icon, themeIcon: extensionTreeItem.iconPath instanceof ThemeIcon ? { id: extensionTreeItem.iconPath.id } : undefined, collapsibleState: isUndefinedOrNull(extensionTreeItem.collapsibleState) ? TreeItemCollapsibleState.None : extensionTreeItem.collapsibleState, - // {{SQL CARBON EDIT}} - payload: extensionTreeItem.payload, - childProvider: extensionTreeItem.childProvider, - type: extensionTreeItem.type + accessibilityInformation: extensionTreeItem.accessibilityInformation, + payload: extensionTreeItem.payload, // {{SQL CARBON EDIT}} + childProvider: extensionTreeItem.childProvider, // {{SQL CARBON EDIT}} + type: extensionTreeItem.type // {{SQL CARBON EDIT}} }; return { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 1151b5c8d7..0e92e06b17 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 0e734ba902..9d957ce2b3 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 401ce6af8c..38da06eabe 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/node/extHostCLIServer.ts b/src/vs/workbench/api/node/extHostCLIServer.ts index 8e8867e180..5d1c20a8a8 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/api/worker/extHostExtensionService.ts b/src/vs/workbench/api/worker/extHostExtensionService.ts index 11172233b5..510489afba 100644 --- a/src/vs/workbench/api/worker/extHostExtensionService.ts +++ b/src/vs/workbench/api/worker/extHostExtensionService.ts @@ -50,7 +50,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService { } // fetch JS sources as text and create a new function around it - const initFn = new Function('module', 'exports', 'require', 'window', await response.text()); + const source = await response.text(); + const initFn = new Function('module', 'exports', 'require', 'window', `${source}\n//# sourceURL=${module.toString(true)}`); // define commonjs globals: `module`, `exports`, and `require` const _exports = {}; diff --git a/src/vs/workbench/browser/actions/developerActions.ts b/src/vs/workbench/browser/actions/developerActions.ts index b804d328d9..8deb526f0d 100644 --- a/src/vs/workbench/browser/actions/developerActions.ts +++ b/src/vs/workbench/browser/actions/developerActions.ts @@ -141,15 +141,25 @@ class ToggleScreencastModeAction extends Action { const keyboardMarker = append(container, $('.screencast-keyboard')); disposables.add(toDisposable(() => keyboardMarker.remove())); + const updateKeyboardFontSize = () => { + keyboardMarker.style.fontSize = `${clamp(this.configurationService.getValue('screencastMode.fontSize') || 56, 20, 100)}px`; + }; + const updateKeyboardMarker = () => { keyboardMarker.style.bottom = `${clamp(this.configurationService.getValue('screencastMode.verticalOffset') || 0, 0, 90)}%`; }; + updateKeyboardFontSize(); updateKeyboardMarker(); + disposables.add(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('screencastMode.verticalOffset')) { updateKeyboardMarker(); } + + if (e.affectsConfiguration('screencastMode.fontSize')) { + updateKeyboardFontSize(); + } })); const onKeyDown = domEvent(window, 'keydown', true); @@ -261,6 +271,13 @@ configurationRegistry.registerConfiguration({ maximum: 90, description: nls.localize('screencastMode.location.verticalPosition', "Controls the vertical offset of the screencast mode overlay from the bottom as a percentage of the workbench height.") }, + 'screencastMode.fontSize': { + type: 'number', + default: 56, + minimum: 20, + maximum: 100, + description: nls.localize('screencastMode.fontSize', "Controls the font size (in pixels) of the screencast mode keyboard.") + }, 'screencastMode.onlyKeyboardShortcuts': { type: 'boolean', description: nls.localize('screencastMode.onlyKeyboardShortcuts', "Only show keyboard shortcuts in Screencast Mode."), diff --git a/src/vs/workbench/browser/actions/layoutActions.ts b/src/vs/workbench/browser/actions/layoutActions.ts index 669d4ef6de..0c7ee02b1c 100644 --- a/src/vs/workbench/browser/actions/layoutActions.ts +++ b/src/vs/workbench/browser/actions/layoutActions.ts @@ -11,7 +11,7 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions } from 'vs/ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { getMenuBarVisibility } from 'vs/platform/windows/common/windows'; @@ -542,6 +542,117 @@ export abstract class ToggleViewAction extends Action { } // --- Move View with Command +export class MoveViewAction extends Action { + static readonly ID = 'workbench.action.moveView'; + static readonly LABEL = nls.localize('moveView', "Move View"); + + constructor( + id: string, + label: string, + @IViewDescriptorService private viewDescriptorService: IViewDescriptorService, + @IInstantiationService private instantiationService: IInstantiationService, + @IQuickInputService private quickInputService: IQuickInputService, + @IContextKeyService private contextKeyService: IContextKeyService, + @IActivityBarService private activityBarService: IActivityBarService, + @IPanelService private panelService: IPanelService + ) { + super(id, label); + } + + private getViewItems(): Array { + const results: Array = []; + + const viewlets = this.activityBarService.getVisibleViewContainerIds(); + viewlets.forEach(viewletId => { + const container = this.viewDescriptorService.getViewContainerById(viewletId)!; + const containerModel = this.viewDescriptorService.getViewContainerModel(container); + + let hasAddedView = false; + containerModel.visibleViewDescriptors.forEach(viewDescriptor => { + if (viewDescriptor.canMoveView) { + if (!hasAddedView) { + results.push({ + type: 'separator', + label: nls.localize('sidebarContainer', "Side Bar / {0}", containerModel.title) + }); + hasAddedView = true; + } + + results.push({ + id: viewDescriptor.id, + label: viewDescriptor.name + }); + } + }); + }); + + const panels = this.panelService.getPinnedPanels(); + panels.forEach(panel => { + const container = this.viewDescriptorService.getViewContainerById(panel.id)!; + const containerModel = this.viewDescriptorService.getViewContainerModel(container); + + let hasAddedView = false; + containerModel.visibleViewDescriptors.forEach(viewDescriptor => { + if (viewDescriptor.canMoveView) { + if (!hasAddedView) { + results.push({ + type: 'separator', + label: nls.localize('panelContainer', "Panel / {0}", containerModel.title) + }); + hasAddedView = true; + } + + results.push({ + id: viewDescriptor.id, + label: viewDescriptor.name + }); + } + }); + }); + + return results; + } + + private async getView(viewId?: string): Promise { + const quickPick = this.quickInputService.createQuickPick(); + quickPick.placeholder = nls.localize('moveFocusedView.selectView', "Select a View to Move"); + quickPick.items = this.getViewItems(); + quickPick.selectedItems = quickPick.items.filter(item => (item as IQuickPickItem).id === viewId) as IQuickPickItem[]; + + return new Promise((resolve, reject) => { + quickPick.onDidAccept(() => { + const viewId = quickPick.selectedItems[0]; + resolve(viewId.id); + quickPick.hide(); + }); + + quickPick.onDidHide(() => reject()); + + quickPick.show(); + }); + } + + async run(): Promise { + const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + let viewId: string; + + if (focusedViewId && this.viewDescriptorService.getViewDescriptorById(focusedViewId)?.canMoveView) { + viewId = focusedViewId; + } + + viewId = await this.getView(viewId!); + + if (!viewId) { + return; + } + + this.instantiationService.createInstance(MoveFocusedViewAction, MoveFocusedViewAction.ID, MoveFocusedViewAction.LABEL).run(viewId); + } +} + +registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveViewAction), 'View: Move View', viewCategory); + +// --- Move Focused View with Command export class MoveFocusedViewAction extends Action { static readonly ID = 'workbench.action.moveFocusedView'; static readonly LABEL = nls.localize('moveFocusedView', "Move Focused View"); @@ -560,8 +671,8 @@ export class MoveFocusedViewAction extends Action { super(id, label); } - async run(): Promise { - const focusedViewId = FocusedViewContext.getValue(this.contextKeyService); + async run(viewId: string): Promise { + const focusedViewId = viewId || FocusedViewContext.getValue(this.contextKeyService); if (focusedViewId === undefined || focusedViewId.trim() === '') { this.notificationService.error(nls.localize('moveFocusedView.error.noFocusedView', "There is no view currently focused.")); @@ -576,27 +687,33 @@ 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)!; + const currentLocation = this.viewDescriptorService.getViewLocationById(focusedViewId)!; + const isViewSolo = this.viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; + + if (!(isViewSolo && currentLocation === ViewContainerLocation.Panel)) { + items.push({ + id: '_.panel.newcontainer', + label: nls.localize('moveFocusedView.newContainerInPanel', "New Panel Entry"), + }); + } + + if (!(isViewSolo && currentLocation === ViewContainerLocation.Sidebar)) { + items.push({ + id: '_.sidebar.newcontainer', + label: nls.localize('moveFocusedView.newContainerInSidebar', "New Side Bar Entry") + }); + } items.push({ type: 'separator', label: nls.localize('sidebar', "Side Bar") }); - const currentContainer = this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!; - const currentLocation = this.viewDescriptorService.getViewLocationById(focusedViewId)!; - const isViewSolo = this.viewDescriptorService.getViewContainerModel(currentContainer).allViewDescriptors.length === 1; - - if (!(isViewSolo && currentLocation === ViewContainerLocation.Sidebar)) { - items.push({ - id: '_.sidebar.newcontainer', - label: nls.localize('moveFocusedView.newContainerInSidebar', "New Container in Side Bar") - }); - } - - const pinnedViewlets = this.activityBarService.getPinnedViewContainerIds(); + const pinnedViewlets = this.activityBarService.getVisibleViewContainerIds(); items.push(...pinnedViewlets .filter(viewletId => { if (viewletId === this.viewDescriptorService.getViewContainerByViewId(focusedViewId)!.id) { @@ -617,13 +734,6 @@ export class MoveFocusedViewAction extends Action { label: nls.localize('panel', "Panel") }); - if (!(isViewSolo && currentLocation === ViewContainerLocation.Panel)) { - items.push({ - id: '_.panel.newcontainer', - label: nls.localize('moveFocusedView.newContainerInPanel', "New Container in Panel"), - }); - } - const pinnedPanels = this.panelService.getPinnedPanels(); items.push(...pinnedPanels .filter(panel => { diff --git a/src/vs/workbench/browser/actions/media/actions.css b/src/vs/workbench/browser/actions/media/actions.css index 0972d3c047..ba24088cc3 100644 --- a/src/vs/workbench/browser/actions/media/actions.css +++ b/src/vs/workbench/browser/actions/media/actions.css @@ -25,15 +25,12 @@ position: absolute; background-color: rgba(0, 0, 0 ,0.5); width: 100%; - height: 100px; - bottom: 20%; left: 0; z-index: 100000; pointer-events: none; color: #eee; - line-height: 100px; + line-height: 1.75em; text-align: center; - font-size: 56px; transition: opacity 0.3s ease-out; white-space: nowrap; overflow: hidden; diff --git a/src/vs/workbench/browser/actions/quickAccessActions.ts b/src/vs/workbench/browser/actions/quickAccessActions.ts index 3da42914b4..41e830ddf0 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 61e3973c11..f5ab03a871 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 3e551bac6b..8852278f79 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 318e96ed07..918bac0869 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -44,6 +44,7 @@ import { LineNumbersType } from 'vs/editor/common/config/editorOptions'; import { ActivitybarPart } from 'vs/workbench/browser/parts/activitybar/activitybarPart'; import { URI } from 'vs/base/common/uri'; import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views'; +import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; export enum Settings { ACTIVITYBAR_VISIBLE = 'workbench.activityBar.visible', @@ -178,6 +179,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private notificationService!: INotificationService; private themeService!: IThemeService; private activityBarService!: IActivityBarService; + private statusBarService!: IStatusbarService; protected readonly state = { fullscreen: false, @@ -262,7 +264,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this.titleService = accessor.get(ITitleService); this.notificationService = accessor.get(INotificationService); this.activityBarService = accessor.get(IActivityBarService); - accessor.get(IStatusbarService); // not used, but called to ensure instantiated + this.statusBarService = accessor.get(IStatusbarService); // Listeners this.registerLayoutListeners(); @@ -397,6 +399,8 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi const newMenubarVisibility = getMenuBarVisibility(this.configurationService, this.environmentService); this.setMenubarVisibility(newMenubarVisibility, !!skipLayout); + // Centered Layout + this.centerEditorLayout(this.state.editor.centered, skipLayout); } private setSideBarPosition(position: Position): void { @@ -850,8 +854,10 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi case Parts.ACTIVITYBAR_PART: this.activityBarService.focusActivityBar(); break; + case Parts.STATUSBAR_PART: + this.statusBarService.focus(); default: - // Status Bar, Activity Bar and Title Bar simply pass focus to container + // Title Bar simply pass focus to container const container = this.getContainer(part); if (container) { container.focus(); @@ -1205,9 +1211,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi let smartActive = active; const activeEditor = this.editorService.activeEditor; - if (this.configurationService.getValue('workbench.editor.centeredLayoutAutoResize') - && (this.editorGroupService.groups.length > 1 || (activeEditor && activeEditor instanceof SideBySideEditorInput))) { - smartActive = false; // Respect the auto resize setting - do not go into centered layout if there is more than 1 group. + + const isSideBySideLayout = activeEditor + && activeEditor instanceof SideBySideEditorInput + // DiffEditorInput inherits from SideBySideEditorInput but can still be functionally an inline editor. + && (!(activeEditor instanceof DiffEditorInput) || this.configurationService.getValue('diffEditor.renderSideBySide')); + + const isCenteredLayoutAutoResizing = this.configurationService.getValue('workbench.editor.centeredLayoutAutoResize'); + if ( + isCenteredLayoutAutoResizing + && (this.editorGroupService.groups.length > 1 || isSideBySideLayout) + ) { + smartActive = false; } // Enter Centered Editor Layout diff --git a/src/vs/workbench/browser/media/style.css b/src/vs/workbench/browser/media/style.css index ab081648b8..f23bcd08b4 100644 --- a/src/vs/workbench/browser/media/style.css +++ b/src/vs/workbench/browser/media/style.css @@ -5,23 +5,23 @@ /* Font Families (with CJK support) */ -.mac { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } -.mac:lang(zh-Hans) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } -.mac:lang(zh-Hant) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } -.mac:lang(ja) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } -.mac:lang(ko) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } +.mac { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; } +.mac:lang(zh-Hans) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } +.mac:lang(zh-Hant) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } +.mac:lang(ja) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } +.mac:lang(ko) { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } -.windows { font-family: "Segoe WPC", "Segoe UI", sans-serif; } -.windows:lang(zh-Hans) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } -.windows:lang(zh-Hant) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } -.windows:lang(ja) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } -.windows:lang(ko) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } +.windows { font-family: system-ui, "Segoe WPC", "Segoe UI", sans-serif; } +.windows:lang(zh-Hans) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } +.windows:lang(zh-Hant) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } +.windows:lang(ja) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } +.windows:lang(ko) { font-family: system-ui, "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } -.linux { font-family: "Ubuntu", "Droid Sans", sans-serif; } -.linux:lang(zh-Hans) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } -.linux:lang(zh-Hant) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } -.linux:lang(ja) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } -.linux:lang(ko) { font-family: "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +.linux { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } +.linux:lang(zh-Hans) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } +.linux:lang(zh-Hant) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } +.linux:lang(ja) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } +.linux:lang(ko) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } .mac { --monaco-monospace-font: "SF Mono", Monaco, Menlo, Courier, monospace; } .windows { --monaco-monospace-font: Consolas, "Courier New", monospace; } diff --git a/src/vs/workbench/browser/panecomposite.ts b/src/vs/workbench/browser/panecomposite.ts index 0203f9e113..55b6108115 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 e27dde8649..e11a7f5460 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -5,21 +5,19 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IPanel } from 'vs/workbench/common/panel'; -import { Composite, CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; +import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite'; import { IConstructorSignature0, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { assertIsDefined } from 'vs/base/common/types'; import { PaneComposite } from 'vs/workbench/browser/panecomposite'; -export abstract class Panel extends Composite implements IPanel { } - -export abstract class PaneCompositePanel extends PaneComposite implements IPanel { } +export abstract class Panel extends PaneComposite implements IPanel { } /** * A panel descriptor is a leightweight descriptor of a panel in the workbench. */ 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/activitybarActions.ts b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts index 17c3680f38..5f98e6a880 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarActions.ts @@ -28,6 +28,8 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { Codicon } from 'vs/base/common/codicons'; +import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { isMacintosh } from 'vs/base/common/platform'; export class ViewContainerActivityAction extends ActivityAction { @@ -287,6 +289,42 @@ export class NextSideBarViewAction extends SwitchSideBarViewAction { export class HomeAction extends Action { + constructor( + private readonly href: string, + name: string, + icon: Codicon + ) { + super('workbench.action.home', name, icon.classNames); + } + + async run(event: MouseEvent): Promise { + let openInNewWindow = false; + if (isMacintosh) { + openInNewWindow = event.metaKey; + } else { + openInNewWindow = event.ctrlKey; + } + + if (openInNewWindow) { + DOM.windowOpenNoOpener(this.href); + } else { + window.location.href = this.href; + } + } +} + +export class HomeActionViewItem extends ActionViewItem { + + constructor(action: IAction) { + super(undefined, action, { icon: true, label: false, useEventAsContext: true }); + } +} + +/** + * @deprecated TODO@ben remove me eventually + */ +export class DeprecatedHomeAction extends Action { + constructor( private readonly command: string, name: string, diff --git a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts index a046a2b2a5..a5aaef6cf0 100644 --- a/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts +++ b/src/vs/workbench/browser/parts/activitybar/activitybarPart.ts @@ -6,16 +6,16 @@ import 'vs/css!./media/activitybarpart'; import * as nls from 'vs/nls'; import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { GLOBAL_ACTIVITY_ID, IActivity } from 'vs/workbench/common/activity'; +import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIIVTY_ID } from 'vs/workbench/common/activity'; import { Part } from 'vs/workbench/browser/part'; -import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; +import { GlobalActivityActionViewItem, ViewContainerActivityAction, PlaceHolderToggleCompositePinnedAction, PlaceHolderViewContainerActivityAction, AccountsActionViewItem, HomeAction, HomeActionViewItem, DeprecatedHomeAction } from 'vs/workbench/browser/parts/activitybar/activitybarActions'; import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IWorkbenchLayoutService, Parts, Position as SideBarPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; 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 @@ -98,7 +99,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { private globalActivityActionBar: ActionBar | undefined; private readonly globalActivity: ICompositeActivity[] = []; - private readonly cachedViewContainers: ICachedViewContainer[] = []; + private accountsActivityAction: ActivityAction | undefined; + private readonly compositeActions = new Map(); private readonly viewContainerDisposables = new Map(); @@ -121,9 +123,9 @@ 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(); - this.cachedViewContainers = this.getCachedViewContainers(); for (const cachedViewContainer of this.cachedViewContainers) { if (environmentService.configuration.remoteAuthority // In remote window, hide activity bar entries until registered. || this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer) @@ -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(); @@ -256,6 +272,14 @@ export class ActivitybarPart extends Part implements IActivityBarService { return this.showGlobalActivity(badge, clazz, priority); } + if (viewContainerOrActionId === ACCOUNTS_ACTIIVTY_ID) { + if (this.accountsActivityAction) { + this.accountsActivityAction.setBadge(badge, clazz); + + return toDisposable(() => this.accountsActivityAction?.setBadge(undefined)); + } + } + return Disposable.None; } @@ -354,7 +378,8 @@ export class ActivitybarPart extends Part implements IActivityBarService { console.warn(`Unknown home indicator icon ${homeIndicator.icon}`); codicon = Codicon.code; } - this.createHomeBar(homeIndicator.command, homeIndicator.title, codicon); + this.createHomeBar(homeIndicator.href, homeIndicator.command, homeIndicator.title, codicon); + this.onDidChangeHomeBarVisibility(); } // Install menubar if compact @@ -375,7 +400,7 @@ export class ActivitybarPart extends Part implements IActivityBarService { return this.content; } - private createHomeBar(command: string, title: string, icon: Codicon): void { + private createHomeBar(href: string, command: string | undefined, title: string, icon: Codicon): void { this.homeBarContainer = document.createElement('div'); this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home")); this.homeBarContainer.setAttribute('role', 'toolbar'); @@ -383,14 +408,21 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.homeBar = this._register(new ActionBar(this.homeBarContainer, { orientation: ActionsOrientation.VERTICAL, - animated: false + animated: false, + ariaLabel: nls.localize('home', "Home"), + actionViewItemProvider: command ? undefined : action => new HomeActionViewItem(action), + allowContextMenu: true })); const homeBarIconBadge = document.createElement('div'); addClass(homeBarIconBadge, 'home-bar-icon-badge'); this.homeBarContainer.appendChild(homeBarIconBadge); - this.homeBar.push(this._register(this.instantiationService.createInstance(HomeAction, command, title, icon)), { icon: true, label: false }); + if (command) { + this.homeBar.push(this._register(this.instantiationService.createInstance(DeprecatedHomeAction, command, title, icon)), { icon: true, label: false }); + } else { + this.homeBar.push(this._register(this.instantiationService.createInstance(HomeAction, href, title, icon))); + } const content = assertIsDefined(this.content); content.prepend(this.homeBarContainer); @@ -422,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, }; } @@ -452,13 +484,13 @@ export class ActivitybarPart extends Part implements IActivityBarService { }); if (getUserDataSyncStore(this.productService, this.configurationService)) { - const profileAction = new ActivityAction({ + this.accountsActivityAction = new ActivityAction({ id: 'workbench.actions.accounts', name: nls.localize('accounts', "Accounts"), cssClass: Codicon.account.classNames }); - this.globalActivityActionBar.push(profileAction); + this.globalActivityActionBar.push(this.accountsActivityAction); } this.globalActivityActionBar.push(this.globalActivityAction); @@ -528,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 { @@ -605,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() @@ -654,22 +697,19 @@ export class ActivitybarPart extends Part implements IActivityBarService { if (e.key === ActivitybarPart.PINNED_VIEW_CONTAINERS && e.scope === StorageScope.GLOBAL && this.pinnedViewContainersValue !== this.getStoredPinnedViewContainersValue() /* This checks if current window changed the value or not */) { this._pinnedViewContainersValue = undefined; + this._cachedViewContainers = undefined; + const newCompositeItems: ICompositeBarItem[] = []; const compositeItems = this.compositeBar.getCompositeBarItems(); - const cachedViewContainers = this.getCachedViewContainers(); - for (const cachedViewContainer of cachedViewContainers) { - // Add and update existing items - const existingItem = compositeItems.filter(({ id }) => id === cachedViewContainer.id)[0]; - if (existingItem) { - newCompositeItems.push({ - id: existingItem.id, - name: existingItem.name, - order: existingItem.order, - pinned: cachedViewContainer.pinned, - visible: existingItem.visible - }); - } + for (const cachedViewContainer of this.cachedViewContainers) { + newCompositeItems.push({ + id: cachedViewContainer.id, + name: cachedViewContainer.name, + order: cachedViewContainer.order, + pinned: cachedViewContainer.pinned, + visible: !!compositeItems.find(({ id }) => id === cachedViewContainer.id) + }); } for (let index = 0; index < compositeItems.length; index++) { @@ -681,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 { @@ -713,19 +757,21 @@ export class ActivitybarPart extends Part implements IActivityBarService { this.storeCachedViewContainersState(state); } - private getCachedViewContainers(): ICachedViewContainer[] { - const cachedViewContainers: ICachedViewContainer[] = this.getPinnedViewContainers(); - for (const placeholderViewContainer of this.getPlaceholderViewContainers()) { - const cachedViewContainer = cachedViewContainers.filter(cached => cached.id === placeholderViewContainer.id)[0]; - if (cachedViewContainer) { - cachedViewContainer.name = placeholderViewContainer.name; - cachedViewContainer.icon = placeholderViewContainer.iconCSS ? placeholderViewContainer.iconCSS : - placeholderViewContainer.iconUrl ? URI.revive(placeholderViewContainer.iconUrl) : undefined; - cachedViewContainer.views = placeholderViewContainer.views; + private _cachedViewContainers: ICachedViewContainer[] | undefined = undefined; + private get cachedViewContainers(): ICachedViewContainer[] { + if (this._cachedViewContainers === undefined) { + this._cachedViewContainers = this.getPinnedViewContainers(); + for (const placeholderViewContainer of this.getPlaceholderViewContainers()) { + const cachedViewContainer = this._cachedViewContainers.filter(cached => cached.id === placeholderViewContainer.id)[0]; + if (cachedViewContainer) { + cachedViewContainer.name = placeholderViewContainer.name; + cachedViewContainer.icon = placeholderViewContainer.iconCSS ? placeholderViewContainer.iconCSS : + placeholderViewContainer.iconUrl ? URI.revive(placeholderViewContainer.iconUrl) : undefined; + cachedViewContainer.views = placeholderViewContainer.views; + } } } - - return cachedViewContainers; + return this._cachedViewContainers; } private storeCachedViewContainersState(cachedViewContainers: ICachedViewContainer[]): void { @@ -808,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 2cf32f6f36..e290c5145c 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 { @@ -101,7 +100,7 @@ } /* Hides active elements in high contrast mode */ -.hc-black .monaco-workbench .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator { +.monaco-workbench.hc-black .activitybar > .content :not(.monaco-menu) > .monaco-action-bar .action-item.checked .active-item-indicator { display: none; } @@ -120,8 +119,8 @@ } /* Hides outline on HC as focus is handled by border */ -.hc-black .monaco-workbench .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before, -.hc-black .monaco-workbench .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { +.monaco-workbench.hc-black .activitybar.left > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before, +.monaco-workbench.hc-black .activitybar.right > .content :not(.monaco-menu) > .monaco-action-bar .action-item:focus:before { outline: none; } diff --git a/src/vs/workbench/browser/parts/compositeBar.ts b/src/vs/workbench/browser/parts/compositeBar.ts index e144f9914f..7d12b33004 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(); @@ -226,8 +233,8 @@ export class CompositeBar extends Widget implements ICompositeBar { // Register a drop target on the whole bar to prevent forbidden feedback this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(parent, { onDragOver: (e: IDraggedCompositeData) => { - // don't add feedback if this is over the composite bar actions - if (e.eventData.target && isAncestor(e.eventData.target as HTMLElement, actionBarDiv)) { + // don't add feedback if this is over the composite bar actions or there are no actions + if (!(this.compositeSwitcherBar?.length()) || (e.eventData.target && isAncestor(e.eventData.target as HTMLElement, actionBarDiv))) { toggleClass(parent, 'dragged-over', false); return; } @@ -245,7 +252,9 @@ export class CompositeBar extends Widget implements ICompositeBar { }, onDrop: (e: IDraggedCompositeData) => { const pinnedItems = this.getPinnedComposites(); - this.options.dndHandler.drop(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData, { horizontallyBefore: false, verticallyBefore: false }); + if (pinnedItems.length) { + this.options.dndHandler.drop(e.dragAndDropData, pinnedItems[pinnedItems.length - 1].id, e.eventData, { horizontallyBefore: false, verticallyBefore: false }); + } toggleClass(parent, 'dragged-over', false); } })); @@ -664,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); } @@ -702,6 +720,8 @@ class CompositeBarModel { item.visible = true; changed = true; } + + this.updateItemsOrder(); return changed; } else { const item = this.createCompositeBarItem(id, name, order, true, true); @@ -714,6 +734,8 @@ class CompositeBarModel { } this.items.splice(index, 0, item); } + + this.updateItemsOrder(); return true; } } @@ -722,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; } } @@ -757,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 ae5a50c44e..2d35325281 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/compositePart.ts b/src/vs/workbench/browser/parts/compositePart.ts index 6b7a5145f6..3ad91fd4d6 100644 --- a/src/vs/workbench/browser/parts/compositePart.ts +++ b/src/vs/workbench/browser/parts/compositePart.ts @@ -316,11 +316,11 @@ export abstract class CompositePart extends Part { toolBar.setAriaLabel(nls.localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle)); } - private collectCompositeActions(composite: Composite): () => void { + private collectCompositeActions(composite?: Composite): () => void { // From Composite - const primaryActions: IAction[] = composite.getActions().slice(0); - const secondaryActions: IAction[] = composite.getSecondaryActions().slice(0); + const primaryActions: IAction[] = composite?.getActions().slice(0) || []; + const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || []; // From Part primaryActions.push(...this.getActions()); @@ -368,7 +368,7 @@ export abstract class CompositePart extends Part { // Empty Actions if (this.toolBar) { - this.toolBar.setActions([])(); + this.collectCompositeActions()(); } this.onDidCompositeClose.fire(composite); @@ -395,6 +395,8 @@ export abstract class CompositePart extends Part { anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment() })); + this.collectCompositeActions()(); + return titleArea; } diff --git a/src/vs/workbench/browser/parts/editor/baseEditor.ts b/src/vs/workbench/browser/parts/editor/baseEditor.ts index 42e827ebbf..226038a510 100644 --- a/src/vs/workbench/browser/parts/editor/baseEditor.ts +++ b/src/vs/workbench/browser/parts/editor/baseEditor.ts @@ -10,7 +10,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { LRUCache } from 'vs/base/common/map'; +import { LRUCache, Touch } from 'vs/base/common/map'; import { URI } from 'vs/base/common/uri'; import { Event } from 'vs/base/common/event'; import { isEmptyObject } from 'vs/base/common/types'; @@ -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}`; @@ -256,7 +262,9 @@ export class EditorMemento implements IEditorMemento { moveEditorState(source: URI, target: URI): void { const cache = this.doLoad(); - const cacheKeys = cache.keys(); + // We need a copy of the keys to not iterate over + // newly inserted elements. + const cacheKeys = [...cache.keys()]; for (const cacheKey of cacheKeys) { const resource = URI.parse(cacheKey); @@ -273,7 +281,8 @@ export class EditorMemento implements IEditorMemento { targetResource = joinPath(target, resource.path.substr(index + source.path.length + 1)); // parent folder got moved } - const value = cache.get(cacheKey); + // Don't modify LRU state. + const value = cache.get(cacheKey, Touch.None); if (value) { cache.delete(cacheKey); cache.set(targetResource.toString(), value); @@ -318,18 +327,19 @@ export class EditorMemento implements IEditorMemento { private cleanUp(): void { const cache = this.doLoad(); - // Remove groups from states that no longer exist - cache.forEach((mapGroupToMemento, resource) => { + // Remove groups from states that no longer exist. Since we modify the + // cache and its is a LRU cache make a copy to ensure iteration succeeds + const entries = [...cache.entries()]; + for (const [resource, mapGroupToMemento] of entries) { Object.keys(mapGroupToMemento).forEach(group => { const groupId: GroupIdentifier = Number(group); if (!this.editorGroupService.getGroup(groupId)) { delete mapGroupToMemento[groupId]; - if (isEmptyObject(mapGroupToMemento)) { cache.delete(resource); } } }); - }); + } } } diff --git a/src/vs/workbench/browser/parts/editor/binaryEditor.ts b/src/vs/workbench/browser/parts/editor/binaryEditor.ts index 2a9f8b8b34..c0a95d4ed9 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 8c8e607e90..b275012604 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 8bd6e52faa..266c4c9638 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 f46d470920..2e15712fc9 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 3a26f9ac61..4942e6c61d 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 1e32861d54..933ee44d9f 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 3757a16a9c..2b316504fd 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/editorgroupview'; - import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditorPane, EditorGroupEditorsCountContext, SaveReason, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, EditorStickyContext, EditorPinnedContext } from 'vs/workbench/common/editor'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom'; @@ -17,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'; @@ -220,8 +219,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private handleGroupContextKeys(contextKeyService: IContextKeyService): void { const groupActiveEditorDirtyContextKey = EditorGroupActiveEditorDirtyContext.bindTo(contextKeyService); const groupEditorsCountContext = EditorGroupEditorsCountContext.bindTo(contextKeyService); + const groupActiveEditorPinnedContext = EditorPinnedContext.bindTo(contextKeyService); + const groupActiveEditorStickyContext = EditorStickyContext.bindTo(contextKeyService); - let activeEditorListener = new MutableDisposable(); + const activeEditorListener = new MutableDisposable(); const observeActiveEditor = () => { activeEditorListener.clear(); @@ -237,11 +238,22 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Update group contexts based on group changes this._register(this.onDidGroupChange(e => { - - // Track the active editor and update context key that reflects - // the dirty state of this editor - if (e.kind === GroupChangeKind.EDITOR_ACTIVE) { - observeActiveEditor(); + switch (e.kind) { + case GroupChangeKind.EDITOR_ACTIVE: + // Track the active editor and update context key that reflects + // the dirty state of this editor + observeActiveEditor(); + break; + case GroupChangeKind.EDITOR_PIN: + if (e.editor && e.editor === this._group.activeEditor) { + groupActiveEditorPinnedContext.set(this._group.isPinned(this._group.activeEditor)); + } + break; + case GroupChangeKind.EDITOR_STICKY: + if (e.editor && e.editor === this._group.activeEditor) { + groupActiveEditorStickyContext.set(this._group.isSticky(this._group.activeEditor)); + } + break; } // Group editors count context @@ -464,6 +476,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Model Events this._register(this._group.onDidChangeEditorPinned(editor => this.onDidChangeEditorPinned(editor))); + this._register(this._group.onDidChangeEditorSticky(editor => this.onDidChangeEditorSticky(editor))); this._register(this._group.onDidOpenEditor(editor => this.onDidOpenEditor(editor))); this._register(this._group.onDidCloseEditor(editor => this.handleOnDidCloseEditor(editor))); this._register(this._group.onDidDisposeEditor(editor => this.onDidDisposeEditor(editor))); @@ -478,11 +491,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } private onDidChangeEditorPinned(editor: EditorInput): void { - - // Event this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_PIN, editor }); } + private onDidChangeEditorSticky(editor: EditorInput): void { + this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_STICKY, editor }); + } + private onDidOpenEditor(editor: EditorInput): void { /* __GDPR__ @@ -596,11 +611,11 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Title control Switch between showing tabs <=> not showing tabs if (event.oldPartOptions.showTabs !== event.newPartOptions.showTabs) { - // Recreate and layout control + // Recreate title control this.createTitleAreaControl(); - if (this.dimension) { - this.layoutTitleAreaControl(this.dimension.width); - } + + // Re-layout + this.relayout(); // Ensure to show active editor if any if (this._group.activeEditor) { @@ -848,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) { @@ -856,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) { @@ -1110,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); } } @@ -1156,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) { @@ -1702,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 ) { } @@ -1718,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/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index ce2fffacbe..bea7c1520d 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -25,7 +25,7 @@ import { EditorDropTarget, EditorDropTargetDelegate } from 'vs/workbench/browser import { Color } from 'vs/base/common/color'; import { CenteredViewLayout } from 'vs/base/browser/ui/centered/centeredViewLayout'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { Parts, IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { MementoObject } from 'vs/workbench/common/memento'; import { assertIsDefined } from 'vs/base/common/types'; @@ -839,12 +839,62 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro } })); + let panelOpenerTimeout: any; this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(overlay, { onDragOver: e => { EventHelper.stop(e.eventData, true); if (e.eventData.dataTransfer) { e.eventData.dataTransfer.dropEffect = 'none'; } + + if (!this.layoutService.isVisible(Parts.PANEL_PART)) { + const boundingRect = overlay.getBoundingClientRect(); + + let openPanel = false; + const proximity = 100; + switch (this.layoutService.getPanelPosition()) { + case Position.BOTTOM: + if (e.eventData.clientY > boundingRect.bottom - proximity) { + openPanel = true; + } + break; + case Position.LEFT: + if (e.eventData.clientX < boundingRect.left + proximity) { + openPanel = true; + } + break; + case Position.RIGHT: + if (e.eventData.clientX > boundingRect.right - proximity) { + openPanel = true; + } + break; + } + + if (!panelOpenerTimeout && openPanel) { + panelOpenerTimeout = setTimeout(() => this.layoutService.setPanelHidden(false), 200); + } else if (panelOpenerTimeout && !openPanel) { + clearTimeout(panelOpenerTimeout); + panelOpenerTimeout = undefined; + } + } + }, + onDragLeave: () => { + if (panelOpenerTimeout) { + clearTimeout(panelOpenerTimeout); + panelOpenerTimeout = undefined; + } + }, + onDragEnd: () => { + if (panelOpenerTimeout) { + clearTimeout(panelOpenerTimeout); + panelOpenerTimeout = undefined; + } + }, + onDrop: () => { + if (panelOpenerTimeout) { + clearTimeout(panelOpenerTimeout); + panelOpenerTimeout = undefined; + } } })); diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts index 3631bff815..a03127155c 100644 --- a/src/vs/workbench/browser/parts/editor/editorsObserver.ts +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -48,7 +48,7 @@ export class EditorsObserver extends Disposable { } get editors(): IEditorIdentifier[] { - return this.mostRecentEditorsMap.values(); + return [...this.mostRecentEditorsMap.values()]; } hasEditor(resource: URI): boolean { @@ -283,7 +283,7 @@ export class EditorsObserver extends Disposable { // Across all editor groups else { - await this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude); + await this.doEnsureOpenedEditorsLimit(limit, [...this.mostRecentEditorsMap.values()], exclude); } } @@ -346,7 +346,7 @@ export class EditorsObserver extends Disposable { private serialize(): ISerializedEditorsList { const registry = Registry.as(Extensions.EditorInputFactories); - const entries = this.mostRecentEditorsMap.values(); + const entries = [...this.mostRecentEditorsMap.values()]; const mapGroupToSerializableEditorsOfGroup = new Map(); return { diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 3d6cd72b8b..ad1969b82c 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -211,7 +211,7 @@ text-overflow: clip; } -.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container { +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .monaco-icon-label > .monaco-icon-label-container { text-overflow: ellipsis; } @@ -290,13 +290,12 @@ padding-right: 5px; /* we need less room when sizing is shrink (unless tab is sticky) */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty-border-top > .tab-close { display: none; /* hide dirty state when highlightModifiedTabs is enabled and when running without close button */ } -.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top) { - padding-right: 0; /* remove extra padding when we are running without close button */ +.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off.dirty:not(.dirty-border-top):not(.sticky) { + padding-right: 0; /* remove extra padding when we are running without close button (unless tab is sticky) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.close-button-off > .tab-close { diff --git a/src/vs/workbench/browser/parts/editor/media/titlecontrol.css b/src/vs/workbench/browser/parts/editor/media/titlecontrol.css index 4bfd74fd74..9ce2526d6b 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; @@ -40,8 +40,8 @@ background-repeat: no-repeat; } -.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label, -.hc-black .monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions .action-label:not(.codicon) { +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .title-actions .action-label, +.monaco-workbench.hc-black .part.editor > .content .editor-group-container > .title .editor-actions .action-label:not(.codicon) { line-height: initial; } diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index e2c1943dce..edcbc7f457 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -9,7 +9,7 @@ import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels'; import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme'; import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch'; -import { addDisposableListener, EventType, addClass, EventHelper, removeClass, toggleClass } from 'vs/base/browser/dom'; +import { addDisposableListener, EventType, addClass, EventHelper, removeClass, toggleClass, Dimension } from 'vs/base/browser/dom'; import { EDITOR_TITLE_HEIGHT } from 'vs/workbench/browser/parts/editor/editor'; import { IAction } from 'vs/base/common/actions'; import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; @@ -232,7 +232,7 @@ export class NoTabsTitleControl extends TitleControl { private redraw(): void { const editor = withNullAsUndefined(this.group.activeEditor); - const isEditorPinned = this.group.activeEditor ? this.group.isPinned(this.group.activeEditor) : false; + const isEditorPinned = editor ? this.group.isPinned(editor) : false; const isGroupActive = this.accessor.activeGroup === this.group; this.activeLabel = { editor, pinned: isEditorPinned }; @@ -320,4 +320,10 @@ export class NoTabsTitleControl extends TitleControl { // Group inactive: only show close action return { primaryEditorActions: editorActions.primary.filter(action => action.id === CLOSE_EDITOR_COMMAND_ID), secondaryEditorActions: [] }; } + + layout(dimension: Dimension): void { + if (this.breadcrumbsControl) { + this.breadcrumbsControl.layout(undefined); + } + } } diff --git a/src/vs/workbench/browser/parts/editor/rangeDecorations.ts b/src/vs/workbench/browser/parts/editor/rangeDecorations.ts index 93d55ce1ef..a9d2b8e997 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 cfe1295d2a..c6f8b82de6 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 } 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'; @@ -107,7 +107,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); @@ -199,15 +200,12 @@ export class TabsTitleControl extends TitleControl { private updateBreadcrumbsControl(): void { if (this.breadcrumbsControl && this.breadcrumbsControl.update()) { - // relayout when we have a breadcrumbs and when update changed - // its hidden-status - this.group.relayout(); + this.group.relayout(); // relayout when we have a breadcrumbs and when update changed its hidden-status } } protected handleBreadcrumbsEnablementChange(): void { - // relayout when breadcrumbs are enable/disabled - this.group.relayout(); + this.group.relayout(); // relayout when breadcrumbs are enable/disabled } private registerTabsContainerListeners(tabsContainer: HTMLElement, tabsScrollbar: ScrollableElement): void { @@ -898,12 +896,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 @@ -1211,6 +1209,10 @@ export class TabsTitleControl extends TitleControl { return hasModifiedBorderColor; } + getPreferredHeight(): number { + return EDITOR_TITLE_HEIGHT + (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden() ? BreadcrumbsControl.HEIGHT : 0); + } + layout(dimension: Dimension | undefined): void { this.dimension = dimension; diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 4c554db927..8de98e8f6d 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -223,19 +223,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 969ee2776a..bf853cc97c 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 0bae5ab608..22c2934069 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/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 38c91390b1..b087da6e63 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -31,7 +31,7 @@ import { DraggedEditorGroupIdentifier, DraggedEditorIdentifier, fillResourceData import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs'; import { BreadcrumbsControl, IBreadcrumbsControlOptions } from 'vs/workbench/browser/parts/editor/breadcrumbsControl'; -import { EDITOR_TITLE_HEIGHT, IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorCommandsContextActionRunner, IEditorCommandsContext, IEditorInput, toResource, IEditorPartOptions, SideBySideEditor, EditorPinnedContext, EditorStickyContext } from 'vs/workbench/common/editor'; import { ResourceContextKey } from 'vs/workbench/common/resources'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; @@ -51,7 +51,7 @@ export abstract class TitleControl extends Themable { protected readonly groupTransfer = LocalSelectionTransfer.getInstance(); protected readonly editorTransfer = LocalSelectionTransfer.getInstance(); - protected breadcrumbsControl?: BreadcrumbsControl; + protected breadcrumbsControl: BreadcrumbsControl | undefined = undefined; private currentPrimaryEditorActionIds: string[] = []; private currentSecondaryEditorActionIds: string[] = []; @@ -118,6 +118,7 @@ export abstract class TitleControl extends Themable { this.handleBreadcrumbsEnablementChange(); } })); + if (config.getValue()) { this.breadcrumbsControl = this.instantiationService.createInstance(BreadcrumbsControl, container, options, this.group); } @@ -401,15 +402,9 @@ export abstract class TitleControl extends Themable { abstract updateStyles(): void; - layout(dimension: Dimension): void { - if (this.breadcrumbsControl) { - this.breadcrumbsControl.layout(undefined); - } - } + abstract layout(dimension: Dimension): void; - getPreferredHeight(): number { - return EDITOR_TITLE_HEIGHT + (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden() ? BreadcrumbsControl.HEIGHT : 0); - } + abstract getPreferredHeight(): number; dispose(): void { dispose(this.breadcrumbsControl); diff --git a/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts b/src/vs/workbench/browser/parts/notifications/notificationsCenter.ts index 132c75afb1..e9ad258a1b 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/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index f98575645d..c4e606b5c6 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -63,12 +63,9 @@ } .monaco-workbench .part.panel .empty-panel-message-area { - position: absolute; display: none; - top: 0px; height: 100%; width: 100%; - z-index: 10; } .monaco-workbench .part.panel .empty-panel-message-area.visible { diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 54baa9b488..2845c09222 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 329f21a63d..863716a1b6 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'; @@ -50,11 +50,17 @@ interface ICachedPanel { views?: { when?: string }[]; } +interface IPlaceholderViewContainer { + id: string; + name?: string; +} + export class PanelPart extends CompositePart implements IPanelService { static readonly activePanelSettingsKey = 'workbench.panelpart.activepanelid'; static readonly PINNED_PANELS = 'workbench.panel.pinnedPanels'; + static readonly PLACEHOLDER_VIEW_CONTAINERS = 'workbench.panel.placeholderPanels'; private static readonly MIN_COMPOSITE_BAR_WIDTH = 50; _serviceBrand: undefined; @@ -94,6 +100,8 @@ export class PanelPart extends CompositePart implements IPanelService { private blockOpeningPanel = false; private contentDimension: Dimension | undefined; + private extensionsRegistered = false; + private panelRegistry: PanelRegistry; private dndHandler: ICompositeDragAndDrop; @@ -135,8 +143,9 @@ export class PanelPart extends CompositePart implements IPanelService { storageKeysSyncRegistryService.registerStorageKey({ key: PanelPart.PINNED_PANELS, version: 1 }); 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) + (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), + () => this.compositeBar.getCompositeBarItems() ); this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { @@ -167,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) }) })); @@ -188,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; } @@ -196,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) { @@ -255,9 +279,11 @@ export class PanelPart extends CompositePart implements IPanelService { } private updateActivity(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { + const cachedTitle = this.getPlaceholderViewContainers().filter(panel => panel.id === viewContainer.id)[0]?.name; + const activity: IActivity = { id: viewContainer.id, - name: viewContainerModel.title, + name: this.extensionsRegistered || cachedTitle === undefined ? viewContainerModel.title : cachedTitle, keybindingId: viewContainer.focusCommand?.id }; @@ -268,7 +294,10 @@ export class PanelPart extends CompositePart implements IPanelService { pinnedAction.setActivity(activity); } - this.saveCachedPanels(); + // only update our cached panel info after extensions are done registering + if (this.extensionsRegistered) { + this.saveCachedPanels(); + } } private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void { @@ -291,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))); @@ -313,6 +343,7 @@ export class PanelPart extends CompositePart implements IPanelService { } private onDidRegisterExtensions(): void { + this.extensionsRegistered = true; this.removeNotExistingComposites(); this.saveCachedPanels(); @@ -388,15 +419,16 @@ export class PanelPart extends CompositePart implements IPanelService { } private createEmptyPanelMessage(): void { + const contentArea = this.getContentArea()!; this.emptyPanelMessageElement = document.createElement('div'); addClass(this.emptyPanelMessageElement, 'empty-panel-message-area'); const messageElement = document.createElement('div'); addClass(messageElement, 'empty-panel-message'); - messageElement.innerText = localize('panel.emptyMessage', "No panels to display. Drag a view into the panel."); + messageElement.innerText = localize('panel.emptyMessage', "Drag a view into the panel to display."); this.emptyPanelMessageElement.appendChild(messageElement); - this.element.appendChild(this.emptyPanelMessageElement); + contentArea.appendChild(this.emptyPanelMessageElement); this._register(CompositeDragAndDropObserver.INSTANCE.registerTarget(this.emptyPanelMessageElement, { onDragOver: (e) => { @@ -455,7 +487,7 @@ export class PanelPart extends CompositePart implements IPanelService { } } - return this.openComposite(id, focus); + return this.openComposite(id, focus) as Panel; } async openPanel(id?: string, focus?: boolean): Promise { @@ -670,6 +702,7 @@ export class PanelPart extends CompositePart implements IPanelService { private saveCachedPanels(): void { const state: ICachedPanel[] = []; + const placeholders: IPlaceholderViewContainer[] = []; const compositeItems = this.compositeBar.getCompositeBarItems(); for (const compositeItem of compositeItems) { @@ -677,10 +710,12 @@ export class PanelPart extends CompositePart implements IPanelService { if (viewContainer) { const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer); state.push({ id: compositeItem.id, name: viewContainerModel.title, pinned: compositeItem.pinned, order: compositeItem.order, visible: compositeItem.visible }); + placeholders.push({ id: compositeItem.id, name: this.getCompositeActions(compositeItem.id).activityAction.label }); } } this.cachedPanelsValue = JSON.stringify(state); + this.setPlaceholderViewContainers(placeholders); } private getCachedPanels(): ICachedPanel[] { @@ -694,6 +729,13 @@ export class PanelPart extends CompositePart implements IPanelService { return serialized; }); + for (const placeholderViewContainer of this.getPlaceholderViewContainers()) { + const cachedViewContainer = cachedPanels.filter(cached => cached.id === placeholderViewContainer.id)[0]; + if (cachedViewContainer) { + cachedViewContainer.name = placeholderViewContainer.name; + } + } + return cachedPanels; } @@ -721,6 +763,38 @@ export class PanelPart extends CompositePart implements IPanelService { this.storageService.store(PanelPart.PINNED_PANELS, value, StorageScope.GLOBAL); } + private getPlaceholderViewContainers(): IPlaceholderViewContainer[] { + return JSON.parse(this.placeholderViewContainersValue); + } + + private setPlaceholderViewContainers(placeholderViewContainers: IPlaceholderViewContainer[]): void { + this.placeholderViewContainersValue = JSON.stringify(placeholderViewContainers); + } + + private _placeholderViewContainersValue: string | undefined; + private get placeholderViewContainersValue(): string { + if (!this._placeholderViewContainersValue) { + this._placeholderViewContainersValue = this.getStoredPlaceholderViewContainersValue(); + } + + return this._placeholderViewContainersValue; + } + + private set placeholderViewContainersValue(placeholderViewContainesValue: string) { + if (this.placeholderViewContainersValue !== placeholderViewContainesValue) { + this._placeholderViewContainersValue = placeholderViewContainesValue; + this.setStoredPlaceholderViewContainersValue(placeholderViewContainesValue); + } + } + + private getStoredPlaceholderViewContainersValue(): string { + return this.storageService.get(PanelPart.PLACEHOLDER_VIEW_CONTAINERS, StorageScope.WORKSPACE, '[]'); + } + + private setStoredPlaceholderViewContainersValue(value: string): void { + this.storageService.store(PanelPart.PLACEHOLDER_VIEW_CONTAINERS, value, StorageScope.WORKSPACE); + } + private getViewContainer(panelId: string): ViewContainer | undefined { return this.viewDescriptorService.getViewContainerById(panelId) || undefined; } diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 9a7b13a40f..af7c4788e3 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -98,6 +98,7 @@ align-items: center; text-overflow: ellipsis; overflow: hidden; + outline-width: 0px; /* do not render focus outline, we already have background */ } .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover { diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts b/src/vs/workbench/browser/parts/statusbar/statusbarPart.ts index c7dfb191a0..470bb0b9a1 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'; @@ -69,6 +68,10 @@ class StatusbarViewModel extends Disposable { get entries(): IStatusbarViewModelEntry[] { return this._entries; } private hidden!: Set; + get lastFocusedEntry(): IStatusbarViewModelEntry | undefined { + return this._lastFocusedEntry && !this.isHidden(this._lastFocusedEntry.id) ? this._lastFocusedEntry : undefined; + } + private _lastFocusedEntry: IStatusbarViewModelEntry | undefined; private readonly _onDidChangeEntryVisibility = this._register(new Emitter<{ id: string, visible: boolean }>()); readonly onDidChangeEntryVisibility = this._onDidChangeEntryVisibility.event; @@ -219,6 +222,7 @@ class StatusbarViewModel extends Disposable { if (focused) { const entry = getVisibleEntry(this._entries.indexOf(focused) + delta); if (entry) { + this._lastFocusedEntry = entry; entry.labelContainer.focus(); return; } @@ -226,6 +230,7 @@ class StatusbarViewModel extends Disposable { const entry = getVisibleEntry(restartPosition); if (entry) { + this._lastFocusedEntry = entry; entry.labelContainer.focus(); } } @@ -493,6 +498,15 @@ export class StatusbarPart extends Part implements IStatusbarService { this.viewModel.focusPreviousEntry(); } + focus(preserveEntryFocus = true): void { + this.getContainer()?.focus(); + const lastFocusedEntry = this.viewModel.lastFocusedEntry; + if (preserveEntryFocus && lastFocusedEntry) { + // Need a timeout, for some reason without it the inner label container will not get focused + setTimeout(() => lastFocusedEntry.labelContainer.focus(), 0); + } + } + createContentArea(parent: HTMLElement): HTMLElement { this.element = parent; @@ -680,10 +694,6 @@ export class StatusbarPart extends Part implements IStatusbarService { return itemContainer; } - focus(): void { - this.getContainer(); - } - layout(width: number, height: number): void { super.layout(width, height); super.layoutContents(width, height); @@ -715,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(); @@ -773,7 +782,7 @@ class StatusbarEntryItem extends Disposable { const command = entry.command; if (command) { this.commandMouseListener.value = addDisposableListener(this.labelContainer, EventType.CLICK, () => this.executeCommand(command)); - this.commandKeyboardListener.value = addDisposableListener(this.labelContainer, EventType.KEY_UP, e => { + this.commandKeyboardListener.value = addDisposableListener(this.labelContainer, EventType.KEY_DOWN, e => { const event = new StandardKeyboardEvent(e); if (event.equals(KeyCode.Space) || event.equals(KeyCode.Enter)) { this.executeCommand(command); @@ -818,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); @@ -935,3 +938,38 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ statusBarService.focusNextEntry(); } }); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.statusBar.focusFirst', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Home, + when: CONTEXT_STATUS_BAR_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const statusBarService = accessor.get(IStatusbarService); + statusBarService.focus(false); + statusBarService.focusNextEntry(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.statusBar.focusLast', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.End, + when: CONTEXT_STATUS_BAR_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const statusBarService = accessor.get(IStatusbarService); + statusBarService.focus(false); + statusBarService.focusPreviousEntry(); + } +}); + +KeybindingsRegistry.registerCommandAndKeybindingRule({ + id: 'workbench.statusBar.clearFocus', + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyCode.Escape, + when: CONTEXT_STATUS_BAR_FOCUSED, + handler: (accessor: ServicesAccessor) => { + const statusBarService = accessor.get(IStatusbarService); + statusBarService.focus(false); + } +}); diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 28ed889004..4e2bd04188 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/media/paneviewlet.css b/src/vs/workbench/browser/parts/views/media/paneviewlet.css index 65649b45f1..21d6925fd5 100644 --- a/src/vs/workbench/browser/parts/views/media/paneviewlet.css +++ b/src/vs/workbench/browser/parts/views/media/paneviewlet.css @@ -18,6 +18,16 @@ .monaco-pane-view .pane > .pane-header > .actions.show { display: initial; } +.monaco-pane-view .pane > .pane-header .icon { + display: none; + width: 16px; + height: 16px; +} + +.monaco-pane-view .pane.pane.horizontal:not(.expanded) > .pane-header .icon { + display: inline; + margin-top: 4px; +} .monaco-pane-view .pane > .pane-header h3.title { white-space: nowrap; @@ -28,6 +38,11 @@ -webkit-margin-after: 0; } +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header h3.title, +.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header .description { + display: none; +} + .monaco-pane-view .pane .monaco-progress-container { position: absolute; left: 0; diff --git a/src/vs/workbench/browser/parts/views/treeView.ts b/src/vs/workbench/browser/parts/views/treeView.ts index 9076b80733..5128b0e426 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 4818f442ec..a19006c4e7 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 a4da520a55..fbfe8ff71a 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -6,10 +6,10 @@ import 'vs/css!./media/paneviewlet'; import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; -import { ColorIdentifier, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry'; +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 { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom'; +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'; import { IAction } from 'vs/base/common/actions'; @@ -20,20 +20,20 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService, Themable } from 'vs/platform/theme/common/themeService'; -import { PaneView, IPaneViewOptions, IPaneOptions, Pane } from 'vs/base/browser/ui/splitview/paneview'; +import { PaneView, IPaneViewOptions, IPaneOptions, Pane, IPaneStyles } from 'vs/base/browser/ui/splitview/paneview'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService, Position } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewDescriptor, ViewContainer, IViewDescriptorService, ViewContainerLocation, IViewPaneContainer, IViewsRegistry, IViewContentDescriptor, IAddedViewDescriptorRef, IViewDescriptorRef, IViewContainerModel } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { assertIsDefined } from 'vs/base/common/types'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { assertIsDefined, isString } from 'vs/base/common/types'; +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'; @@ -48,6 +48,9 @@ import { IProgressIndicator } from 'vs/platform/progress/common/progress'; 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; @@ -185,6 +188,7 @@ export abstract class ViewPane extends Pane implements IView { private readonly showActionsAlways: boolean = false; private headerContainer?: HTMLElement; private titleContainer?: HTMLElement; + private iconContainer?: HTMLElement; protected twistiesContainer?: HTMLElement; private bodyContainer!: HTMLElement; @@ -290,6 +294,10 @@ export abstract class ViewPane extends Pane implements IView { this._register(this.toolbar); this.setActions(); + this._register(this.viewDescriptorService.getViewContainerModel(this.viewDescriptorService.getViewContainerByViewId(this.id)!)!.onDidChangeContainerInfo(({ title }) => { + this.updateTitle(this.title); + })); + const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig)); this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this)); this.updateActionsVisibility(); @@ -299,18 +307,86 @@ export abstract class ViewPane extends Pane implements IView { this.twistiesContainer = append(container, $('.twisties.codicon.codicon-chevron-right')); } + style(styles: IPaneStyles): void { + super.style(styles); + + const icon = this.getIcon(); + if (this.iconContainer) { + const fgColor = styles.headerForeground || this.themeService.getColorTheme().getColor(foreground); + if (URI.isUri(icon)) { + // Apply background color to activity bar item provided with iconUrls + this.iconContainer.style.backgroundColor = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.color = ''; + } else { + // Apply foreground color to activity bar items provided with codicons + this.iconContainer.style.color = fgColor ? fgColor.toString() : ''; + this.iconContainer.style.backgroundColor = ''; + } + } + } + + private getIcon(): string | URI { + return this.viewDescriptorService.getViewDescriptorById(this.id)?.containerIcon || 'codicon-window'; + } + protected renderHeaderTitle(container: HTMLElement, title: string): void { - this.titleContainer = append(container, $('h3.title', undefined, title)); + this.iconContainer = append(container, $('.icon', undefined)); + const icon = this.getIcon(); + + let cssClass: string | undefined = undefined; + if (URI.isUri(icon)) { + cssClass = `view-${this.id.replace(/[\.\:]/g, '-')}`; + const iconClass = `.pane-header .icon.${cssClass}`; + + createCSSRule(iconClass, ` + mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + mask-size: 24px; + -webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%; + -webkit-mask-size: 16px; + `); + } else if (isString(icon)) { + addClass(this.iconContainer, 'codicon'); + cssClass = icon; + } + + if (cssClass) { + addClasses(this.iconContainer, cssClass); + } + + const calculatedTitle = this.calculateTitle(title); + this.titleContainer = append(container, $('h3.title', undefined, calculatedTitle)); + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); } protected updateTitle(title: string): void { + const calculatedTitle = this.calculateTitle(title); if (this.titleContainer) { - this.titleContainer.textContent = title; + this.titleContainer.textContent = calculatedTitle; } + + if (this.iconContainer) { + this.iconContainer.title = calculatedTitle; + this.iconContainer.setAttribute('aria-label', calculatedTitle); + } + this.title = title; this._onDidChangeTitleArea.fire(); } + 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 isDefault = this.viewDescriptorService.getDefaultContainerById(this.id) === viewContainer; + + if (!isDefault && viewDescriptor?.containerTitle && model.title !== viewDescriptor.containerTitle) { + return `${viewDescriptor.containerTitle}: ${title}`; + } + + return title; + } + private scrollableElement!: DomScrollableElement; protected renderBody(container: HTMLElement): void { @@ -510,7 +586,6 @@ export abstract class ViewPane extends Pane implements IView { export interface IViewPaneContainerOptions extends IPaneViewOptions { mergeViewWithContainerWhenSingleView: boolean; - donotShowContainerTitleWhenMergedWithContainer?: boolean; } interface IViewPaneItem { @@ -546,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)); @@ -587,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); @@ -830,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) { @@ -838,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); } } @@ -904,7 +980,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { if (this.isViewMergedWithContainer()) { const paneItemTitle = this.paneItems[0].pane.title; - if (this.options.donotShowContainerTitleWhenMergedWithContainer || containerTitle === paneItemTitle) { + if (containerTitle === paneItemTitle) { return this.paneItems[0].pane.title; } return paneItemTitle ? `${containerTitle}: ${paneItemTitle}` : containerTitle; @@ -1227,6 +1303,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.updateTitleArea(); } }); + const onDidChangeVisibility = pane.onDidChangeBodyVisibility(() => this._onDidChangeViewVisibility.fire(pane)); const onDidChange = pane.onDidChange(() => { if (pane === this.lastFocusedPane && !pane.isExpanded()) { @@ -1234,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 }; @@ -1265,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) { @@ -1273,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); } } } @@ -1466,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 591346aa9d..f30ead6ee2 100644 --- a/src/vs/workbench/browser/parts/views/viewsService.ts +++ b/src/vs/workbench/browser/parts/views/viewsService.ts @@ -20,7 +20,7 @@ import { IPaneComposite } from 'vs/workbench/common/panecomposite'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { PaneCompositePanel, PanelRegistry, PanelDescriptor, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; +import { PanelRegistry, PanelDescriptor, Extensions as PanelExtensions, Panel } from 'vs/workbench/browser/panel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -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); } @@ -414,7 +423,7 @@ export class ViewsService extends Disposable implements IViewsService { private registerPanel(viewContainer: ViewContainer): void { const that = this; - class PaneContainerPanel extends PaneCompositePanel { + class PaneContainerPanel extends Panel { constructor( @ITelemetryService telemetryService: ITelemetryService, @IStorageService storageService: IStorageService, diff --git a/src/vs/workbench/browser/web.main.ts b/src/vs/workbench/browser/web.main.ts index 7ef541546c..bc4f4e3bdc 100644 --- a/src/vs/workbench/browser/web.main.ts +++ b/src/vs/workbench/browser/web.main.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { mark } from 'vs/base/common/performance'; -import { domContentLoaded, addDisposableListener, EventType, addClass, EventHelper } from 'vs/base/browser/dom'; +import { domContentLoaded, addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { ILogService, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log'; import { ConsoleLogInAutomationService } from 'vs/platform/log/browser/log'; @@ -39,7 +39,6 @@ import { BACKUPS } from 'vs/platform/environment/common/environment'; import { joinPath } from 'vs/base/common/resources'; import { BrowserStorageService } from 'vs/platform/storage/browser/storageService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { getThemeTypeSelector, DARK, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService'; import { registerWindowDriver } from 'vs/platform/driver/browser/driver'; import { BufferLogService } from 'vs/platform/log/common/bufferLog'; import { FileLogService } from 'vs/platform/log/common/fileLogService'; @@ -74,9 +73,6 @@ class BrowserMain extends Disposable { await domContentLoaded(); mark('willStartWorkbench'); - // Base Theme - this.restoreBaseTheme(); - // Create Workbench const workbench = new Workbench( this.domElement, @@ -131,7 +127,6 @@ class BrowserMain extends Disposable { })); this._register(workbench.onWillShutdown(() => { storageService.close(); - this.saveBaseTheme(); })); this._register(workbench.onShutdown(() => this.dispose())); @@ -147,21 +142,6 @@ class BrowserMain extends Disposable { }); } - private restoreBaseTheme(): void { - addClass(this.domElement, window.localStorage.getItem('vscode.baseTheme') || getThemeTypeSelector(LIGHT) /* Fallback to a light theme by default on web */); - } - - private saveBaseTheme(): void { - const classes = this.domElement.className; - const baseThemes = [DARK, LIGHT, HIGH_CONTRAST].map(baseTheme => getThemeTypeSelector(baseTheme)); - for (const baseTheme of baseThemes) { - if (classes.indexOf(baseTheme) >= 0) { - window.localStorage.setItem('vscode.baseTheme', baseTheme); - break; - } - } - } - private async initServices(): Promise<{ serviceCollection: ServiceCollection, logService: ILogService, storageService: BrowserStorageService }> { const serviceCollection = new ServiceCollection(); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 07605aae37..e924394572 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -34,7 +34,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', - 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls wether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration."), + 'description': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration."), 'default': false }, 'workbench.editor.highlightModifiedTabs': { diff --git a/src/vs/workbench/common/activity.ts b/src/vs/workbench/common/activity.ts index 666bed860f..9023e0ec95 100644 --- a/src/vs/workbench/common/activity.ts +++ b/src/vs/workbench/common/activity.ts @@ -14,3 +14,4 @@ export interface IActivity { } export const GLOBAL_ACTIVITY_ID = 'workbench.action.globalActivity'; +export const ACCOUNTS_ACTIIVTY_ID = 'workbench.action.accountsActivity'; diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index d91b4c626d..db2dd8904c 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); @@ -403,6 +397,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 +511,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 +598,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 9e81f5253a..8d8245ae8a 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/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 3982bb00a7..6f1af4a6a2 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -84,6 +84,9 @@ export class EditorGroup extends Disposable { private readonly _onDidChangeEditorPinned = this._register(new Emitter()); readonly onDidChangeEditorPinned = this._onDidChangeEditorPinned.event; + private readonly _onDidChangeEditorSticky = this._register(new Emitter()); + readonly onDidChangeEditorSticky = this._onDidChangeEditorSticky.event; + //#endregion private _id: GroupIdentifier; @@ -123,6 +126,12 @@ export class EditorGroup extends Disposable { private onConfigurationUpdated(): void { this.editorOpenPositioning = this.configurationService.getValue('workbench.editor.openPositioning'); this.focusRecentEditorAfterClose = this.configurationService.getValue('workbench.editor.focusRecentEditorAfterClose'); + + if (this.configurationService.getValue('workbench.editor.showTabs') === false) { + // Disabling tabs disables sticky editors until we support + // an indication of sticky editors when tabs are disabled + this.sticky = -1; + } } get count(): number { @@ -555,6 +564,9 @@ export class EditorGroup extends Disposable { // Adjust sticky index this.sticky++; + + // Event + this._onDidChangeEditorSticky.fire(editor); } unstick(candidate: EditorInput): EditorInput | undefined { @@ -580,6 +592,9 @@ export class EditorGroup extends Disposable { // Adjust sticky index this.sticky--; + + // Event + this._onDidChangeEditorSticky.fire(editor); } isSticky(candidateOrIndex: EditorInput | number): boolean { diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 1692de85e7..7ac37b0c7f 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. 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 0960337ad4..db26f6e31e 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/textResourceEditorInput.ts b/src/vs/workbench/common/editor/textResourceEditorInput.ts new file mode 100644 index 0000000000..4433a43fea --- /dev/null +++ b/src/vs/workbench/common/editor/textResourceEditorInput.ts @@ -0,0 +1,177 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. 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, isEqual } 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 + } + + 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); + } +} diff --git a/src/vs/workbench/common/theme.ts b/src/vs/workbench/common/theme.ts index a283e9a498..ec11af4178 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', { @@ -510,31 +542,31 @@ export const TITLE_BAR_ACTIVE_FOREGROUND = registerColor('titleBar.activeForegro dark: '#CCCCCC', light: '#333333', hc: '#FFFFFF' -}, nls.localize('titleBarActiveForeground', "Title bar foreground when the window is active. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarActiveForeground', "Title bar foreground when the window is active.")); export const TITLE_BAR_INACTIVE_FOREGROUND = registerColor('titleBar.inactiveForeground', { dark: transparent(TITLE_BAR_ACTIVE_FOREGROUND, 0.6), light: transparent(TITLE_BAR_ACTIVE_FOREGROUND, 0.6), hc: null -}, nls.localize('titleBarInactiveForeground', "Title bar foreground when the window is inactive. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarInactiveForeground', "Title bar foreground when the window is inactive.")); export const TITLE_BAR_ACTIVE_BACKGROUND = registerColor('titleBar.activeBackground', { dark: '#3C3C3C', light: '#DDDDDD', hc: '#000000' -}, nls.localize('titleBarActiveBackground', "Title bar background when the window is active. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarActiveBackground', "Title bar background when the window is active.")); export const TITLE_BAR_INACTIVE_BACKGROUND = registerColor('titleBar.inactiveBackground', { dark: transparent(TITLE_BAR_ACTIVE_BACKGROUND, 0.6), light: transparent(TITLE_BAR_ACTIVE_BACKGROUND, 0.6), hc: null -}, nls.localize('titleBarInactiveBackground', "Title bar background when the window is inactive. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarInactiveBackground', "Title bar background when the window is inactive.")); export const TITLE_BAR_BORDER = registerColor('titleBar.border', { dark: null, light: null, hc: contrastBorder -}, nls.localize('titleBarBorder', "Title bar border color. Note that this color is currently only supported on macOS.")); +}, nls.localize('titleBarBorder', "Title bar border color.")); // < --- Menubar --- > diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index b9cf1c3d63..ab8e9998c2 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 { @@ -211,6 +212,8 @@ export interface IViewDescriptor { readonly containerIcon?: string | URI; + readonly containerTitle?: string; + // Applies only to newly created views readonly hideByDefault?: boolean; @@ -234,6 +237,11 @@ export interface IAddedViewDescriptorRef extends IViewDescriptorRef { size?: number; } +export interface IAddedViewDescriptorState { + viewDescriptor: IViewDescriptor, + collapsed?: boolean; +} + export interface IViewContainerModel { readonly title: string; @@ -507,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; @@ -632,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 d1112cc0f8..c2eee641b3 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 46a7c5e2ea..0c6037b40d 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 2b979cd9f1..e6796d10b4 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 b4609285d8..b393bbd8a5 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/bulkEdit.css b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css index 23654667f9..f1079a34db 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEdit.css @@ -70,10 +70,10 @@ min-width: 16px; } -.vs-dark .monaco-workbench .bulk-edit-panel .monaco-tl-contents.category .uri-icon, -.hc-black .monaco-workbench .bulk-edit-panel .monaco-tl-contents.category .uri-icon, -.vs-dark .monaco-workbench .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon, -.hc-black .monaco-workbench .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon +.monaco-workbench.vs-dark .bulk-edit-panel .monaco-tl-contents.category .uri-icon, +.monaco-workbench.hc-black .bulk-edit-panel .monaco-tl-contents.category .uri-icon, +.monaco-workbench.vs-dark .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon, +.monaco-workbench.hc-black .bulk-edit-panel .monaco-tl-contents.textedit .uri-icon { background-image: var(--background-dark); } diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditPreview.ts index 8a5453ba6a..b172a61559 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 3b2a8f60a8..eb92751f44 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/find/simpleFindReplaceWidget.ts @@ -350,6 +350,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); @@ -361,6 +365,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/quickaccess/gotoSymbolQuickAccess.ts b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts index 528d027fae..9dcd8d6f3a 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess.ts @@ -14,17 +14,18 @@ import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor'; import { ITextModel } from 'vs/editor/common/model'; -import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { DisposableStore, IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; import { timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Action } from 'vs/base/common/actions'; -import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions'; -import { SyncActionDescriptor } from 'vs/platform/actions/common/actions'; +import { registerAction2, Action2 } from 'vs/platform/actions/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { prepareQuery } from 'vs/base/common/fuzzyScorer'; import { SymbolKind } from 'vs/editor/common/modes'; import { fuzzyScore, createMatches } from 'vs/base/common/filters'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider { @@ -41,6 +42,13 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#region DocumentSymbols (text editor required) + protected provideWithTextEditor(editor: IEditor, picker: IQuickPick, token: CancellationToken): IDisposable { + if (this.canPickFromTableOfContents()) { + return this.doGetTableOfContentsPicks(picker); + } + return super.provideWithTextEditor(editor, picker, token); + } + private get configuration() { const editorConfig = this.configurationService.getValue().workbench.editor; @@ -106,12 +114,21 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess //#endregion protected provideWithoutTextEditor(picker: IQuickPick): IDisposable { - const pane = this.editorService.activeEditorPane; - if (!pane || !TableOfContentsProviderRegistry.has(pane.getId())) { - // - return super.provideWithoutTextEditor(picker); + if (this.canPickFromTableOfContents()) { + return this.doGetTableOfContentsPicks(picker); } + return super.provideWithoutTextEditor(picker); + } + private canPickFromTableOfContents(): boolean { + return this.editorService.activeEditorPane ? TableOfContentsProviderRegistry.has(this.editorService.activeEditorPane.getId()) : false; + } + + private doGetTableOfContentsPicks(picker: IQuickPick): IDisposable { + const pane = this.editorService.activeEditorPane; + if (!pane) { + return Disposable.None; + } const provider = TableOfContentsProviderRegistry.get(pane.getId())!; const cts = new CancellationTokenSource(); @@ -133,7 +150,8 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess kind: SymbolKind.File, index: idx, score: 0, - label: entry.label, + label: entry.icon ? `$(${entry.icon.id}) ${entry.label}` : entry.label, + ariaLabel: entry.detail ? `${entry.label}, ${entry.detail}` : entry.label, detail: entry.detail, description: entry.description, }; @@ -142,7 +160,7 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess disposables.add(picker.onDidAccept(() => { picker.hide(); const [entry] = picker.selectedItems; - entries[entry.index]?.reveal(); + entries[entry.index]?.pick(); })); const updatePickerItems = () => { @@ -175,14 +193,11 @@ export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccess let ignoreFirstActiveEvent = true; disposables.add(picker.onDidChangeActive(() => { const [entry] = picker.activeItems; - if (entry && entries[entry.index]) { - if (ignoreFirstActiveEvent) { - ignoreFirstActiveEvent = false; - return; + if (!ignoreFirstActiveEvent) { + entries[entry.index]?.preview(); } - - entries[entry.index]?.reveal(); + ignoreFirstActiveEvent = false; } })); @@ -206,36 +221,38 @@ Registry.as(QuickaccessExtensions.Quickaccess).registerQui ] }); -export class GotoSymbolAction extends Action { +registerAction2(class GotoSymbolAction extends Action2 { - static readonly ID = 'workbench.action.gotoSymbol'; - static readonly LABEL = localize('gotoSymbol', "Go to Symbol in Editor..."); - - constructor( - id: string, - label: string, - @IQuickInputService private readonly quickInputService: IQuickInputService - ) { - super(id, label); + constructor() { + super({ + id: 'workbench.action.gotoSymbol', + title: { + value: localize('gotoSymbol', "Go to Symbol in Editor..."), + original: 'Go to Symbol in Editor...' + }, + f1: true, + keybinding: { + when: undefined, + weight: KeybindingWeight.WorkbenchContrib, + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O + } + }); } - async run(): Promise { - this.quickInputService.quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); + run(accessor: ServicesAccessor) { + accessor.get(IQuickInputService).quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX); } -} - -Registry.as(ActionExtensions.WorkbenchActions).registerWorkbenchAction(SyncActionDescriptor.from(GotoSymbolAction, { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O -}), 'Go to Symbol in Editor...'); - +}); //#region toc definition and logic export interface ITableOfContentsEntry { + icon?: ThemeIcon; label: string; detail?: string; description?: string; - reveal(): any; + pick(): any; + preview(): any; } export interface ITableOfContentsProvider { diff --git a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts index 2dae825a17..e87c6d73ee 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/suggestEnabledInput/suggestEnabledInput.ts @@ -311,7 +311,7 @@ function getSuggestEnabledInputOptions(ariaLabel?: string): IEditorOptions { roundedSelection: false, renderIndentGuides: false, cursorWidth: 1, - fontFamily: ' -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', + fontFamily: ' system-ui, -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif', ariaLabel: ariaLabel || '', snippetSuggestions: 'none', diff --git a/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts b/src/vs/workbench/contrib/codeEditor/browser/toggleColumnSelection.ts index 59360b84fb..c6e2cc833f 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.convertModelPositionToViewPosition(modelSelectionStart); + const viewSelectionStart = viewModel.coordinatesConverter.convertModelPositionToViewPosition(modelSelectionStart); const modelPosition = new Position(selection.positionLineNumber, selection.positionColumn); - const viewPosition = cursors.context.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.convertViewPositionToModelPosition(columnSelectData.fromViewLineNumber, fromViewColumn); - const toViewColumn = CursorColumns.columnFromVisibleColumn2(cursors.context.config, cursors.context.viewModel, columnSelectData.toViewLineNumber, columnSelectData.toViewVisualColumn); - const toPosition = cursors.context.convertViewPositionToModelPosition(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 3afbb4cc49..3075d54892 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 Source EULA. 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 1110be61f5..a0f7bd254a 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 0000000000..60e52a103c --- /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 Source EULA. 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 b2c9259394..e15dd4f674 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/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 1a7b42ef52..98d46a6552 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -33,6 +33,7 @@ import { ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { CommentFormActions } from 'vs/workbench/contrib/comments/browser/commentFormActions'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; export class CommentNode extends Disposable { private _domNode: HTMLElement; @@ -100,7 +101,7 @@ export class CommentNode extends Disposable { this.createHeader(this._commentDetailsContainer); - this._body = dom.append(this._commentDetailsContainer, dom.$('div.comment-body')); + this._body = dom.append(this._commentDetailsContainer, dom.$(`div.comment-body.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); this._md = this.markdownRenderer.render(comment.body).element; this._body.appendChild(this._md); @@ -120,7 +121,7 @@ export class CommentNode extends Disposable { } private createHeader(commentDetailsContainer: HTMLElement): void { - const header = dom.append(commentDetailsContainer, dom.$('div.comment-title')); + const header = dom.append(commentDetailsContainer, dom.$(`div.comment-title.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); const author = dom.append(header, dom.$('strong.author')); author.innerText = this.comment.userName; diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index cae261c41f..f63de79381 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -48,6 +48,7 @@ import { EditorOption } from 'vs/editor/common/config/editorOptions'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration'; const COLLAPSE_ACTION_CLASS = 'expand-review-action codicon-chevron-up'; @@ -720,7 +721,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget } private createReplyButton() { - this._reviewThreadReplyButton = dom.append(this._commentForm, dom.$('button.review-thread-reply-button')); + this._reviewThreadReplyButton = dom.append(this._commentForm, dom.$(`button.review-thread-reply-button.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); this._reviewThreadReplyButton.title = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); this._reviewThreadReplyButton.textContent = this._commentOptions?.prompt || nls.localize('reply', "Reply..."); diff --git a/src/vs/workbench/contrib/comments/browser/media/review.css b/src/vs/workbench/contrib/comments/browser/media/review.css index f5d014e46e..889d7588ac 100644 --- a/src/vs/workbench/contrib/comments/browser/media/review.css +++ b/src/vs/workbench/contrib/comments/browser/media/review.css @@ -257,7 +257,6 @@ line-height: 20px; white-space: nowrap; border: 0px; - cursor: text; outline: 1px solid transparent; } diff --git a/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts b/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts index 5ffd5914f3..bc91e3c823 100644 --- a/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts +++ b/src/vs/workbench/contrib/configExporter/electron-browser/configurationExportHelper.ts @@ -61,7 +61,8 @@ export class DefaultConfigurationExportHelper { const processProperty = (name: string, prop: IConfigurationPropertySchema) => { if (processedNames.has(name)) { - throw new Error('Setting is registered twice: ' + name); + console.warn('Setting is registered twice: ' + name); + return; } processedNames.add(name); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInputFactory.ts index b9b244d95d..91ce48350f 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 83c714b891..705cfe7dc3 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,19 +421,23 @@ 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[] => { const currentEditor = group?.editors.find(editor => isEqual(editor.resource, resource)); const customEditors = this.customEditorService.getAllCustomEditors(resource); + if (!customEditors.length) { + return []; + } + return [ { ...defaultEditorOverrideEntry, diff --git a/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts b/src/vs/workbench/contrib/customEditor/common/contributedCustomEditors.ts index b97a91e8fc..ff983aeccd 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 8172d7f871..c30796488f 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/breakpointsView.ts b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts index 0202571c2a..7810263202 100644 --- a/src/vs/workbench/contrib/debug/browser/breakpointsView.ts +++ b/src/vs/workbench/contrib/debug/browser/breakpointsView.ts @@ -61,6 +61,7 @@ export class BreakpointsView extends ViewPane { private list!: WorkbenchList; private needsRefresh = false; + private ignoreLayout = false; constructor( options: IViewletViewOptions, @@ -79,7 +80,6 @@ export class BreakpointsView extends ViewPane { ) { super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService); - this.updateSize(); this._register(this.debugService.getModel().onDidChangeBreakpoints(() => this.onBreakpointsChange())); } @@ -164,10 +164,20 @@ export class BreakpointsView extends ViewPane { } protected layoutBody(height: number, width: number): void { + if (this.ignoreLayout) { + return; + } + super.layoutBody(height, width); if (this.list) { this.list.layout(height, width); } + try { + this.ignoreLayout = true; + this.updateSize(); + } finally { + this.ignoreLayout = false; + } } private onListContextMenu(e: IListContextMenuEvent): void { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index 08570508fd..190e776696 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: 13 // {{SQL CARBON EDIT}} }, 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 c25772e4c5..edef138795 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -20,7 +20,6 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ADD_CONFIGURATION_ID } from 'vs/workbench/contrib/debug/browser/debugCommands'; -import { StartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; const $ = dom.$; @@ -32,7 +31,7 @@ export class StartDebugActionViewItem implements IActionViewItem { private container!: HTMLElement; private start!: HTMLElement; private selectBox: SelectBox; - private options: { label: string, handler?: (() => boolean) }[] = []; + private options: { label: string, handler: (() => Promise) }[] = []; private toDispose: IDisposable[]; private selected = 0; private providers: { label: string, pick: () => Promise<{ launch: ILaunch, config: IConfig } | undefined> }[] = []; @@ -101,9 +100,9 @@ export class StartDebugActionViewItem implements IActionViewItem { event.stopPropagation(); } })); - this.toDispose.push(this.selectBox.onDidSelect(e => { + this.toDispose.push(this.selectBox.onDidSelect(async e => { const target = this.options[e.index]; - const shouldBeSelected = target.handler ? target.handler() : false; + const shouldBeSelected = target.handler ? await target.handler() : false; if (shouldBeSelected) { this.selected = e.index; } else { @@ -126,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; @@ -173,7 +171,7 @@ export class StartDebugActionViewItem implements IActionViewItem { if (lastGroup !== presentation?.group) { lastGroup = presentation?.group; if (this.options.length) { - this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: undefined }); + this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: () => Promise.resolve(false) }); disabledIdxs.push(this.options.length - 1); } } @@ -183,8 +181,7 @@ export class StartDebugActionViewItem implements IActionViewItem { const label = inWorkspace ? `${name} (${launch.name})` : name; this.options.push({ - label, handler: () => { - StartAction.GET_CONFIG_AND_LAUNCH = undefined; + label, handler: async () => { manager.selectConfiguration(launch, name); return true; } @@ -192,31 +189,39 @@ export class StartDebugActionViewItem implements IActionViewItem { }); if (this.options.length === 0) { - this.options.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: () => false }); + this.options.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: async () => false }); } else { - this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: undefined }); + this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: () => Promise.resolve(false) }); disabledIdxs.push(this.options.length - 1); } this.providers.forEach(p => { + if (p.label === manager.selectedConfiguration.name) { + this.selected = this.options.length; + } + this.options.push({ - label: `${p.label}...`, handler: () => { - StartAction.GET_CONFIG_AND_LAUNCH = p.pick; - return true; + label: `${p.label}...`, handler: async () => { + const picked = await p.pick(); + if (picked) { + manager.selectConfiguration(picked.launch, p.label, picked.config); + return true; + } + return false; } }); }); if (this.providers.length > 0) { - this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: undefined }); + this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: () => Promise.resolve(false) }); disabledIdxs.push(this.options.length - 1); } manager.getLaunches().filter(l => !l.hidden).forEach(l => { const label = inWorkspace ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); this.options.push({ - label, handler: () => { - this.commandService.executeCommand(ADD_CONFIGURATION_ID, l.uri.toString()); + label, handler: async () => { + await this.commandService.executeCommand(ADD_CONFIGURATION_ID, l.uri.toString()); return false; } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts index 07ecc28487..2f704e49b7 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActions.ts @@ -7,8 +7,8 @@ import * as nls from 'vs/nls'; 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, IConfig } from 'vs/workbench/contrib/debug/common/debug'; -import { Variable, Breakpoint, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; +import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; +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'; @@ -81,8 +81,8 @@ export class ConfigureAction extends AbstractDebugAction { this.class = configurationManager.selectedConfiguration.name ? 'debug-action codicon codicon-gear' : 'debug-action codicon codicon-gear notification'; } - async run(event?: any): Promise { - if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + async run(): Promise { + if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY || this.contextService.getWorkspace().folders.length === 0) { this.notificationService.info(nls.localize('noFolderDebugConfig', "Please first open a folder in order to do advanced debug configuration.")); return; } @@ -92,12 +92,12 @@ export class ConfigureAction extends AbstractDebugAction { if (configurationManager.selectedConfiguration.name) { launch = configurationManager.selectedConfiguration.launch; } else { - const launches = configurationManager.getLaunches().filter(l => !!l.workspace); + const launches = configurationManager.getLaunches().filter(l => !l.hidden); if (launches.length === 1) { launch = launches[0]; } else { const picks = launches.map(l => ({ label: l.name, launch: l })); - const picked = await this.quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { activeItem: picks[0], placeHolder: nls.localize('selectWorkspaceFolder', "Select a workspace folder to create a launch.json file in") }); + const picked = await this.quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { activeItem: picks[0], placeHolder: nls.localize('selectWorkspaceFolder', "Select a workspace folder to create a launch.json file in or add it to the workspace config file") }); if (picked) { launch = picked.launch; } @@ -105,8 +105,7 @@ export class ConfigureAction extends AbstractDebugAction { } if (launch) { - const sideBySide = !!(event && (event.ctrlKey || event.metaKey)); - return launch.openConfigFile(sideBySide, false); + return launch.openConfigFile(false); } } } @@ -114,7 +113,6 @@ export class ConfigureAction extends AbstractDebugAction { export class StartAction extends AbstractDebugAction { static ID = 'workbench.action.debug.start'; static LABEL = nls.localize('startDebug', "Start Debugging"); - static GET_CONFIG_AND_LAUNCH: (() => Promise<{ config: IConfig, launch: ILaunch } | undefined>) | undefined; constructor(id: string, label: string, @IDebugService debugService: IDebugService, @@ -130,16 +128,8 @@ export class StartAction extends AbstractDebugAction { } async run(): Promise { - if (StartAction.GET_CONFIG_AND_LAUNCH) { - const picked = await StartAction.GET_CONFIG_AND_LAUNCH(); - if (picked) { - return this.debugService.startDebugging(picked.launch, picked.config, { noDebug: this.isNoDebug() }); - } - return Promise.resolve(false); - } else { - let { launch, name } = this.debugService.getConfigurationManager().selectedConfiguration; - return this.debugService.startDebugging(launch, name, { noDebug: this.isNoDebug() }); - } + let { launch, name, config } = this.debugService.getConfigurationManager().selectedConfiguration; + return this.debugService.startDebugging(launch, config || name, { noDebug: this.isNoDebug() }); } protected isNoDebug(): boolean { @@ -152,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; } @@ -390,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 { @@ -406,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/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 939b78c497..de234405e4 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -504,7 +504,7 @@ export function registerCommands(): void { const launch = manager.getLaunches().find(l => l.uri.toString() === launchUri) || manager.selectedConfiguration.launch; if (launch) { - const { editor, created } = await launch.openConfigFile(false, false); + const { editor, created } = await launch.openConfigFile(false); if (editor && !created) { const codeEditor = editor.getControl(); if (codeEditor) { diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index 3a71d9617f..2f769ceeb6 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -8,6 +8,7 @@ import { dispose, IDisposable } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import * as strings from 'vs/base/common/strings'; import * as objects from 'vs/base/common/objects'; +import * as json from 'vs/base/common/json'; import { URI as uri } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; @@ -15,14 +16,14 @@ import { ITextModel } from 'vs/editor/common/model'; import { IEditorPane } from 'vs/workbench/common/editor'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; -import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorService, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { launchSchemaId } from 'vs/workbench/services/configuration/common/configuration'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; @@ -45,6 +46,7 @@ jsonRegistry.registerSchema(launchSchemaId, launchSchema); const DEBUG_SELECTED_CONFIG_NAME_KEY = 'debug.selectedconfigname'; const DEBUG_SELECTED_ROOT = 'debug.selectedroot'; +const DEBUG_SELECTED_CONFIG = 'debug.selectedconfig'; export class ConfigurationManager implements IConfigurationManager { private debuggers: Debugger[]; @@ -52,6 +54,7 @@ export class ConfigurationManager implements IConfigurationManager { private launches!: ILaunch[]; private selectedName: string | undefined; private selectedLaunch: ILaunch | undefined; + private selectedConfig: IConfig | undefined; private toDispose: IDisposable[]; private readonly _onDidSelectConfigurationName = new Emitter(); private configProviders: IDebugConfigurationProvider[]; @@ -81,10 +84,12 @@ export class ConfigurationManager implements IConfigurationManager { const previousSelectedRoot = this.storageService.get(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); const previousSelectedLaunch = this.launches.find(l => l.uri.toString() === previousSelectedRoot); this.debugConfigurationTypeContext = CONTEXT_DEBUG_CONFIGURATION_TYPE.bindTo(contextKeyService); + const storedConfig = this.storageService.get(DEBUG_SELECTED_CONFIG, StorageScope.WORKSPACE); + const selectedConfig = typeof storedConfig === 'string' ? JSON.parse(storedConfig) : undefined; if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) { - this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); + this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE), selectedConfig); } else if (this.launches.length > 0) { - this.selectConfiguration(undefined); + this.selectConfiguration(undefined, selectedConfig ? selectedConfig.name : undefined, selectedConfig); } } @@ -257,7 +262,7 @@ export class ConfigurationManager implements IConfigurationManager { } return undefined; - }).filter(e => typeof e === 'string') as string[]; + }).filter(type => typeof type === 'string' && !!this.getDebuggerLabel(type)) as string[]; return debugDynamicExtensionsTypes.map(type => { return { @@ -272,13 +277,27 @@ export class ConfigurationManager implements IConfigurationManager { picks.push(provider.provideDebugConfigurations!(launch.workspace.uri, token.token).then(configurations => configurations.map(config => ({ label: config.name, config, + buttons: [{ + iconClass: 'codicon-gear', + tooltip: nls.localize('editLaunchConfig', "Edit Debug Configuration in launch.json") + }], launch })))); } }); const promiseOfPicks = Promise.all(picks).then(result => result.reduce((first, second) => first.concat(second), [])); - const result = await this.quickInputService.pick<{ label: string, launch: ILaunch, config: IConfig }>(promiseOfPicks, { placeHolder: nls.localize('selectConfiguration', "Select Debug Configuration") }); + const result = await this.quickInputService.pick<{ label: string, launch: ILaunch, config: IConfig }>(promiseOfPicks, { + placeHolder: nls.localize('selectConfiguration', "Select Launch Configuration"), + onDidTriggerItemButton: async (context) => { + await this.quickInputService.cancel(); + const { launch, config } = context.item; + await launch.openConfigFile(false, config.type); + // Only Launch have a pin trigger button + await (launch as Launch).writeConfiguration(config); + this.selectConfiguration(launch, config.name); + } + }); if (!result) { // User canceled quick input we should notify the provider to cancel computing configurations token.cancel(); @@ -385,9 +404,9 @@ export class ConfigurationManager implements IConfigurationManager { private initLaunches(): void { this.launches = this.contextService.getWorkspace().folders.map(folder => this.instantiationService.createInstance(Launch, this, folder)); if (this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { - this.launches.push(this.instantiationService.createInstance(WorkspaceLaunch)); + this.launches.push(this.instantiationService.createInstance(WorkspaceLaunch, this)); } - this.launches.push(this.instantiationService.createInstance(UserLaunch)); + this.launches.push(this.instantiationService.createInstance(UserLaunch, this)); if (this.selectedLaunch && this.launches.indexOf(this.selectedLaunch) === -1) { this.selectConfiguration(undefined); @@ -419,10 +438,11 @@ export class ConfigurationManager implements IConfigurationManager { return this.launches.find(l => l.workspace && l.workspace.uri.toString() === workspaceUri.toString()); } - get selectedConfiguration(): { launch: ILaunch | undefined, name: string | undefined } { + get selectedConfiguration(): { launch: ILaunch | undefined, name: string | undefined, config: IConfig | undefined } { return { launch: this.selectedLaunch, - name: this.selectedName + name: this.selectedName, + config: this.selectedConfig }; } @@ -438,7 +458,7 @@ export class ConfigurationManager implements IConfigurationManager { return undefined; } - selectConfiguration(launch: ILaunch | undefined, name?: string): void { + selectConfiguration(launch: ILaunch | undefined, name?: string, config?: IConfig): void { if (typeof launch === 'undefined') { const rootUri = this.historyService.getLastActiveWorkspaceRoot(); launch = this.getLaunch(rootUri); @@ -457,18 +477,19 @@ export class ConfigurationManager implements IConfigurationManager { this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); } const names = launch ? launch.getConfigurationNames() : []; - if (name && names.indexOf(name) >= 0) { + if ((name && names.indexOf(name) >= 0) || config) { this.setSelectedLaunchName(name); - } - if (!this.selectedName || names.indexOf(this.selectedName) === -1) { + } else if (!this.selectedName || names.indexOf(this.selectedName) === -1) { this.setSelectedLaunchName(names.length ? names[0] : undefined); } - const configuration = this.selectedLaunch && this.selectedName ? this.selectedLaunch.getConfiguration(this.selectedName) : undefined; - if (configuration) { - this.debugConfigurationTypeContext.set(configuration.type); + this.selectedConfig = config || (this.selectedLaunch && this.selectedName ? this.selectedLaunch.getConfiguration(this.selectedName) : undefined); + if (this.selectedConfig) { + this.debugConfigurationTypeContext.set(this.selectedConfig.type); + this.storageService.store(DEBUG_SELECTED_CONFIG, JSON.stringify(this.selectedConfig), StorageScope.WORKSPACE); } else { this.debugConfigurationTypeContext.reset(); + this.storageService.remove(DEBUG_SELECTED_CONFIG, StorageScope.WORKSPACE); } if (this.selectedLaunch !== previousLaunch || this.selectedName !== previousName) { @@ -565,6 +586,9 @@ export class ConfigurationManager implements IConfigurationManager { abstract class AbstractLaunch { protected abstract getConfig(): IGlobalConfig | undefined; + constructor(protected configurationManager: ConfigurationManager) { + } + getCompound(name: string): ICompound | undefined { const config = this.getConfig(); if (!config || !config.compounds) { @@ -605,6 +629,16 @@ abstract class AbstractLaunch { return config.configurations.find(config => config && config.name === name); } + async getInitialConfigurationContent(folderUri?: uri, type?: string, token?: CancellationToken): Promise { + let content = ''; + const adapter = await this.configurationManager.guessDebugger(type); + if (adapter) { + const initialConfigs = await this.configurationManager.provideDebugConfigurations(folderUri, adapter.type, token || CancellationToken.None); + content = await adapter.getInitialConfigurationContent(initialConfigs); + } + return content; + } + get hidden(): boolean { return false; } @@ -613,14 +647,14 @@ abstract class AbstractLaunch { class Launch extends AbstractLaunch implements ILaunch { constructor( - private configurationManager: ConfigurationManager, + configurationManager: ConfigurationManager, public workspace: IWorkspaceFolder, @IFileService private readonly fileService: IFileService, @ITextFileService private readonly textFileService: ITextFileService, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService ) { - super(); + super(configurationManager); } get uri(): uri { @@ -635,7 +669,7 @@ class Launch extends AbstractLaunch implements ILaunch { return this.configurationService.inspect('launch', { resource: this.workspace.uri }).workspaceFolderValue; } - async openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }> { + async openConfigFile(preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }> { const resource = this.uri; let created = false; let content = ''; @@ -644,12 +678,7 @@ class Launch extends AbstractLaunch implements ILaunch { content = fileContent.value.toString(); } catch { // launch.json not found: create one by collecting launch configs from debugConfigProviders - const adapter = await this.configurationManager.guessDebugger(type); - - if (adapter) { - const initialConfigs = await this.configurationManager.provideDebugConfigurations(this.workspace.uri, adapter.type, token || CancellationToken.None); - content = await adapter.getInitialConfigurationContent(initialConfigs); - } + content = await this.getInitialConfigurationContent(this.workspace.uri, type, token); if (content) { created = true; // pin only if config file is created #8727 try { @@ -681,22 +710,29 @@ class Launch extends AbstractLaunch implements ILaunch { pinned: created, revealIfVisible: true }, - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + }, ACTIVE_GROUP); return ({ editor: withUndefinedAsNull(editor), created }); } + + async writeConfiguration(configuration: IConfig): Promise { + const fullConfig = objects.deepClone(this.getConfig()!); + fullConfig.configurations.push(configuration); + await this.configurationService.updateValue('launch', fullConfig, { resource: this.workspace.uri }, ConfigurationTarget.WORKSPACE_FOLDER); + } } class WorkspaceLaunch extends AbstractLaunch implements ILaunch { constructor( + configurationManager: ConfigurationManager, @IEditorService private readonly editorService: IEditorService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { - super(); + super(configurationManager); } get workspace(): undefined { @@ -715,12 +751,22 @@ class WorkspaceLaunch extends AbstractLaunch implements ILaunch { return this.configurationService.inspect('launch').workspaceValue; } - async openConfigFile(sideBySide: boolean, preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { + async openConfigFile(preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }> { + let launchExistInFile = !!this.getConfig(); + if (!launchExistInFile) { + // Launch property in workspace config not found: create one by collecting launch configs from debugConfigProviders + let content = await this.getInitialConfigurationContent(undefined, type, token); + if (content) { + await this.configurationService.updateValue('launch', json.parse(content), ConfigurationTarget.WORKSPACE); + } else { + return { editor: null, created: false }; + } + } const editor = await this.editorService.openEditor({ resource: this.contextService.getWorkspace().configuration!, options: { preserveFocus } - }, sideBySide ? SIDE_GROUP : ACTIVE_GROUP); + }, ACTIVE_GROUP); return ({ editor: withUndefinedAsNull(editor), @@ -732,10 +778,11 @@ class WorkspaceLaunch extends AbstractLaunch implements ILaunch { class UserLaunch extends AbstractLaunch implements ILaunch { constructor( + configurationManager: ConfigurationManager, @IConfigurationService private readonly configurationService: IConfigurationService, @IPreferencesService private readonly preferencesService: IPreferencesService ) { - super(); + super(configurationManager); } get workspace(): undefined { @@ -758,7 +805,7 @@ class UserLaunch extends AbstractLaunch implements ILaunch { return this.configurationService.inspect('launch').userValue; } - async openConfigFile(_: boolean, preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { + async openConfigFile(preserveFocus: boolean): Promise<{ editor: IEditorPane | null, created: boolean }> { const editor = await this.preferencesService.openGlobalSettings(true, { preserveFocus }); return ({ editor: withUndefinedAsNull(editor), diff --git a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts index dca5551774..d8231194a0 100644 --- a/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugEditorActions.ts @@ -9,7 +9,7 @@ import { Range } from 'vs/editor/common/core/range'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { ServicesAccessor, registerEditorAction, EditorAction, IActionOptions } from 'vs/editor/browser/editorExtensions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; -import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE, State, VIEWLET_ID, IDebugEditorContribution, EDITOR_CONTRIBUTION_ID, BreakpointWidgetContext, IBreakpoint, BREAKPOINT_EDITOR_CONTRIBUTION_ID, IBreakpointEditorContribution, REPL_VIEW_ID, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -17,6 +17,9 @@ import { openBreakpointSource } from 'vs/workbench/contrib/debug/browser/breakpo import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { PanelFocusContext } from 'vs/workbench/common/panel'; import { IViewsService } from 'vs/workbench/common/views'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { Action } from 'vs/base/common/actions'; +import { getDomNodePagePosition } from 'vs/base/browser/dom'; export const TOGGLE_BREAKPOINT_ID = 'editor.debug.action.toggleBreakpoint'; class ToggleBreakpointAction extends EditorAction { @@ -78,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 @@ -241,6 +244,48 @@ class ShowDebugHoverAction extends EditorAction { } } +class StepIntoTargetsAction extends EditorAction { + + public static readonly ID = 'editor.debug.action.stepIntoTargets'; + public static readonly LABEL = nls.localize('stepIntoTargets', "Step Into Targets..."); + + constructor() { + super({ + id: StepIntoTargetsAction.ID, + label: StepIntoTargetsAction.LABEL, + alias: 'Debug: Step Into Targets...', + precondition: ContextKeyExpr.and(CONTEXT_STEP_INTO_TARGETS_SUPPORTED, CONTEXT_IN_DEBUG_MODE, CONTEXT_DEBUG_STATE.isEqualTo('stopped'), EditorContextKeys.editorTextFocus), + contextMenuOpts: { + group: 'debug', + order: 1.5 + } + }); + } + + async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise { + const debugService = accessor.get(IDebugService); + const contextMenuService = accessor.get(IContextMenuService); + const session = debugService.getViewModel().focusedSession; + const frame = debugService.getViewModel().focusedStackFrame; + + if (session && frame && editor.hasModel()) { + const targets = await session.stepInTargets(frame.frameId); + const position = editor.getPosition(); + const cursorCoords = editor.getScrolledVisiblePosition(position); + const editorCoords = getDomNodePagePosition(editor.getDomNode()); + const x = editorCoords.left + cursorCoords.left; + const y = editorCoords.top + cursorCoords.top + cursorCoords.height; + + contextMenuService.showContextMenu({ + getAnchor: () => ({ x, y }), + getActions: () => { + return targets.map(t => new Action(`stepIntoTarget:${t.id}`, t.label, undefined, true, () => session.stepIn(frame.thread.threadId, t.id))); + } + }); + } + } +} + class GoToBreakpointAction extends EditorAction { constructor(private isNext: boolean, opts: IActionOptions) { super(opts); @@ -307,6 +352,7 @@ registerEditorAction(ToggleBreakpointAction); registerEditorAction(ConditionalBreakpointAction); registerEditorAction(LogPointAction); registerEditorAction(RunToCursorAction); +registerEditorAction(StepIntoTargetsAction); registerEditorAction(SelectionToReplAction); registerEditorAction(SelectionToWatchExpressionsAction); registerEditorAction(ShowDebugHoverAction); diff --git a/src/vs/workbench/contrib/debug/browser/debugHover.ts b/src/vs/workbench/contrib/debug/browser/debugHover.ts index 606bb03e58..d60c0c19e4 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 { - config.launch.openConfigFile(false, false); + config.launch.openConfigFile(false); return TriggerAction.CLOSE_PICKER; }, @@ -81,13 +81,18 @@ export class StartDebugQuickAccessProvider extends PickerQuickAccessProvider 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 dc398688bb..ed95490ab0 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; @@ -405,7 +409,7 @@ export class DebugService implements IDebugService { const cfg = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, type, resolvedConfig, initCancellationToken.token); if (!cfg) { if (launch && type && cfg === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null". - await launch.openConfigFile(false, true, type, initCancellationToken.token); + await launch.openConfigFile(true, type, initCancellationToken.token); } return false; } @@ -439,7 +443,7 @@ export class DebugService implements IDebugService { await this.showError(nls.localize('noFolderWorkspaceDebugError', "The active file can not be debugged. Make sure it is saved and that you have a debug extension installed for that file type.")); } if (launch && !initCancellationToken.token.isCancellationRequested) { - await launch.openConfigFile(false, true, undefined, initCancellationToken.token); + await launch.openConfigFile(true, undefined, initCancellationToken.token); } return false; @@ -447,7 +451,7 @@ export class DebugService implements IDebugService { } if (launch && type && configByProviders === null && !initCancellationToken.token.isCancellationRequested) { // show launch.json only for "config" being "null". - await launch.openConfigFile(false, true, type, initCancellationToken.token); + await launch.openConfigFile(true, type, initCancellationToken.token); } return false; diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 98a81f630d..96dd51ce31 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -35,6 +35,7 @@ import { INotificationService } from 'vs/platform/notification/common/notificati import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { localize } from 'vs/nls'; import { canceled } from 'vs/base/common/errors'; +import { filterExceptionsFromTelemetry } from 'vs/workbench/contrib/debug/common/debugUtils'; export class DebugSession implements IDebugSession { @@ -97,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 { @@ -279,6 +285,10 @@ export class DebugSession implements IDebugSession { } else { await this.raw.disconnect(restart); } + + if (!restart) { + this._options.compoundRoot?.sessionStopped(); + } } /** @@ -291,6 +301,10 @@ export class DebugSession implements IDebugSession { this.cancelAllRequests(); await this.raw.disconnect(restart); + + if (!restart) { + this._options.compoundRoot?.sessionStopped(); + } } /** @@ -366,7 +380,7 @@ export class DebugSession implements IDebugSession { } } - async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean }> { + async dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean } | undefined> { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'data breakpoints info')); } @@ -488,12 +502,12 @@ export class DebugSession implements IDebugSession { await this.raw.next({ threadId }); } - async stepIn(threadId: number): Promise { + async stepIn(threadId: number, targetId?: number): Promise { if (!this.raw) { throw new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepIn')); } - await this.raw.stepIn({ threadId }); + await this.raw.stepIn({ threadId, targetId }); } async stepOut(threadId: number): Promise { @@ -592,7 +606,7 @@ export class DebugSession implements IDebugSession { } const response = await this.raw.loadedSources({}); - if (response.body && response.body.sources) { + if (response && response.body && response.body.sources) { return response.body.sources.map(src => this.getSource(src)); } else { return []; @@ -612,6 +626,15 @@ export class DebugSession implements IDebugSession { }, token); } + async stepInTargets(frameId: number): Promise<{ id: number, label: string }[]> { + if (!this.raw) { + return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'stepInTargets'))); + } + + const response = await this.raw.stepInTargets({ frameId }); + return response.body.targets; + } + async cancel(progressId: string): Promise { if (!this.raw) { return Promise.reject(new Error(localize('noDebugAdapter', "No debug adapter, can not send '{0}'", 'cancel'))); @@ -735,6 +758,7 @@ export class DebugSession implements IDebugSession { await this.raw.configurationDone(); } catch (e) { // Disconnect the debug session on configuration done error #10596 + this.notificationService.error(e); if (this.raw) { this.raw.disconnect(); } @@ -847,7 +871,12 @@ export class DebugSession implements IDebugSession { // and the user opted in telemetry if (this.raw.customTelemetryService && this.telemetryService.isOptedIn) { // __GDPR__TODO__ We're sending events in the name of the debug extension and we can not ensure that those are declared correctly. - this.raw.customTelemetryService.publicLog(event.body.output, event.body.data); + let data = event.body.data; + if (!this.raw.customTelemetryService.sendErrorTelemetry && event.body.data) { + data = filterExceptionsFromTelemetry(event.body.data); + } + + this.raw.customTelemetryService.publicLog(event.body.output, data); } return; diff --git a/src/vs/workbench/contrib/debug/browser/media/debugHover.css b/src/vs/workbench/contrib/debug/browser/media/debugHover.css index ad053f7a06..09fd3d15ae 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugHover.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugHover.css @@ -39,6 +39,7 @@ .monaco-editor .debug-hover-widget .debug-hover-tree .monaco-list-row .monaco-tl-contents { user-select: text; -webkit-user-select: text; + white-space: pre; } /* Disable tree highlight in debug hover tree. */ @@ -56,7 +57,6 @@ } .monaco-editor .debug-hover-widget .value { - white-space: pre-wrap; color: rgba(108, 108, 108, 0.8); overflow: auto; font-family: var(--monaco-monospace-font); diff --git a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css index 39e4b01889..e9d0935297 100644 --- a/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css +++ b/src/vs/workbench/contrib/debug/browser/media/debugViewlet.css @@ -82,7 +82,8 @@ /* Call stack */ -.debug-pane .debug-call-stack-title { +.debug-pane.expanded .debug-call-stack-title, +.debug-pane.vertical .debug-call-stack-title { display: flex; width: 100%; } diff --git a/src/vs/workbench/contrib/debug/browser/media/repl.css b/src/vs/workbench/contrib/debug/browser/media/repl.css index 3b5b5b1603..f43656aa6b 100644 --- a/src/vs/workbench/contrib/debug/browser/media/repl.css +++ b/src/vs/workbench/contrib/debug/browser/media/repl.css @@ -5,19 +5,19 @@ /* Debug repl */ -.repl { +.monaco-workbench .repl { height: 100%; box-sizing: border-box; overflow: hidden; } -.repl .repl-tree .monaco-tl-contents { +.monaco-workbench .repl .repl-tree .monaco-tl-contents { user-select: text; -webkit-user-select: text; white-space: pre; } -.repl .repl-tree.word-wrap .monaco-tl-contents { +.monaco-workbench .repl .repl-tree.word-wrap .monaco-tl-contents { /* Wrap words but also do not trim whitespace #6275 */ word-wrap: break-word; white-space: pre-wrap; @@ -30,25 +30,20 @@ cursor: pointer; } -.repl .repl-tree .output.expression.value-and-source { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source { display: flex; } -.repl .repl-tree .output.expression.value-and-source .value { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source .value { flex: 1; } -.repl .repl-tree .monaco-tl-contents .arrow { +.monaco-workbench .repl .repl-tree .monaco-tl-contents .arrow { position:absolute; left: 2px; - opacity: 0.25; } -.vs-dark .repl .repl-tree .monaco-tl-contents .arrow { - opacity: 0.4; -} - -.repl .repl-tree .output.expression.value-and-source .source { +.monaco-workbench .repl .repl-tree .output.expression.value-and-source .source { margin-left: 4px; margin-right: 8px; cursor: pointer; @@ -59,36 +54,21 @@ max-width: 150px; } -.repl .repl-tree .monaco-list-row { - cursor: text; -} - -.repl .repl-tree .output.expression > .value, -.repl .repl-tree .evaluation-result.expression > .value { +.monaco-workbench .repl .repl-tree .output.expression > .value, +.monaco-workbench .repl .repl-tree .evaluation-result.expression > .value { margin-left: 0px; } -.repl .repl-tree .output.expression > .annotation, -.repl .repl-tree .evaluation-result.expression > .annotation { - font-size: inherit; - padding-left: 6px; -} - -.repl .repl-tree .output.expression .name:not(:empty) { +.monaco-workbench .repl .repl-tree .output.expression .name:not(:empty) { margin-right: 6px; } -.repl .repl-input-wrapper { +.monaco-workbench .repl .repl-input-wrapper { display: flex; align-items: center; } -/* Only show 'stale expansion' info when the element gets expanded. */ -.repl .repl-tree .evaluation-result > .annotation::before { - content: ''; -} - -.repl .repl-input-wrapper .repl-input-chevron { +.monaco-workbench .repl .repl-input-wrapper .repl-input-chevron { padding: 0 6px 0 8px; width: 16px; height: 100%; @@ -99,34 +79,10 @@ } /* Output coloring and styling */ -.repl .repl-tree .output.expression > .ignore { +.monaco-workbench .repl .repl-tree .output.expression > .ignore { font-style: italic; } -.vs .repl .repl-tree .output.expression > .annotation { - color: #007ACC; -} - -.vs-dark .repl .repl-tree .output.expression > .annotation { - color: #1B80B2; -} - -.hc-black .repl .repl-tree .output.expression > .annotation { - color: #0000FF; -} - -.vs .repl .repl-tree .output.expression > .warn { - color: #cd9731; -} - -.vs-dark .repl .repl-tree .output.expression > .warn { - color: #cd9731; -} - -.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/rawDebugSession.ts b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts index c09054ca8b..26d62e6365 100644 --- a/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/rawDebugSession.ts @@ -349,6 +349,13 @@ export class RawDebugSession implements IDisposable { return Promise.reject(new Error('restartFrame not supported')); } + stepInTargets(args: DebugProtocol.StepInTargetsArguments): Promise { + if (this.capabilities.supportsStepInTargetsRequest) { + return this.send('stepInTargets', args); + } + return Promise.reject(new Error('stepInTargets not supported')); + } + completions(args: DebugProtocol.CompletionsArguments, token: CancellationToken): Promise { if (this.capabilities.supportsCompletionsRequest) { return this.send('completions', args, token); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index d064e70cc0..defc4896a6 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'; @@ -60,6 +59,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ReplGroup } from 'vs/workbench/contrib/debug/common/replModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { EDITOR_FONT_DEFAULTS, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME } from 'vs/base/browser/ui/mouseCursor/mouseCursor'; const $ = dom.$; @@ -87,7 +87,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { private replInputContainer!: HTMLElement; private dimension!: dom.Dimension; private replInputLineCount = 1; - private model!: ITextModel; + private model: ITextModel | undefined; private historyNavigationEnablement!: IContextKey; private scopedInstantiationService!: IInstantiationService; private replElementsChangeListener: IDisposable | undefined; @@ -271,7 +271,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { if (isCodeEditor(activeEditorControl)) { this.modelChangeListener.dispose(); this.modelChangeListener = activeEditorControl.onDidChangeModelLanguage(() => this.setMode()); - if (activeEditorControl.hasModel()) { + if (this.model && activeEditorControl.hasModel()) { this.model.setMode(activeEditorControl.getModel().getLanguageIdentifier()); } } @@ -397,16 +397,18 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { getVisibleContent(): string { let text = ''; - const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri); - const traverseAndAppend = (node: ITreeNode) => { - node.children.forEach(child => { - text += child.element.toString().trimRight() + lineDelimiter; - if (!child.collapsed && child.children.length) { - traverseAndAppend(child); - } - }); - }; - traverseAndAppend(this.tree.getNode()); + if (this.model) { + const lineDelimiter = this.textResourcePropertiesService.getEOL(this.model.uri); + const traverseAndAppend = (node: ITreeNode) => { + node.children.forEach(child => { + text += child.element.toString().trimRight() + lineDelimiter; + if (!child.collapsed && child.children.length) { + traverseAndAppend(child); + } + }); + }; + traverseAndAppend(this.tree.getNode()); + } return removeAnsiEscapeCodes(text); } @@ -508,7 +510,7 @@ export class Repl extends ViewPane implements IHistoryNavigationWidget { super.renderBody(parent); this.container = dom.append(parent, $('.repl')); - const treeContainer = dom.append(this.container, $('.repl-tree')); + const treeContainer = dom.append(this.container, $(`.repl-tree.${MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`)); this.createReplInput(this.container); this.replDelegate = new ReplDelegate(this.configurationService); @@ -812,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 a5b8204a77..07c6fa43ca 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 { return this.debugService.addDataBreakpoint(response.description, dataid, !!response.canPersist, response.accessTypes); @@ -359,7 +359,7 @@ class VariablesAccessibilityProvider implements IListAccessibilityProvider { diff --git a/src/vs/workbench/contrib/debug/browser/welcomeView.ts b/src/vs/workbench/contrib/debug/browser/welcomeView.ts index 3b3bdf78d6..fc3b181ef3 100644 --- a/src/vs/workbench/contrib/debug/browser/welcomeView.ts +++ b/src/vs/workbench/contrib/debug/browser/welcomeView.ts @@ -10,12 +10,12 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; -import { StartAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { StartAction, ConfigureAction, SelectAndStartAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IViewDescriptorService, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; +import { IViewDescriptorService, IViewsRegistry, Extensions, ViewContentPriority } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { WorkbenchStateContext } from 'vs/workbench/browser/contextkeys'; @@ -119,6 +119,12 @@ viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { preconditions: [CONTEXT_DEBUGGER_INTERESTED_IN_ACTIVE_EDITOR] }); +viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { + content: localize({ key: 'detectThenRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, + "[Show](command:{0}) all automatic debug configurations.", SelectAndStartAction.ID), + priority: ViewContentPriority.Lowest +}); + viewsRegistry.registerViewWelcomeContent(WelcomeView.ID, { content: localize({ key: 'customizeRunAndDebug', comment: ['Please do not translate the word "commmand", it is part of our internal syntax which must not change'] }, "To customize Run and Debug [create a launch.json file](command:{0}).", ConfigureAction.ID), diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 1f7289523f..4a8de35a4c 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'; @@ -56,6 +57,7 @@ export const CONTEXT_FOCUSED_SESSION_IS_ATTACH = new RawContextKey('foc export const CONTEXT_STEP_BACK_SUPPORTED = new RawContextKey('stepBackSupported', false); export const CONTEXT_RESTART_FRAME_SUPPORTED = new RawContextKey('restartFrameSupported', false); export const CONTEXT_JUMP_TO_CURSOR_SUPPORTED = new RawContextKey('jumpToCursorSupported', false); +export const CONTEXT_STEP_INTO_TARGETS_SUPPORTED = new RawContextKey('stepIntoTargetsSupported', false); export const CONTEXT_BREAKPOINTS_EXIST = new RawContextKey('breakpointsExist', false); export const EDITOR_CONTRIBUTION_ID = 'editor.contrib.debug'; @@ -154,6 +156,7 @@ export interface IDebugSessionOptions { noDebug?: boolean; parentSession?: IDebugSession; repl?: IDebugSessionReplMode; + compoundRoot?: DebugCompoundRoot; } export interface IDebugSession extends ITreeElement { @@ -214,7 +217,7 @@ export interface IDebugSession extends ITreeElement { sendBreakpoints(modelUri: uri, bpts: IBreakpoint[], sourceModified: boolean): Promise; sendFunctionBreakpoints(fbps: IFunctionBreakpoint[]): Promise; - dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] }>; + dataBreakpointInfo(name: string, variablesReference?: number): Promise<{ dataId: string | null, description: string, canPersist?: boolean, accessTypes?: DebugProtocol.DataBreakpointAccessType[] } | undefined>; sendDataBreakpoints(dbps: IDataBreakpoint[]): Promise; sendExceptionBreakpoints(exbpts: IExceptionBreakpoint[]): Promise; breakpointsLocations(uri: uri, lineNumber: number): Promise; @@ -229,7 +232,7 @@ export interface IDebugSession extends ITreeElement { restartFrame(frameId: number, threadId: number): Promise; next(threadId: number): Promise; - stepIn(threadId: number): Promise; + stepIn(threadId: number, targetId?: number): Promise; stepOut(threadId: number): Promise; stepBack(threadId: number): Promise; continue(threadId: number): Promise; @@ -237,6 +240,7 @@ export interface IDebugSession extends ITreeElement { pause(threadId: number): Promise; terminateThreads(threadIds: number[]): Promise; + stepInTargets(frameId: number): Promise<{ id: number, label: string }[]>; completions(frameId: number | undefined, text: string, position: Position, overwriteBefore: number, token: CancellationToken): Promise; setVariable(variablesReference: number | undefined, name: string, value: string): Promise; loadSource(resource: uri): Promise; @@ -516,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; @@ -633,10 +638,11 @@ export interface IConfigurationManager { */ readonly selectedConfiguration: { launch: ILaunch | undefined; + config: IConfig | undefined; name: string | undefined; }; - selectConfiguration(launch: ILaunch | undefined, name?: string, debugStarted?: boolean): void; + selectConfiguration(launch: ILaunch | undefined, name?: string, config?: IConfig): void; getLaunches(): ReadonlyArray; @@ -717,7 +723,7 @@ export interface ILaunch { /** * Opens the launch.json file. Creates if it does not exist. */ - openConfigFile(sideBySide: boolean, preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }>; + openConfigFile(preserveFocus: boolean, type?: string, token?: CancellationToken): Promise<{ editor: IEditorPane | null, created: boolean }>; } // Debug service interfaces 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 0000000000..887542fbb2 --- /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 Source EULA. 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 28325a50b9..b501f6f8c3 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/common/debugUtils.ts b/src/vs/workbench/contrib/debug/common/debugUtils.ts index 016d925b9b..4b5074175d 100644 --- a/src/vs/workbench/contrib/debug/common/debugUtils.ts +++ b/src/vs/workbench/contrib/debug/common/debugUtils.ts @@ -23,6 +23,22 @@ export function formatPII(value: string, excludePII: boolean, args: { [key: stri }); } +/** + * Filters exceptions (keys marked with "!") from the given object. Used to + * ensure exception data is not sent on web remotes, see #97628. + */ +export function filterExceptionsFromTelemetry(data: T): Partial { + const output: Partial = {}; + for (const key of Object.keys(data) as (keyof T & string)[]) { + if (!key.startsWith('!')) { + output[key] = data[key]; + } + } + + return output; +} + + export function isSessionAttach(session: IDebugSession): boolean { return session.configuration.request === 'attach' && !getExtensionHostDebugSession(session); } diff --git a/src/vs/workbench/contrib/debug/common/debugViewModel.ts b/src/vs/workbench/contrib/debug/common/debugViewModel.ts index 71f1b10eb6..6e6ee537e3 100644 --- a/src/vs/workbench/contrib/debug/common/debugViewModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugViewModel.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; +import { CONTEXT_EXPRESSION_SELECTED, IViewModel, IStackFrame, IDebugSession, IThread, IExpression, IFunctionBreakpoint, CONTEXT_BREAKPOINT_SELECTED, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_STEP_INTO_TARGETS_SUPPORTED } from 'vs/workbench/contrib/debug/common/debug'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { isSessionAttach } from 'vs/workbench/contrib/debug/common/debugUtils'; @@ -27,6 +27,7 @@ export class ViewModel implements IViewModel { private stepBackSupportedContextKey: IContextKey; private focusedSessionIsAttach: IContextKey; private restartFrameSupportedContextKey: IContextKey; + private stepIntoTargetsSupported: IContextKey; private jumpToCursorSupported: IContextKey; constructor(contextKeyService: IContextKeyService) { @@ -37,6 +38,7 @@ export class ViewModel implements IViewModel { this.stepBackSupportedContextKey = CONTEXT_STEP_BACK_SUPPORTED.bindTo(contextKeyService); this.focusedSessionIsAttach = CONTEXT_FOCUSED_SESSION_IS_ATTACH.bindTo(contextKeyService); this.restartFrameSupportedContextKey = CONTEXT_RESTART_FRAME_SUPPORTED.bindTo(contextKeyService); + this.stepIntoTargetsSupported = CONTEXT_STEP_INTO_TARGETS_SUPPORTED.bindTo(contextKeyService); this.jumpToCursorSupported = CONTEXT_JUMP_TO_CURSOR_SUPPORTED.bindTo(contextKeyService); } @@ -67,6 +69,7 @@ export class ViewModel implements IViewModel { this.loadedScriptsSupportedContextKey.set(session ? !!session.capabilities.supportsLoadedSourcesRequest : false); this.stepBackSupportedContextKey.set(session ? !!session.capabilities.supportsStepBack : false); this.restartFrameSupportedContextKey.set(session ? !!session.capabilities.supportsRestartFrame : false); + this.stepIntoTargetsSupported.set(session ? !!session.capabilities.supportsStepInTargetsRequest : false); this.jumpToCursorSupported.set(session ? !!session.capabilities.supportsGotoTargetsRequest : false); const attach = !!session && isSessionAttach(session); this.focusedSessionIsAttach.set(attach); 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 44baf87d95..fc732371e4 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/debugHelperService.ts b/src/vs/workbench/contrib/debug/node/debugHelperService.ts index a624e5eb05..260c6d8051 100644 --- a/src/vs/workbench/contrib/debug/node/debugHelperService.ts +++ b/src/vs/workbench/contrib/debug/node/debugHelperService.ts @@ -10,10 +10,17 @@ import { getPathFromAmdModule } from 'vs/base/common/amd'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { cleanRemoteAuthority } from 'vs/platform/telemetry/common/telemetryUtils'; export class NodeDebugHelperService implements IDebugHelperService { _serviceBrand: undefined; + constructor( + @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService, + ) { } + + createTelemetryService(configurationService: IConfigurationService, args: string[]): TelemetryService | undefined { const client = new TelemetryClient( @@ -33,7 +40,10 @@ export class NodeDebugHelperService implements IDebugHelperService { const channel = client.getChannel('telemetryAppender'); const appender = new TelemetryAppenderClient(channel); - return new TelemetryService({ appender, sendErrorTelemetry: true }, configurationService); + return new TelemetryService({ + appender, + sendErrorTelemetry: cleanRemoteAuthority(this.environmentService.configuration.remoteAuthority) !== 'other' + }, configurationService); } } diff --git a/src/vs/workbench/contrib/debug/node/terminals.ts b/src/vs/workbench/contrib/debug/node/terminals.ts index 41823658d0..0d24de4d1a 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/debug/test/browser/telemetry.test.ts b/src/vs/workbench/contrib/debug/test/browser/telemetry.test.ts new file mode 100644 index 0000000000..63b9aecc98 --- /dev/null +++ b/src/vs/workbench/contrib/debug/test/browser/telemetry.test.ts @@ -0,0 +1,74 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { MockDebugAdapter, createMockDebugModel } from 'vs/workbench/contrib/debug/test/common/mockDebug'; +import { DebugModel } from 'vs/workbench/contrib/debug/common/debugModel'; +import { DebugSession } from 'vs/workbench/contrib/debug/browser/debugSession'; +import { generateUuid } from 'vs/base/common/uuid'; +import { NullOpenerService } from 'vs/platform/opener/common/opener'; +import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSession'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { stub, SinonStub } from 'sinon'; +import { timeout } from 'vs/base/common/async'; + +suite('Debug - DebugSession telemetry', () => { + let model: DebugModel; + let session: DebugSession; + let adapter: MockDebugAdapter; + let telemetry: { isOptedIn: boolean; sendErrorTelemetry: boolean; publicLog: SinonStub }; + + setup(() => { + telemetry = { isOptedIn: true, sendErrorTelemetry: true, publicLog: stub() }; + adapter = new MockDebugAdapter(); + model = createMockDebugModel(); + + const telemetryService = telemetry as Partial as ITelemetryService; + session = new DebugSession(generateUuid(), undefined!, undefined!, model, undefined, undefined!, telemetryService, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!, undefined!); + session.initializeForTest(new RawDebugSession(adapter, undefined!, undefined!, telemetryService, undefined!, undefined!, undefined!)); + }); + + test('does not send telemetry when opted out', async () => { + telemetry.isOptedIn = false; + adapter.sendEventBody('output', { + category: 'telemetry', + output: 'someEvent', + data: { foo: 'bar', '!err': 'oh no!' } + }); + + await timeout(0); + assert.strictEqual(telemetry.publicLog.callCount, 0); + }); + + test('logs telemetry and exceptions when enabled', async () => { + adapter.sendEventBody('output', { + category: 'telemetry', + output: 'someEvent', + data: { foo: 'bar', '!err': 'oh no!' } + }); + + await timeout(0); + assert.deepStrictEqual(telemetry.publicLog.args[0], [ + 'someEvent', + { foo: 'bar', '!err': 'oh no!' } + ]); + }); + + test('filters exceptions when error reporting disabled', async () => { + telemetry.sendErrorTelemetry = false; + + adapter.sendEventBody('output', { + category: 'telemetry', + output: 'someEvent', + data: { foo: 'bar', '!err': 'oh no!' } + }); + + await timeout(0); + assert.deepStrictEqual(telemetry.publicLog.args[0], [ + 'someEvent', + { foo: 'bar' } + ]); + }); +}); diff --git a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts index efeb659ecb..3e43baaf86 100644 --- a/src/vs/workbench/contrib/debug/test/common/mockDebug.ts +++ b/src/vs/workbench/contrib/debug/test/common/mockDebug.ts @@ -136,6 +136,10 @@ export class MockDebugService implements IDebugService { export class MockSession implements IDebugSession { + stepInTargets(frameId: number): Promise<{ id: number; label: string; }[]> { + throw new Error('Method not implemented.'); + } + cancel(_progressId: string): Promise { throw new Error('Method not implemented.'); } @@ -144,7 +148,7 @@ export class MockSession implements IDebugSession { throw new Error('Method not implemented.'); } - dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined; }> { + dataBreakpointInfo(name: string, variablesReference?: number | undefined): Promise<{ dataId: string | null; description: string; canPersist?: boolean | undefined; } | undefined> { throw new Error('Method not implemented.'); } @@ -304,7 +308,7 @@ export class MockSession implements IDebugSession { next(threadId: number): Promise { throw new Error('Method not implemented.'); } - stepIn(threadId: number): Promise { + stepIn(threadId: number, targetId?: number): Promise { throw new Error('Method not implemented.'); } stepOut(threadId: number): Promise { 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 ba7e374cc2..cdb949a14f 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 a391721dfa..19e008591e 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 d0b4562e73..764f6577c1 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 f412fb45cf..2a4018b4a4 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionEditor.ts @@ -610,7 +610,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 86b5929224..432e6ca0d5 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); @@ -458,15 +457,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 b8173e4d74..2280086b95 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -236,7 +236,7 @@ export class InstallAction extends ExtensionAction { const extension = await this.install(this.extension); - alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); + alert(localize('installExtensionComplete', "Installing extension {0} is completed.", this.extension.displayName)); // {{SQL CARBON EDIT}} Add extension object check since ADS third party extensions will be directed to a download page // and the extension object will be undefined. @@ -827,7 +827,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); } @@ -837,7 +837,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); } } @@ -1380,7 +1380,7 @@ export class ReloadAction extends ExtensionAction { this.enabled = true; this.label = localize('reloadRequired', "Reload Required"); this.tooltip = localize('postEnableTooltip', "Please reload Azure Data Studio to enable this extension."); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio - alert(localize('installExtensionComplete', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio + alert(localize('installExtensionCompletedAndReloadRequired', "Installing extension {0} is completed. Please reload Azure Data Studio to enable it.", this.extension.displayName)); // {{SQL CARBON EDIT}} - replace Visual Studio Code with Azure Data Studio return; } } @@ -2681,7 +2681,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())); @@ -2691,11 +2692,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/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index fb38c6f116..2c5500a908 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -456,8 +456,8 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } }, onDragOver: (e: DragEvent) => { - if (e.dataTransfer) { - e.dataTransfer.dropEffect = this.isSupportedDragElement(e) ? 'copy' : 'none'; + if (this.isSupportedDragElement(e)) { + e.dataTransfer!.dropEffect = 'copy'; } }, onDrop: async (e: DragEvent) => { @@ -482,7 +482,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE } } } - }, + } })); super.create(append(this.root, $('.extensions'))); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts index 2b064393ee..b2a113aef0 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsWorkbenchService.ts @@ -20,7 +20,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'; @@ -38,6 +38,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'; import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'; // {{SQL CARBON EDIT}} import { IOpenerService } from 'vs/platform/opener/common/opener'; // {{SQL CARBON EDIT}} @@ -98,8 +99,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; @@ -386,7 +387,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 }); @@ -923,6 +924,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 4f4628073f..cef709ab29 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/media/extensionEditor.css b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css index 15d110b7be..a6efaa0e8e 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionEditor.css @@ -91,7 +91,7 @@ margin-left: 10px; } -.vs .extension-editor > .header > .details > .title > .preview { +.monaco-workbench.vs .extension-editor > .header > .details > .title > .preview { color: white; } @@ -408,8 +408,8 @@ vertical-align: middle; } -.vs-dark .extension-editor > .body > .content table .colorBox, -.hc-black .extension-editor > .body > .content table .colorBox { +.monaco-workbench.vs-dark .extension-editor > .body > .content table .colorBox, +.monaco-workbench.hc-black .extension-editor > .body > .content table .colorBox { border-color: rgb(238, 238, 238); } diff --git a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css index dfe87d3d3d..8eabd76049 100644 --- a/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/browser/media/extensionsViewlet.css @@ -112,16 +112,16 @@ max-width: 100px; } -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .description, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .description, -.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .footer > .author, -.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .footer > .author { +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .bookmark, +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .icon-container > .icon, +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .header-container, +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .description, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .description, +.monaco-workbench.vs .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .footer > .author, +.monaco-workbench.vs-dark .extensions-viewlet > .extensions .monaco-list-row.disabled > .extension-list-item > .details > .footer > .author { opacity: 0.5; } diff --git a/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts b/src/vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller.ts index 44595ecaf9..27087c3fd0 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 e09c0dc1c6..292c5806e3 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 cbd04a3e42..cb9675a974 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -89,6 +89,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 dd451b3976..eb305cdac4 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 875d79409f..bdc20f8893 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/media/runtimeExtensionsEditor.css b/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css index 81fa04cacc..0014d6d3e6 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/runtimeExtensionsEditor.css @@ -57,8 +57,8 @@ color: currentColor; } -.vs .runtime-extensions-editor .extension > .icon-container > .icon, -.vs-dark .runtime-extensions-editor .extension > .icon-container > .icon { +.monaco-workbench.vs .runtime-extensions-editor .extension > .icon-container > .icon, +.monaco-workbench.vs-dark .runtime-extensions-editor .extension > .icon-container > .icon { opacity: 0.5; } diff --git a/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts b/src/vs/workbench/contrib/extensions/electron-browser/runtimeExtensionsEditor.ts index 267c8da1ea..ae7aef7697 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'; 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 f8fef33c22..d4521f11e5 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.skip('ExtensionRecommendationsService Test', () => { // {{SQL CARBON EDIT} 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 c1bacdc167..07e8d33b1e 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 7215be935c..48664d6209 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 1e77acc982..59fd339b30 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/feedback/browser/media/feedback.css b/src/vs/workbench/contrib/feedback/browser/media/feedback.css index 7e98aa611e..36c7729aa0 100644 --- a/src/vs/workbench/contrib/feedback/browser/media/feedback.css +++ b/src/vs/workbench/contrib/feedback/browser/media/feedback.css @@ -115,7 +115,8 @@ } /* Theming */ -.vs .monaco-workbench .feedback-form .feedback-alias, .vs .monaco-workbench .feedback-form .feedback-description { +.monaco-workbench.vs .feedback-form .feedback-alias, +.monaco-workbench.vs .feedback-form .feedback-description { font-family: inherit; border: 1px solid transparent; } @@ -167,16 +168,17 @@ background-color: #E51400; } -.vs-dark .monaco-workbench .feedback-form h3 { +.monaco-workbench.vs-dark .feedback-form h3 { font-weight: normal; font-size: 1.2em; } -.vs-dark .monaco-workbench .feedback-form .sentiment:hover { +.monaco-workbench.vs-dark .feedback-form .sentiment:hover { background-color: rgba(30,30,30,0.8); } -.vs-dark .monaco-workbench .feedback-form .feedback-alias, .vs-dark .monaco-workbench .feedback-form .feedback-description { +.monaco-workbench.vs-dark .feedback-form .feedback-alias, +.monaco-workbench.vs-dark .feedback-form .feedback-description { font-family: inherit; } @@ -193,28 +195,29 @@ } /* High Contrast Theming */ -.hc-black .monaco-workbench .feedback-form { +.monaco-workbench.hc-black .feedback-form { outline: 2px solid #6fc3df; outline-offset: -2px; } -.hc-black .monaco-workbench .feedback-form .feedback-alias, .hc-black .monaco-workbench .feedback-form .feedback-description { +.monaco-workbench.hc-black .feedback-form .feedback-alias, +.monaco-workbench.hc-black .feedback-form .feedback-description { font-family: inherit; } -.hc-black .monaco-workbench .feedback-form .content .contactus { +.monaco-workbench.hc-black .feedback-form .content .contactus { padding: 10px; float: right; } -.hc-black .monaco-workbench .feedback-form .form-buttons .send, -.hc-black .monaco-workbench .feedback-form .form-buttons .send.in-progress, -.hc-black .monaco-workbench .feedback-form .form-buttons .send.success { +.monaco-workbench.hc-black .feedback-form .form-buttons .send, +.monaco-workbench.hc-black .feedback-form .form-buttons .send.in-progress, +.monaco-workbench.hc-black .feedback-form .form-buttons .send.success { background-color: #0C141F; color: #D4D4D4; border: 1px solid #6FC3DF; } -.hc-black .monaco-workbench .feedback-form .form-buttons .send:hover { +.monaco-workbench.hc-black .feedback-form .form-buttons .send:hover { background-color: #0C141F; } diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 3799844c36..814d1e6d44 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 a58188f9d6..a4f352ad8b 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler.ts @@ -350,18 +350,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 +384,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/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index b9513dbed3..b5b7a72605 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -109,6 +109,7 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor id: OpenEditorsView.ID, name: OpenEditorsView.NAME, ctorDescriptor: new SyncDescriptor(OpenEditorsView), + containerIcon: 'codicon-files', order: 0, when: OpenEditorsVisibleContext, canToggleVisibility: true, diff --git a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts index 93783558c2..5e5d763943 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts @@ -145,9 +145,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 e4980b2acf..9da2cbb7b5 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'; @@ -597,7 +597,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; } } @@ -1081,20 +1081,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; } }; @@ -1161,7 +1161,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 @@ -1199,7 +1199,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/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 26cbbc518e..118e1e8937 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -369,9 +369,15 @@ configurationRegistry.registerConfiguration({ 'default': 9 }, 'explorer.autoReveal': { - 'type': 'boolean', - 'description': nls.localize('autoReveal', "Controls whether the explorer should automatically reveal and select files when opening them."), - 'default': true + 'type': ['boolean', 'string'], + 'enum': [true, false, 'focusNoScroll'], + 'default': true, + 'enumDescriptions': [ + nls.localize('autoReveal.on', 'Files will be revealed and selected.'), + nls.localize('autoReveal.off', 'Files will not be revealed and selected.'), + nls.localize('autoReveal.focusNoScroll', 'Files will not be scrolled into view, but will still be focused.'), + ], + 'description': nls.localize('autoReveal', "Controls whether the explorer should automatically reveal and select files when opening them.") }, 'explorer.enableDragAndDrop': { 'type': 'boolean', diff --git a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css index 1f7baf8a48..0a0d9e09c1 100644 --- a/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css +++ b/src/vs/workbench/contrib/files/browser/media/explorerviewlet.css @@ -95,6 +95,6 @@ } /* High Contrast Theming */ -.hc-black .monaco-workbench .explorer-viewlet .explorer-item { +.monaco-workbench.hc-black .explorer-viewlet .explorer-item { line-height: 20px; } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 66d10d1163..760dd52b21 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; @@ -146,7 +146,7 @@ export class ExplorerView extends ViewPane { // Refresh is needed on the initial explorer open private shouldRefresh = true; private dragHandler!: DelayedDragHandler; - private autoReveal = false; + private autoReveal: boolean | 'focusNoScroll' = false; private actions: IAction[] | undefined; private decorationsProvider: ExplorerDecorationsProvider | 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) { @@ -674,11 +674,9 @@ export class ExplorerView extends ViewPane { } try { - if (reveal) { - // Don't scroll to the item if it's already visible - if (this.tree.getRelativeTop(item) === null) { - this.tree.reveal(item, 0.5); - } + if (reveal === true && this.tree.getRelativeTop(item) === null) { + // Don't scroll to the item if it's already visible, or if set not to. + this.tree.reveal(item, 0.5); } this.tree.setFocus([item]); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 32d2699f7f..f81b002626 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'; @@ -29,7 +29,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { equals, deepClone } from 'vs/base/common/objects'; import * as path from 'vs/base/common/path'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { compareFileExtensions, compareFileNames } from 'vs/base/common/comparers'; +import { compareFileExtensionsNumeric, compareFileNamesNumeric } from 'vs/base/common/comparers'; import { fillResourceDataTransfers, CodeDataTransfers, extractResources, containsDragType } from 'vs/workbench/browser/dnd'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; @@ -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 { @@ -616,7 +617,7 @@ export class FilesFilter implements ITreeFilter { } } -// // Explorer Sorter +// Explorer Sorter export class FileSorter implements ITreeSorter { constructor( @@ -624,7 +625,7 @@ export class FileSorter implements ITreeSorter { @IWorkspaceContextService private readonly contextService: IWorkspaceContextService ) { } - public compare(statA: ExplorerItem, statB: ExplorerItem): number { + compare(statA: ExplorerItem, statB: ExplorerItem): number { // Do not sort roots if (statA.isRoot) { if (statB.isRoot) { @@ -654,7 +655,7 @@ export class FileSorter implements ITreeSorter { } if (statA.isDirectory && statB.isDirectory) { - return compareFileNames(statA.name, statB.name); + return compareFileNamesNumeric(statA.name, statB.name); } break; @@ -688,17 +689,17 @@ export class FileSorter implements ITreeSorter { // Sort Files switch (sortOrder) { case 'type': - return compareFileExtensions(statA.name, statB.name); + return compareFileExtensionsNumeric(statA.name, statB.name); case 'modified': if (statA.mtime !== statB.mtime) { return (statA.mtime && statB.mtime && statA.mtime < statB.mtime) ? 1 : -1; } - return compareFileNames(statA.name, statB.name); + return compareFileNamesNumeric(statA.name, statB.name); default: /* 'default', 'mixed', 'filesFirst' */ - return compareFileNames(statA.name, statB.name); + return compareFileNamesNumeric(statA.name, statB.name); } } } @@ -712,6 +713,27 @@ const getFileOverwriteConfirm = (name: string) => { }; }; +interface IWebkitDataTransfer { + items: IWebkitDataTransferItem[]; +} + +interface IWebkitDataTransferItem { + webkitGetAsEntry(): IWebkitDataTransferItemEntry; +} + +interface IWebkitDataTransferItemEntry { + name: string | undefined; + isFile: boolean; + isDirectory: boolean; + + file(resolve: (file: File) => void, reject: () => void): void; + createReader(): IWebkitDataTransferItemEntryReader; +} + +interface IWebkitDataTransferItemEntryReader { + readEntries(resolve: (file: IWebkitDataTransferItemEntry[]) => void, reject: () => void): void +} + export class FileDragAndDrop implements ITreeDragAndDrop { private static readonly CONFIRM_DND_SETTING_KEY = 'explorer.confirmDragAndDrop'; @@ -732,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 = []; @@ -756,7 +779,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { const iconLabelName = getIconLabelNameFromHTMLElement(originalEvent.target); if (iconLabelName && iconLabelName.index < iconLabelName.count - 1) { - const result = this._onDragOver(data, compressedTarget, targetIndex, originalEvent); + const result = this.handleDragOver(data, compressedTarget, targetIndex, originalEvent); if (result) { if (iconLabelName.element !== this.compressedDragOverElement) { @@ -780,10 +803,10 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } this.compressedDropTargetDisposable.dispose(); - return this._onDragOver(data, target, targetIndex, originalEvent); + return this.handleDragOver(data, target, targetIndex, originalEvent); } - private _onDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { + private handleDragOver(data: IDragAndDropData, target: ExplorerItem | undefined, targetIndex: number | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction { const isCopy = originalEvent && ((originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh)); const fromDesktop = data instanceof DesktopDragAndDropData; const effect = (fromDesktop || isCopy) ? ListDragOverEffect.Copy : ListDragOverEffect.Move; @@ -939,32 +962,163 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } private async handleWebExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { - data.files.forEach(file => { + const items = (originalEvent.dataTransfer as unknown as IWebkitDataTransfer).items; + + // Somehow the items thing is being modified at random, maybe as a security + // measure since this is a DND operation. As such, we copy the items into + // an array we own as early as possible before using it. + const entries: IWebkitDataTransferItemEntry[] = []; + for (const item of items) { + entries.push(item.webkitGetAsEntry()); + } + + const results: { isFile: boolean, resource: URI }[] = []; + const cts = new CancellationTokenSource(); + + // 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 (!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, progress: IProgress, token: CancellationToken): Promise<{ isFile: boolean, resource: URI } | undefined> { + if (token.isCancellationRequested || !entry.name || (!entry.isFile && !entry.isDirectory)) { + return undefined; + } + + const resource = joinPath(parentResource, entry.name); + + // Confirm overwrite as needed + if (target && target.getChild(entry.name)) { + const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(resource.path)); + if (!confirmed) { + return undefined; + } + } + + 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)); + + 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(); + const childEntries = await new Promise((resolve, reject) => { + dirReader.readEntries(resolve, reject); + }); + + for (let childEntry of childEntries) { + 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.readAsArrayBuffer(file); - reader.onload = async (event) => { - const name = file.name; - if (typeof name === 'string' && event.target?.result instanceof ArrayBuffer) { - if (target.getChild(name)) { - const { confirmed } = await this.dialogService.confirm(getFileOverwriteConfirm(name)); - if (!confirmed) { - return; - } + 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.'); } - const resource = joinPath(target.resource, name); - await this.fileService.writeFile(resource, VSBuffer.wrap(new Uint8Array(event.target.result))); - if (data.files.length === 1) { - await this.editorService.openEditor({ resource, options: { pinned: true } }); - } + 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 { - const droppedResources = extractResources(originalEvent, true); + // Check for dropped external files to be folders + const droppedResources = extractResources(originalEvent, true); const result = await this.fileService.resolveAll(droppedResources.map(droppedResource => ({ resource: droppedResource.resource }))); // Pass focus to window @@ -973,7 +1127,6 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Handle folders by adding to workspace if we are in workspace context const folders = result.filter(r => r.success && r.stat && r.stat.isDirectory).map(result => ({ uri: result.stat!.resource })); if (folders.length > 0) { - const buttons = [ folders.length > 1 ? localize('copyFolders', "&&Copy Folders") : localize('copyFolder', "&&Copy Folder"), localize('cancel', "Cancel") diff --git a/src/vs/workbench/contrib/files/browser/views/media/openeditors.css b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css index fd2783dc0b..92b0226bba 100644 --- a/src/vs/workbench/contrib/files/browser/views/media/openeditors.css +++ b/src/vs/workbench/contrib/files/browser/views/media/openeditors.css @@ -81,7 +81,7 @@ overflow: hidden; } -.hc-black .monaco-workbench .open-editors .open-editor, -.hc-black .monaco-workbench .open-editors .editor-group { +.monaco-workbench.hc-black .open-editors .open-editor, +.monaco-workbench.hc-black .open-editors .editor-group { line-height: 20px; } diff --git a/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/common/editors/fileEditorInput.ts index ef0705362a..b65fea13a2 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'; @@ -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; diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index d4ffb37cff..18f763fed6 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); } @@ -150,10 +150,11 @@ export class ExplorerService implements IExplorerService { return !!this.editable && (this.editable.stat === stat || !stat); } - async select(resource: URI, reveal?: boolean): Promise { + async select(resource: URI, reveal?: boolean | string): Promise { if (!this.view) { return; } + const fileStat = this.findClosest(resource); if (fileStat) { await this.view.selectResource(fileStat.resource, reveal); @@ -197,7 +198,7 @@ export class ExplorerService implements IExplorerService { if (reveal && resource && autoReveal) { // We did a top level refresh, reveal the active file #67118 - this.select(resource, true); + this.select(resource, autoReveal); } } } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index 80c52885e2..f4526ff3af 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -51,14 +51,14 @@ 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; /** * Selects and reveal the file element provided by the given resource if its found in the explorer. * Will try to resolve the path in case the explorer is not yet expanded to the file yet. */ - select(resource: URI, reveal?: boolean): Promise; + select(resource: URI, reveal?: boolean | string): Promise; registerView(contextAndRefreshProvider: IExplorerView): void; } @@ -66,7 +66,7 @@ export interface IExplorerService { export interface IExplorerView { getContext(respectMultiSelection: boolean): ExplorerItem[]; refresh(recursive: boolean, item?: ExplorerItem): Promise; - selectResource(resource: URI | undefined, reveal?: boolean): Promise; + selectResource(resource: URI | undefined, reveal?: boolean | string): Promise; setTreeInput(): Promise; itemsCopied(tats: ExplorerItem[], cut: boolean, previousCut: ExplorerItem[] | undefined): void; setEditable(stat: ExplorerItem, isEditing: boolean): Promise; @@ -121,7 +121,7 @@ export interface IFilesConfiguration extends PlatformIFilesConfiguration, IWorkb openEditors: { visible: number; }; - autoReveal: boolean; + autoReveal: boolean | 'focusNoScroll'; enableDragAndDrop: boolean; confirmDelete: boolean; sortOrder: SortOrder; diff --git a/src/vs/workbench/contrib/files/common/openWith.ts b/src/vs/workbench/contrib/files/common/openWith.ts index 323e9b178f..dfe649de63 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 74d40a3c0b..45796a9fc7 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 968c3bc8fb..7c952a0227 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 118202aeef..b3038084fe 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 Source EULA. 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 16a3c297f3..8ad433527d 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/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 ffb48adc51..bf662040dd 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 eaf1a4f596..47bdbee95c 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 e4b632984f..41c3e3621a 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 b04f8e3818..92ddd2b2f1 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 29f040a425..1fac8010a6 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 Source EULA. 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/localizations/browser/localizationsActions.ts b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts index c03a999066..fb0e5ee9f2 100644 --- a/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts +++ b/src/vs/workbench/contrib/localizations/browser/localizationsActions.ts @@ -46,7 +46,7 @@ export class ConfigureLocaleAction extends Action { .concat({ label: localize('installAdditionalLanguages', "Install additional languages...") }); } - public async run(event?: any): Promise { + public async run(): Promise { const languageOptions = await this.getLanguageOptions(); const currentLanguageIndex = firstIndex(languageOptions, l => l.label === language); diff --git a/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts b/src/vs/workbench/contrib/logs/electron-browser/logsActions.ts index 5840cc6947..769b966c9a 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 105fd3a783..18ca8b05da 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 Source EULA. 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 edb27d8b19..d08a03347e 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/markersViewActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 459d333752..05cabdefe1 100644 --- a/src/vs/workbench/contrib/markers/browser/markersViewActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -323,7 +323,6 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { ariaLabel: Messages.MARKERS_PANEL_FILTER_ARIA_LABEL, history: this.filterController.filters.filterHistory })); - this.filterInputBox.inputElement.setAttribute('aria-labelledby', 'markers-panel-arialabel'); this._register(attachInputBoxStyler(this.filterInputBox, this.themeService)); this.filterInputBox.value = this.filterController.filters.filterText; this._register(this.filterInputBox.onDidChange(filter => this.delayedFilterUpdate.trigger(() => this.onDidInputChange(this.filterInputBox!)))); diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index f192164825..9bb3dd918a 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -20,9 +20,8 @@ flex: 1; } -.vs .monaco-action-bar .markers-panel-action-filter .monaco-inputbox { +.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 { @@ -101,7 +100,7 @@ padding-right: 10px; } -.hc-black .markers-panel .markers-panel-container .tree-container .monaco-tl-contents { +.monaco-workbench.hc-black .markers-panel .markers-panel-container .tree-container .monaco-tl-contents { line-height: 20px; } 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 31b12d251b..add850c001 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 0e516d4fcd..bf6f35b9f9 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -3,6 +3,10 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// Scrollable Element + +export const SCROLLABLE_ELEMENT_PADDING_TOP = 16; + // Cell sizing related export const CELL_MARGIN = 20; export const CELL_RUN_GUTTER = 32; @@ -17,3 +21,4 @@ export const EDITOR_TOP_MARGIN = 0; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 12; export const EDITOR_BOTTOM_PADDING = 12; + diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index def77010a0..8a94c86536 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -4,24 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { URI } from 'vs/base/common/uri'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; +import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IModeService } from 'vs/editor/common/services/modeService'; import { localize } from 'vs/nls'; import { Action2, IAction2Options, MenuId, MenuItemAction, MenuRegistry, registerAction2 } from 'vs/platform/actions/common/actions'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContext, InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { BaseCellRenderTemplate, CellEditState, CellRunState, ICellViewModel, INotebookEditor, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_CELL_RUNNABLE, NOTEBOOK_CELL_TYPE, NOTEBOOK_CELL_MARKDOWN_EDIT_MODE, NOTEBOOK_CELL_EDITABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { CellKind, NOTEBOOK_EDITOR_CURSOR_BOUNDARY } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IModeService } from 'vs/editor/common/services/modeService'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IQuickInputService, IQuickPickItem, QuickPickInput } from 'vs/platform/quickinput/common/quickInput'; -import { URI } from 'vs/base/common/uri'; +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'; // Notebook Commands const EXECUTE_NOTEBOOK_COMMAND_ID = 'notebook.execute'; @@ -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,24 +66,62 @@ 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'; export const NOTEBOOK_ACTIONS_CATEGORY = localize('notebookActions.category', "Notebook"); 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 undefined; // {{SQL CARBON EDIT}} strict-null-checks + } + + const activeCell = editor.getActiveCell(); + if (!activeCell) { + return undefined; // {{SQL CARBON EDIT}} strict-null-checks + } + + return { + cell: activeCell, + notebookEditor: editor + }; + } +} + +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_CELL_COMMAND_ID, @@ -96,42 +135,30 @@ registerAction2(class extends Action2 { }, weight: EDITOR_WIDGET_ACTION_WEIGHT }, + 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, title: localize('notebookActions.cancel', "Stop Cell Execution"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/primitive-square' }, + 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 { return context.notebookEditor.cancelNotebookCellExecution(context.cell); } }); @@ -173,7 +200,7 @@ export class CancelCellAction extends MenuItemAction { } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_CELL_SELECT_BELOW, @@ -183,41 +210,34 @@ registerAction2(class extends Action2 { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.Shift | KeyCode.Enter, weight: EDITOR_WIDGET_ACTION_WEIGHT - } + }, + 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); + 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, true); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: EXECUTE_CELL_INSERT_BELOW, @@ -227,72 +247,79 @@ registerAction2(class extends Action2 { when: NOTEBOOK_EDITOR_FOCUSED, primary: KeyMod.Alt | KeyCode.Enter, weight: EDITOR_WIDGET_ACTION_WEIGHT - } + }, + 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, true); + 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, title: localize('notebookActions.executeNotebook', "Execute Notebook"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - 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, title: localize('notebookActions.cancelNotebook', "Cancel Notebook Execution"), category: NOTEBOOK_ACTIONS_CATEGORY, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } - 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, @@ -302,26 +329,17 @@ registerAction2(class extends Action2 { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext), primary: KeyCode.Escape, weight: EDITOR_WIDGET_ACTION_WEIGHT - 5 - } + }, + 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, false); - } + await context.notebookEditor.focusNotebookCell(context.cell, 'container'); } }); @@ -362,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, @@ -373,16 +391,17 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, category: NOTEBOOK_ACTIONS_CATEGORY, + 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, @@ -393,60 +412,30 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, category: NOTEBOOK_ACTIONS_CATEGORY, + 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); } }); export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { // TODO can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? const activeEditorPane = editorService.activeEditorPane as any | undefined; - return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined; -} - -async function runActiveCell(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const editor = getActiveNotebookEditor(editorService); - if (!editor) { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - - editor.executeNotebookCell(activeCell); - return activeCell; + return activeEditorPane?.isNotebookEditor ? activeEditorPane.getControl() : undefined; } 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; @@ -473,43 +462,20 @@ export async function changeCellToKind(kind: CellKind, context: INotebookCellAct newCell.model.language = language; } - notebookEditor.focusNotebookCell(newCell, cell.editState === CellEditState.Editing); + 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 undefined; // {{SQL CARBON EDIT}} strict-null-check - } - - const activeCell = editor.getActiveCell(); - if (!activeCell) { - return undefined; // {{SQL CARBON EDIT}} strict-null-check - } - - return { - cell: activeCell, - notebookEditor: editor - }; -} - -abstract class InsertCellCommand extends Action2 { +abstract class InsertCellCommand extends NotebookAction { constructor( desc: Readonly, private kind: CellKind, @@ -518,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, true); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } @@ -540,12 +499,13 @@ registerAction2(class extends InsertCellCommand { id: INSERT_CODE_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.insertCodeCellAbove', "Insert Code Cell Above"), category: NOTEBOOK_ACTIONS_CATEGORY, - f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, + f1: true }, CellKind.Code, 'above'); @@ -577,12 +537,13 @@ registerAction2(class extends InsertCellCommand { title: localize('notebookActions.insertCodeCellBelow', "Insert Code Cell Below"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/add' }, - f1: true, keybinding: { primary: KeyMod.CtrlCmd | KeyCode.Enter, when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, InputFocusedContext.toNegated()), weight: KeybindingWeight.WorkbenchContrib - } + }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, + f1: true, }, CellKind.Code, 'below'); @@ -596,6 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }, CellKind.Markdown, @@ -627,6 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }, CellKind.Markdown, @@ -634,7 +597,7 @@ registerAction2(class extends InsertCellCommand { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -657,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( { @@ -687,20 +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( { @@ -721,18 +669,12 @@ registerAction2(class extends Action2 { weight: KeybindingWeight.WorkbenchContrib }, icon: { id: 'codicon/trash' }, + 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); @@ -740,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], false); + 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, true); + await context.notebookEditor.focusNotebookCell(newCell, 'editor'); } } } @@ -759,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, false); + await context.notebookEditor.focusNotebookCell(context.cell, 'container'); } } @@ -768,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, false); + await context.notebookEditor.focusNotebookCell(newCell, 'container'); } } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -780,6 +722,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellUp', "Move Cell Up"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-up' }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyCode.UpArrow, @@ -789,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( { @@ -809,6 +745,7 @@ registerAction2(class extends Action2 { title: localize('notebookActions.moveCellDown', "Move Cell Down"), category: NOTEBOOK_ACTIONS_CATEGORY, icon: { id: 'codicon/arrow-down' }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyCode.DownArrow, @@ -818,25 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -846,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()); @@ -861,13 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -877,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()); @@ -899,13 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -915,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() || []; @@ -940,13 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { when: ContextKeyExpr.and(NOTEBOOK_EDITOR_FOCUSED, ContextKeyExpr.not(InputFocusedContextKey)), @@ -956,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() || []; @@ -982,13 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow, @@ -998,25 +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: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true, keybinding: { primary: KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow, @@ -1026,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, @@ -1052,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; @@ -1074,11 +961,11 @@ registerAction2(class extends Action2 { return; } - editor.focusNotebookCell(newCell, true); + await editor.focusNotebookCell(newCell, 'editor'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_CURSOR_UP, @@ -1092,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; @@ -1119,11 +999,60 @@ registerAction2(class extends Action2 { return; } - editor.focusNotebookCell(newCell, true); + 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: NOTEBOOK_EDITOR_FOCUSED, + primary: KeyMod.CtrlCmd | KeyCode.DownArrow, + mac: { primary: KeyMod.WinCtrl | KeyMod.CtrlCmd | KeyCode.DownArrow, }, + weight: KeybindingWeight.WorkbenchContrib + }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, + f1: true + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + const editor = context.notebookEditor; + const activeCell = context.cell; + await editor.focusNotebookCell(activeCell, 'output'); + } +}); + +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: NOTEBOOK_EDITOR_FOCUSED, + primary: KeyMod.CtrlCmd | KeyCode.UpArrow, + mac: { primary: KeyMod.WinCtrl | KeyMod.CtrlCmd | KeyCode.UpArrow, }, + weight: KeybindingWeight.WorkbenchContrib + }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, + f1: true + }); + } + + async runWithContext(accessor: ServicesAccessor, context: INotebookCellActionContext): Promise { + const editor = context.notebookEditor; + const activeCell = context.cell; + await editor.focusNotebookCell(activeCell, 'editor'); + } +}); + + +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_UNDO, @@ -1137,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; @@ -1155,7 +1077,7 @@ registerAction2(class extends Action2 { } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_REDO, @@ -1169,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, @@ -1199,29 +1108,23 @@ registerAction2(class extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyCode.UpArrow }, weight: KeybindingWeight.WorkbenchContrib }, + 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, false); + await editor.focusNotebookCell(firstCell, 'container'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: NOTEBOOK_FOCUS_BOTTOM, @@ -1233,29 +1136,23 @@ registerAction2(class extends Action2 { mac: { primary: KeyMod.CtrlCmd | KeyCode.DownArrow }, weight: KeybindingWeight.WorkbenchContrib }, + 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[editor.viewModel.length - 1]; - editor.focusNotebookCell(firstCell, false); + await editor.focusNotebookCell(firstCell, 'container'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super({ id: CLEAR_CELL_OUTPUTS_COMMAND_ID, @@ -1267,18 +1164,12 @@ registerAction2(class extends Action2 { order: CellToolbarOrder.ClearCellOutput }, icon: { id: 'codicon/clear-all' }, + 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; @@ -1293,24 +1184,18 @@ interface ILanguagePickInput extends IQuickPickItem { description: string; } -export class ChangeCellLanguageAction extends Action2 { +export class ChangeCellLanguageAction extends NotebookAction { constructor() { super({ id: CHANGE_CELL_LANGUAGE, title: localize('changeLanguage', 'Change Cell Language'), category: NOTEBOOK_ACTIONS_CATEGORY, + 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 { this.showLanguagePicker(accessor, context); } @@ -1366,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, true); + 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); @@ -1397,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, @@ -1410,18 +1295,12 @@ registerAction2(class extends Action2 { order: 0 }, icon: { id: 'codicon/clear-all' }, + 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; @@ -1435,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], true); + await context.notebookEditor.focusNotebookCell(newCells[newCells.length - 1], 'editor'); } } } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { @@ -1453,18 +1332,12 @@ registerAction2(class extends Action2 { order: CellToolbarOrder.SplitCell }, icon: { id: 'codicon/split-vertical' }, + 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) { return splitCell(context); } }); @@ -1473,53 +1346,64 @@ 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, true); + await context.notebookEditor.focusNotebookCell(cell, 'editor'); } } -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: JOIN_CELL_ABOVE_COMMAND_ID, title: localize('notebookActions.joinCellAbove', "Join with Previous Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + 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) { return joinCells(context, 'above'); } }); -registerAction2(class extends Action2 { +registerAction2(class extends NotebookAction { constructor() { super( { id: JOIN_CELL_BELOW_COMMAND_ID, title: localize('notebookActions.joinCellBelow', "Join with Next Cell"), category: NOTEBOOK_ACTIONS_CATEGORY, + 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) { 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/fold/folding.ts b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts index 75c64346a1..5e75afe012 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/fold/folding.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; -import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { INotebookEditor, INotebookEditorMouseEvent, ICellRange, INotebookEditorContribution, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_IS_ACTIVE_EDITOR } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import * as DOM from 'vs/base/browser/dom'; import { CellFoldingState, FoldingModel } from 'vs/workbench/contrib/notebook/browser/contrib/fold/foldingModel'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; @@ -141,6 +141,7 @@ registerAction2(class extends Action2 { primary: KeyCode.LeftArrow, weight: KeybindingWeight.WorkbenchContrib }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } @@ -179,6 +180,7 @@ registerAction2(class extends Action2 { primary: KeyCode.RightArrow, weight: KeybindingWeight.WorkbenchContrib }, + precondition: NOTEBOOK_IS_ACTIVE_EDITOR, f1: true }); } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts new file mode 100644 index 0000000000..fd75a74238 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider.ts @@ -0,0 +1,47 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { URI } from 'vs/base/common/uri'; +import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; +import { IMarkerListProvider, MarkerList, IMarkerNavigationService } from 'vs/editor/contrib/gotoError/markerNavigationService'; +import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { IMarkerService } from 'vs/platform/markers/common/markers'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +class MarkerListProvider implements IMarkerListProvider { + + private readonly _dispoables: IDisposable; + + constructor( + @IMarkerService private readonly _markerService: IMarkerService, + @IMarkerNavigationService markerNavigation: IMarkerNavigationService, + ) { + this._dispoables = markerNavigation.registerProvider(this); + } + + dispose() { + this._dispoables.dispose(); + } + + getMarkerList(resource: URI | undefined): MarkerList | undefined { + if (!resource) { + return undefined; + } + const data = CellUri.parse(resource); + if (!data) { + return undefined; + } + return new MarkerList(uri => { + const otherData = CellUri.parse(uri); + return otherData?.notebook.toString() === data.notebook.toString(); + }, this._markerService); + } +} + +Registry + .as(WorkbenchExtensions.Workbench) + .registerWorkbenchContribution(MarkerListProvider, LifecyclePhase.Ready); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts new file mode 100644 index 0000000000..ff9b3876b9 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/contrib/status/editorStatus.ts @@ -0,0 +1,82 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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 { 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, 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'; + + +registerAction2(class extends Action2 { + constructor() { + super({ + id: 'notebook.selectKernel', + 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 + }); + } + + async run(accessor: ServicesAccessor, context?: INotebookCellActionContext): Promise { + const editorService = accessor.get(IEditorService); + const notebookService = accessor.get(INotebookService); + const quickInputService = accessor.get(IQuickInputService); + + const activeEditorPane = editorService.activeEditorPane as any | undefined; + if (!activeEditorPane?.isNotebookEditor) { + return; + } + const editor = activeEditorPane.getControl() as INotebookEditor; + const activeKernel = editor.activeKernel; + + const availableKernels = notebookService.getContributedNotebookKernels(editor.viewModel!.viewType, editor.viewModel!.uri); + const picks: QuickPickInput[] = availableKernels.map((a) => { + return { + id: a.id, + label: a.label, + picked: a.id === activeKernel?.id, + description: a.extension.value + (a.id === activeKernel?.id + ? nls.localize('currentActiveKernel', " (Currently Active)") + : ''), + run: () => { + editor.activeKernel = a; + } + }; + }); + + 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 82f1ee3689..41a071a847 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts @@ -6,7 +6,7 @@ import { TableOfContentsProviderRegistry, ITableOfContentsProvider, ITableOfContentsEntry } from 'vs/workbench/contrib/codeEditor/browser/quickaccess/gotoSymbolQuickAccess'; import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { CellKind } from 'vs/workbench/contrib/notebook/common/notebookCommon'; - +import { Codicon } from 'vs/base/common/codicons'; TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements ITableOfContentsProvider { async provideTableOfContents(editor: NotebookEditor) { @@ -14,18 +14,29 @@ TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements return undefined; } // return an entry per markdown header + const notebookWidget = editor.getControl(); const result: ITableOfContentsEntry[] = []; - for (let cell of editor.viewModel.viewCells) { - if (cell.cellKind === CellKind.Code) { - continue; - } + for (const cell of editor.viewModel.viewCells) { const content = cell.getText(); - const matches = content.match(/^[ \t]*(\#+)(.+)$/gm); + const regexp = cell.cellKind === CellKind.Markdown + ? /^[ \t]*(\#+)(.+)$/gm // md: header + : /^.*\w+.*\w*$/m; // code: none empty line + + const matches = content.match(regexp); if (matches && matches.length) { for (let j = 0; j < matches.length; j++) { result.push({ + icon: cell.cellKind === CellKind.Markdown ? Codicon.markdown : Codicon.code, label: matches[j].replace(/^[ \t]*(\#+)/, ''), - reveal: () => editor.revealInCenterIfOutsideViewport(cell) + pick() { + notebookWidget?.revealInCenterIfOutsideViewport(cell); + notebookWidget?.selectElement(cell); + notebookWidget?.focusNotebookCell(cell, cell.cellKind === CellKind.Markdown ? 'container' : 'editor'); + }, + preview() { + 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 5896d6add1..10fb06c197 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 Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.editor > .content .notebook-editor { +.monaco-workbench .notebookOverlay.notebook-editor { box-sizing: border-box; line-height: 22px; user-select: initial; @@ -17,44 +17,40 @@ white-space: initial; } -/* .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element { - overflow: visible !important; -} */ - -.monaco-workbench .part.editor > .content .notebook-editor .simple-fr-find-part-wrapper.visible { +.notebookOverlay .simple-fr-find-part-wrapper.visible { z-index: 100; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div { +.notebookOverlay .cell-list-container .overflowingContentWidgets > div { z-index: 600 !important; /* @rebornix: larger than the editor title bar */ } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows { +.notebookOverlay .cell-list-container .monaco-list-rows { min-height: 100%; overflow: visible !important; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container { +.notebookOverlay .cell-list-container { position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor.global-drag-active .webview { +.notebookOverlay.global-drag-active .webview { pointer-events: none; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .webview-cover { +.notebookOverlay .cell-list-container .webview-cover { position: absolute; top: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .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 { cursor: default; overflow: visible !important; width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { +.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; @@ -62,55 +58,55 @@ padding-top: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { display: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { +.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 */ } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { display: flex; } -.monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets { +.notebookOverlay .notebook-content-widgets { position: absolute; top: 0; left: 0; width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor .output { +.notebookOverlay .output { padding-left: 8px; padding-right: 8px; user-select: text; @@ -119,22 +115,28 @@ box-sizing: border-box; } -.monaco-workbench .part.editor > .content .notebook-editor .output p { +.notebookOverlay .output p { white-space: initial; overflow-x: auto; margin: 0px; } -.monaco-workbench .part.editor > .content .notebook-editor .output > div.foreground { +.notebookOverlay .output > div.foreground { padding: 8px; box-sizing: border-box; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .output .multi-mimetype-output { +.notebookOverlay .output > div.foreground .output-stream, +.notebookOverlay .output > div.foreground .output-plaintext { + font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; + white-space: pre-wrap; +} + +.notebookOverlay .cell-drag-image .output .multi-mimetype-output { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor .output .multi-mimetype-output { +.notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; left: -32px; @@ -143,27 +145,27 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error_message { +.notebookOverlay .output .error_message { color: red; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error > div { +.notebookOverlay .output .error > div { white-space: normal; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error pre.traceback { +.notebookOverlay .output .error pre.traceback { margin: 8px 0; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error .traceback > span { +.notebookOverlay .output .error .traceback > span { display: block; } -.monaco-workbench .part.editor > .content .notebook-editor .output .display img { +.notebookOverlay .output .display img { max-width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { position: absolute; left: 0; top: 28px; @@ -174,33 +176,29 @@ } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { +.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 { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { +.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 { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { +.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 { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { +.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 { cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element { - padding-top: 16px; -} - -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { visibility: hidden; display: inline-block; position: absolute; @@ -211,7 +209,7 @@ z-index: 30; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { +.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; @@ -219,55 +217,59 @@ margin: 1px 2px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { +.notebookOverlay .cell-statusbar-container { height: 21px; font-size: 12px; display: flex; position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-left { +.notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-right { +.notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker { +.notebookOverlay .cell-statusbar-container .cell-language-picker { padding: 0px 6px; cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.6); } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.9); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.monaco-workbench.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.15); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:active { +.monaco-workbench.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:active { background-color: rgba(255, 255, 255, 0.2); } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-message { +.notebookOverlay .cell-statusbar-container .cell-run-duration { + margin-right: 8px; +} + +.notebookOverlay .cell-statusbar-container .cell-status-message { display: flex; align-items: center; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status { +.notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; align-items: center; @@ -276,19 +278,19 @@ margin-right: 2px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .codicon { +.notebookOverlay .cell-statusbar-container .codicon { font-size: 14px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { +.notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: #89D185; } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { +.monaco-workbench.vs .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: #388A34; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { +.notebookOverlay .cell-status-placeholder { position: absolute; left: 18px; color: #ccc9; @@ -298,32 +300,32 @@ top: 0px; } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { +.monaco-workbench.vs .notebookOverlay .cell-status-placeholder { color: #616161e6; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { +.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 { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { +.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; @@ -337,33 +339,42 @@ opacity: .6; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, -.monaco-workbench .part.editor > .content .notebook-editor > .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 .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { +.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 { visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-part { +.notebookOverlay .cell .cell-editor-part { position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-progress-container { +.notebookOverlay .cell .monaco-progress-container { top: -5px; + + position: absolute; + left: 0; + z-index: 5; + height: 2px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .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 .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { +.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 { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list:not(.element-focused):focus:before { +.notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .notebook-cell-focus-indicator { +.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { display: block; content: ' '; position: absolute; @@ -374,16 +385,29 @@ top: 22px; bottom: 36px; visibility: hidden; + opacity: 0.6; +} + +.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator:hover { + cursor: grab; +} + +.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator .codicon:hover { cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { +.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 { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-editor-focus .cell-editor-part:before { +.notebookOverlay .monaco-list:focus .monaco-list-row .notebook-cell-focus-indicator, +.notebookOverlay .monaco-list:focus-within .monaco-list-row .notebook-cell-focus-indicator { + opacity: 1; +} + +.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { z-index: 20; content: ""; right: 0px; @@ -396,31 +420,25 @@ pointer-events: none; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator-top { +.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { top: -15px; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator-bottom { - bottom: 13px; -} - -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragover-top .cell-insertion-indicator-top, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragover-bottom .cell-insertion-indicator-bottom { - opacity: 1; -} - -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator { - opacity: 0; - transition: opacity 0.2s ease-in-out; +.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { position: absolute; height: 2px; + left: 0px; + right: 0px; + opacity: 0; + /* transition: opacity 0.2s ease-in-out; */ + z-index: 10; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragging { - opacity: 0.5; +.notebookOverlay > .cell-list-container > .monaco-list .monaco-list-row.cell-dragging { + opacity: 0.5 !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { +.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; @@ -429,28 +447,28 @@ padding: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor.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 .part.editor > .content .notebook-editor.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.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 { opacity: 1; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { +.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; @@ -461,7 +479,7 @@ font-size: 12px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { +.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; @@ -470,34 +488,34 @@ /* markdown */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown img { +.notebookOverlay .cell.markdown img { max-width: 100%; max-height: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { +.notebookOverlay .cell.markdown a { text-decoration: none; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:hover { +.notebookOverlay .cell.markdown a:hover { text-decoration: underline; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown input:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown select:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown textarea:focus { +.notebookOverlay .cell.markdown a:focus, +.notebookOverlay .cell.markdown input:focus, +.notebookOverlay .cell.markdown select:focus, +.notebookOverlay .cell.markdown textarea:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr { +.notebookOverlay .cell.markdown hr { border: 0; height: 2px; border-bottom: 2px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.notebookOverlay .cell.markdown h1 { padding-bottom: 0.3em; line-height: 1.2; border-bottom-width: 1px; @@ -505,156 +523,159 @@ border-color: rgba(255, 255, 255, 0.18); } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.monaco-workbench.vs .notebookOverlay .cell.markdown h1 { border-color: rgba(0, 0, 0, 0.18); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h2, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h3 { +.notebookOverlay .cell.markdown h1, +.notebookOverlay .cell.markdown h2, +.notebookOverlay .cell.markdown h3 { font-weight: normal; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div { +.notebookOverlay .cell.markdown div { width: 100%; } /* Adjust margin of first item in markdown cell */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:first-child { +.notebookOverlay .cell.markdown div *:first-child { margin-top: 4px; } /* h1 tags don't need top margin */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div h1:first-child { +.notebookOverlay .cell.markdown div h1:first-child { margin-top: 0; } /* Removes bottom margin when only one item exists in markdown cell */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:only-child, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:last-child { +.notebookOverlay .cell.markdown div *:only-child, +.notebookOverlay .cell.markdown div *:last-child { margin-bottom: 0; } /* makes all markdown cells consistent */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div { +.notebookOverlay .cell.markdown div { min-height: 32px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table { +.notebookOverlay .cell.markdown table { border-collapse: collapse; border-spacing: 0; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table td { +.notebookOverlay .cell.markdown table th, +.notebookOverlay .cell.markdown table td { border: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.notebookOverlay .cell.markdown table > thead > tr > th { text-align: left; border-bottom: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > td, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.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 { padding: 5px 10px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr + tr > td { +.notebookOverlay .cell.markdown table > tbody > tr + tr > td { border-top: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown blockquote { +.notebookOverlay .cell.markdown blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left-width: 5px; border-left-style: solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown code { +.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; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown body.wordWrap pre { +.notebookOverlay .cell.markdown body.wordWrap pre { white-space: pre-wrap; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre:not(.hljs), -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre.hljs code > div { +.notebookOverlay .cell.markdown pre:not(.hljs), +.notebookOverlay .cell.markdown pre.hljs code > div { padding: 16px; border-radius: 3px; overflow: auto; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre code { +.notebookOverlay .cell.markdown pre code { color: var(--vscode-editor-foreground); tab-size: 4; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block { +.notebookOverlay .cell.markdown .latex-block { display: block; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex { +.notebookOverlay .cell.markdown .latex { vertical-align: middle; display: inline-block; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { +.notebookOverlay .cell.markdown .latex img, +.notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(0) } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { +.monaco-workbench.vs-dark .notebookOverlay .cell.markdown .latex img, +.monaco-workbench.vs-dark .notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(1) } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container .notebook-folding-indicator { +.notebookOverlay > .cell-list-container .notebook-folding-indicator { position: absolute; top: 8px; left: 6px; - cursor: pointer; +} + +.notebookOverlay > .cell-list-container .notebook-folding-indicator .codicon { + visibility: visible; } /** Theming */ -/* .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +/* .notebookOverlay .cell.markdown pre { background-color: rgba(220, 220, 220, 0.4); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +.monaco-workbench.vs-dark .notebookOverlay .cell.markdown pre { background-color: rgba(10, 10, 10, 0.4); } -.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +.monaco-workbench.hc-black .notebookOverlay .cell.markdown pre { background-color: rgb(0, 0, 0); } -.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.monaco-workbench.hc-black .notebookOverlay .cell.markdown h1 { border-color: rgb(0, 0, 0); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.monaco-workbench.vs-dark .notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(255, 255, 255, 0.18); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.notebookOverlay .cell.markdown h1, +.notebookOverlay .cell.markdown hr, +.notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.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 { 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 0b657bf78c..cedbd1cce5 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IDisposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ResourceMap } from 'vs/base/common/map'; import { parse } from 'vs/base/common/marshalling'; import { basename, isEqual } from 'vs/base/common/resources'; @@ -24,18 +24,22 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorInput, Extensions as EditorInputExtensions, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { NotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; import { CellKind, CellUri } 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 @@ -44,6 +48,8 @@ import 'vs/workbench/contrib/notebook/browser/contrib/find/findController'; import 'vs/workbench/contrib/notebook/browser/contrib/fold/folding'; import 'vs/workbench/contrib/notebook/browser/contrib/format/formatting'; import 'vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider'; +import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; +import 'vs/workbench/contrib/notebook/browser/contrib/status/editorStatus'; // Output renderers registration @@ -76,10 +82,11 @@ Registry.as(EditorInputExtensions.EditorInputFactor resource: input.resource, name: input.name, viewType: input.viewType, + group: input.group }); } deserialize(instantiationService: IInstantiationService, raw: string) { - type Data = { resource: URI, name: string, viewType: string }; + type Data = { resource: URI, name: string, viewType: string, group: number }; const data = parse(raw); if (!data) { return undefined; @@ -88,7 +95,15 @@ Registry.as(EditorInputExtensions.EditorInputFactor if (!data || !URI.isUri(resource) || typeof name !== 'string' || typeof viewType !== 'string') { return undefined; } - return 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); + } + + return input; } } ); @@ -97,17 +112,41 @@ function getFirstNotebookInfo(notebookService: INotebookService, uri: URI): Note return notebookService.getContributedNotebookProviders(uri)[0]; } -export class NotebookContribution implements IWorkbenchContribution { +export class NotebookContribution extends Disposable implements IWorkbenchContribution { private _resourceMapping = new ResourceMap(); constructor( @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 ) { - this.editorService.overrideOpenEditor({ + 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.scheme + ':' + data.notebook.fsPath; + + // const documentUri = this._resourceMapping.get(data.notebook)?.resource; + // if (documentUri) { + // return documentUri.toString(); + // } + + // return null; + } + })); + + this._register(this.editorService.overrideOpenEditor({ getEditorOverrides: (resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined) => { const currentEditorForResource = group?.editors.find(editor => isEqual(editor.resource, resource)); @@ -125,15 +164,43 @@ export class NotebookContribution implements IWorkbenchContribution { }; }); }, - 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.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!); + 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(() => { + 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); } - }); + })); + + this._register(this.editorService.onDidCloseEditor(({ editor }) => { + if (!(editor instanceof NotebookEditorInput)) { + return; + } + + if (!this.editorService.editors.some(other => ( + other.resource === editor.resource + && other instanceof NotebookEditorInput + && other.viewType === editor.viewType + ))) { + editor.clearTextModel(); + } + + editor.dispose(); + })); } getUserAssociatedEditors(resource: URI) { @@ -155,7 +222,36 @@ export class NotebookContribution implements IWorkbenchContribution { 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 { + 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) + }; + } + } + let resource = originalInput.resource; if (!resource) { return undefined; @@ -175,13 +271,23 @@ export class NotebookContribution implements IWorkbenchContribution { // 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)) { const input = this._resourceMapping.get(resource); 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); } } @@ -196,9 +302,11 @@ export class NotebookContribution implements IWorkbenchContribution { 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); } + + input.updateGroup(group.id); return { override: this.editorService.openEditor(input, new NotebookEditorOptions({ ...options, forceReload: true, cellOptions: { resource, options } }), group) }; } } @@ -210,10 +318,20 @@ export class NotebookContribution implements IWorkbenchContribution { 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) }; } } @@ -227,7 +345,7 @@ class CellContentProvider implements ITextModelContentProvider { @IModeService private readonly _modeService: IModeService, @INotebookService private readonly _notebookService: INotebookService, ) { - this._registration = textModelService.registerTextModelContentProvider('vscode-notebook', this); + this._registration = textModelService.registerTextModelContentProvider(CellUri.scheme, this); } dispose(): void { @@ -249,7 +367,7 @@ class CellContentProvider implements ITextModelContentProvider { return null; } - const editorModel = await this._notebookService.modelManager.get(data.notebook); + const editorModel = this._notebookService.modelManager.get(data.notebook); if (!editorModel) { return null; } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts index 8ceb73551b..6a38702042 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookBrowser.ts @@ -15,16 +15,17 @@ import { ScrollEvent } from 'vs/base/common/scrollable'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { Range } from 'vs/editor/common/core/range'; import { IPosition } from 'vs/editor/common/core/position'; -import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { Range } from 'vs/editor/common/core/range'; import { FindMatch, IReadonlyTextBuffer, ITextModel } from 'vs/editor/common/model'; +import { ContextKeyExpr, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { CellLanguageStatusBarItem } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +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 } 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 { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; export const KEYBINDING_CONTEXT_NOTEBOOK_FIND_WIDGET_FOCUSED = new RawContextKey('notebookFindWidgetFocused', false); @@ -46,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; @@ -80,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; } @@ -88,6 +94,7 @@ export interface MarkdownCellLayoutChangeEvent { font?: BareFontInfo; outerWidth?: number; totalHeight?: number; + editorHeight?: boolean; } export interface ICellViewModel { @@ -100,7 +107,6 @@ export interface ICellViewModel { language: string; cellKind: CellKind; editState: CellEditState; - readonly runState: CellRunState; currentTokenSource: CancellationTokenSource | undefined; focusMode: CellFocusMode; getText(): string; @@ -136,7 +142,7 @@ export interface INotebookEditorContribution { restoreViewState?(state: any): void; } -export interface INotebookEditor { +export interface INotebookEditor extends IEditor { /** * Notebook view model attached to the current editor @@ -147,9 +153,13 @@ export interface INotebookEditor { * 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; @@ -158,6 +168,8 @@ export interface INotebookEditor { */ focus(): void; + hasFocus(): boolean; + /** * Select & focus cell */ @@ -223,7 +235,7 @@ export interface INotebookEditor { /** * Focus the container of a cell (the monaco editor inside is not focused). */ - focusNotebookCell(cell: ICellViewModel, focusEditor: boolean): void; + focusNotebookCell(cell: ICellViewModel, focus: 'editor' | 'container' | 'output'): void; /** * Execute the given notebook cell @@ -258,12 +270,12 @@ export interface INotebookEditor { /** * 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. @@ -354,6 +366,9 @@ export interface INotebookEditor { } export interface INotebookCellList { + readonly contextKeyService: IContextKeyService; + elementAt(position: number): ICellViewModel | undefined; + elementHeight(element: ICellViewModel): number; onWillScroll: Event; onDidChangeFocus: Event>; onDidChangeContentHeight: Event; @@ -362,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; @@ -402,6 +417,7 @@ export interface INotebookCellList { } export interface BaseCellRenderTemplate { + contextKeyService: IContextKeyService; container: HTMLElement; cellContainer: HTMLElement; toolbar: ToolBar; @@ -431,6 +447,7 @@ export interface CodeCellRenderTemplate extends BaseCellRenderTemplate { outputContainer: HTMLElement; editor: ICodeEditor; progressBar: ProgressBar; + timer: TimerRenderer; } export interface IOutputTransformContribution { @@ -439,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 { @@ -457,11 +474,6 @@ export enum CellRevealPosition { Center } -export enum CellRunState { - Idle, - Running -} - export enum CellEditState { /** * Default state. @@ -493,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 c2030ad417..415c58ff21 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -3,144 +3,46 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import 'vs/css!./media/notebook'; -import { getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; -import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Color, RGBA } from 'vs/base/common/color'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore, MutableDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { Range } from 'vs/editor/common/core/range'; -import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; -import * as nls from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { 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'; -import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorOptions, IEditorCloseEvent, IEditorMemento } from 'vs/workbench/common/editor'; -import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, EDITOR_BOTTOM_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, INotebookCellList, INotebookEditor, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, INotebookEditorContribution, NOTEBOOK_EDITOR_RUNNABLE, IEditableCellViewModel } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; -import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate, CellDragAndDropController } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, CellUri, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; +import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; -import { onUnexpectedError } from 'vs/base/common/errors'; -import { IPosition, Position } from 'vs/editor/common/core/position'; -import { IReadonlyTextBuffer } from 'vs/editor/common/model'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; +import { NotebookRegistry } from 'vs/workbench/contrib/notebook/browser/notebookRegistry'; -const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } -} - -export class NotebookCodeEditors implements ICompositeCodeEditor { - - private readonly _disposables = new DisposableStore(); - private readonly _onDidChangeActiveEditor = new Emitter(); - readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; - - constructor( - private _list: INotebookCellList, - private _renderedEditors: Map - ) { - _list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables); - } - - dispose(): void { - this._onDidChangeActiveEditor.dispose(); - this._disposables.dispose(); - } - - get activeCodeEditor(): IEditor | undefined { - const [focused] = this._list.getFocusedElements(); - return this._renderedEditors.get(focused); - } -} - -export class NotebookEditor extends BaseEditor implements INotebookEditor { +export class NotebookEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.notebook'; - private _rootElement!: HTMLElement; - private body!: HTMLElement; - private titleBar: HTMLElement | null = null; - private webview: BackLayerWebView | null = null; - private webviewTransparentCover: HTMLElement | null = null; - private list: INotebookCellList | undefined; - private control: ICompositeCodeEditor | undefined; - private renderedEditors: Map = new Map(); - private eventDispatcher: NotebookEventDispatcher | undefined; - private notebookViewModel: NotebookViewModel | undefined; - private localStore: DisposableStore = this._register(new DisposableStore()); private editorMemento: IEditorMemento; private readonly groupListener = this._register(new MutableDisposable()); - private fontInfo: BareFontInfo | undefined; + private _widget?: NotebookEditorWidget; + private _rootElement!: HTMLElement; private dimension: DOM.Dimension | null = null; - private editorFocus: IContextKey | null = null; - private editorEditable: IContextKey | null = null; - private editorRunnable: IContextKey | null = null; - private editorExecutingNotebook: IContextKey | null = null; - private outputRenderer: OutputRenderer; - protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; - private scrollBeyondLastLine: boolean; + private _widgetDisposableStore: DisposableStore = new DisposableStore(); + private readonly _onDidFocusWidget = this._register(new Emitter()); + public get onDidFocus(): Event { return this._onDidFocusWidget.event; } constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, - @INotebookService private notebookService: INotebookService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { + @IEditorGroupsService editorGroupService: IEditorGroupsService) { super(NotebookEditor.ID, telemetryService, themeService, storageService); + // this._widget = this.instantiationService.createInstance(NotebookEditorWidget); this.editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this.outputRenderer = new OutputRenderer(this, this.instantiationService); - this._contributions = {}; - this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - - this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { - this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - if (this.dimension) { - this.layout(this.dimension); - } - } - }); } private readonly _onDidChangeModel = new Emitter(); @@ -148,12 +50,14 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { set viewModel(newModel: NotebookViewModel | undefined) { - this.notebookViewModel = newModel; - this._onDidChangeModel.fire(); + if (this._widget) { + this._widget.viewModel = newModel; + this._onDidChangeModel.fire(); + } } get viewModel() { - return this.notebookViewModel; + return this._widget?.viewModel; } get minimumWidth(): number { return 375; } @@ -171,185 +75,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return true; } - private updateEditorFocus() { - // Note - focus going to the webview will fire 'blur', but the webview element will be - // a descendent of the notebook editor root. - this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.getDomNode())); - } - protected createEditor(parent: HTMLElement): void { - this._rootElement = DOM.append(parent, $('.notebook-editor')); - this.createBody(this._rootElement); - this.generateFontInfo(); - this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); - this.editorFocus.set(true); - this._register(this.onDidFocus(() => this.updateEditorFocus())); - this._register(this.onDidBlur(() => this.updateEditorFocus())); - - this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); - this.editorEditable.set(true); - this.editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); - this.editorRunnable.set(true); - this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); - - const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); - - for (const desc of contributions) { - try { - const contribution = this.instantiationService.createInstance(desc.ctor, this); - this._contributions[desc.id] = contribution; - } catch (err) { - onUnexpectedError(err); - } - } - } - - populateEditorTitlebar() { - for (let element: HTMLElement | null = this._rootElement.parentElement; element; element = element.parentElement) { - if (DOM.hasClass(element, 'editor-group-container')) { - // elemnet is editor group container - for (let i = 0; i < element.childElementCount; i++) { - const child = element.childNodes.item(i) as HTMLElement; - - if (DOM.hasClass(child, 'title')) { - this.titleBar = child; - break; - } - } - break; - } - } - } - - clearEditorTitlebarZindex() { - if (this.titleBar === null) { - this.populateEditorTitlebar(); - } - - if (this.titleBar) { - this.titleBar.style.zIndex = 'auto'; - } - } - - increaseEditorTitlebarZindex() { - if (this.titleBar === null) { - this.populateEditorTitlebar(); - } - - if (this.titleBar) { - this.titleBar.style.zIndex = '500'; - } - } - - private generateFontInfo(): void { - const editorOptions = this.configurationService.getValue('editor'); - this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); - } - - private createBody(parent: HTMLElement): void { - this.body = document.createElement('div'); - DOM.addClass(this.body, 'cell-list-container'); - this.createCellList(); - DOM.append(parent, this.body); - } - - private createCellList(): void { - DOM.addClass(this.body, 'cell-list-container'); - - const dndController = this._register(new CellDragAndDropController(this)); - const renders = [ - this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, dndController), - this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this, dndController, this.renderedEditors), - ]; - - this.list = this.instantiationService.createInstance( - NotebookCellList, - 'NotebookCellList', - this.body, - this.instantiationService.createInstance(NotebookCellListDelegate), - renders, - this.contextKeyService, - { - setRowLineHeight: false, - setRowHeight: false, - supportDynamicHeights: true, - horizontalScrolling: false, - keyboardSupport: false, - mouseSupport: true, - multipleSelectionSupport: false, - enableKeyboardNavigation: true, - additionalScrollHeight: 0, - transformOptimization: false, - styleController: (_suffix: string) => { return this.list!; }, - overrideStyles: { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listHoverOutline: focusBorder, - listFocusOutline: focusBorder, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground, - }, - accessibilityProvider: { - getAriaLabel() { return null; }, - getWidgetAriaLabel() { - return nls.localize('notebookTreeAriaLabel', "Notebook"); - } - } - }, - ); - - this.control = new NotebookCodeEditors(this.list, this.renderedEditors); - 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); - - this._register(this.list); - this._register(combinedDisposable(...renders)); - - // transparent cover - this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); - this.webviewTransparentCover.style.display = 'none'; - - this._register(DOM.addStandardDisposableGenericMouseDownListner(this._rootElement, (e: StandardMouseEvent) => { - if (DOM.hasClass(e.target, 'slider') && this.webviewTransparentCover) { - this.webviewTransparentCover.style.display = 'block'; - } - })); - - this._register(DOM.addStandardDisposableGenericMouseUpListner(this._rootElement, (e: StandardMouseEvent) => { - if (this.webviewTransparentCover) { - // no matter when - this.webviewTransparentCover.style.display = 'none'; - } - })); - - this._register(this.list.onMouseDown(e => { - if (e.element) { - this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); - } - })); - - this._register(this.list.onMouseUp(e => { - if (e.element) { - this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); - } - })); + 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())); } getDomNode() { @@ -357,21 +88,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } getControl() { - return this.control; - } - - getInnerWebview(): Webview | undefined { - return this.webview?.webview; - } - - setVisible(visible: boolean, group?: IEditorGroup): void { - if (visible) { - this.increaseEditorTitlebarZindex(); - } else { - this.clearEditorTitlebarZindex(); - } - - super.setVisible(visible, group); + return this._widget; } onWillHide() { @@ -379,16 +96,8 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.saveEditorViewState(this.input); } - this.editorFocus?.set(false); - if (this.webview) { - this.localStore.clear(); - this.list?.rowsContainer.removeChild(this.webview?.element); - this.webview?.dispose(); - this.webview = null; - } - - this.list?.clear(); - super.onHide(); + this._widget?.onWillHide(); + super.onWillHide(); } setEditorVisible(visible: boolean, group: IEditorGroup | undefined): void { @@ -403,244 +112,77 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } if (editor === this.input) { - this.clearEditorTitlebarZindex(); this.saveEditorViewState(editor); } } focus() { super.focus(); - this.editorFocus?.set(true); - this.list?.domFocus(); + 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(); - if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model.notebook) || this.webview === null) { - this.detachModel(); - await this.attachModel(input, model); - } - - // reveal cell if editor options tell to do so - if (options instanceof NotebookEditorOptions && options.cellOptions) { - const cellOptions = options.cellOptions; - const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); - if (cell) { - this.selectElement(cell); - this.revealInCenterIfOutsideViewport(cell); - const editor = this.renderedEditors.get(cell)!; - if (editor) { - if (cellOptions.options?.selection) { - const { selection } = cellOptions.options; - editor.setSelection({ - ...selection, - endLineNumber: selection.endLineNumber || selection.startLineNumber, - endColumn: selection.endColumn || selection.startColumn - }); - } - if (!cellOptions.options?.preserveFocus) { - editor.focus(); - } - } + // 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); + this._widgetDisposableStore.add(this._widget.onDidFocus(() => this._onDidFocusWidget.fire())); } clearInput(): void { + const existingEditorWidgetForInput = NotebookRegistry.getNotebookEditorWidget(this.input as NotebookEditorInput); + existingEditorWidgetForInput?.onWillHide(); + this._widget = undefined; super.clearInput(); } - private detachModel() { - this.localStore.clear(); - this.list?.detachViewModel(); - this.viewModel?.dispose(); - // avoid event - this.notebookViewModel = undefined; - this.webview?.clearInsets(); - this.webview?.clearPreloadsCache(); - this.list?.clear(); - } - - private updateForMetadata(): void { - this.editorEditable?.set(!!this.viewModel!.metadata?.editable); - this.editorRunnable?.set(!!this.viewModel!.metadata?.runnable); - DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); - } - - private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) { - if (!this.webview) { - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); - } - - await this.webview.waitForInitialization(); - - this.eventDispatcher = new NotebookEventDispatcher(); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model.notebook, this.eventDispatcher, this.getLayoutInfo()); - this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); - - this.updateForMetadata(); - this.localStore.add(this.eventDispatcher.onDidChangeMetadata((e) => { - this.updateForMetadata(); - })); - - // restore view states, including contributions - const viewState = this.loadTextEditorViewState(input); - - { - // restore view state - this.viewModel.restoreEditorViewState(viewState); - - // contribution state restore - - const contributionsState = viewState?.contributionsState || {}; - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const id = keys[i]; - const contribution = this._contributions[id]; - if (typeof contribution.restoreViewState === 'function') { - contribution.restoreViewState(contributionsState[id]); - } - } - } - - this.webview?.updateRendererPreloads(this.viewModel.renderers); - - this.localStore.add(this.list!.onWillScroll(e => { - this.webview!.updateViewScrollTop(-e.scrollTop, []); - this.webviewTransparentCover!.style.top = `${e.scrollTop}px`; - })); - - this.localStore.add(this.list!.onDidChangeContentHeight(() => { - DOM.scheduleAtNextAnimationFrame(() => { - const scrollTop = this.list?.scrollTop || 0; - const scrollHeight = this.list?.scrollHeight || 0; - this.webview!.element.style.height = `${scrollHeight}px`; - - if (this.webview?.insetMapping) { - let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; - let removedItems: IOutput[] = []; - this.webview?.insetMapping.forEach((value, key) => { - const cell = value.cell; - const viewIndex = this.list?.getViewIndex(cell); - - if (viewIndex === undefined) { - return; - } - - if (cell.outputs.indexOf(key) < 0) { - // output is already gone - removedItems.push(key); - } - - const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { - updateItems.push({ - cell: cell, - output: key, - cellTop: cellTop - }); - } - }); - - removedItems.forEach(output => this.webview?.removeInset(output)); - - if (updateItems.length) { - this.webview?.updateViewScrollTop(-scrollTop, updateItems); - } - } - }); - })); - - this.list!.attachViewModel(this.viewModel); - this.localStore.add(this.list!.onDidRemoveOutput(output => { - this.removeInset(output); - })); - this.localStore.add(this.list!.onDidHideOutput(output => { - this.hideInset(output); - })); - - this.list!.layout(); - - // restore list state at last, it must be after list layout - this.restoreListViewState(viewState); - } - - private restoreListViewState(viewState: INotebookEditorViewState | undefined): void { - if (viewState?.scrollPosition !== undefined) { - this.list!.scrollTop = viewState!.scrollPosition.top; - this.list!.scrollLeft = viewState!.scrollPosition.left; - } else { - this.list!.scrollTop = 0; - this.list!.scrollLeft = 0; - } - - const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; - if (focusIdx < this.list!.length) { - this.list!.setFocus([focusIdx]); - this.list!.setSelection([focusIdx]); - } else if (this.list!.length > 0) { - this.list!.setFocus([0]); - } - - if (viewState?.editorFocused) { - this.list?.focusView(); - const cell = this.notebookViewModel?.viewCells[focusIdx]; - if (cell) { - cell.focusMode = CellFocusMode.Editor; - } - } - } - private saveEditorViewState(input: NotebookEditorInput): void { - if (this.group && this.notebookViewModel) { - const state = this.notebookViewModel.geteEditorViewState(); - if (this.list) { - state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; - let cellHeights: { [key: number]: number } = {}; - for (let i = 0; i < this.viewModel!.length; i++) { - const elm = this.viewModel!.viewCells[i] as CellViewModel; - if (elm.cellKind === CellKind.Code) { - cellHeights[i] = elm.layoutInfo.totalHeight; - } else { - cellHeights[i] = 0; - } - } - - state.cellTotalHeights = cellHeights; - - const focus = this.list.getFocus()[0]; - if (focus) { - const element = this.notebookViewModel!.viewCells[focus]; - const itemDOM = this.list?.domElementOfElement(element!); - let editorFocused = false; - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - editorFocused = true; - } - - state.editorFocused = editorFocused; - state.focus = focus; - } - } - - // Save contribution view states - const contributionsState: { [key: string]: any } = {}; - - const keys = Object.keys(this._contributions); - for (const id of keys) { - const contribution = this._contributions[id]; - if (typeof contribution.saveViewState === 'function') { - contributionsState[id] = contribution.saveViewState(); - } - } - - state.contributionsState = contributionsState; + if (this.group && this._widget) { + const state = this._widget.getEditorViewState(); this.editorMemento.saveEditorState(this.group, input.resource, state); } } @@ -654,19 +196,25 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } layout(dimension: DOM.Dimension): void { - this.dimension = new DOM.Dimension(dimension.width, dimension.height); DOM.toggleClass(this._rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); DOM.toggleClass(this._rootElement, 'narrow-width', dimension.width < 600); - DOM.size(this.body, dimension.width, dimension.height); - this.list?.updateOptions({ additionalScrollHeight: this.scrollBeyondLastLine ? dimension.height : 0 }); - this.list?.layout(dimension.height, dimension.width); + this.dimension = dimension; - if (this.webviewTransparentCover) { - this.webviewTransparentCover.style.height = `${dimension.height}px`; - this.webviewTransparentCover.style.width = `${dimension.width}px`; + if (this._input === undefined || this._widget === undefined) { + return; } - this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + 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 { @@ -681,544 +229,9 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { //#region Editor Features - selectElement(cell: ICellViewModel) { - this.list?.selectElement(cell); - // this.viewModel!.selectionHandles = [cell.handle]; - } - - revealInView(cell: ICellViewModel) { - this.list?.revealElementInView(cell); - } - - revealInCenterIfOutsideViewport(cell: ICellViewModel) { - this.list?.revealElementInCenterIfOutsideViewport(cell); - } - - revealInCenter(cell: ICellViewModel) { - this.list?.revealElementInCenter(cell); - } - - revealLineInView(cell: ICellViewModel, line: number): void { - this.list?.revealElementLineInView(cell, line); - } - - revealLineInCenter(cell: ICellViewModel, line: number) { - this.list?.revealElementLineInCenter(cell, line); - } - - revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { - this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); - } - - revealRangeInView(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInView(cell, range); - } - - revealRangeInCenter(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInCenter(cell, range); - } - - revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); - } - - setCellSelection(cell: ICellViewModel, range: Range): void { - this.list?.setCellSelection(cell, range); - } - - changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { - return this.notebookViewModel?.changeDecorations(callback); - } - - setHiddenAreas(_ranges: ICellRange[]): boolean { - return this.list!.setHiddenAreas(_ranges, true); - } - - //#endregion - - //#region Mouse Events - private readonly _onMouseUp: Emitter = this._register(new Emitter()); - public readonly onMouseUp: Event = this._onMouseUp.event; - - private readonly _onMouseDown: Emitter = this._register(new Emitter()); - public readonly onMouseDown: Event = this._onMouseDown.event; - - //#endregion - - //#region Cell operations - async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { - const viewIndex = this.list!.getViewIndex(cell); - if (viewIndex === undefined) { - // the cell is hidden - return; - } - - let relayout = (cell: ICellViewModel, height: number) => { - this.list?.updateElementHeight2(cell, height); - }; - - let r: () => void; - DOM.scheduleAtNextAnimationFrame(() => { - relayout(cell, height); - r(); - }); - - return new Promise(resolve => { r = resolve; }); - } - - insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - const newLanguages = this.notebookViewModel!.languages; - const language = (type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'; - const index = cell ? this.notebookViewModel!.getCellIndex(cell) : 0; - const nextIndex = ui ? this.notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; - const insertIndex = cell ? - (direction === 'above' ? index : nextIndex) : - index; - const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); - return newCell; - } - - private pushIfAbsent(positions: IPosition[], p: IPosition) { - const last = positions.length > 0 ? positions[positions.length - 1] : undefined; - if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { - positions.push(p); - } - } - - /** - * Add split point at the beginning and the end; - * Move end of line split points to the beginning of the next line; - * Avoid duplicate split points - */ - private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { - const boundaries: IPosition[] = []; - const lineCnt = textBuffer.getLineCount(); - const getLineLen = (lineNumber: number) => { - return textBuffer.getLineLength(lineNumber); - }; - - // split points need to be sorted - splitPoints = splitPoints.sort((l, r) => { - const lineDiff = l.lineNumber - r.lineNumber; - const columnDiff = l.column - r.column; - return lineDiff !== 0 ? lineDiff : columnDiff; - }); - - // eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning - this.pushIfAbsent(boundaries, new Position(1, 1)); - - for (let sp of splitPoints) { - if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { - sp = new Position(sp.lineNumber + 1, 1); - } - this.pushIfAbsent(boundaries, sp); - } - - // eat-up any split point at the beginning, i.e. we ignore the split point at the very end - this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1)); - - // if we only have two then they describe the whole range and nothing needs to be split - return boundaries.length > 2 ? boundaries : null; - } - - private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { - const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer); - if (!rangeBoundaries) { - return null; - } - const newLineModels: string[] = []; - for (let i = 1; i < rangeBoundaries.length; i++) { - const start = rangeBoundaries[i - 1]; - const end = rangeBoundaries[i]; - - newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); - } - - return newLineModels; - } - - async splitNotebookCell(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - let splitPoints = cell.getSelectionsStartPosition(); - if (splitPoints && splitPoints.length > 0) { - await cell.resolveTextModel(); - - if (!cell.hasModel()) { - return null; - } - - let newLinesContents = this.computeCellLinesContents(cell, splitPoints); - if (newLinesContents) { - - // update the contents of the first cell - cell.textModel.applyEdits([ - { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } - ], true); - - // create new cells based on the new text models - const language = cell.model.language; - const kind = cell.cellKind; - let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1; - const newCells = []; - for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { - newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true)); - } - return newCells; - } - } - - return null; - } - - async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - if (constraint && cell.cellKind !== constraint) { - return null; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === 0 && direction === 'above') { - return null; - } - - if (index === this.notebookViewModel!.length - 1 && direction === 'below') { - return null; - } - - if (direction === 'above') { - const above = this.notebookViewModel!.viewCells[index - 1]; - if (constraint && above.cellKind !== constraint) { - return null; - } - - await above.resolveTextModel(); - if (!above.hasModel()) { - return null; - } - - const insertContent = cell.getText(); - const aboveCellLineCount = above.textModel.getLineCount(); - const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); - above.textModel.applyEdits([ - { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } - ]); - - await this.deleteNotebookCell(cell); - return above; - } else { - const below = this.notebookViewModel!.viewCells[index + 1]; - if (constraint && below.cellKind !== constraint) { - return null; - } - - await cell.resolveTextModel(); - if (!cell.hasModel()) { - return null; - } - - const insertContent = below.getText(); - - const cellLineCount = cell.textModel.getLineCount(); - const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); - cell.textModel.applyEdits([ - { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } - ]); - - await this.deleteNotebookCell(below); - return cell; - } - } - - async deleteNotebookCell(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - this.notebookViewModel!.deleteCell(index, true); - return true; - } - - async moveCellDown(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === this.notebookViewModel!.length - 1) { - return false; - } - - const newIdx = index + 1; - return this.moveCellToIndex(index, newIdx); - } - - async moveCellUp(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === 0) { - return false; - } - - const newIdx = index - 1; - return this.moveCellToIndex(index, newIdx); - } - - async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - if (cell === relativeToCell) { - return false; - } - - const originalIdx = this.notebookViewModel!.getCellIndex(cell); - const relativeToIndex = this.notebookViewModel!.getCellIndex(relativeToCell); - - let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; - if (originalIdx < newIdx) { - newIdx--; - } - - return this.moveCellToIndex(originalIdx, newIdx); - } - - private async moveCellToIndex(index: number, newIdx: number): Promise { - if (index === newIdx) { - return false; - } - - if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { - throw new Error('Notebook Editor move cell, index out of range'); - } - - let r: (val: boolean) => void; - DOM.scheduleAtNextAnimationFrame(() => { - this.list?.revealElementInView(this.notebookViewModel!.viewCells[newIdx]); - r(true); - }); - - return new Promise(resolve => { r = resolve; }); - } - - editNotebookCell(cell: CellViewModel): void { - if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).editable) { - return; - } - - cell.editState = CellEditState.Editing; - - this.renderedEditors.get(cell)?.focus(); - } - - saveNotebookCell(cell: ICellViewModel): void { - cell.editState = CellEditState.Preview; - } - - getActiveCell() { - let elements = this.list?.getFocusedElements(); - - if (elements && elements.length) { - return elements[0]; - } - - return undefined; - } - - cancelNotebookExecution(): void { - if (!this.notebookViewModel!.currentTokenSource) { - throw new Error('Notebook is not executing'); - } - - - this.notebookViewModel!.currentTokenSource.cancel(); - this.notebookViewModel!.currentTokenSource = undefined; - } - - async executeNotebook(): Promise { - if (!this.notebookViewModel!.metadata.runnable) { - return; - } - - // return this.progressService.showWhile(this._executeNotebook()); - return this._executeNotebook(); - } - - async _executeNotebook(): Promise { - if (this.notebookViewModel!.currentTokenSource) { - return; - } - - const tokenSource = new CancellationTokenSource(); - try { - this.editorExecutingNotebook!.set(true); - this.notebookViewModel!.currentTokenSource = tokenSource; - - for (let cell of this.notebookViewModel!.viewCells) { - if (cell.cellKind === CellKind.Code) { - await this._executeNotebookCell(cell, tokenSource); - } - } - } finally { - this.editorExecutingNotebook!.set(false); - this.notebookViewModel!.currentTokenSource = undefined; - tokenSource.dispose(); - } - } - - cancelNotebookCellExecution(cell: ICellViewModel): void { - if (!cell.currentTokenSource) { - throw new Error('Cell is not executing'); - } - - cell.currentTokenSource.cancel(); - cell.currentTokenSource = undefined; - } - - async executeNotebookCell(cell: ICellViewModel): Promise { - if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).runnable) { - return; - } - - const tokenSource = new CancellationTokenSource(); - try { - this._executeNotebookCell(cell, tokenSource); - } finally { - tokenSource.dispose(); - } - } - - private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { - try { - cell.currentTokenSource = tokenSource; - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = CellUri.parse(cell.uri)?.notebook; - if (notebookUri) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); - } - } - } finally { - cell.currentTokenSource = undefined; - } - } - - focusNotebookCell(cell: ICellViewModel, focusEditor: boolean) { - if (focusEditor) { - this.selectElement(cell); - this.list?.focusView(); - - cell.editState = CellEditState.Editing; - cell.focusMode = CellFocusMode.Editor; - this.revealInCenterIfOutsideViewport(cell); - } else { - let itemDOM = this.list?.domElementOfElement(cell); - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - (document.activeElement as HTMLElement).blur(); - } - - cell.editState = CellEditState.Preview; - cell.focusMode = CellFocusMode.Container; - - this.selectElement(cell); - this.revealInCenterIfOutsideViewport(cell); - this.list?.focusView(); - } - } - - //#endregion - - //#region MISC - - getLayoutInfo(): NotebookLayoutInfo { - if (!this.list) { - throw new Error('Editor is not initalized successfully'); - } - - return { - width: this.dimension!.width, - height: this.dimension!.height, - fontInfo: this.fontInfo! - }; - } - - triggerScroll(event: IMouseWheelEvent) { - this.list?.triggerScrollFromMouseWheelEvent(event); - } - - createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { - if (!this.webview) { - return; - } - - let preloads = this.notebookViewModel!.renderers; - - if (!this.webview!.insetMapping.has(output)) { - let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); - } else { - let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - let scrollTop = this.list?.scrollTop || 0; - - this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); - } - } - - removeInset(output: IOutput) { - if (!this.webview) { - return; - } - - this.webview!.removeInset(output); - } - - hideInset(output: IOutput) { - if (!this.webview) { - return; - } - - this.webview!.hideInset(output); - } - - getOutputRenderer(): OutputRenderer { - return this.outputRenderer; - } - - postMessage(message: any) { - this.webview?.webview.sendMessage(message); - } - - //#endregion - - //#region Editor Contributions - public getContribution(id: string): T { - return (this._contributions[id] || null); - } - //#endregion dispose() { - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const contributionId = keys[i]; - this._contributions[contributionId].dispose(); - } - super.dispose(); } @@ -1229,111 +242,3 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } } -const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; - -export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { - light: new Color(new RGBA(102, 175, 224)), - dark: new Color(new RGBA(12, 125, 157)), - hc: new Color(new RGBA(0, 73, 122)) -}, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); - -export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { - dark: new Color(new RGBA(255, 255, 255, 0.06)), - light: new Color(new RGBA(237, 239, 249)), - hc: null -} - , 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', { - dark: Color.fromHex('#808080').transparent(0.35), - light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder -}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); - - -registerThemingParticipant((theme, collector) => { - const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); - if (color) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-editor-background, - .monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays, - .monaco-workbench .part.editor > .content .notebook-editor .cell .cell-statusbar-container { background: ${color}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); - } - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output a, - .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { color: ${link};} `); - } - const activeLink = theme.getColor(textLinkActiveForeground); - if (activeLink) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output a:hover, - .monaco-workbench .part.editor > .content .notebook-editor .cell .output a:active { color: ${activeLink}; }`); - } - const shortcut = theme.getColor(textPreformatForeground); - if (shortcut) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor code, - .monaco-workbench .part.editor > .content .notebook-editor .shortcut { color: ${shortcut}; }`); - } - const border = theme.getColor(contrastBorder); - if (border) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-editor { border-color: ${border}; }`); - } - const quoteBackground = theme.getColor(textBlockQuoteBackground); - if (quoteBackground) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { background: ${quoteBackground}; }`); - } - const quoteBorder = theme.getColor(textBlockQuoteBorder); - if (quoteBorder) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { border-color: ${quoteBorder}; }`); - } - - const containerBackground = theme.getColor(notebookOutputContainerColor); - if (containerBackground) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${containerBackground}; }`); - } - - const editorBackgroundColor = theme.getColor(editorBackground); - if (editorBackgroundColor) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); - } - - const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); - if (cellToolbarSeperator) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .notebook-cell-focus-indicator, - .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); - } - - const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); - if (focusedCellIndicatorColor) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .cell-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); - } - - // const widgetShadowColor = theme.getColor(widgetShadow); - // if (widgetShadowColor) { - // collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { - // box-shadow: 0 0 8px 4px ${widgetShadowColor} - // }`) - // } - - // Cell Margin - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); - - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .markdown-cell-row .cell .cell-editor-part { margin-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; }`); -}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts index 2880d46b8f..82827b9487 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorInput.ts @@ -12,29 +12,26 @@ import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/ import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; 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'; private textModel: NotebookEditorModel | null = null; + private _group: GroupIdentifier | undefined; + + public get group(): GroupIdentifier | undefined { + return this._group; + } + + public updateGroup(group: GroupIdentifier): void { + this._group = group; + } + + readonly id: number = NOTEBOOK_EDITOR_INPUT_HANDLE++; constructor( public resource: URI, public name: string, @@ -107,6 +104,7 @@ export class NotebookEditorInput extends EditorInput { return this._move(group, target)?.editor; } + // called when users rename a notebook document move(group: GroupIdentifier, target: URI): IMoveResult | undefined { if (this.textModel) { const contributedNotebookProviders = this.notebookService.getContributedNotebookProviders(target); @@ -119,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 }; } @@ -131,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(); @@ -160,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 new file mode 100644 index 0000000000..45f1459d15 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -0,0 +1,1434 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import * as DOM from 'vs/base/browser/dom'; +import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +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, 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'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { IEditor } from 'vs/editor/common/editorCommon'; +import { IReadonlyTextBuffer } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } 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, 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'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind, IProcessedOutput, INotebookKernelInfo, INotebookKernelInfoDto } 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'; + +const $ = DOM.$; + +export class NotebookEditorOptions extends EditorOptions { + + readonly cellOptions?: IResourceEditorInput; + + constructor(options: Partial) { + super(); + this.overwrite(options); + this.cellOptions = options.cellOptions; + } + + with(options: Partial): NotebookEditorOptions { + return new NotebookEditorOptions({ ...this, ...options }); + } +} + +export class NotebookEditorWidget extends Disposable implements INotebookEditor { + static readonly ID: string = 'workbench.editor.notebook'; + private static readonly EDITOR_MEMENTOS = new Map>(); + private overlayContainer!: HTMLElement; + private body!: HTMLElement; + 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; + private localStore: DisposableStore = this._register(new DisposableStore()); + private fontInfo: BareFontInfo | undefined; + private dimension: DOM.Dimension | null = null; + private shadowElementViewInfo: { height: number, width: number, top: number; left: number; } | null = null; + private editorFocus: IContextKey | null = null; + 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 readonly contextKeyService: IContextKeyService, + @ILayoutService private readonly _layoutService: ILayoutService + ) { + super(); + this.memento = new Memento(NotebookEditorWidget.ID, storageService); + + this.outputRenderer = new OutputRenderer(this, this.instantiationService); + this._contributions = {}; + this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); + + this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { + this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); + if (this.dimension) { + this.layout(this.dimension); + } + } + }); + + this.notebookService.addNotebookEditor(this); + } + + 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(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; + + get activeKernel() { + return this._activeKernel; + } + + set activeKernel(kernel: INotebookKernelInfo | undefined) { + this._activeKernel = kernel; + this._onDidChangeKernel.fire(); + } + + private readonly _onDidChangeActiveEditor = this._register(new Emitter()); + readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; + + get activeCodeEditor(): IEditor | undefined { + const [focused] = this.list!.getFocusedElements(); + return this.renderedEditors.get(focused); + } + + //#region Editor Core + + protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { + const mementoKey = `${NotebookEditorWidget.ID}${key}`; + + let editorMemento = NotebookEditorWidget.EDITOR_MEMENTOS.get(mementoKey); + if (!editorMemento) { + editorMemento = new EditorMemento(NotebookEditorWidget.ID, key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); + NotebookEditorWidget.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + } + + return editorMemento; + } + + protected getMemento(scope: StorageScope): MementoObject { + return this.memento.getMemento(scope); + } + + public get isNotebookEditor() { + return true; + } + + updateEditorFocus() { + // Note - focus going to the webview will fire 'blur', but the webview element will be + // a descendent of the notebook editor root. + 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(); + this.overlayContainer.id = `notebook-${id}`; + this.overlayContainer.className = 'notebookOverlay'; + DOM.addClass(this.overlayContainer, 'notebook-editor'); + this.overlayContainer.style.visibility = 'hidden'; + + this._layoutService.container.appendChild(this.overlayContainer); + this.createBody(this.overlayContainer); + this.generateFontInfo(); + this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this.editorFocus.set(true); + this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); + this.editorEditable.set(true); + 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(); + + for (const desc of contributions) { + try { + const contribution = this.instantiationService.createInstance(desc.ctor, this); + this._contributions[desc.id] = contribution; + } catch (err) { + onUnexpectedError(err); + } + } + } + + private generateFontInfo(): void { + const editorOptions = this.configurationService.getValue('editor'); + this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + } + + private createBody(parent: HTMLElement): void { + this.body = document.createElement('div'); + DOM.addClass(this.body, 'cell-list-container'); + this.createCellList(); + DOM.append(parent, this.body); + } + + private createCellList(): void { + DOM.addClass(this.body, 'cell-list-container'); + + 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( + NotebookCellList, + 'NotebookCellList', + this.body, + this.instantiationService.createInstance(NotebookCellListDelegate), + renderers, + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + additionalScrollHeight: 0, + transformOptimization: false, + styleController: (_suffix: string) => { return this.list!; }, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + }, + accessibilityProvider: { + getAriaLabel() { return null; }, + getWidgetAriaLabel() { + return nls.localize('notebookTreeAriaLabel', "Notebook"); + } + } + }, + ); + this.dndController.setList(this.list); + + // create Webview + + this._register(this.list); + this._register(combinedDisposable(...renderers)); + + // transparent cover + this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); + this.webviewTransparentCover.style.display = 'none'; + + this._register(DOM.addStandardDisposableGenericMouseDownListner(this.overlayContainer, (e: StandardMouseEvent) => { + if (DOM.hasClass(e.target, 'slider') && this.webviewTransparentCover) { + this.webviewTransparentCover.style.display = 'block'; + } + })); + + this._register(DOM.addStandardDisposableGenericMouseUpListner(this.overlayContainer, () => { + if (this.webviewTransparentCover) { + // no matter when + this.webviewTransparentCover.style.display = 'none'; + } + })); + + this._register(this.list.onMouseDown(e => { + if (e.element) { + this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); + } + })); + + this._register(this.list.onMouseUp(e => { + if (e.element) { + this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); + } + })); + + 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() { + return this.overlayContainer; + } + + onWillHide() { + this.editorFocus?.set(false); + this.overlayContainer.style.visibility = 'hidden'; + this.overlayContainer.style.display = 'none'; + } + + getInnerWebview(): Webview | undefined { + return this.webview?.webview; + } + + + 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.detachModel(); + await this.attachModel(textModel, viewState); + } + + // clear state + this.dndController?.clearGlobalDragState(); + + this._setKernels(textModel); + + this.localStore.add(this.notebookService.onDidChangeKernels(() => { + if (this.activeKernel === undefined) { + this._setKernels(textModel); + } + })); + + // reveal cell if editor options tell to do so + if (options instanceof NotebookEditorOptions && options.cellOptions) { + const cellOptions = options.cellOptions; + const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); + if (cell) { + this.selectElement(cell); + this.revealInCenterIfOutsideViewport(cell); + const editor = this.renderedEditors.get(cell)!; + if (editor) { + if (cellOptions.options?.selection) { + const { selection } = cellOptions.options; + editor.setSelection({ + ...selection, + endLineNumber: selection.endLineNumber || selection.startLineNumber, + endColumn: selection.endColumn || selection.startColumn + }); + } + if (!cellOptions.options?.preserveFocus) { + editor.focus(); + } + } + } + } + } + + private detachModel() { + this.localStore.clear(); + this.list?.detachViewModel(); + this.viewModel?.dispose(); + // avoid event + this.notebookViewModel = undefined; + // 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); + DOM.toggleClass(this.overlayContainer, 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + } + + 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()); + this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + + this.updateForMetadata(); + this.localStore.add(this.eventDispatcher.onDidChangeMetadata(() => { + this.updateForMetadata(); + })); + + // restore view states, including contributions + + { + // restore view state + this.viewModel.restoreEditorViewState(viewState); + + // contribution state restore + + const contributionsState = viewState?.contributionsState || {}; + const keys = Object.keys(this._contributions); + for (let i = 0, len = keys.length; i < len; i++) { + const id = keys[i]; + const contribution = this._contributions[id]; + if (typeof contribution.restoreViewState === 'function') { + contribution.restoreViewState(contributionsState[id]); + } + } + } + + this.webview?.updateRendererPreloads(this.viewModel.renderers); + + this.localStore.add(this.list!.onWillScroll(e => { + this.webview!.updateViewScrollTop(-e.scrollTop, []); + this.webviewTransparentCover!.style.top = `${e.scrollTop}px`; + })); + + this.localStore.add(this.list!.onDidChangeContentHeight(() => { + DOM.scheduleAtNextAnimationFrame(() => { + const scrollTop = this.list?.scrollTop || 0; + const scrollHeight = this.list?.scrollHeight || 0; + this.webview!.element.style.height = `${scrollHeight}px`; + + if (this.webview?.insetMapping) { + 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); + + if (viewIndex === undefined) { + return; + } + + if (cell.outputs.indexOf(key) < 0) { + // output is already gone + removedItems.push(key); + } + + const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { + updateItems.push({ + cell: cell, + output: key, + cellTop: cellTop + }); + } + }); + + removedItems.forEach(output => this.webview?.removeInset(output)); + + if (updateItems.length) { + this.webview?.updateViewScrollTop(-scrollTop, updateItems); + } + } + }); + })); + + this.list!.attachViewModel(this.viewModel); + this.localStore.add(this.list!.onDidRemoveOutput(output => { + this.removeInset(output); + })); + this.localStore.add(this.list!.onDidHideOutput(output => { + this.hideInset(output); + })); + + this.list!.layout(); + this.dndController?.clearGlobalDragState(); + + // restore list state at last, it must be after list layout + this.restoreListViewState(viewState); + } + + private restoreListViewState(viewState: INotebookEditorViewState | undefined): void { + if (viewState?.scrollPosition !== undefined) { + this.list!.scrollTop = viewState!.scrollPosition.top; + this.list!.scrollLeft = viewState!.scrollPosition.left; + } else { + this.list!.scrollTop = 0; + this.list!.scrollLeft = 0; + } + + const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; + if (focusIdx < this.list!.length) { + this.list!.setFocus([focusIdx]); + this.list!.setSelection([focusIdx]); + } else if (this.list!.length > 0) { + this.list!.setFocus([0]); + } + + if (viewState?.editorFocused) { + this.list?.focusView(); + const cell = this.notebookViewModel?.viewCells[focusIdx]; + if (cell) { + cell.focusMode = CellFocusMode.Editor; + } + } + } + + 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 } = {}; + for (let i = 0; i < this.viewModel!.length; i++) { + const elm = this.viewModel!.viewCells[i] as CellViewModel; + if (elm.cellKind === CellKind.Code) { + cellHeights[i] = elm.layoutInfo.totalHeight; + } else { + cellHeights[i] = 0; + } + } + + state.cellTotalHeights = cellHeights; + + const focus = this.list.getFocus()[0]; + if (typeof focus === 'number') { + const element = this.notebookViewModel!.viewCells[focus]; + const itemDOM = this.list?.domElementOfElement(element!); + let editorFocused = false; + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + editorFocused = true; + } + + state.editorFocused = editorFocused; + state.focus = focus; + } + } + + // Save contribution view states + const contributionsState: { [key: string]: any } = {}; + + const keys = Object.keys(this._contributions); + for (const id of keys) { + const contribution = this._contributions[id]; + if (typeof contribution.saveViewState === 'function') { + contributionsState[id] = contribution.saveViewState(); + } + } + + state.contributionsState = contributionsState; + return state; + } + + // private saveEditorViewState(input: NotebookEditorInput): void { + // if (this.group && this.notebookViewModel) { + // } + // } + + // private loadTextEditorViewState(): INotebookEditorViewState | undefined { + // return this.editorMemento.loadEditorState(this.group, input.resource); + // } + + layout(dimension: DOM.Dimension, shadowElement?: HTMLElement): void { + if (!shadowElement && this.shadowElementViewInfo === null) { + this.dimension = dimension; + return; + } + + if (shadowElement) { + const containerRect = shadowElement.getBoundingClientRect(); + + this.shadowElementViewInfo = { + height: containerRect.height, + width: containerRect.width, + top: containerRect.top, + left: containerRect.left + }; + } + + this.dimension = new DOM.Dimension(dimension.width, dimension.height); + DOM.size(this.body, dimension.width, dimension.height); + this.list?.updateOptions({ additionalScrollHeight: this.scrollBeyondLastLine ? dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP : 0 }); + this.list?.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + + this.overlayContainer.style.visibility = 'visible'; + this.overlayContainer.style.display = 'block'; + this.overlayContainer.style.position = 'absolute'; + this.overlayContainer.style.top = `${this.shadowElementViewInfo!.top}px`; + this.overlayContainer.style.left = `${this.shadowElementViewInfo!.left}px`; + this.overlayContainer.style.width = `${dimension ? dimension.width : this.shadowElementViewInfo!.width}px`; + this.overlayContainer.style.height = `${dimension ? dimension.height : this.shadowElementViewInfo!.height}px`; + + if (this.webviewTransparentCover) { + this.webviewTransparentCover.style.height = `${dimension.height}px`; + this.webviewTransparentCover.style.width = `${dimension.width}px`; + } + + this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } + + // protected saveState(): void { + // if (this.input instanceof NotebookEditorInput) { + // this.saveEditorViewState(this.input); + // } + + // super.saveState(); + // } + + //#endregion + + //#region Editor Features + + selectElement(cell: ICellViewModel) { + this.list?.selectElement(cell); + // this.viewModel!.selectionHandles = [cell.handle]; + } + + revealInView(cell: ICellViewModel) { + this.list?.revealElementInView(cell); + } + + revealInCenterIfOutsideViewport(cell: ICellViewModel) { + this.list?.revealElementInCenterIfOutsideViewport(cell); + } + + revealInCenter(cell: ICellViewModel) { + this.list?.revealElementInCenter(cell); + } + + revealLineInView(cell: ICellViewModel, line: number): void { + this.list?.revealElementLineInView(cell, line); + } + + revealLineInCenter(cell: ICellViewModel, line: number) { + this.list?.revealElementLineInCenter(cell, line); + } + + revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { + this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); + } + + revealRangeInView(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInView(cell, range); + } + + revealRangeInCenter(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInCenter(cell, range); + } + + revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); + } + + setCellSelection(cell: ICellViewModel, range: Range): void { + this.list?.setCellSelection(cell, range); + } + + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { + return this.notebookViewModel?.changeDecorations(callback); + } + + setHiddenAreas(_ranges: ICellRange[]): boolean { + return this.list!.setHiddenAreas(_ranges, true); + } + + //#endregion + + //#region Mouse Events + private readonly _onMouseUp: Emitter = this._register(new Emitter()); + public readonly onMouseUp: Event = this._onMouseUp.event; + + private readonly _onMouseDown: Emitter = this._register(new Emitter()); + public readonly onMouseDown: Event = this._onMouseDown.event; + + private pendingLayouts = new WeakMap(); + + //#endregion + + //#region Cell operations + async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { + const viewIndex = this.list!.getViewIndex(cell); + if (viewIndex === undefined) { + // the cell is hidden + return; + } + + let relayout = (cell: ICellViewModel, height: number) => { + this.list?.updateElementHeight2(cell, height); + }; + + if (this.pendingLayouts.has(cell)) { + this.pendingLayouts.get(cell)!.dispose(); + } + + let r: () => void; + 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; }); + } + + insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + const newLanguages = this.notebookViewModel!.languages; + const language = (type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'; + const index = cell ? this.notebookViewModel!.getCellIndex(cell) : 0; + const nextIndex = ui ? this.notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; + const insertIndex = cell ? + (direction === 'above' ? index : nextIndex) : + index; + const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); + return newCell; + } + + private pushIfAbsent(positions: IPosition[], p: IPosition) { + const last = positions.length > 0 ? positions[positions.length - 1] : undefined; + if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { + positions.push(p); + } + } + + /** + * Add split point at the beginning and the end; + * Move end of line split points to the beginning of the next line; + * Avoid duplicate split points + */ + private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { + const boundaries: IPosition[] = []; + const lineCnt = textBuffer.getLineCount(); + const getLineLen = (lineNumber: number) => { + return textBuffer.getLineLength(lineNumber); + }; + + // split points need to be sorted + splitPoints = splitPoints.sort((l, r) => { + const lineDiff = l.lineNumber - r.lineNumber; + const columnDiff = l.column - r.column; + return lineDiff !== 0 ? lineDiff : columnDiff; + }); + + // eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning + this.pushIfAbsent(boundaries, new Position(1, 1)); + + for (let sp of splitPoints) { + if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { + sp = new Position(sp.lineNumber + 1, 1); + } + this.pushIfAbsent(boundaries, sp); + } + + // eat-up any split point at the beginning, i.e. we ignore the split point at the very end + this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1)); + + // if we only have two then they describe the whole range and nothing needs to be split + return boundaries.length > 2 ? boundaries : null; + } + + private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { + const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer); + if (!rangeBoundaries) { + return null; + } + const newLineModels: string[] = []; + for (let i = 1; i < rangeBoundaries.length; i++) { + const start = rangeBoundaries[i - 1]; + const end = rangeBoundaries[i]; + + newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); + } + + return newLineModels; + } + + async splitNotebookCell(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + let splitPoints = cell.getSelectionsStartPosition(); + if (splitPoints && splitPoints.length > 0) { + await cell.resolveTextModel(); + + if (!cell.hasModel()) { + return null; + } + + let newLinesContents = this.computeCellLinesContents(cell, splitPoints); + if (newLinesContents) { + + // update the contents of the first cell + cell.textModel.applyEdits([ + { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } + ], true); + + // create new cells based on the new text models + const language = cell.model.language; + const kind = cell.cellKind; + let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1; + const newCells = []; + for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { + newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true)); + } + return newCells; + } + } + + return null; + } + + async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + if (constraint && cell.cellKind !== constraint) { + return null; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === 0 && direction === 'above') { + return null; + } + + if (index === this.notebookViewModel!.length - 1 && direction === 'below') { + return null; + } + + if (direction === 'above') { + const above = this.notebookViewModel!.viewCells[index - 1]; + if (constraint && above.cellKind !== constraint) { + return null; + } + + await above.resolveTextModel(); + if (!above.hasModel()) { + return null; + } + + const insertContent = cell.getText(); + const aboveCellLineCount = above.textModel.getLineCount(); + const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); + above.textModel.applyEdits([ + { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } + ]); + + await this.deleteNotebookCell(cell); + return above; + } else { + const below = this.notebookViewModel!.viewCells[index + 1]; + if (constraint && below.cellKind !== constraint) { + return null; + } + + await cell.resolveTextModel(); + if (!cell.hasModel()) { + return null; + } + + const insertContent = below.getText(); + + const cellLineCount = cell.textModel.getLineCount(); + const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); + cell.textModel.applyEdits([ + { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } + ]); + + await this.deleteNotebookCell(below); + return cell; + } + } + + async deleteNotebookCell(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + this.notebookViewModel!.deleteCell(index, true); + return true; + } + + async moveCellDown(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === this.notebookViewModel!.length - 1) { + return false; + } + + const newIdx = index + 1; + return this.moveCellToIndex(index, newIdx); + } + + async moveCellUp(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === 0) { + return false; + } + + const newIdx = index - 1; + return this.moveCellToIndex(index, newIdx); + } + + async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + if (cell === relativeToCell) { + return false; + } + + const originalIdx = this.notebookViewModel!.getCellIndex(cell); + const relativeToIndex = this.notebookViewModel!.getCellIndex(relativeToCell); + + let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; + if (originalIdx < newIdx) { + newIdx--; + } + + return this.moveCellToIndex(originalIdx, newIdx); + } + + private async moveCellToIndex(index: number, newIdx: number): Promise { + if (index === newIdx) { + return false; + } + + if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { + throw new Error('Notebook Editor move cell, index out of range'); + } + + let r: (val: boolean) => void; + DOM.scheduleAtNextAnimationFrame(() => { + this.list?.revealElementInView(this.notebookViewModel!.viewCells[newIdx]); + r(true); + }); + + return new Promise(resolve => { r = resolve; }); + } + + editNotebookCell(cell: CellViewModel): void { + if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).editable) { + return; + } + + cell.editState = CellEditState.Editing; + + this.renderedEditors.get(cell)?.focus(); + } + + saveNotebookCell(cell: ICellViewModel): void { + cell.editState = CellEditState.Preview; + } + + getActiveCell() { + let elements = this.list?.getFocusedElements(); + + if (elements && elements.length) { + return elements[0]; + } + + return undefined; + } + + cancelNotebookExecution(): void { + if (!this.notebookViewModel!.currentTokenSource) { + throw new Error('Notebook is not executing'); + } + + + this.notebookViewModel!.currentTokenSource.cancel(); + this.notebookViewModel!.currentTokenSource = undefined; + } + + async executeNotebook(): Promise { + if (!this.notebookViewModel!.metadata.runnable) { + return; + } + + return this._executeNotebook(); + } + + async _executeNotebook(): Promise { + if (this.notebookViewModel!.currentTokenSource) { + return; + } + + const tokenSource = new CancellationTokenSource(); + 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 if (provider.kernel) { + return await this.notebookService.executeNotebook(viewType, notebookUri, true, tokenSource.token); + } else { + return await this.notebookService.executeNotebook(viewType, notebookUri, false, tokenSource.token); + } + } + + } finally { + this.editorExecutingNotebook!.set(false); + this.notebookViewModel!.currentTokenSource = undefined; + tokenSource.dispose(); + } + } + + cancelNotebookCellExecution(cell: ICellViewModel): void { + if (!cell.currentTokenSource) { + throw new Error('Cell is not executing'); + } + + cell.currentTokenSource.cancel(); + cell.currentTokenSource = undefined; + } + + 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 { + await this._executeNotebookCell(cell, tokenSource); + } finally { + tokenSource.dispose(); + } + } + + private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { + try { + cell.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) { + 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, false, tokenSource.token); + } + } + } finally { + cell.currentTokenSource = undefined; + } + } + + focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + if (focusItem === 'editor') { + this.selectElement(cell); + this.list?.focusView(); + + cell.editState = CellEditState.Editing; + cell.focusMode = CellFocusMode.Editor; + this.revealInCenterIfOutsideViewport(cell); + } else if (focusItem === 'output') { + this.selectElement(cell); + this.list?.focusView(); + + if (!this.webview) { + return; + } + this.webview.focusOutput(cell.id); + + cell.editState = CellEditState.Preview; + cell.focusMode = CellFocusMode.Container; + this.revealInCenterIfOutsideViewport(cell); + } else { + let itemDOM = this.list?.domElementOfElement(cell); + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + (document.activeElement as HTMLElement).blur(); + } + + cell.editState = CellEditState.Preview; + cell.focusMode = CellFocusMode.Container; + + this.selectElement(cell); + this.revealInCenterIfOutsideViewport(cell); + this.list?.focusView(); + } + } + + //#endregion + + //#region MISC + + getLayoutInfo(): NotebookLayoutInfo { + if (!this.list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this.dimension!.width, + height: this.dimension!.height, + fontInfo: this.fontInfo! + }; + } + + triggerScroll(event: IMouseWheelEvent) { + this.list?.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cell: CodeCellViewModel, output: IProcessedOutput, shadowContent: string, offset: number) { + if (!this.webview) { + return; + } + + let preloads = this.notebookViewModel!.renderers; + + if (!this.webview!.insetMapping.has(output)) { + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); + } else { + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + let scrollTop = this.list?.scrollTop || 0; + + this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); + } + } + + removeInset(output: IProcessedOutput) { + if (!this.webview) { + return; + } + + this.webview!.removeInset(output); + } + + hideInset(output: IProcessedOutput) { + if (!this.webview) { + return; + } + + this.webview!.hideInset(output); + } + + getOutputRenderer(): OutputRenderer { + return this.outputRenderer; + } + + postMessage(message: any) { + this.webview?.webview.sendMessage(message); + } + + //#endregion + + //#region Editor Contributions + public getContribution(id: string): T { + return (this._contributions[id] || null); + } + + //#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); + + super.dispose(); + } + + toJSON(): any { + return { + notebookHandle: this.viewModel?.handle + }; + } +} + +const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; + +export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { + light: new Color(new RGBA(102, 175, 224)), + dark: new Color(new RGBA(12, 125, 157)), + hc: new Color(new RGBA(0, 73, 122)) +}, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); + +export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { + dark: new Color(new RGBA(255, 255, 255, 0.06)), + light: new Color(new RGBA(237, 239, 249)), + hc: null +} + , 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', { + dark: Color.fromHex('#808080').transparent(0.35), + light: Color.fromHex('#808080').transparent(0.35), + hc: contrastBorder +}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); + + +registerThemingParticipant((theme, collector) => { + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { + padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; + box-sizing: border-box; + }`); + + const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); + if (color) { + collector.addRule(`.notebookOverlay .cell .monaco-editor-background, + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${color}; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.notebookOverlay .output a, + .notebookOverlay .cell.markdown a { color: ${link};} `); + } + const activeLink = theme.getColor(textLinkActiveForeground); + if (activeLink) { + collector.addRule(`.notebookOverlay .output a:hover, + .notebookOverlay .cell .output a:active { color: ${activeLink}; }`); + } + const shortcut = theme.getColor(textPreformatForeground); + if (shortcut) { + collector.addRule(`.notebookOverlay code, + .notebookOverlay .shortcut { color: ${shortcut}; }`); + } + const border = theme.getColor(contrastBorder); + if (border) { + collector.addRule(`.notebookOverlay .monaco-editor { border-color: ${border}; }`); + } + const quoteBackground = theme.getColor(textBlockQuoteBackground); + if (quoteBackground) { + collector.addRule(`.notebookOverlay blockquote { background: ${quoteBackground}; }`); + } + const quoteBorder = theme.getColor(textBlockQuoteBorder); + if (quoteBorder) { + collector.addRule(`.notebookOverlay blockquote { border-color: ${quoteBorder}; }`); + } + + const containerBackground = theme.getColor(notebookOutputContainerColor); + if (containerBackground) { + collector.addRule(`.notebookOverlay .output { background-color: ${containerBackground}; }`); + } + + const editorBackgroundColor = theme.getColor(editorBackground); + if (editorBackgroundColor) { + collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); + } + + const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); + if (cellToolbarSeperator) { + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row:hover .notebook-cell-focus-indicator, + .notebookOverlay .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); + } + + const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); + if (focusedCellIndicatorColor) { + collector.addRule(`.notebookOverlay .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); + } + + // 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 { + // box-shadow: 0 0 8px 4px ${widgetShadowColor} + // }`) + // } + + // Cell Margin + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + + collector.addRule(`.notebookOverlay .markdown-cell-row .cell .cell-editor-part { margin-left: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay > .cell-list-container > .cell-list-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; }`); +}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts b/src/vs/workbench/contrib/notebook/browser/notebookRegistry.ts index 7c27fa6863..84403af5d4 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 f807ed8626..19faad7e98 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookServiceImpl.ts @@ -6,21 +6,26 @@ 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 } 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'; import { NotebookTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookTextModel'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService, ICustomEditorViewTypesHandler, ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService'; import { NotebookCellTextModel } from 'vs/workbench/contrib/notebook/common/model/notebookCellTextModel'; import { 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 { 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(); @@ -39,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); @@ -67,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); @@ -96,27 +99,42 @@ 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 _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 _onNotebookEditorRemove: Emitter = this._register(new Emitter()); + public readonly onNotebookEditorRemove: Event = this._onNotebookEditorRemove.event; + private readonly _notebookEditors: { [editorId: string]: INotebookEditor; }; private readonly _onDidChangeViewTypes = new Emitter(); onDidChangeViewTypes: Event = this._onDidChangeViewTypes.event; + + private readonly _onDidChangeKernels = new Emitter(); + onDidChangeKernels: Event = this._onDidChangeKernels.event; 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._notebookEditors = Object.create(null); this.modelManager = this.instantiationService.createInstance(NotebookEditorModelManager); notebookProviderExtensionPoint.setHandler((extensions) => { @@ -129,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 })); } } @@ -152,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[] { @@ -172,6 +211,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(); } @@ -180,35 +220,78 @@ 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); } - getRendererInfo(handle: number): INotebookRendererInfo | undefined { - const renderer = this._notebookRenderers.get(handle); + registerNotebookKernel(notebook: INotebookKernelInfo): void { + this._notebookKernels.set(notebook.id, notebook); + this._onDidChangeKernels.fire(); + } - if (renderer) { - return { - id: renderer.extensionData.id, - extensionLocation: URI.revive(renderer.extensionData.location), - preloads: renderer.preloads - }; + unregisterNotebookKernel(id: string): void { + this._notebookKernels.delete(id); + this._onDidChangeKernels.fire(); + } + + getContributedNotebookKernels(viewType: string, resource: URI): INotebookKernelInfo[] { + let kernelInfos: INotebookKernelInfo[] = []; + this._notebookKernels.forEach(kernel => { + if (this._notebookKernelMatch(resource, kernel!.selectors)) { + kernelInfos.push(kernel!); + } + }); + + // sort by extensions + + const notebookContentProvider = this._notebookProviders.get(viewType); + + if (!notebookContentProvider) { + return kernelInfos; } - return undefined; // {{SQL CARBON EDIT}} strict-null-check + kernelInfos = kernelInfos.sort((a, b) => { + if (a.extension.value === notebookContentProvider!.extensionData.id.value) { + return -1; + } else if (b.extension.value === notebookContentProvider!.extensionData.id.value) { + return 1; + } else { + return 0; + } + }); + + return kernelInfos; } - async createNotebookFromBackup(viewType: string, uri: URI, metadata: NotebookDocumentMetadata, languages: string[], cells: ICellDto2[]): Promise { + 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())) { + return true; + } + } + + return false; + } + + getRendererInfo(id: string): INotebookRendererInfo | undefined { + const renderer = this._notebookRenderers.get(id); + + return renderer; + } + + 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; } @@ -220,30 +303,17 @@ export class NotebookService extends Disposable implements INotebookService, ICu (model) => this._onWillDispose(model), ); this._models[modelId] = modelData; - - notebookModel.metadata = metadata; - notebookModel.languages = languages; - - notebookModel.applyEdit(notebookModel.versionId, [ - { - editType: CellEditType.Insert, - index: 0, - cells: cells - } - ]); - 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); @@ -256,20 +326,265 @@ export class NotebookService extends Disposable implements INotebookService, ICu return modelData.model; } - async executeNotebook(viewType: string, uri: URI): Promise { + 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) + }; + }); + + 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 undefined; // {{SQL CARBON EDIT}} strict-null-checks + } + + 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, new CancellationTokenSource().token); // Cancellation for notebooks - TODO + return provider.controller.executeNotebook(viewType, uri, useAttachedKernel, token); } return; } - async executeNotebookCell(viewType: string, uri: URI, handle: number, token: CancellationToken): Promise { + 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) { + 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) { + await kernel.executeNotebook(viewType, uri, handle, token); } } @@ -294,6 +609,25 @@ export class NotebookService extends Disposable implements INotebookService, ICu return ret; } + removeNotebookEditor(editor: INotebookEditor) { + if (delete this._notebookEditors[editor.getId()]) { + this._onNotebookEditorRemove.fire(editor); + } + } + + addNotebookEditor(editor: INotebookEditor) { + this._notebookEditors[editor.getId()] = editor; + this._onNotebookEditorAdd.fire(editor); + } + + listNotebookEditors(): INotebookEditor[] { + return Object.keys(this._notebookEditors).map(id => this._notebookEditors[id]); + } + + listNotebookDocuments(): NotebookTextModel[] { + return Object.keys(this._models).map(id => this._models[id].model); + } + destoryNotebookDocument(viewType: string, notebook: INotebookTextModel): void { let provider = this._notebookProviders.get(viewType); @@ -302,8 +636,12 @@ export class NotebookService extends Disposable implements INotebookService, ICu } } - updateActiveNotebookDocument(viewType: string, resource: URI): void { - this._onDidChangeActiveEditor.fire({ viewType, uri: resource }); + updateActiveNotebookEditor(editor: INotebookEditor | null) { + this._onDidChangeActiveEditor.fire(editor ? editor.getId() : null); + } + + updateVisibleNotebookEditor(editors: string[]) { + this._onDidChangeVisibleEditors.fire(editors); } setToCopy(items: NotebookCellTextModel[]) { @@ -334,11 +672,11 @@ 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); } } diff --git a/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts b/src/vs/workbench/contrib/notebook/browser/view/notebookCellList.ts index e4d2abbe18..40f55898f6 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[] = []; @@ -57,7 +58,6 @@ export class NotebookCellList extends WorkbenchList implements ID ) { super(listUser, container, delegate, renderers, options, contextKeyService, listService, themeService, configurationService, keybindingService); - this._previousFocusedElements = this.getFocusedElements(); this._localDisposableStore.add(this.onDidChangeFocus((e) => { this._previousFocusedElements.forEach(element => { @@ -66,6 +66,19 @@ export class NotebookCellList extends WorkbenchList implements ID } }); this._previousFocusedElements = e.elements; + + // 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); @@ -122,6 +135,26 @@ export class NotebookCellList extends WorkbenchList implements ID } + 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 { + let index = this._getViewIndexUpperBound(element); + if (index === undefined || index < 0 || index >= this.length) { + this._getViewIndexUpperBound(element); + throw new ListError(this.listUser, `Invalid index ${index}`); + } + + return this.view.elementHeight(index); + } + protected createMouseController(_options: IListOptions): MouseController { return new NotebookMouseController(this); } @@ -152,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); @@ -172,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); @@ -198,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(); @@ -294,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); @@ -324,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]; } @@ -377,8 +410,12 @@ export class NotebookCellList extends WorkbenchList implements ID } } - setFocus(indexes: number[], browserEvent?: UIEvent): void { - if (this._viewModel) { + setFocus(indexes: number[], browserEvent?: UIEvent, ignoreTextModelUpdate?: boolean): void { + if (!indexes.length) { + return; + } + + 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 4089d6b90d..aeb789d1b3 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 258637febe..a9fdd18633 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,9 +26,10 @@ 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); + this._mdRenderer = instantiationService.createInstance(MarkdownRenderer, undefined); this._richMimeTypeRenderers.set('application/json', this.renderJSON.bind(this)); this._richMimeTypeRenderers.set('application/javascript', this.renderJavaScript.bind(this)); this._richMimeTypeRenderers.set('text/html', this.renderHTML.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 ceee938e6e..922ad34050 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 Source EULA. 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 fcc0d22d39..e5871cd2e4 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -4,22 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import * as DOM from 'vs/base/browser/dom'; +import { getPathFromAmdModule } from 'vs/base/common/amd'; +import { Emitter, Event } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import * as path from 'vs/base/common/path'; +import { isWeb } from 'vs/base/common/platform'; import { URI } from 'vs/base/common/uri'; import * as UUID from 'vs/base/common/uuid'; -import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { IWebviewService, WebviewElement } from 'vs/workbench/contrib/webview/browser/webview'; -import { WebviewResourceScheme } from 'vs/workbench/contrib/webview/common/resourceLoader'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { CELL_MARGIN, CELL_RUN_GUTTER } from 'vs/workbench/contrib/notebook/browser/constants'; -import { Emitter, Event } from 'vs/base/common/event'; -import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { isWeb } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +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 { 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 { 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; @@ -54,10 +67,29 @@ export interface IScrollAckMessage { version: number; } +export interface IBlurOutputMessage { + __vscode_notebook_message: boolean; + type: 'focus-editor'; + id: string; + focusNext?: boolean; +} + +export interface IClickedDataUrlMessage { + __vscode_notebook_message: string; + type: 'clicked-data-url'; + data: string; + downloadName?: string; +} + export interface IClearMessage { type: 'clear'; } +export interface IFocusOutputMessage { + type: 'focus-output'; + id: string; +} + export interface ICreationRequestMessage { type: 'html'; content: string; @@ -65,6 +97,7 @@ export interface ICreationRequestMessage { outputId: string; top: number; left: number; + initiallyHidden?: boolean; } export interface IContentWidgetTopRequest { @@ -91,31 +124,55 @@ export interface IScrollRequestMessage { export interface IUpdatePreloadResourceMessage { type: 'preload'; resources: string[]; + source: string; } -type IMessage = IDimensionMessage | IScrollAckMessage | IWheelMessage | IMouseEnterMessage | IMouseLeaveMessage; +interface ICachedInset { + outputId: string; + cell: CodeCellViewModel; + preloads: ReadonlySet; + cachedCreation: ICreationRequestMessage; +} + +function html(strings: TemplateStringsArray, ...values: any[]): string { + let str = ''; + strings.forEach((string, i) => { + str += string + (values[i] || ''); + }); + return str; +} + +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'); @@ -126,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; @@ -135,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 { @@ -153,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) { - return /* html */` + generateContent(outputNodePadding: number, coreDependencies: string, baseUrl: string) { + return html` +