diff --git a/.eslintrc.json b/.eslintrc.json index af5be347d61a..7563e2e3172c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,7 +26,17 @@ "no-throw-literal": "warn", "no-unsafe-finally": "warn", "no-unused-labels": "warn", - "no-restricted-globals": ["warn", "name", "length", "event", "closed", "external", "status", "origin", "orientation"], // non-complete list of globals that are easy to access unintentionally + "no-restricted-globals": [ + "warn", + "name", + "length", + "event", + "closed", + "external", + "status", + "origin", + "orientation" + ], // non-complete list of globals that are easy to access unintentionally "no-var": "warn", "jsdoc/no-types": "warn", "semi": "off", @@ -259,6 +269,50 @@ "rules": { "jsdoc/no-types": "off" } + }, + { + "files": [ + "**/vscode.d.ts", + "**/vscode.proposed.d.ts" + ], + "rules": { + "vscode-dts-create-func": "warn", + "vscode-dts-literal-or-types": "warn", + "vscode-dts-interface-naming": "warn", + "vscode-dts-event-naming": [ + "warn", + { + "allowed": [ + "onCancellationRequested", + "event" + ], + "verbs": [ + "accept", + "change", + "close", + "collapse", + "create", + "delete", + "dispose", + "end", + "expand", + "hide", + "open", + "override", + "receive", + "register", + "rename", + "save", + "send", + "start", + "terminate", + "trigger", + "unregister", + "write" + ] + } + ] + } } ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index b0c78892d1f6..e3ad9c874e78 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -95,7 +95,9 @@ "webRoot": "${workspaceFolder}", // Settings for js-debug: "pauseForSourceMap": false, - "outFiles": ["${workspaceFolder}/out/**/*.js"], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], }, { "type": "node", @@ -197,7 +199,7 @@ "type": "node", "request": "launch", "name": "Run Unit Tests", - "program": "${workspaceFolder}/test/electron/index.js", + "program": "${workspaceFolder}/test/unit/electron/index.js", "runtimeExecutable": "${workspaceFolder}/.build/electron/Azure Data Studio.app/Contents/MacOS/Electron", "windows": { "runtimeExecutable": "${workspaceFolder}/.build/electron/azuredatastudio.exe" diff --git a/.vscode/searches/ts36031.code-search b/.vscode/searches/ts36031.code-search index fb6cf8a431d3..a232dfbd3444 100644 --- a/.vscode/searches/ts36031.code-search +++ b/.vscode/searches/ts36031.code-search @@ -1,7 +1,6 @@ # Query: \\w+\\?\\..+![(.[] # Flags: RegExp # ContextLines: 2 - src/vs/base/browser/ui/tree/asyncDataTree.ts: 270 } : undefined, 271 isChecked: options.ariaProvider!.isChecked ? (e) => { diff --git a/.yarnrc b/.yarnrc index 85baaa63a786..7808166004a1 100644 --- a/.yarnrc +++ b/.yarnrc @@ -1,3 +1,3 @@ disturl "https://atom.io/download/electron" -target "6.1.6" +target "7.1.11" runtime "electron" diff --git a/build/azure-pipelines/common/createAsset.ts b/build/azure-pipelines/common/createAsset.ts index 7c41748f570f..12eadf7de06d 100644 --- a/build/azure-pipelines/common/createAsset.ts +++ b/build/azure-pipelines/common/createAsset.ts @@ -44,15 +44,16 @@ async function doesAssetExist(blobService: azure.BlobService, quality: string, b return existsResult.exists; } -async function uploadBlob(blobService: azure.BlobService, quality: string, blobName: string, file: string): Promise { +async function uploadBlob(blobService: azure.BlobService, quality: string, blobName: string, filePath: string, fileName: string): Promise { const blobOptions: azure.BlobService.CreateBlockBlobRequestOptions = { contentSettings: { - contentType: mime.lookup(file), + contentType: mime.lookup(filePath), + contentDisposition: `attachment; filename="${fileName}"`, cacheControl: 'max-age=31536000, public' } }; - await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, file, blobOptions, err => err ? e(err) : c())); + await new Promise((c, e) => blobService.createBlockBlobFromLocalFile(quality, blobName, filePath, blobOptions, err => err ? e(err) : c())); } function getEnv(name: string): string { @@ -66,24 +67,24 @@ function getEnv(name: string): string { } async function main(): Promise { - const [, , platform, type, name, file] = process.argv; + const [, , platform, type, fileName, filePath] = process.argv; const quality = getEnv('VSCODE_QUALITY'); const commit = getEnv('BUILD_SOURCEVERSION'); console.log('Creating asset...'); - const stat = await new Promise((c, e) => fs.stat(file, (err, stat) => err ? e(err) : c(stat))); + const stat = await new Promise((c, e) => fs.stat(filePath, (err, stat) => err ? e(err) : c(stat))); const size = stat.size; console.log('Size:', size); - const stream = fs.createReadStream(file); + const stream = fs.createReadStream(filePath); const [sha1hash, sha256hash] = await Promise.all([hashStream('sha1', stream), hashStream('sha256', stream)]); console.log('SHA1:', sha1hash); console.log('SHA256:', sha256hash); - const blobName = commit + '/' + name; + const blobName = commit + '/' + fileName; const storageAccount = process.env['AZURE_STORAGE_ACCOUNT_2']!; const blobService = azure.createBlobService(storageAccount, process.env['AZURE_STORAGE_ACCESS_KEY_2']!) @@ -98,7 +99,7 @@ async function main(): Promise { console.log('Uploading blobs to Azure storage...'); - await uploadBlob(blobService, quality, blobName, file); + await uploadBlob(blobService, quality, blobName, filePath, fileName); console.log('Blobs successfully uploaded.'); diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index 409f8631af23..afdd39fcaf02 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -44,6 +44,9 @@ steps: - script: | ./scripts/test.sh --tfs "Unit Tests" displayName: Run Unit Tests +- script: | + yarn test-browser --browser chromium --browser webkit + displayName: Run Unit Tests (Browsers) # - script: | {{SQL CARBON EDIT}} remove step # ./scripts/test-integration.sh --tfs "Integration Tests" # displayName: Run Integration Tests diff --git a/build/azure-pipelines/darwin/entitlements.plist b/build/azure-pipelines/darwin/entitlements.plist new file mode 100644 index 000000000000..46f675661149 --- /dev/null +++ b/build/azure-pipelines/darwin/entitlements.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + + diff --git a/build/azure-pipelines/darwin/product-build-darwin.yml b/build/azure-pipelines/darwin/product-build-darwin.yml index 77f4af38c8e6..55f55e0eadeb 100644 --- a/build/azure-pipelines/darwin/product-build-darwin.yml +++ b/build/azure-pipelines/darwin/product-build-darwin.yml @@ -44,6 +44,13 @@ steps: git config user.email "vscode@microsoft.com" git config user.name "VSCode" + + 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 displayName: Prepare tooling - script: | @@ -114,30 +121,25 @@ steps: displayName: Run integration tests condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) -# Web Smoke Tests disabled due to https://github.com/microsoft/vscode/issues/80308 -# - script: | -# set -e -# cd test/smoke -# yarn compile -# cd - -# yarn smoketest --web --headless -# continueOnError: true -# displayName: Run web smoke tests -# condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - - script: | set -e cd test/smoke yarn compile cd - + VSCODE_REMOTE_SERVER_PATH="$(agent.builddirectory)/vscode-reh-web-darwin" \ yarn smoketest --web --headless continueOnError: true - displayName: Run smoke tests + displayName: Run web smoke tests condition: and(succeeded(), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | set -e - pushd ../VSCode-darwin && zip -r -X -y ../VSCode-darwin.zip * && popd + codesign -s 99FM488X57 --deep --force --options runtime --entitlements build/azure-pipelines/darwin/entitlements.plist $(agent.builddirectory)/VSCode-darwin/*.app + displayName: Set Hardened Entitlements + +- script: | + set -e + pushd $(agent.builddirectory)/VSCode-darwin && zip -r -X -y $(agent.builddirectory)/VSCode-darwin.zip * && popd displayName: Archive build - task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 @@ -151,14 +153,54 @@ steps: { "keyCode": "CP-401337-Apple", "operationSetCode": "MacAppDeveloperSign", - "parameters": [ ], + "parameters": [ + { + "parameterName": "Hardening", + "parameterValue": "--options=runtime" + } + ], "toolName": "sign", "toolVersion": "1.0" } ] - SessionTimeout: 120 + SessionTimeout: 60 displayName: Codesign +- script: | + zip -d $(agent.builddirectory)/VSCode-darwin.zip "*.pkg" + displayName: Clean Archive + +- script: | + APP_ROOT=$(agent.builddirectory)/VSCode-darwin + APP_NAME="`ls $APP_ROOT | head -n 1`" + BUNDLE_IDENTIFIER=$(node -p "require(\"$APP_ROOT/$APP_NAME/Contents/Resources/app/product.json\").darwinBundleIdentifier") + echo "##vso[task.setvariable variable=BundleIdentifier]$BUNDLE_IDENTIFIER" + displayName: Export bundle identifier + +- task: SFP.build-tasks.custom-build-task-1.EsrpCodeSigning@1 + inputs: + ConnectedServiceName: 'ESRP CodeSign' + FolderPath: '$(agent.builddirectory)' + Pattern: 'VSCode-darwin.zip' + signConfigType: inlineSignParams + inlineOperation: | + [ + { + "keyCode": "CP-401337-Apple", + "operationSetCode": "MacAppNotarize", + "parameters": [ + { + "parameterName": "BundleId", + "parameterValue": "$(BundleIdentifier)" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + } + ] + SessionTimeout: 120 + displayName: Notarization + - script: | set -e VSCODE_MIXIN_PASSWORD="$(github-distro-mixin-password)" \ diff --git a/build/azure-pipelines/darwin/publish.sh b/build/azure-pipelines/darwin/publish.sh index a8067a5eefb9..58f110c5df54 100755 --- a/build/azure-pipelines/darwin/publish.sh +++ b/build/azure-pipelines/darwin/publish.sh @@ -1,9 +1,6 @@ #!/usr/bin/env bash set -e -# remove pkg from archive -zip -d ../VSCode-darwin.zip "*.pkg" - # publish the build node build/azure-pipelines/common/createAsset.js \ darwin \ diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index a661b9e5d5f5..e5349d097d4d 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -52,6 +52,9 @@ steps: - script: | DISPLAY=:10 ./scripts/test.sh --tfs "Unit Tests" displayName: Run Unit Tests +- script: | + DISPLAY=:10 yarn test-browser --browser chromium + displayName: Run Unit Tests (Browser) # - script: | {{SQL CARBON EDIT}} remove step # DISPLAY=:10 ./scripts/test-integration.sh --tfs "Integration Tests" # displayName: Run Integration Tests diff --git a/build/azure-pipelines/linux/xvfb.init b/build/azure-pipelines/linux/xvfb.init index 4d77d253a264..2365c09f3a46 100644 --- a/build/azure-pipelines/linux/xvfb.init +++ b/build/azure-pipelines/linux/xvfb.init @@ -19,7 +19,7 @@ [ "${NETWORKING}" = "no" ] && exit 0 PROG="/usr/bin/Xvfb" -PROG_OPTIONS=":10 -ac" +PROG_OPTIONS=":10 -ac -screen 0 1024x768x24" PROG_OUTPUT="/tmp/Xvfb.out" case "$1" in @@ -50,4 +50,4 @@ case "$1" in exit 1 esac -exit $RETVAL \ No newline at end of file +exit $RETVAL diff --git a/build/azure-pipelines/win32/ESRPClient/packages.config b/build/azure-pipelines/win32/ESRPClient/packages.config index d7a6f144f477..c10bed141215 100644 --- a/build/azure-pipelines/win32/ESRPClient/packages.config +++ b/build/azure-pipelines/win32/ESRPClient/packages.config @@ -1,4 +1,4 @@ - + diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index a7938ffb4774..fc80b0bef1d4 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -49,6 +49,9 @@ steps: - powershell: | .\scripts\test.bat --tfs "Unit Tests" displayName: Run Unit Tests +- powershell: | + yarn test-browser --browser chromium --browser webkit + displayName: Run Unit Tests (Browser) # - powershell: | {{SQL CARBON EDIT}} remove step # .\scripts\test-integration.bat --tfs "Integration Tests" # displayName: Run Integration Tests diff --git a/build/azure-pipelines/win32/sign.ps1 b/build/azure-pipelines/win32/sign.ps1 index 00c4d42d9dbf..840cbe4071f8 100644 --- a/build/azure-pipelines/win32/sign.ps1 +++ b/build/azure-pipelines/win32/sign.ps1 @@ -67,4 +67,4 @@ $Input = Create-TmpJson @{ $Output = [System.IO.Path]::GetTempFileName() $ScriptPath = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent -& "$ScriptPath\ESRPClient\packages\EsrpClient.1.0.27\tools\ESRPClient.exe" Sign -a $Auth -p $Policy -i $Input -o $Output \ No newline at end of file +& "$ScriptPath\ESRPClient\packages\Microsoft.ESRPClient.1.2.25\tools\ESRPClient.exe" Sign -a $Auth -p $Policy -i $Input -o $Output diff --git a/build/gulpfile.editor.js b/build/gulpfile.editor.js index 993db9099903..188b5df6193a 100644 --- a/build/gulpfile.editor.js +++ b/build/gulpfile.editor.js @@ -227,8 +227,13 @@ function toExternalDTS(contents) { if (line.indexOf('declare namespace monaco.') === 0) { lines[i] = line.replace('declare namespace monaco.', 'export namespace '); } + + if (line.indexOf('declare let MonacoEnvironment') === 0) { + lines[i] = `declare global {\n let MonacoEnvironment: Environment | undefined;\n}`; + // lines[i] = line.replace('declare namespace monaco.', 'export namespace '); + } } - return lines.join('\n'); + return lines.join('\n').replace(/\n\n\n+/g, '\n\n'); } function filterStream(testFunc) { diff --git a/build/gulpfile.hygiene.js b/build/gulpfile.hygiene.js index 4d36ca661cf5..429d545a8af4 100644 --- a/build/gulpfile.hygiene.js +++ b/build/gulpfile.hygiene.js @@ -33,6 +33,7 @@ const all = [ 'scripts/**/*', 'src/**/*', 'test/**/*', + '!test/**/out/**', '!**/node_modules/**' ]; @@ -53,8 +54,7 @@ const indentationFilter = [ '!src/vs/base/common/marked/marked.js', '!src/vs/base/node/terminateProcess.sh', '!src/vs/base/node/cpuUsage.sh', - '!test/assert.js', - '!build/testSetup.js', + '!test/unit/assert.js', // except specific folders '!test/automation/out/**', @@ -84,7 +84,7 @@ const indentationFilter = [ '!src/vs/*/**/*.d.ts', '!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}', + '!**/*.{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/**/*.sh', '!build/azure-pipelines/**/*.js', diff --git a/build/lib/eslint/vscode-dts-create-func.js b/build/lib/eslint/vscode-dts-create-func.js new file mode 100644 index 000000000000..facc0a2bd2d5 --- /dev/null +++ b/build/lib/eslint/vscode-dts-create-func.js @@ -0,0 +1,35 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +module.exports = new class ApiLiteralOrTypes { + constructor() { + this.meta = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#creating-objects' }, + messages: { sync: '`createXYZ`-functions are constructor-replacements and therefore must return sync', } + }; + } + create(context) { + return { + ['TSDeclareFunction Identifier[name=/create.*/]']: (node) => { + var _a; + const decl = node.parent; + if (((_a = decl.returnType) === null || _a === void 0 ? void 0 : _a.typeAnnotation.type) !== experimental_utils_1.AST_NODE_TYPES.TSTypeReference) { + return; + } + if (decl.returnType.typeAnnotation.typeName.type !== experimental_utils_1.AST_NODE_TYPES.Identifier) { + return; + } + const ident = decl.returnType.typeAnnotation.typeName.name; + if (ident === 'Promise' || ident === 'Thenable') { + context.report({ + node, + messageId: 'sync' + }); + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-create-func.ts b/build/lib/eslint/vscode-dts-create-func.ts new file mode 100644 index 000000000000..1d93dc4dfb04 --- /dev/null +++ b/build/lib/eslint/vscode-dts-create-func.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; + +export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#creating-objects' }, + messages: { sync: '`createXYZ`-functions are constructor-replacements and therefore must return sync', } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + return { + ['TSDeclareFunction Identifier[name=/create.*/]']: (node: any) => { + + const decl = (node).parent; + + if (decl.returnType?.typeAnnotation.type !== AST_NODE_TYPES.TSTypeReference) { + return; + } + if (decl.returnType.typeAnnotation.typeName.type !== AST_NODE_TYPES.Identifier) { + return; + } + + const ident = decl.returnType.typeAnnotation.typeName.name; + if (ident === 'Promise' || ident === 'Thenable') { + context.report({ + node, + messageId: 'sync' + }); + } + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-event-naming.js b/build/lib/eslint/vscode-dts-event-naming.js new file mode 100644 index 000000000000..0b399e9539f9 --- /dev/null +++ b/build/lib/eslint/vscode-dts-event-naming.js @@ -0,0 +1,81 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +const experimental_utils_1 = require("@typescript-eslint/experimental-utils"); +module.exports = new (_a = class ApiEventNaming { + constructor() { + this.meta = { + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#event-naming' + }, + messages: { + naming: 'Event names must follow this patten: `on[Did|Will]`', + verb: 'Unknown verb \'{{verb}}\' - is this really a verb? Iff so, then add this verb to the configuration', + subject: 'Unknown subject \'{{subject}}\' - This subject has not been used before but it should refer to something in the API', + unknown: 'UNKNOWN event declaration, lint-rule needs tweaking' + } + }; + } + create(context) { + const config = context.options[0]; + const allowed = new Set(config.allowed); + const verbs = new Set(config.verbs); + return { + ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node) => { + var _a, _b; + const def = (_b = (_a = node.parent) === null || _a === void 0 ? void 0 : _a.parent) === null || _b === void 0 ? void 0 : _b.parent; + let ident; + if ((def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.Identifier) { + ident = def; + } + else if (((def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.TSPropertySignature || (def === null || def === void 0 ? void 0 : def.type) === experimental_utils_1.AST_NODE_TYPES.ClassProperty) && def.key.type === experimental_utils_1.AST_NODE_TYPES.Identifier) { + ident = def.key; + } + if (!ident) { + // event on unknown structure... + return context.report({ + node, + message: 'unknown' + }); + } + if (allowed.has(ident.name)) { + // configured exception + return; + } + const match = ApiEventNaming._nameRegExp.exec(ident.name); + if (!match) { + context.report({ + node: ident, + messageId: 'naming' + }); + return; + } + // check that is spelled out (configured) as verb + if (!verbs.has(match[2].toLowerCase())) { + context.report({ + node: ident, + messageId: 'verb', + data: { verb: match[2] } + }); + } + // check that a subject (if present) has occurred + if (match[3]) { + const regex = new RegExp(match[3], 'ig'); + const parts = context.getSourceCode().getText().split(regex); + if (parts.length < 3) { + context.report({ + node: ident, + messageId: 'subject', + data: { subject: match[3] } + }); + } + } + } + }; + } + }, + _a._nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/, + _a); diff --git a/build/lib/eslint/vscode-dts-event-naming.ts b/build/lib/eslint/vscode-dts-event-naming.ts new file mode 100644 index 000000000000..6e235626287f --- /dev/null +++ b/build/lib/eslint/vscode-dts-event-naming.ts @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree, AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; + +export = new class ApiEventNaming implements eslint.Rule.RuleModule { + + private static _nameRegExp = /on(Did|Will)([A-Z][a-z]+)([A-Z][a-z]+)?/; + + readonly meta: eslint.Rule.RuleMetaData = { + docs: { + url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#event-naming' + }, + messages: { + naming: 'Event names must follow this patten: `on[Did|Will]`', + verb: 'Unknown verb \'{{verb}}\' - is this really a verb? Iff so, then add this verb to the configuration', + subject: 'Unknown subject \'{{subject}}\' - This subject has not been used before but it should refer to something in the API', + unknown: 'UNKNOWN event declaration, lint-rule needs tweaking' + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + const config = <{ allowed: string[], verbs: string[] }>context.options[0]; + const allowed = new Set(config.allowed); + const verbs = new Set(config.verbs); + + return { + ['TSTypeAnnotation TSTypeReference Identifier[name="Event"]']: (node: any) => { + + const def = (node).parent?.parent?.parent; + let ident: TSESTree.Identifier | undefined; + + if (def?.type === AST_NODE_TYPES.Identifier) { + ident = def; + + } else if ((def?.type === AST_NODE_TYPES.TSPropertySignature || def?.type === AST_NODE_TYPES.ClassProperty) && def.key.type === AST_NODE_TYPES.Identifier) { + ident = def.key; + } + + if (!ident) { + // event on unknown structure... + return context.report({ + node, + message: 'unknown' + }); + } + + if (allowed.has(ident.name)) { + // configured exception + return; + } + + const match = ApiEventNaming._nameRegExp.exec(ident.name); + if (!match) { + context.report({ + node: ident, + messageId: 'naming' + }); + return; + } + + // check that is spelled out (configured) as verb + if (!verbs.has(match[2].toLowerCase())) { + context.report({ + node: ident, + messageId: 'verb', + data: { verb: match[2] } + }); + } + + // check that a subject (if present) has occurred + if (match[3]) { + const regex = new RegExp(match[3], 'ig'); + const parts = context.getSourceCode().getText().split(regex); + if (parts.length < 3) { + context.report({ + node: ident, + messageId: 'subject', + data: { subject: match[3] } + }); + } + } + } + }; + } +}; + diff --git a/build/lib/eslint/vscode-dts-interface-naming.js b/build/lib/eslint/vscode-dts-interface-naming.js new file mode 100644 index 000000000000..4b63aa203c7f --- /dev/null +++ b/build/lib/eslint/vscode-dts-interface-naming.js @@ -0,0 +1,30 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +var _a; +module.exports = new (_a = class ApiInterfaceNaming { + constructor() { + this.meta = { + messages: { + naming: 'Interfaces must not be prefixed with uppercase `I`', + } + }; + } + create(context) { + return { + ['TSInterfaceDeclaration Identifier']: (node) => { + const name = node.name; + if (ApiInterfaceNaming._nameRegExp.test(name)) { + context.report({ + node, + messageId: 'naming' + }); + } + } + }; + } + }, + _a._nameRegExp = /I[A-Z]/, + _a); diff --git a/build/lib/eslint/vscode-dts-interface-naming.ts b/build/lib/eslint/vscode-dts-interface-naming.ts new file mode 100644 index 000000000000..01f18b621caf --- /dev/null +++ b/build/lib/eslint/vscode-dts-interface-naming.ts @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; +import { TSESTree } from '@typescript-eslint/experimental-utils'; + +export = new class ApiInterfaceNaming implements eslint.Rule.RuleModule { + + private static _nameRegExp = /I[A-Z]/; + + readonly meta: eslint.Rule.RuleMetaData = { + messages: { + naming: 'Interfaces must not be prefixed with uppercase `I`', + } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + + return { + ['TSInterfaceDeclaration Identifier']: (node: any) => { + + const name = (node).name; + if (ApiInterfaceNaming._nameRegExp.test(name)) { + context.report({ + node, + messageId: 'naming' + }); + } + } + }; + } +}; + diff --git a/build/lib/eslint/vscode-dts-literal-or-types.js b/build/lib/eslint/vscode-dts-literal-or-types.js new file mode 100644 index 000000000000..b2aadf2bf991 --- /dev/null +++ b/build/lib/eslint/vscode-dts-literal-or-types.js @@ -0,0 +1,23 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +module.exports = new class ApiLiteralOrTypes { + constructor() { + this.meta = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#enums' }, + messages: { useEnum: 'Use enums, not literal-or-types', } + }; + } + create(context) { + return { + ['TSTypeAnnotation TSUnionType TSLiteralType']: (node) => { + context.report({ + node: node, + messageId: 'useEnum' + }); + } + }; + } +}; diff --git a/build/lib/eslint/vscode-dts-literal-or-types.ts b/build/lib/eslint/vscode-dts-literal-or-types.ts new file mode 100644 index 000000000000..5258bfd4552d --- /dev/null +++ b/build/lib/eslint/vscode-dts-literal-or-types.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as eslint from 'eslint'; + +export = new class ApiLiteralOrTypes implements eslint.Rule.RuleModule { + + readonly meta: eslint.Rule.RuleMetaData = { + docs: { url: 'https://github.com/microsoft/vscode/wiki/Extension-API-guidelines#enums' }, + messages: { useEnum: 'Use enums, not literal-or-types', } + }; + + create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener { + return { + ['TSTypeAnnotation TSUnionType TSLiteralType']: (node: any) => { + context.report({ + node: node, + messageId: 'useEnum' + }); + } + }; + } +}; diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 174083dac88b..31446bfe4d92 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -142,6 +142,10 @@ "name": "vs/workbench/contrib/search", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/searchEditor", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/snippets", "project": "vscode-workbench" @@ -321,6 +325,10 @@ { "name": "vs/workbench/services/userDataSync", "project": "vscode-workbench" + }, + { + "name": "vs/workbench/contrib/timeline", + "project": "vscode-workbench" } ] } diff --git a/build/lib/layersChecker.js b/build/lib/layersChecker.js index 7a0e12a61599..d093dfda8cb3 100644 --- a/build/lib/layersChecker.js +++ b/build/lib/layersChecker.js @@ -38,6 +38,7 @@ const CORE_TYPES = [ 'group', 'groupEnd', 'table', + 'assert', 'Error', 'String', 'throws', @@ -60,7 +61,7 @@ const RULES = [ }, // Common: vs/base/common/platform.ts { - target: '**/vs/base/common/platform.ts', + target: '**/{vs,sql}/base/common/platform.ts', allowedTypes: [ ...CORE_TYPES, // Safe access to postMessage() and friends @@ -74,7 +75,7 @@ const RULES = [ }, // Common: vs/workbench/api/common/extHostExtensionService.ts { - target: '**/vs/workbench/api/common/extHostExtensionService.ts', + target: '**/{vs,sql}/workbench/api/common/extHostExtensionService.ts', allowedTypes: [ ...CORE_TYPES, // Safe access to global @@ -102,6 +103,14 @@ const RULES = [ '@types/node' // no node.js ] }, + // Browser (editor contrib) + { + target: '**/src/{vs,sql}/editor/contrib/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, // node.js { target: '**/{vs,sql}/**/node/**', diff --git a/build/lib/layersChecker.ts b/build/lib/layersChecker.ts index 827064c5015a..6c52cab5b2e5 100644 --- a/build/lib/layersChecker.ts +++ b/build/lib/layersChecker.ts @@ -39,6 +39,7 @@ const CORE_TYPES = [ 'group', 'groupEnd', 'table', + 'assert', 'Error', 'String', 'throws', @@ -64,7 +65,7 @@ const RULES = [ // Common: vs/base/common/platform.ts { - target: '**/vs/base/common/platform.ts', + target: '**/{vs,sql}/base/common/platform.ts', allowedTypes: [ ...CORE_TYPES, @@ -80,7 +81,7 @@ const RULES = [ // Common: vs/workbench/api/common/extHostExtensionService.ts { - target: '**/vs/workbench/api/common/extHostExtensionService.ts', + target: '**/{vs,sql}/workbench/api/common/extHostExtensionService.ts', allowedTypes: [ ...CORE_TYPES, @@ -112,6 +113,15 @@ const RULES = [ ] }, + // Browser (editor contrib) + { + target: '**/src/{vs,sql}/editor/contrib/**', + allowedTypes: CORE_TYPES, + disallowedDefinitions: [ + '@types/node' // no node.js + ] + }, + // node.js { target: '**/{vs,sql}/**/node/**', diff --git a/build/testSetup.js b/build/lib/testSetup.js similarity index 100% rename from build/testSetup.js rename to build/lib/testSetup.js diff --git a/build/testSetup.ts b/build/lib/testSetup.ts similarity index 100% rename from build/testSetup.ts rename to build/lib/testSetup.ts diff --git a/build/monaco/monaco.d.ts.recipe b/build/monaco/monaco.d.ts.recipe index 1f90daf38e23..e7129d5ec080 100644 --- a/build/monaco/monaco.d.ts.recipe +++ b/build/monaco/monaco.d.ts.recipe @@ -3,12 +3,18 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -declare namespace monaco { +declare let MonacoEnvironment: monaco.Environment | undefined; - // THIS IS A GENERATED FILE. DO NOT EDIT DIRECTLY. +declare namespace monaco { export type Thenable = PromiseLike; + export interface Environment { + baseUrl?: string; + getWorker?(workerId: string, label: string): Worker; + getWorkerUrl?(workerId: string, label: string): string; + } + export interface IDisposable { dispose(): void; } diff --git a/build/npm/postinstall.js b/build/npm/postinstall.js index 86f41ba3dc72..f202636c87f5 100644 --- a/build/npm/postinstall.js +++ b/build/npm/postinstall.js @@ -72,4 +72,5 @@ runtime "${runtime}"`; yarnInstall(`build`); // node modules required for build yarnInstall('test/automation'); // node modules required for smoketest yarnInstall('test/smoke'); // node modules required for smoketest +yarnInstall('test/integration/browser'); // node modules required for integration yarnInstallBuildDependencies(); // node modules for watching, specific to host node version, not electron diff --git a/build/package.json b/build/package.json index d005a4f4413e..b8eb76f4e2b9 100644 --- a/build/package.json +++ b/build/package.json @@ -50,7 +50,7 @@ "rollup-plugin-node-resolve": "^5.2.0", "service-downloader": "github:anthonydresser/service-downloader#0.1.7", "terser": "4.3.8", - "typescript": " 3.8.0-beta", + "typescript": "^3.8.1-rc", "vsce": "1.48.0", "vscode-telemetry-extractor": "^1.5.4", "xml2js": "^0.4.17" diff --git a/build/yarn.lock b/build/yarn.lock index b58fe6a1c858..60d5a6dd6278 100644 --- a/build/yarn.lock +++ b/build/yarn.lock @@ -3823,16 +3823,16 @@ typed-rest-client@^0.9.0: tunnel "0.0.4" underscore "1.8.3" -"typescript@ 3.8.0-beta": - version "3.8.0-beta" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.0-beta.tgz#acdcaf9f24c7e20b1ff0a6329d1e8d63691e2e13" - integrity sha512-mQEmQUJg0CQBhf/GSVnGscKv/jrKsrLxE01AhdjYmBNoXX2Iah3i38ufxXByXacK6Fc5Nr9oMz7MjpjgddiknA== - typescript@^3.0.1: version "3.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.5.3.tgz#c830f657f93f1ea846819e929092f5fe5983e977" integrity sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g== +typescript@^3.8.1-rc: + version "3.8.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.1-rc.tgz#f94333c14da70927ccd887be2e91be652a9a09f6" + integrity sha512-aOIe066DyZn2uYIiND6fXMUUJ70nxwu/lKhA92QuQzXyC86fr0ywo1qvO8l2m0EnDcfjprYPuFRgNgDj7U2GlQ== + typical@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" diff --git a/cgmanifest.json b/cgmanifest.json index c102a04f7054..40d6e42aa783 100644 --- a/cgmanifest.json +++ b/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "chromium", "repositoryUrl": "https://chromium.googlesource.com/chromium/src", - "commitHash": "91f08db83c2ce8c722ddf0911ead8f7c473bedfa" + "commitHash": "e4745133a1d3745f066e068b8033c6a269b59caf" } }, "licenseDetail": [ @@ -40,7 +40,7 @@ "SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." ], "isOnlyProductionDependency": true, - "version": "76.0.3809.146" + "version": "78.0.3904.130" }, { "component": { @@ -48,11 +48,11 @@ "git": { "name": "nodejs", "repositoryUrl": "https://github.com/nodejs/node", - "commitHash": "64219741218aa87e259cf8257596073b8e747f0a" + "commitHash": "787378879acfb212ed4ff824bf9f767a24a5cb43a" } }, "isOnlyProductionDependency": true, - "version": "12.4.0" + "version": "12.8.1" }, { "component": { @@ -60,12 +60,12 @@ "git": { "name": "electron", "repositoryUrl": "https://github.com/electron/electron", - "commitHash": "19c705ab80cd6fdccca3d65803ec2c4addb9540a" + "commitHash": "d17dfabfcba7bd0bc994b8dac5f5d2000bef572c" } }, "isOnlyProductionDependency": true, "license": "MIT", - "version": "6.1.6" + "version": "7.1.11" }, { "component": { @@ -98,7 +98,7 @@ "git": { "name": "vscode-codicons", "repositoryUrl": "https://github.com/microsoft/vscode-codicons", - "commitHash": "65d11e0839d0ce09faa1a159dc0b3c0bd1aa50be" + "commitHash": "f0caa623812a8ed5059516277675b4158d4c4867" } }, "license": "MIT and Creative Commons Attribution 4.0", diff --git a/extensions/configuration-editing/schemas/attachContainer.schema.json b/extensions/configuration-editing/schemas/attachContainer.schema.json index 58e59d72ee0d..015e32e8018f 100644 --- a/extensions/configuration-editing/schemas/attachContainer.schema.json +++ b/extensions/configuration-editing/schemas/attachContainer.schema.json @@ -2,6 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "description": "Configures an attached to container", "allowComments": true, + "allowTrailingCommas": true, "type": "object", "definitions": { "attachContainer": { diff --git a/extensions/configuration-editing/schemas/devContainer.schema.json b/extensions/configuration-editing/schemas/devContainer.schema.json index 292fe629937e..0d47b27bec95 100644 --- a/extensions/configuration-editing/schemas/devContainer.schema.json +++ b/extensions/configuration-editing/schemas/devContainer.schema.json @@ -2,6 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "description": "Defines a dev container", "allowComments": true, + "allowTrailingCommas": true, "type": "object", "definitions": { "devContainerCommon": { @@ -28,7 +29,9 @@ "type": "array", "description": "Ports that are forwarded from the container to the local machine.", "items": { - "type": "integer" + "type": "integer", + "maximum": 65535, + "minimum": 0 } }, "remoteEnv": { diff --git a/extensions/git/package.json b/extensions/git/package.json index 2ba6515e963f..c1cd8b5135c0 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -26,6 +26,11 @@ }, "contributes": { "commands": [ + { + "command": "git.setLogLevel", + "title": "%command.setLogLevel%", + "category": "Git" + }, { "command": "git.clone", "title": "%command.clone%", @@ -1760,6 +1765,7 @@ }, "dependencies": { "byline": "^5.0.0", + "dayjs": "1.8.19", "file-type": "^7.2.0", "iconv-lite": "^0.4.24", "jschardet": "2.1.1", @@ -1774,6 +1780,9 @@ "@types/mocha": "2.2.43", "@types/node": "^12.11.7", "@types/which": "^1.0.28", - "mocha": "^3.2.0" + "mocha": "^3.2.0", + "mocha-junit-reporter": "^1.23.3", + "mocha-multi-reporters": "^1.1.7", + "vscode": "^1.1.36" } } diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 5c357b40eb07..f3d755196534 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -1,6 +1,7 @@ { "displayName": "Git", "description": "Git SCM Integration", + "command.setLogLevel": "Set Log Level...", "command.clone": "Clone", "command.init": "Initialize Repository", "command.openRepository": "Open Repository", diff --git a/extensions/git/src/api/api1.ts b/extensions/git/src/api/api1.ts index d5d2cb00118c..03318f6481c5 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 } from './git'; +import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions } from './git'; import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode'; import { mapEvent } from '../util'; import { toGitUri } from '../uri'; @@ -202,6 +202,10 @@ export class ApiRepository implements Repository { log(options?: LogOptions): Promise { return this._repository.log(options); } + + commit(message: string, opts?: CommitOptions): Promise { + return this._repository.commit(message, opts); + } } export class ApiGit implements Git { diff --git a/extensions/git/src/api/git.d.ts b/extensions/git/src/api/git.d.ts index 16ec429152eb..72b986ee7b73 100644 --- a/extensions/git/src/api/git.d.ts +++ b/extensions/git/src/api/git.d.ts @@ -41,7 +41,9 @@ export interface Commit { readonly hash: string; readonly message: string; readonly parents: string[]; - readonly authorEmail?: string | undefined; + readonly authorDate?: Date; + readonly authorName?: string; + readonly authorEmail?: string; } export interface Submodule { @@ -119,6 +121,14 @@ export interface LogOptions { readonly maxEntries?: number; } +export interface CommitOptions { + all?: boolean | 'tracked'; + amend?: boolean; + signoff?: boolean; + signCommit?: boolean; + empty?: boolean; +} + export interface Repository { readonly rootUri: Uri; @@ -174,6 +184,8 @@ export interface Repository { blame(path: string): Promise; log(options?: LogOptions): Promise; + + commit(message: string, opts?: CommitOptions): Promise; } export type APIState = 'uninitialized' | 'initialized'; diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 6771bda76037..6264b22232d6 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -9,13 +9,14 @@ import * as path from 'path'; import { commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder } from 'vscode'; import TelemetryReporter from 'vscode-extension-telemetry'; import * as nls from 'vscode-nls'; -import { Branch, GitErrorCodes, Ref, RefType, Status } from './api/git'; -import { CommitOptions, ForcePushMode, Git, Stash } from './git'; +import { Branch, GitErrorCodes, Ref, RefType, Status, CommitOptions } from './api/git'; +import { ForcePushMode, Git, Stash } from './git'; import { Model } from './model'; import { Repository, Resource, ResourceGroupType } from './repository'; import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging'; import { fromGitUri, toGitUri, isGitUri } from './uri'; import { grep, isDescendant, pathEquals } from './util'; +import { Log, LogLevel } from './log'; const localize = nls.loadMessageBundle(); @@ -252,6 +253,36 @@ export class CommandCenter { }); } + @command('git.setLogLevel') + async setLogLevel(): Promise { + const createItem = (logLevel: LogLevel) => ({ + label: LogLevel[logLevel], + logLevel, + description: Log.logLevel === logLevel ? localize('current', "Current") : undefined + }); + + const items = [ + createItem(LogLevel.Trace), + createItem(LogLevel.Debug), + createItem(LogLevel.Info), + createItem(LogLevel.Warning), + createItem(LogLevel.Error), + createItem(LogLevel.Critical), + createItem(LogLevel.Off) + ]; + + const choice = await window.showQuickPick(items, { + placeHolder: localize('select log level', "Select log level") + }); + + if (!choice) { + return; + } + + Log.logLevel = choice.logLevel; + this.outputChannel.appendLine(localize('changed', "Log level changed to: {0}", LogLevel[Log.logLevel])); + } + @command('git.refresh', { repository: true }) async refresh(repository: Repository): Promise { await repository.status(); @@ -1292,6 +1323,9 @@ export class CommandCenter { } const enableSmartCommit = config.get('enableSmartCommit') === true; + const enableCommitSigning = config.get('enableCommitSigning') === true; + const noStagedChanges = repository.indexGroup.resourceStates.length === 0; + const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; if (promptToSaveFilesBeforeCommit !== 'never') { let documents = workspace.textDocuments @@ -1312,17 +1346,13 @@ export class CommandCenter { if (pick === saveAndCommit) { await Promise.all(documents.map(d => d.save())); - await repository.add([]); + await repository.add(documents.map(d => d.uri)); } else if (pick !== commit) { return false; // do not commit on cancel } } } - const enableCommitSigning = config.get('enableCommitSigning') === true; - const noStagedChanges = repository.indexGroup.resourceStates.length === 0; - const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0; - // no changes, and the user has not configured to commit all in this case if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit) { const suggestSmartCommit = config.get('suggestSmartCommit') === true; @@ -1590,7 +1620,7 @@ export class CommandCenter { const rawBranchName = defaultName || await window.showInputBox({ placeHolder: localize('branch name', "Branch name"), - prompt: localize('provide branch name', "Please provide a branch name"), + prompt: localize('provide branch name', "Please provide a new branch name"), value: initialValue, ignoreFocusOut: true, validateInput: (name: string) => { @@ -2301,6 +2331,23 @@ export class CommandCenter { return result && result.stash; } + @command('git.openDiff', { repository: false }) + async openDiff(uri: Uri, lhs: string, rhs: string) { + const basename = path.basename(uri.fsPath); + + let title; + if ((lhs === 'HEAD' || lhs === '~') && rhs === '') { + title = `${basename} (Working Tree)`; + } + else if (lhs === 'HEAD' && rhs === '~') { + title = `${basename} (Index)`; + } else { + title = `${basename} (${lhs.endsWith('^') ? `${lhs.substr(0, 8)}^` : lhs.substr(0, 8)}) \u27f7 ${basename} (${rhs.endsWith('^') ? `${rhs.substr(0, 8)}^` : rhs.substr(0, 8)})`; + } + + return commands.executeCommand('vscode.diff', toGitUri(uri, lhs), rhs === '' ? uri : toGitUri(uri, rhs), title); + } + private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any { const result = (...args: any[]) => { let result: Promise; diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 6be990e828b8..19567d741bce 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -12,10 +12,10 @@ import { EventEmitter } from 'events'; import iconv = require('iconv-lite'); import * as filetype from 'file-type'; import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util'; -import { CancellationToken, Progress } from 'vscode'; +import { CancellationToken, Progress, Uri } from 'vscode'; import { URI } from 'vscode-uri'; import { detectEncoding } from './encoding'; -import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git'; +import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions } from './api/git'; import * as byline from 'byline'; import { StringDecoder } from 'string_decoder'; @@ -45,6 +45,15 @@ interface MutableRemote extends Remote { isReadOnly: boolean; } +// TODO[ECA]: Move to git.d.ts once we are good with the api +/** + * Log file options. + */ +export interface LogFileOptions { + /** Max number of log entries to retrieve. If not specified, the default is 32. */ + readonly maxEntries?: number; +} + function parseVersion(raw: string): string { return raw.replace(/^git version /, ''); } @@ -318,7 +327,13 @@ function getGitErrorCode(stderr: string): string | undefined { return undefined; } -const COMMIT_FORMAT = '%H\n%ae\n%P\n%B'; +// https://github.com/microsoft/vscode/issues/89373 +// https://github.com/git-for-windows/git/issues/2478 +function sanitizePath(path: string): string { + return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`); +} + +const COMMIT_FORMAT = '%H\n%aN\n%aE\n%at\n%P\n%B'; export class Git { @@ -487,6 +502,10 @@ export class Git { LANG: 'en_US.UTF-8' }); + if (options.cwd) { + options.cwd = sanitizePath(options.cwd); + } + if (options.log !== false) { this.log(`> git ${args.join(' ')}\n`); } @@ -503,7 +522,9 @@ export interface Commit { hash: string; message: string; parents: string[]; - authorEmail?: string | undefined; + authorDate?: Date; + authorName?: string; + authorEmail?: string; } export class GitStatusParser { @@ -634,14 +655,43 @@ export function parseGitmodules(raw: string): Submodule[] { return result; } -export function parseGitCommit(raw: string): Commit | null { - const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim()); - if (!match) { - return null; - } +const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm; + +export function parseGitCommits(data: string): Commit[] { + let commits: Commit[] = []; + + let ref; + let name; + let email; + let date; + let parents; + let message; + let match; - const parents = match[3] ? match[3].split(' ') : []; - return { hash: match[1], message: match[5], parents, authorEmail: match[2] }; + do { + match = commitRegex.exec(data); + if (match === null) { + break; + } + + [, ref, name, email, date, parents, message] = match; + + if (message[message.length - 1] === '\n') { + message = message.substr(0, message.length - 1); + } + + // Stop excessive memory usage by using substr -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + commits.push({ + hash: ` ${ref}`.substr(1), + message: ` ${message}`.substr(1), + parents: parents ? parents.split(' ') : [], + authorDate: new Date(Number(date) * 1000), + authorName: ` ${name}`.substr(1), + authorEmail: ` ${email}`.substr(1) + }); + } while (true); + + return commits; } interface LsTreeElement { @@ -675,14 +725,6 @@ export function parseLsFiles(raw: string): LsFilesElement[] { .map(([, mode, object, stage, file]) => ({ mode, object, stage, file })); } -export interface CommitOptions { - all?: boolean | 'tracked'; - amend?: boolean; - signoff?: boolean; - signCommit?: boolean; - empty?: boolean; -} - export interface PullOptions { unshallow?: boolean; tags?: boolean; @@ -760,38 +802,28 @@ export class Repository { async log(options?: LogOptions): Promise { const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32; - const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`]; + const args = ['log', '-' + maxEntries, `--format:${COMMIT_FORMAT}`, '-z']; - const gitResult = await this.run(args); - if (gitResult.exitCode) { + const result = await this.run(args); + if (result.exitCode) { // An empty repo return []; } - const s = gitResult.stdout; - const result: Commit[] = []; - let index = 0; - while (index < s.length) { - let nextIndex = s.indexOf('\x00\x00', index); - if (nextIndex === -1) { - nextIndex = s.length; - } + return parseGitCommits(result.stdout); + } - let entry = s.substr(index, nextIndex - index); - if (entry.startsWith('\n')) { - entry = entry.substring(1); - } + async logFile(uri: Uri, options?: LogFileOptions): Promise { + const maxEntries = options?.maxEntries ?? 32; + const args = ['log', `-${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--', uri.fsPath]; - const commit = parseGitCommit(entry); - if (!commit) { - break; - } - - result.push(commit); - index = nextIndex + 2; + const result = await this.run(args); + if (result.exitCode) { + // No file history, e.g. a new file or untracked + return []; } - return result; + return parseGitCommits(result.stdout); } async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise { @@ -857,12 +889,12 @@ export class Repository { } async lstree(treeish: string, path: string): Promise { - const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]); + const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', sanitizePath(path)]); return parseLsTree(stdout); } async lsfiles(path: string): Promise { - const { stdout } = await this.run(['ls-files', '--stage', '--', path]); + const { stdout } = await this.run(['ls-files', '--stage', '--', sanitizePath(path)]); return parseLsFiles(stdout); } @@ -956,7 +988,7 @@ export class Repository { return await this.diffFiles(false); } - const args = ['diff', '--', path]; + const args = ['diff', '--', sanitizePath(path)]; const result = await this.run(args); return result.stdout; } @@ -969,7 +1001,7 @@ export class Repository { return await this.diffFiles(false, ref); } - const args = ['diff', ref, '--', path]; + const args = ['diff', ref, '--', sanitizePath(path)]; const result = await this.run(args); return result.stdout; } @@ -982,7 +1014,7 @@ export class Repository { return await this.diffFiles(true); } - const args = ['diff', '--cached', '--', path]; + const args = ['diff', '--cached', '--', sanitizePath(path)]; const result = await this.run(args); return result.stdout; } @@ -995,7 +1027,7 @@ export class Repository { return await this.diffFiles(true, ref); } - const args = ['diff', '--cached', ref, '--', path]; + const args = ['diff', '--cached', ref, '--', sanitizePath(path)]; const result = await this.run(args); return result.stdout; } @@ -1015,7 +1047,7 @@ export class Repository { return await this.diffFiles(false, range); } - const args = ['diff', range, '--', path]; + const args = ['diff', range, '--', sanitizePath(path)]; const result = await this.run(args); return result.stdout.trim(); @@ -1128,7 +1160,7 @@ export class Repository { args.push('--'); if (paths && paths.length) { - args.push.apply(args, paths); + args.push.apply(args, paths.map(sanitizePath)); } else { args.push('.'); } @@ -1143,13 +1175,13 @@ export class Repository { return; } - args.push(...paths); + args.push(...paths.map(sanitizePath)); await this.run(args); } async stage(path: string, data: string): Promise { - const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] }); + const child = this.stream(['hash-object', '--stdin', '-w', '--path', sanitizePath(path)], { stdio: [null, null, null] }); child.stdin!.end(data, 'utf8'); const { exitCode, stdout } = await exec(child); @@ -1194,7 +1226,7 @@ export class Repository { try { if (paths && paths.length > 0) { - for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { await this.run([...args, '--', ...chunk]); } } else { @@ -1333,7 +1365,7 @@ export class Repository { } async clean(paths: string[]): Promise { - const pathsByGroup = groupBy(paths, p => path.dirname(p)); + const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p)); const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]); const limiter = new Limiter(5); @@ -1379,7 +1411,7 @@ export class Repository { } if (paths && paths.length) { - args.push.apply(args, paths); + args.push.apply(args, paths.map(sanitizePath)); } else { args.push('.'); } @@ -1530,11 +1562,8 @@ export class Repository { async blame(path: string): Promise { try { - const args = ['blame']; - args.push(path); - - let result = await this.run(args); - + const args = ['blame', sanitizePath(path)]; + const result = await this.run(args); return result.stdout.trim(); } catch (err) { if (/^fatal: no such path/.test(err.stderr || '')) { @@ -1853,14 +1882,18 @@ export class Repository { } async getCommit(ref: string): Promise { - const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]); - return parseGitCommit(result.stdout) || Promise.reject('bad commit format'); + const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]); + const commits = parseGitCommits(result.stdout); + if (commits.length === 0) { + return Promise.reject('bad commit format'); + } + return commits[0]; } async updateSubmodules(paths: string[]): Promise { const args = ['submodule', 'update', '--']; - for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) { + for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) { await this.run([...args, ...chunk]); } } diff --git a/extensions/git/src/log.ts b/extensions/git/src/log.ts new file mode 100644 index 000000000000..7f18054bef9e --- /dev/null +++ b/extensions/git/src/log.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, EventEmitter } from 'vscode'; + +/** + * The severity level of a log message + */ +export enum LogLevel { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6, + Off = 7 +} + +let _logLevel: LogLevel = LogLevel.Info; +const _onDidChangeLogLevel = new EventEmitter(); + +export const Log = { + /** + * Current logging level. + */ + get logLevel(): LogLevel { + return _logLevel; + }, + + /** + * Current logging level. + */ + set logLevel(logLevel: LogLevel) { + if (_logLevel === logLevel) { + return; + } + + _logLevel = logLevel; + _onDidChangeLogLevel.fire(logLevel); + }, + + /** + * An [event](#Event) that fires when the log level has changed. + */ + get onDidChangeLogLevel(): Event { + return _onDidChangeLogLevel.event; + } +}; diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 5e85b3420b40..b3f3ae7fdcd5 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -22,6 +22,7 @@ import { GitExtensionImpl } from './api/extension'; // import * as path from 'path'; // import * as fs from 'fs'; import { createIPCServer, IIPCServer } from './ipc/ipcServer'; +import { GitTimelineProvider } from './timelineProvider'; const deactivateTasks: { (): Promise; }[] = []; @@ -82,7 +83,8 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann new GitContentProvider(model), new GitFileSystemProvider(model), new GitDecorations(model), - new GitProtocolHandler() + new GitProtocolHandler(), + new GitTimelineProvider(model) ); await checkGitVersion(info); diff --git a/extensions/git/src/repository.ts b/extensions/git/src/repository.ts index beafc4afcc53..fd49a8024c38 100644 --- a/extensions/git/src/repository.ts +++ b/extensions/git/src/repository.ts @@ -5,16 +5,17 @@ import * as fs from 'fs'; import * as path from 'path'; -import { CancellationToken, Command, Disposable, env, Event, EventEmitter, LogLevel, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode'; +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 } from './api/git'; +import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions } from './api/git'; import { AutoFetcher } from './autofetch'; import { debounce, memoize, throttle } from './decorators'; -import { Commit, CommitOptions, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule } from './git'; +import { Commit, ForcePushMode, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git'; import { StatusBarCommands } from './statusbar'; 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'; const timeout = (millis: number) => new Promise(c => setTimeout(c, millis)); @@ -303,15 +304,22 @@ export const enum Operation { Apply = 'Apply', Blame = 'Blame', Log = 'Log', + LogFile = 'LogFile', } function isReadOnly(operation: Operation): boolean { switch (operation) { - case Operation.Show: - case Operation.GetCommitTemplate: + case Operation.Blame: case Operation.CheckIgnore: + case Operation.Diff: + case Operation.FindTrackingBranches: + case Operation.GetBranch: + case Operation.GetCommitTemplate: case Operation.GetObjectDetails: + case Operation.Log: + case Operation.LogFile: case Operation.MergeBase: + case Operation.Show: return true; default: return false; @@ -457,8 +465,8 @@ class FileEventLogger { private onDotGitFileChange: Event, private outputChannel: OutputChannel ) { - this.logLevelDisposable = env.onDidChangeLogLevel(this.onDidChangeLogLevel, this); - this.onDidChangeLogLevel(env.logLevel); + this.logLevelDisposable = Log.onDidChangeLogLevel(this.onDidChangeLogLevel, this); + this.onDidChangeLogLevel(Log.logLevel); } private onDidChangeLogLevel(level: LogLevel): void { @@ -519,7 +527,7 @@ class DotGitWatcher implements IFileWatcher { this.transientDisposables.push(upstreamWatcher); upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables); } catch (err) { - if (env.logLevel <= LogLevel.Error) { + if (Log.logLevel <= LogLevel.Error) { this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}', is most likely packed.\n${err.stack || err}`); } } @@ -682,7 +690,7 @@ export class Repository implements Disposable { onDotGitFileChange = dotGitFileWatcher.event; this.disposables.push(dotGitFileWatcher); } catch (err) { - if (env.logLevel <= LogLevel.Error) { + if (Log.logLevel <= LogLevel.Error) { outputChannel.appendLine(`Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`); } @@ -867,6 +875,11 @@ export class Repository implements Disposable { return this.run(Operation.Log, () => this.repository.log(options)); } + logFile(uri: Uri, options?: LogFileOptions): Promise { + // TODO: This probably needs per-uri granularity + return this.run(Operation.LogFile, () => this.repository.logFile(uri, options)); + } + @throttle async status(): Promise { await this.run(Operation.Status); diff --git a/extensions/git/src/test/git.test.ts b/extensions/git/src/test/git.test.ts index 54e6aaab5c61..c2090574ab64 100644 --- a/extensions/git/src/test/git.test.ts +++ b/extensions/git/src/test/git.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'mocha'; -import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git'; +import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git'; import * as assert from 'assert'; import { splitInChunks } from '../util'; @@ -189,44 +189,56 @@ suite('git', () => { suite('parseGitCommit', () => { test('single parent commit', function () { const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1 +John Doe john.doe@mail.com +1580811030 8e5a374372b8393906c7e380dbb09349c5385554 -This is a commit message.`; +This is a commit message.\x00`; - assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), { + assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: ['8e5a374372b8393906c7e380dbb09349c5385554'], + authorDate: new Date(1580811030000), + authorName: 'John Doe', authorEmail: 'john.doe@mail.com', - }); + }]); }); test('multiple parent commits', function () { const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 +John Doe john.doe@mail.com +1580811030 8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217 -This is a commit message.`; +This is a commit message.\x00`; - assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), { + assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'], + authorDate: new Date(1580811030000), + authorName: 'John Doe', authorEmail: 'john.doe@mail.com', - }); + }]); }); test('no parent commits', function () { const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1 +John Doe john.doe@mail.com +1580811030 -This is a commit message.`; +This is a commit message.\x00`; - assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), { + assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{ hash: '52c293a05038d865604c2284aa8698bd087915a1', message: 'This is a commit message.', parents: [], + authorDate: new Date(1580811030000), + authorName: 'John Doe', authorEmail: 'john.doe@mail.com', - }); + }]); }); }); diff --git a/extensions/git/src/test/index.ts b/extensions/git/src/test/index.ts new file mode 100644 index 000000000000..5ea8c3bd9aeb --- /dev/null +++ b/extensions/git/src/test/index.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('vscode/lib/testrunner'); + +const suite = 'Integration Git Tests'; + +const options: any = { + ui: 'tdd', + useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), + timeout: 60000 +}; + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/git/src/test/smoke.test.ts b/extensions/git/src/test/smoke.test.ts new file mode 100644 index 000000000000..9fd8722cfc34 --- /dev/null +++ b/extensions/git/src/test/smoke.test.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'mocha'; +import * as assert from 'assert'; +import { workspace, commands, window, Uri, WorkspaceEdit, Range, TextDocument, extensions } from 'vscode'; +import * as cp from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { GitExtension, API, Repository, Status } from '../api/git'; +import { eventToPromise } from '../util'; + +suite('git smoke test', function () { + const cwd = fs.realpathSync(workspace.workspaceFolders![0].uri.fsPath); + + function file(relativePath: string) { + return path.join(cwd, relativePath); + } + + function uri(relativePath: string) { + return Uri.file(file(relativePath)); + } + + async function open(relativePath: string) { + const doc = await workspace.openTextDocument(uri(relativePath)); + await window.showTextDocument(doc); + return doc; + } + + async function type(doc: TextDocument, text: string) { + const edit = new WorkspaceEdit(); + const end = doc.lineAt(doc.lineCount - 1).range.end; + edit.replace(doc.uri, new Range(end, end), text); + await workspace.applyEdit(edit); + } + + let git: API; + let repository: Repository; + + suiteSetup(async function () { + fs.writeFileSync(file('app.js'), 'hello', 'utf8'); + fs.writeFileSync(file('index.pug'), 'hello', 'utf8'); + cp.execSync('git init', { cwd }); + cp.execSync('git config user.name testuser', { cwd }); + cp.execSync('git config user.email monacotools@microsoft.com', { cwd }); + cp.execSync('git add .', { cwd }); + cp.execSync('git commit -m "initial commit"', { cwd }); + + // make sure git is activated + const ext = extensions.getExtension('vscode.git'); + await ext?.activate(); + git = ext!.exports.getAPI(1); + + if (git.repositories.length === 0) { + await eventToPromise(git.onDidOpenRepository); + } + + assert.equal(git.repositories.length, 1); + assert.equal(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd); + + repository = git.repositories[0]; + }); + + test('reflects working tree changes', async function () { + await commands.executeCommand('workbench.view.scm'); + + const appjs = await open('app.js'); + await type(appjs, ' world'); + await appjs.save(); + await repository.status(); + assert.equal(repository.state.workingTreeChanges.length, 1); + repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED); + + fs.writeFileSync(file('newfile.txt'), ''); + const newfile = await open('newfile.txt'); + await type(newfile, 'hey there'); + await newfile.save(); + await repository.status(); + assert.equal(repository.state.workingTreeChanges.length, 2); + repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED); + repository.state.workingTreeChanges.some(r => r.uri.path === newfile.uri.path && r.status === Status.UNTRACKED); + }); + + test('opens diff editor', async function () { + const appjs = uri('app.js'); + await commands.executeCommand('git.openChange', appjs); + + assert(window.activeTextEditor); + assert.equal(window.activeTextEditor!.document.uri.path, appjs.path); + + // TODO: how do we really know this is a diff editor? + }); + + test('stages correctly', async function () { + const appjs = uri('app.js'); + const newfile = uri('newfile.txt'); + + await commands.executeCommand('git.stage', appjs); + assert.equal(repository.state.workingTreeChanges.length, 1); + repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); + assert.equal(repository.state.indexChanges.length, 1); + repository.state.indexChanges.some(r => r.uri.path === appjs.path && r.status === Status.INDEX_MODIFIED); + + await commands.executeCommand('git.unstage', appjs); + assert.equal(repository.state.workingTreeChanges.length, 2); + repository.state.workingTreeChanges.some(r => r.uri.path === appjs.path && r.status === Status.MODIFIED); + repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); + }); + + test('stages, commits changes and verifies outgoing change', async function () { + const appjs = uri('app.js'); + const newfile = uri('newfile.txt'); + + await commands.executeCommand('git.stage', appjs); + await repository.commit('second commit'); + assert.equal(repository.state.workingTreeChanges.length, 1); + repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED); + assert.equal(repository.state.indexChanges.length, 0); + + await commands.executeCommand('git.stageAll', appjs); + await repository.commit('third commit'); + assert.equal(repository.state.workingTreeChanges.length, 0); + assert.equal(repository.state.indexChanges.length, 0); + }); +}); diff --git a/extensions/git/src/timelineProvider.ts b/extensions/git/src/timelineProvider.ts new file mode 100644 index 000000000000..9edb4d683c2b --- /dev/null +++ b/extensions/git/src/timelineProvider.ts @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as dayjs from 'dayjs'; +import * as advancedFormat from 'dayjs/plugin/advancedFormat'; +import * as relativeTime from 'dayjs/plugin/relativeTime'; +import { CancellationToken, Disposable, Event, EventEmitter, ThemeIcon, TimelineItem, TimelineProvider, Uri, workspace, TimelineChangeEvent } from 'vscode'; +import { Model } from './model'; +import { Repository } from './repository'; +import { debounce } from './decorators'; +import { Status } from './api/git'; + +dayjs.extend(advancedFormat); +dayjs.extend(relativeTime); + +// TODO[ECA]: Localize all the strings +// TODO[ECA]: Localize or use a setting for date format + +export class GitTimelineProvider implements TimelineProvider { + private _onDidChange = new EventEmitter(); + get onDidChange(): Event { + return this._onDidChange.event; + } + + readonly id = 'git-history'; + readonly label = 'Git History'; + + private _disposable: Disposable; + + private _repo: Repository | undefined; + private _repoDisposable: Disposable | undefined; + private _repoStatusDate: Date | undefined; + + constructor(private readonly _model: Model) { + this._disposable = Disposable.from( + _model.onDidOpenRepository(this.onRepositoriesChanged, this), + workspace.registerTimelineProvider('*', this), + ); + } + + dispose() { + this._disposable.dispose(); + } + + async provideTimeline(uri: Uri, _token: CancellationToken): Promise { + // console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`); + + const repo = this._model.getRepository(uri); + if (!repo) { + this._repoDisposable?.dispose(); + this._repoStatusDate = undefined; + this._repo = undefined; + + return []; + } + + if (this._repo?.root !== repo.root) { + this._repoDisposable?.dispose(); + + this._repo = repo; + this._repoStatusDate = new Date(); + this._repoDisposable = Disposable.from( + repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)), + repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo)) + ); + } + + // TODO[ECA]: Ensure that the uri is a file -- if not we could get the history of the repo? + + const commits = await repo.logFile(uri); + + let dateFormatter: dayjs.Dayjs; + const items = commits.map(c => { + let message = c.message; + + const index = message.indexOf('\n'); + if (index !== -1) { + message = `${message.substring(0, index)} \u2026`; + } + + dateFormatter = dayjs(c.authorDate); + + const item = new TimelineItem(message, c.authorDate?.getTime() ?? 0); + item.id = c.hash; + item.iconPath = new (ThemeIcon as any)('git-commit'); + item.description = `${dateFormatter.fromNow()} \u2022 ${c.authorName}`; + item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n\n${c.message}`; + item.command = { + title: 'Open Diff', + command: 'git.openDiff', + arguments: [uri, `${c.hash}^`, c.hash] + }; + + return item; + }); + + const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); + if (index) { + const date = this._repoStatusDate ?? new Date(); + dateFormatter = dayjs(date); + + let status; + switch (index.type) { + case Status.INDEX_MODIFIED: + status = 'Modified'; + break; + case Status.INDEX_ADDED: + status = 'Added'; + break; + case Status.INDEX_DELETED: + status = 'Deleted'; + break; + case Status.INDEX_RENAMED: + status = 'Renamed'; + break; + case Status.INDEX_COPIED: + status = 'Copied'; + break; + default: + status = ''; + break; + } + + const item = new TimelineItem('Staged Changes', date.getTime()); + item.id = 'index'; + // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? + item.iconPath = new (ThemeIcon as any)('git-commit'); + item.description = `${dateFormatter.fromNow()} \u2022 You`; + item.detail = `You \u2014 Index\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`; + item.command = { + title: 'Open Comparison', + command: 'git.openDiff', + arguments: [uri, 'HEAD', '~'] + }; + + items.push(item); + } + + + const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath); + if (working) { + const date = new Date(); + dateFormatter = dayjs(date); + + let status; + switch (working.type) { + case Status.INDEX_MODIFIED: + status = 'Modified'; + break; + case Status.INDEX_ADDED: + status = 'Added'; + break; + case Status.INDEX_DELETED: + status = 'Deleted'; + break; + case Status.INDEX_RENAMED: + status = 'Renamed'; + break; + case Status.INDEX_COPIED: + status = 'Copied'; + break; + default: + status = ''; + break; + } + + const item = new TimelineItem('Uncommited Changes', date.getTime()); + item.id = 'working'; + // TODO[ECA]: Replace with a better icon -- reflecting its status maybe? + item.iconPath = new (ThemeIcon as any)('git-commit'); + item.description = `${dateFormatter.fromNow()} \u2022 You`; + item.detail = `You \u2014 Working Tree\n${dateFormatter.fromNow()} (${dateFormatter.format('MMMM Do, YYYY h:mma')})\n${status}`; + item.command = { + title: 'Open Comparison', + command: 'git.openDiff', + arguments: [uri, index ? '~' : 'HEAD', ''] + }; + + items.push(item); + } + + return items; + } + + private onRepositoriesChanged(_repo: Repository) { + // console.log(`GitTimelineProvider.onRepositoriesChanged`); + + // TODO[ECA]: Being naive for now and just always refreshing each time there is a new repository + this.fireChanged(); + } + + private onRepositoryChanged(_repo: Repository, _uri: Uri) { + // console.log(`GitTimelineProvider.onRepositoryChanged: uri=${uri.toString(true)}`); + + this.fireChanged(); + } + + private onRepositoryStatusChanged(_repo: Repository) { + // console.log(`GitTimelineProvider.onRepositoryStatusChanged`); + + // This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items + this._repoStatusDate = new Date(); + + this.fireChanged(); + } + + @debounce(500) + private fireChanged() { + this._onDidChange.fire(); + } +} diff --git a/extensions/git/src/typings/refs.d.ts b/extensions/git/src/typings/refs.d.ts index 4912c31c1ab6..ffe0c6c693c2 100644 --- a/extensions/git/src/typings/refs.d.ts +++ b/extensions/git/src/typings/refs.d.ts @@ -4,4 +4,5 @@ *--------------------------------------------------------------------------------------------*/ /// -/// \ No newline at end of file +/// +/// diff --git a/extensions/git/src/util.ts b/extensions/git/src/util.ts index 742bcbfa0530..2d3a764f8094 100644 --- a/extensions/git/src/util.ts +++ b/extensions/git/src/util.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event } from 'vscode'; +import { Event, Disposable } from 'vscode'; import { dirname, sep } from 'path'; import { Readable } from 'stream'; import { promises as fs, createReadStream } from 'fs'; @@ -33,15 +33,15 @@ export function combinedDisposable(disposables: IDisposable[]): IDisposable { export const EmptyDisposable = toDisposable(() => null); export function fireEvent(event: Event): Event { - return (listener, thisArgs = null, disposables?) => event(_ => (listener as any).call(thisArgs), null, disposables); + return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(_ => (listener as any).call(thisArgs), null, disposables); } export function mapEvent(event: Event, map: (i: I) => O): Event { - return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables); + return (listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, map(i)), null, disposables); } export function filterEvent(event: Event, filter: (e: T) => boolean): Event { - return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables); + return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables); } export function latchEvent(event: Event): Event { @@ -57,7 +57,7 @@ export function latchEvent(event: Event): Event { } export function anyEvent(...events: Event[]): Event { - return (listener, thisArgs = null, disposables?) => { + return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i)))); if (disposables) { @@ -73,7 +73,7 @@ export function done(promise: Promise): Promise { } export function onceEvent(event: Event): Event { - return (listener, thisArgs = null, disposables?) => { + return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { const result = event(e => { result.dispose(); return listener.call(thisArgs, e); @@ -84,7 +84,7 @@ export function onceEvent(event: Event): Event { } export function debounceEvent(event: Event, delay: number): Event { - return (listener, thisArgs = null, disposables?) => { + return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => { let timer: NodeJS.Timer; return event(e => { clearTimeout(timer); diff --git a/extensions/git/yarn.lock b/extensions/git/yarn.lock index 58a82bbe41c8..af5b10127dc4 100644 --- a/extensions/git/yarn.lock +++ b/extensions/git/yarn.lock @@ -36,6 +36,28 @@ resolved "https://registry.yarnpkg.com/@types/which/-/which-1.0.28.tgz#016e387629b8817bed653fe32eab5d11279c8df6" integrity sha1-AW44dim4gXvtZT/jLqtdESecjfY= +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.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.11.0.tgz#c3607cbc8ae392d8a5a536f25b21f8e5f3f87fe9" + integrity sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + applicationinsights@1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5" @@ -45,11 +67,45 @@ applicationinsights@1.0.8: diagnostic-channel-publishers "0.2.1" zone.js "0.7.6" +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.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + 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.8" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" @@ -63,11 +119,43 @@ browser-stdout@1.3.0: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8= +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== + byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + +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== + commander@2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" @@ -80,6 +168,28 @@ concat-map@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= + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + +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" + +dayjs@1.8.19: + version "1.8.19" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.8.19.tgz#5117dc390d8f8e586d53891dbff3fa308f51abfe" + integrity sha512-7kqOoj3oQSmqbvtvGFLU5iYqies+SqUiEGNT0UtUPPxcPYgY1BrkXR0Cq2R9HYSimBXN+xHkEN4Hi399W+Ovlg== + debug@2.6.8: version "2.6.8" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.8.tgz#e731531ca2ede27d188222427da17821d68ff4fc" @@ -87,6 +197,32 @@ debug@2.6.8: dependencies: ms "2.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@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +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= + diagnostic-channel-publishers@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3" @@ -104,21 +240,92 @@ diff@3.2.0: resolved "https://registry.yarnpkg.com/diff/-/diff-3.2.0.tgz#c9ce393a4b7cbd0b058a725c93df299027868ff9" integrity sha1-yc45Okt8vQsFinJck98pkCeGj/k= +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@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + file-type@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/file-type/-/file-type-7.2.0.tgz#113cfed52e1d6959ab80248906e2f25a8cdccb74" integrity sha1-ETz+1S4daVmrgCSJBuLyWozcy3Q= +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.1: version "7.1.1" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" @@ -131,26 +338,98 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^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" + "graceful-readlink@>= 1.0.0": version "1.0.1" resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +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== + growl@1.9.2: version "1.9.2" resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f" integrity sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8= +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@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" integrity sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo= +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" + iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -171,21 +450,66 @@ inherits@2: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +is-buffer@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +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= + 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= +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= + jschardet@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.1.1.tgz#af6f8fd0b3b0f5d46a8fd9614a4fce490575c184" integrity sha512-pA5qG9Zwm8CBpGlK/lo2GE9jPxwqRgMV7Lzc/1iaPccw6v4Rhj8Zg2BTyrdmHmxlJojnbLupLeRnaPLsq03x6Q== +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= + json3@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" integrity sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE= +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" + lodash._baseassign@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" @@ -242,7 +566,33 @@ lodash.keys@^3.0.0: lodash.isarguments "^3.0.0" lodash.isarray "^3.0.0" -minimatch@^3.0.2: +lodash@^4.16.4: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +minimatch@3.0.4, minimatch@^3.0.2, 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== @@ -254,13 +604,32 @@ minimist@0.0.8: resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= -mkdirp@0.5.1: +mkdirp@0.5.1, 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-junit-reporter@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.3.tgz#941e219dd759ed732f8641e165918aa8b167c981" + integrity sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA== + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + mocha@^3.2.0: version "3.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-3.5.3.tgz#1e0480fe36d2da5858d1eb6acc38418b26eaa20d" @@ -279,11 +648,38 @@ mocha@^3.2.0: mkdirp "0.5.1" supports-color "3.1.2" +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" @@ -296,7 +692,73 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -"safer-buffer@>= 2.1.2 < 3": +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.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + +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.1.2 < 3", 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== @@ -306,6 +768,46 @@ semver@^5.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" integrity sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA== +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" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + supports-color@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.1.2.tgz#72a262894d9d408b956ca05ff37b2ed8a6e2a2d5" @@ -313,6 +815,62 @@ supports-color@3.1.2: dependencies: has-flag "^1.0.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.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +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-extension-telemetry@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b" @@ -325,11 +883,32 @@ vscode-nls@^4.0.0: resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.0.0.tgz#4001c8a6caba5cedb23a9c5ce1090395c0e44002" integrity sha512-qCfdzcH+0LgQnBpZA53bA32kzp9rpq/f66Som577ObeuDlFIrtbEJ+A/+CCxjIh4G8dpJYNCKIsxpRAHIfsbNw== +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-uri@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-2.0.0.tgz#2df704222f72b8a71ff266ba0830ed6c51ac1542" integrity sha512-lWXWofDSYD8r/TIyu64MdwB4FaSirQ608PP/TzUyslyOeHGwQ0eTHUZeJrK1ILOmwUHaJtV693m2JoUYroUDpw== +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" + which@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" @@ -342,6 +921,11 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= + zone.js@0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009" diff --git a/extensions/json-language-features/server/src/jsonServerMain.ts b/extensions/json-language-features/server/src/jsonServerMain.ts index 69bb48430372..0fd5bee766df 100644 --- a/extensions/json-language-features/server/src/jsonServerMain.ts +++ b/extensions/json-language-features/server/src/jsonServerMain.ts @@ -329,7 +329,7 @@ documents.onDidClose(event => { }); const pendingValidationRequests: { [uri: string]: NodeJS.Timer; } = {}; -const validationDelayMs = 500; +const validationDelayMs = 300; function cleanPendingValidation(textDocument: TextDocument): void { const request = pendingValidationRequests[textDocument.uri]; @@ -363,12 +363,12 @@ function validateTextDocument(textDocument: TextDocument, callback?: (diagnostic const documentSettings: DocumentLanguageSettings = textDocument.languageId === 'jsonc' ? { comments: 'ignore', trailingCommas: 'warning' } : { comments: 'error', trailingCommas: 'error' }; languageService.doValidation(textDocument, jsonDocument, documentSettings).then(diagnostics => { - setTimeout(() => { + setImmediate(() => { const currDocument = documents.get(textDocument.uri); if (currDocument && currDocument.version === version) { respond(diagnostics); // Send the computed diagnostics to VSCode. } - }, 100); + }); }, error => { connection.console.error(formatError(`Error while validating ${textDocument.uri}`, error)); }); diff --git a/extensions/markdown-language-features/preview-src/tsconfig.json b/extensions/markdown-language-features/preview-src/tsconfig.json index 85159a000d95..e19cd4a675d7 100644 --- a/extensions/markdown-language-features/preview-src/tsconfig.json +++ b/extensions/markdown-language-features/preview-src/tsconfig.json @@ -2,6 +2,11 @@ "extends": "../../shared.tsconfig.json", "compilerOptions": { "outDir": "./dist/", - "jsx": "react" + "jsx": "react", + "lib": [ + "es2018", + "DOM", + "DOM.Iterable" + ] } } diff --git a/extensions/package.json b/extensions/package.json index f2f1225c8298..df820979f1a4 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.7.5" + "typescript": "^3.8.1-rc" }, "scripts": { "postinstall": "node ./postinstall" diff --git a/extensions/r/cgmanifest.json b/extensions/r/cgmanifest.json index 0781a1507229..7cf785d7140b 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": "1cd3d42a6b2e54276ef2d71fe33bb3fefb1d6cff" + "commitHash": "bc79e9245682ee09b4f0b742b927a37702d91b82" } }, "license": "MIT", - "version": "0.5.5" + "version": "1.1.8" } ], "version": 1 diff --git a/extensions/r/syntaxes/r.tmLanguage.json b/extensions/r/syntaxes/r.tmLanguage.json index db37b8421fe1..2b6c4368da97 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/1cd3d42a6b2e54276ef2d71fe33bb3fefb1d6cff", + "version": "https://github.com/Ikuyadeu/vscode-R/commit/bc79e9245682ee09b4f0b742b927a37702d91b82", "name": "R", "scopeName": "source.r", "patterns": [ @@ -417,7 +417,7 @@ "end": "(\\))", "endCaptures": { "1": { - "name": "punctuation.definition.parameters.r" + "name": "punctuation.section.parens.end.r" } }, "name": "meta.function-call.r", diff --git a/extensions/r/test/colorize-results/test_r.json b/extensions/r/test/colorize-results/test_r.json index 2d3e504f8def..3ecd0091ce5a 100644 --- a/extensions/r/test/colorize-results/test_r.json +++ b/extensions/r/test/colorize-results/test_r.json @@ -639,7 +639,7 @@ }, { "c": ")", - "t": "source.r meta.function-call.r punctuation.definition.parameters.r", + "t": "source.r meta.function-call.r punctuation.section.parens.end.r", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -716,7 +716,7 @@ }, { "c": ")", - "t": "source.r meta.function-call.r punctuation.definition.parameters.r", + "t": "source.r meta.function-call.r punctuation.section.parens.end.r", "r": { "dark_plus": "default: #D4D4D4", "light_plus": "default: #000000", @@ -1044,4 +1044,4 @@ "hc_black": "default: #FFFFFF" } } -] +] \ No newline at end of file diff --git a/extensions/search-result/src/extension.ts b/extensions/search-result/src/extension.ts index 295b620eeef5..69336137d473 100644 --- a/extensions/search-result/src/extension.ts +++ b/extensions/search-result/src/extension.ts @@ -77,9 +77,7 @@ export function activate(context: vscode.ExtensionContext) { const lineResult = parseSearchResults(document, token)[position.line]; if (!lineResult) { return []; } if (lineResult.type === 'file') { - // TODO: The multi-match peek UX isnt very smooth. - // return lineResult.allLocations.length > 1 ? lineResult.allLocations : [lineResult.location]; - return []; + return lineResult.allLocations; } const translateRangeSidewaysBy = (r: vscode.Range, n: number) => diff --git a/extensions/search-result/syntaxes/generateTMLanguage.js b/extensions/search-result/syntaxes/generateTMLanguage.js index 51d9e0ea7c3c..ce2acc34ff6c 100644 --- a/extensions/search-result/syntaxes/generateTMLanguage.js +++ b/extensions/search-result/syntaxes/generateTMLanguage.js @@ -115,7 +115,7 @@ mappings.forEach(([ext, scope, regexp]) => { name: [scopes.resultBlock.result.meta, scopes.resultBlock.result.metaMultiLine].join(' '), begin: "^ ((\\d+) )", - while: "^ ((\\d+)(:))|((\\d+) )", + while: "^ (?:((\\d+)(:))|((\\d+) ))", beginCaptures: { "0": { name: scopes.resultBlock.result.prefix.meta }, "1": { name: scopes.resultBlock.result.prefix.metaContext }, @@ -215,7 +215,7 @@ const plainText = [ } }, { - match: "^ ((\\d+)(:))|((\\d+)( ))(.*)", + match: "^ (?:((\\d+)(:))|((\\d+)( ))(.*))", name: [scopes.resultBlock.meta, scopes.resultBlock.result.meta].join(' '), captures: { "1": { name: [scopes.resultBlock.result.prefix.meta, scopes.resultBlock.result.prefix.metaMatch].join(' ') }, diff --git a/extensions/search-result/syntaxes/searchResult.tmLanguage.json b/extensions/search-result/syntaxes/searchResult.tmLanguage.json index e16ca1cb95d5..ea4e3efb1549 100644 --- a/extensions/search-result/syntaxes/searchResult.tmLanguage.json +++ b/extensions/search-result/syntaxes/searchResult.tmLanguage.json @@ -253,7 +253,7 @@ } }, { - "match": "^ ((\\d+)(:))|((\\d+)( ))(.*)", + "match": "^ (?:((\\d+)(:))|((\\d+)( ))(.*))", "name": "meta.resultBlock.search meta.resultLine.search", "captures": { "1": { @@ -297,7 +297,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -383,7 +383,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -469,7 +469,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -555,7 +555,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -641,7 +641,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -727,7 +727,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -813,7 +813,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -899,7 +899,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -985,7 +985,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1071,7 +1071,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1157,7 +1157,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1243,7 +1243,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1329,7 +1329,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1415,7 +1415,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1501,7 +1501,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1587,7 +1587,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1673,7 +1673,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1759,7 +1759,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1845,7 +1845,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -1931,7 +1931,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2017,7 +2017,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2103,7 +2103,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2189,7 +2189,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2275,7 +2275,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2361,7 +2361,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2447,7 +2447,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2533,7 +2533,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2619,7 +2619,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2705,7 +2705,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2791,7 +2791,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2877,7 +2877,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -2963,7 +2963,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3049,7 +3049,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3135,7 +3135,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3221,7 +3221,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3307,7 +3307,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3393,7 +3393,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3479,7 +3479,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3565,7 +3565,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3651,7 +3651,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3737,7 +3737,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3823,7 +3823,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3909,7 +3909,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -3995,7 +3995,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4081,7 +4081,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4167,7 +4167,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4253,7 +4253,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4339,7 +4339,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4425,7 +4425,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4511,7 +4511,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4597,7 +4597,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4683,7 +4683,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" @@ -4769,7 +4769,7 @@ { "name": "meta.resultLine.search meta.resultLine.multiLine.search", "begin": "^ ((\\d+) )", - "while": "^ ((\\d+)(:))|((\\d+) )", + "while": "^ (?:((\\d+)(:))|((\\d+) ))", "beginCaptures": { "0": { "name": "constant.numeric.integer meta.resultLinePrefix.search" diff --git a/extensions/shared.tsconfig.json b/extensions/shared.tsconfig.json index 9671893bd7ed..5559704e3044 100644 --- a/extensions/shared.tsconfig.json +++ b/extensions/shared.tsconfig.json @@ -1,6 +1,9 @@ { "compilerOptions": { "target": "es2018", + "lib": [ + "es2018" + ], "module": "commonjs", "strict": true, "alwaysStrict": true, diff --git a/extensions/theme-defaults/themes/light_defaults.json b/extensions/theme-defaults/themes/light_defaults.json index e28c9b8ed0b2..018da38e9396 100644 --- a/extensions/theme-defaults/themes/light_defaults.json +++ b/extensions/theme-defaults/themes/light_defaults.json @@ -13,9 +13,10 @@ "sideBarTitle.foreground": "#6F6F6F", "list.hoverBackground": "#E8E8E8", "input.placeholderForeground": "#767676", + "searchEditor.textInputBorder": "#CECECE", "settings.textInputBorder": "#CECECE", "settings.numberInputBorder": "#CECECE", "statusBarItem.remoteForeground": "#FFF", "statusBarItem.remoteBackground": "#16825D" } -} \ No newline at end of file +} diff --git a/extensions/theme-seti/cgmanifest.json b/extensions/theme-seti/cgmanifest.json index 1c86b8bcb2bc..5ee7c0f5d528 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": "85a222708824c6f19bbecbec71633d2c97077dad" + "commitHash": "4b3e0a3d0ca8999430bc3aa9f2c8324e6922b3de" } }, "version": "0.1.0" diff --git a/extensions/theme-seti/icons/seti.woff b/extensions/theme-seti/icons/seti.woff index b85727d01e46..85f6ee76a5be 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 b339ff6092fb..eed503fcc03e 100644 --- a/extensions/theme-seti/icons/vs-seti-icon-theme.json +++ b/extensions/theme-seti/icons/vs-seti-icon-theme.json @@ -166,1191 +166,1199 @@ "fontCharacter": "\\E012", "fontColor": "#8dc149" }, - "_coffee_light": { + "_code-search_light": { + "fontCharacter": "\\E013", + "fontColor": "#9068b0" + }, + "_code-search": { "fontCharacter": "\\E013", + "fontColor": "#a074c4" + }, + "_coffee_light": { + "fontCharacter": "\\E014", "fontColor": "#b7b73b" }, "_coffee": { - "fontCharacter": "\\E013", + "fontCharacter": "\\E014", "fontColor": "#cbcb41" }, "_coldfusion_light": { - "fontCharacter": "\\E015", + "fontCharacter": "\\E016", "fontColor": "#498ba7" }, "_coldfusion": { - "fontCharacter": "\\E015", + "fontCharacter": "\\E016", "fontColor": "#519aba" }, "_config_light": { - "fontCharacter": "\\E016", + "fontCharacter": "\\E017", "fontColor": "#627379" }, "_config": { - "fontCharacter": "\\E016", + "fontCharacter": "\\E017", "fontColor": "#6d8086" }, "_cpp_light": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E018", "fontColor": "#498ba7" }, "_cpp": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E018", "fontColor": "#519aba" }, "_cpp_1_light": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E018", "fontColor": "#9068b0" }, "_cpp_1": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E018", "fontColor": "#a074c4" }, "_cpp_2_light": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E018", "fontColor": "#b7b73b" }, "_cpp_2": { - "fontCharacter": "\\E017", + "fontCharacter": "\\E018", "fontColor": "#cbcb41" }, "_crystal_light": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E019", "fontColor": "#bfc2c1" }, "_crystal": { - "fontCharacter": "\\E018", + "fontCharacter": "\\E019", "fontColor": "#d4d7d6" }, "_crystal_embedded_light": { - "fontCharacter": "\\E019", + "fontCharacter": "\\E01A", "fontColor": "#bfc2c1" }, "_crystal_embedded": { - "fontCharacter": "\\E019", + "fontCharacter": "\\E01A", "fontColor": "#d4d7d6" }, "_css_light": { - "fontCharacter": "\\E01A", + "fontCharacter": "\\E01B", "fontColor": "#498ba7" }, "_css": { - "fontCharacter": "\\E01A", + "fontCharacter": "\\E01B", "fontColor": "#519aba" }, "_csv_light": { - "fontCharacter": "\\E01B", + "fontCharacter": "\\E01C", "fontColor": "#7fae42" }, "_csv": { - "fontCharacter": "\\E01B", + "fontCharacter": "\\E01C", "fontColor": "#8dc149" }, "_d_light": { - "fontCharacter": "\\E01C", + "fontCharacter": "\\E01D", "fontColor": "#b8383d" }, "_d": { - "fontCharacter": "\\E01C", + "fontCharacter": "\\E01D", "fontColor": "#cc3e44" }, "_dart_light": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01E", "fontColor": "#498ba7" }, "_dart": { - "fontCharacter": "\\E01D", + "fontCharacter": "\\E01E", "fontColor": "#519aba" }, "_db_light": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01F", "fontColor": "#dd4b78" }, "_db": { - "fontCharacter": "\\E01E", + "fontCharacter": "\\E01F", "fontColor": "#f55385" }, "_default_light": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E020", "fontColor": "#bfc2c1" }, "_default": { - "fontCharacter": "\\E01F", + "fontCharacter": "\\E020", "fontColor": "#d4d7d6" }, "_docker_light": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#498ba7" }, "_docker": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#519aba" }, "_docker_1_light": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#455155" }, "_docker_1": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#4d5a5e" }, "_docker_2_light": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#7fae42" }, "_docker_2": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#8dc149" }, "_docker_3_light": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#dd4b78" }, "_docker_3": { - "fontCharacter": "\\E021", + "fontCharacter": "\\E022", "fontColor": "#f55385" }, "_ejs_light": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E024", "fontColor": "#b7b73b" }, "_ejs": { - "fontCharacter": "\\E023", + "fontCharacter": "\\E024", "fontColor": "#cbcb41" }, "_elixir_light": { - "fontCharacter": "\\E024", + "fontCharacter": "\\E025", "fontColor": "#9068b0" }, "_elixir": { - "fontCharacter": "\\E024", + "fontCharacter": "\\E025", "fontColor": "#a074c4" }, "_elixir_script_light": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E026", "fontColor": "#9068b0" }, "_elixir_script": { - "fontCharacter": "\\E025", + "fontCharacter": "\\E026", "fontColor": "#a074c4" }, "_elm_light": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E027", "fontColor": "#498ba7" }, "_elm": { - "fontCharacter": "\\E026", + "fontCharacter": "\\E027", "fontColor": "#519aba" }, "_eslint_light": { - "fontCharacter": "\\E028", + "fontCharacter": "\\E029", "fontColor": "#9068b0" }, "_eslint": { - "fontCharacter": "\\E028", + "fontCharacter": "\\E029", "fontColor": "#a074c4" }, "_eslint_1_light": { - "fontCharacter": "\\E028", + "fontCharacter": "\\E029", "fontColor": "#455155" }, "_eslint_1": { - "fontCharacter": "\\E028", + "fontCharacter": "\\E029", "fontColor": "#4d5a5e" }, "_ethereum_light": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#498ba7" }, "_ethereum": { - "fontCharacter": "\\E029", + "fontCharacter": "\\E02A", "fontColor": "#519aba" }, "_f-sharp_light": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E02B", "fontColor": "#498ba7" }, "_f-sharp": { - "fontCharacter": "\\E02A", + "fontCharacter": "\\E02B", "fontColor": "#519aba" }, "_favicon_light": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02C", "fontColor": "#b7b73b" }, "_favicon": { - "fontCharacter": "\\E02B", + "fontCharacter": "\\E02C", "fontColor": "#cbcb41" }, "_firebase_light": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02D", "fontColor": "#cc6d2e" }, "_firebase": { - "fontCharacter": "\\E02C", + "fontCharacter": "\\E02D", "fontColor": "#e37933" }, "_firefox_light": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02E", "fontColor": "#cc6d2e" }, "_firefox": { - "fontCharacter": "\\E02D", + "fontCharacter": "\\E02E", "fontColor": "#e37933" }, "_font_light": { - "fontCharacter": "\\E02F", + "fontCharacter": "\\E030", "fontColor": "#b8383d" }, "_font": { - "fontCharacter": "\\E02F", + "fontCharacter": "\\E030", "fontColor": "#cc3e44" }, "_git_light": { - "fontCharacter": "\\E030", + "fontCharacter": "\\E031", "fontColor": "#3b4b52" }, "_git": { - "fontCharacter": "\\E030", + "fontCharacter": "\\E031", "fontColor": "#41535b" }, "_go_light": { - "fontCharacter": "\\E034", + "fontCharacter": "\\E035", "fontColor": "#498ba7" }, "_go": { - "fontCharacter": "\\E034", + "fontCharacter": "\\E035", "fontColor": "#519aba" }, "_go2_light": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E036", "fontColor": "#498ba7" }, "_go2": { - "fontCharacter": "\\E035", + "fontCharacter": "\\E036", "fontColor": "#519aba" }, "_gradle_light": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E037", "fontColor": "#7fae42" }, "_gradle": { - "fontCharacter": "\\E036", + "fontCharacter": "\\E037", "fontColor": "#8dc149" }, "_grails_light": { - "fontCharacter": "\\E037", + "fontCharacter": "\\E038", "fontColor": "#7fae42" }, "_grails": { - "fontCharacter": "\\E037", + "fontCharacter": "\\E038", "fontColor": "#8dc149" }, "_graphql_light": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E039", "fontColor": "#dd4b78" }, "_graphql": { - "fontCharacter": "\\E038", + "fontCharacter": "\\E039", "fontColor": "#f55385" }, "_grunt_light": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E03A", "fontColor": "#cc6d2e" }, "_grunt": { - "fontCharacter": "\\E039", + "fontCharacter": "\\E03A", "fontColor": "#e37933" }, "_gulp_light": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E03B", "fontColor": "#b8383d" }, "_gulp": { - "fontCharacter": "\\E03A", + "fontCharacter": "\\E03B", "fontColor": "#cc3e44" }, "_haml_light": { - "fontCharacter": "\\E03C", + "fontCharacter": "\\E03D", "fontColor": "#b8383d" }, "_haml": { - "fontCharacter": "\\E03C", + "fontCharacter": "\\E03D", "fontColor": "#cc3e44" }, "_happenings_light": { - "fontCharacter": "\\E03D", + "fontCharacter": "\\E03E", "fontColor": "#498ba7" }, "_happenings": { - "fontCharacter": "\\E03D", + "fontCharacter": "\\E03E", "fontColor": "#519aba" }, "_haskell_light": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03F", "fontColor": "#9068b0" }, "_haskell": { - "fontCharacter": "\\E03E", + "fontCharacter": "\\E03F", "fontColor": "#a074c4" }, "_haxe_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#cc6d2e" }, "_haxe": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#e37933" }, "_haxe_1_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#b7b73b" }, "_haxe_1": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#cbcb41" }, "_haxe_2_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#498ba7" }, "_haxe_2": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#519aba" }, "_haxe_3_light": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#9068b0" }, "_haxe_3": { - "fontCharacter": "\\E03F", + "fontCharacter": "\\E040", "fontColor": "#a074c4" }, "_heroku_light": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#9068b0" }, "_heroku": { - "fontCharacter": "\\E040", + "fontCharacter": "\\E041", "fontColor": "#a074c4" }, "_hex_light": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E042", "fontColor": "#b8383d" }, "_hex": { - "fontCharacter": "\\E041", + "fontCharacter": "\\E042", "fontColor": "#cc3e44" }, "_html_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#498ba7" }, "_html": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#519aba" }, "_html_1_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#7fae42" }, "_html_1": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#8dc149" }, "_html_2_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#b7b73b" }, "_html_2": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#cbcb41" }, "_html_3_light": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#cc6d2e" }, "_html_3": { - "fontCharacter": "\\E042", + "fontCharacter": "\\E043", "fontColor": "#e37933" }, "_html_erb_light": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#b8383d" }, "_html_erb": { - "fontCharacter": "\\E043", + "fontCharacter": "\\E044", "fontColor": "#cc3e44" }, "_ignored_light": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E045", "fontColor": "#3b4b52" }, "_ignored": { - "fontCharacter": "\\E044", + "fontCharacter": "\\E045", "fontColor": "#41535b" }, "_illustrator_light": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E046", "fontColor": "#b7b73b" }, "_illustrator": { - "fontCharacter": "\\E045", + "fontCharacter": "\\E046", "fontColor": "#cbcb41" }, "_image_light": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E047", "fontColor": "#9068b0" }, "_image": { - "fontCharacter": "\\E046", + "fontCharacter": "\\E047", "fontColor": "#a074c4" }, "_info_light": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E048", "fontColor": "#498ba7" }, "_info": { - "fontCharacter": "\\E047", + "fontCharacter": "\\E048", "fontColor": "#519aba" }, "_ionic_light": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E049", "fontColor": "#498ba7" }, "_ionic": { - "fontCharacter": "\\E048", + "fontCharacter": "\\E049", "fontColor": "#519aba" }, "_jade_light": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E04A", "fontColor": "#b8383d" }, "_jade": { - "fontCharacter": "\\E049", + "fontCharacter": "\\E04A", "fontColor": "#cc3e44" }, "_java_light": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E04B", "fontColor": "#b8383d" }, "_java": { - "fontCharacter": "\\E04A", + "fontCharacter": "\\E04B", "fontColor": "#cc3e44" }, "_javascript_light": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#b7b73b" }, "_javascript": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#cbcb41" }, "_javascript_1_light": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#cc6d2e" }, "_javascript_1": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#e37933" }, "_javascript_2_light": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#498ba7" }, "_javascript_2": { - "fontCharacter": "\\E04B", + "fontCharacter": "\\E04C", "fontColor": "#519aba" }, "_jenkins_light": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#b8383d" }, "_jenkins": { - "fontCharacter": "\\E04C", + "fontCharacter": "\\E04D", "fontColor": "#cc3e44" }, "_jinja_light": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04E", "fontColor": "#b8383d" }, "_jinja": { - "fontCharacter": "\\E04D", + "fontCharacter": "\\E04E", "fontColor": "#cc3e44" }, "_json_light": { - "fontCharacter": "\\E04F", + "fontCharacter": "\\E050", "fontColor": "#b7b73b" }, "_json": { - "fontCharacter": "\\E04F", + "fontCharacter": "\\E050", "fontColor": "#cbcb41" }, "_json_1_light": { - "fontCharacter": "\\E04F", + "fontCharacter": "\\E050", "fontColor": "#7fae42" }, "_json_1": { - "fontCharacter": "\\E04F", + "fontCharacter": "\\E050", "fontColor": "#8dc149" }, "_julia_light": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#9068b0" }, "_julia": { - "fontCharacter": "\\E050", + "fontCharacter": "\\E051", "fontColor": "#a074c4" }, "_karma_light": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E052", "fontColor": "#7fae42" }, "_karma": { - "fontCharacter": "\\E051", + "fontCharacter": "\\E052", "fontColor": "#8dc149" }, "_kotlin_light": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E053", "fontColor": "#cc6d2e" }, "_kotlin": { - "fontCharacter": "\\E052", + "fontCharacter": "\\E053", "fontColor": "#e37933" }, "_less_light": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E054", "fontColor": "#498ba7" }, "_less": { - "fontCharacter": "\\E053", + "fontCharacter": "\\E054", "fontColor": "#519aba" }, "_license_light": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#b7b73b" }, "_license": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#cbcb41" }, "_license_1_light": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#cc6d2e" }, "_license_1": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#e37933" }, "_license_2_light": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#b8383d" }, "_license_2": { - "fontCharacter": "\\E054", + "fontCharacter": "\\E055", "fontColor": "#cc3e44" }, "_liquid_light": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#7fae42" }, "_liquid": { - "fontCharacter": "\\E055", + "fontCharacter": "\\E056", "fontColor": "#8dc149" }, "_livescript_light": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E057", "fontColor": "#498ba7" }, "_livescript": { - "fontCharacter": "\\E056", + "fontCharacter": "\\E057", "fontColor": "#519aba" }, "_lock_light": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E058", "fontColor": "#7fae42" }, "_lock": { - "fontCharacter": "\\E057", + "fontCharacter": "\\E058", "fontColor": "#8dc149" }, "_lua_light": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E059", "fontColor": "#498ba7" }, "_lua": { - "fontCharacter": "\\E058", + "fontCharacter": "\\E059", "fontColor": "#519aba" }, "_makefile_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#cc6d2e" }, "_makefile": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#e37933" }, "_makefile_1_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#9068b0" }, "_makefile_1": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#a074c4" }, "_makefile_2_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#627379" }, "_makefile_2": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#6d8086" }, "_makefile_3_light": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#498ba7" }, "_makefile_3": { - "fontCharacter": "\\E059", + "fontCharacter": "\\E05A", "fontColor": "#519aba" }, "_markdown_light": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#498ba7" }, "_markdown": { - "fontCharacter": "\\E05A", + "fontCharacter": "\\E05B", "fontColor": "#519aba" }, "_maven_light": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05C", "fontColor": "#b8383d" }, "_maven": { - "fontCharacter": "\\E05B", + "fontCharacter": "\\E05C", "fontColor": "#cc3e44" }, "_mdo_light": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05D", "fontColor": "#b8383d" }, "_mdo": { - "fontCharacter": "\\E05C", + "fontCharacter": "\\E05D", "fontColor": "#cc3e44" }, "_mustache_light": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05E", "fontColor": "#cc6d2e" }, "_mustache": { - "fontCharacter": "\\E05D", + "fontCharacter": "\\E05E", "fontColor": "#e37933" }, "_npm_light": { - "fontCharacter": "\\E05F", + "fontCharacter": "\\E060", "fontColor": "#3b4b52" }, "_npm": { - "fontCharacter": "\\E05F", + "fontCharacter": "\\E060", "fontColor": "#41535b" }, "_npm_1_light": { - "fontCharacter": "\\E05F", + "fontCharacter": "\\E060", "fontColor": "#b8383d" }, "_npm_1": { - "fontCharacter": "\\E05F", + "fontCharacter": "\\E060", "fontColor": "#cc3e44" }, "_npm_ignored_light": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E061", "fontColor": "#3b4b52" }, "_npm_ignored": { - "fontCharacter": "\\E060", + "fontCharacter": "\\E061", "fontColor": "#41535b" }, "_nunjucks_light": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E062", "fontColor": "#7fae42" }, "_nunjucks": { - "fontCharacter": "\\E061", + "fontCharacter": "\\E062", "fontColor": "#8dc149" }, "_ocaml_light": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E063", "fontColor": "#cc6d2e" }, "_ocaml": { - "fontCharacter": "\\E062", + "fontCharacter": "\\E063", "fontColor": "#e37933" }, "_odata_light": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E064", "fontColor": "#cc6d2e" }, "_odata": { - "fontCharacter": "\\E063", + "fontCharacter": "\\E064", "fontColor": "#e37933" }, "_pddl_light": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E065", "fontColor": "#9068b0" }, "_pddl": { - "fontCharacter": "\\E064", + "fontCharacter": "\\E065", "fontColor": "#a074c4" }, "_pdf_light": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E066", "fontColor": "#b8383d" }, "_pdf": { - "fontCharacter": "\\E065", + "fontCharacter": "\\E066", "fontColor": "#cc3e44" }, "_perl_light": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E067", "fontColor": "#498ba7" }, "_perl": { - "fontCharacter": "\\E066", + "fontCharacter": "\\E067", "fontColor": "#519aba" }, "_photoshop_light": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E068", "fontColor": "#498ba7" }, "_photoshop": { - "fontCharacter": "\\E067", + "fontCharacter": "\\E068", "fontColor": "#519aba" }, "_php_light": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E069", "fontColor": "#9068b0" }, "_php": { - "fontCharacter": "\\E068", + "fontCharacter": "\\E069", "fontColor": "#a074c4" }, "_plan_light": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E06A", "fontColor": "#7fae42" }, "_plan": { - "fontCharacter": "\\E069", + "fontCharacter": "\\E06A", "fontColor": "#8dc149" }, "_platformio_light": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E06B", "fontColor": "#cc6d2e" }, "_platformio": { - "fontCharacter": "\\E06A", + "fontCharacter": "\\E06B", "fontColor": "#e37933" }, "_powershell_light": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06C", "fontColor": "#498ba7" }, "_powershell": { - "fontCharacter": "\\E06B", + "fontCharacter": "\\E06C", "fontColor": "#519aba" }, "_pug_light": { - "fontCharacter": "\\E06D", + "fontCharacter": "\\E06E", "fontColor": "#b8383d" }, "_pug": { - "fontCharacter": "\\E06D", + "fontCharacter": "\\E06E", "fontColor": "#cc3e44" }, "_puppet_light": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06F", "fontColor": "#b7b73b" }, "_puppet": { - "fontCharacter": "\\E06E", + "fontCharacter": "\\E06F", "fontColor": "#cbcb41" }, "_python_light": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E070", "fontColor": "#498ba7" }, "_python": { - "fontCharacter": "\\E06F", + "fontCharacter": "\\E070", "fontColor": "#519aba" }, "_react_light": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E072", "fontColor": "#498ba7" }, "_react": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E072", "fontColor": "#519aba" }, "_react_1_light": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E072", "fontColor": "#cc6d2e" }, "_react_1": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E072", "fontColor": "#e37933" }, "_react_2_light": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E072", "fontColor": "#b7b73b" }, "_react_2": { - "fontCharacter": "\\E071", + "fontCharacter": "\\E072", "fontColor": "#cbcb41" }, "_reasonml_light": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#b8383d" }, "_reasonml": { - "fontCharacter": "\\E072", + "fontCharacter": "\\E073", "fontColor": "#cc3e44" }, "_rollup_light": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E074", "fontColor": "#b8383d" }, "_rollup": { - "fontCharacter": "\\E073", + "fontCharacter": "\\E074", "fontColor": "#cc3e44" }, "_ruby_light": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E075", "fontColor": "#b8383d" }, "_ruby": { - "fontCharacter": "\\E074", + "fontCharacter": "\\E075", "fontColor": "#cc3e44" }, "_rust_light": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E076", "fontColor": "#627379" }, "_rust": { - "fontCharacter": "\\E075", + "fontCharacter": "\\E076", "fontColor": "#6d8086" }, "_salesforce_light": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E077", "fontColor": "#498ba7" }, "_salesforce": { - "fontCharacter": "\\E076", + "fontCharacter": "\\E077", "fontColor": "#519aba" }, "_sass_light": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E078", "fontColor": "#dd4b78" }, "_sass": { - "fontCharacter": "\\E077", + "fontCharacter": "\\E078", "fontColor": "#f55385" }, "_sbt_light": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E079", "fontColor": "#498ba7" }, "_sbt": { - "fontCharacter": "\\E078", + "fontCharacter": "\\E079", "fontColor": "#519aba" }, "_scala_light": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07A", "fontColor": "#b8383d" }, "_scala": { - "fontCharacter": "\\E079", + "fontCharacter": "\\E07A", "fontColor": "#cc3e44" }, "_shell_light": { - "fontCharacter": "\\E07C", + "fontCharacter": "\\E07D", "fontColor": "#455155" }, "_shell": { - "fontCharacter": "\\E07C", + "fontCharacter": "\\E07D", "fontColor": "#4d5a5e" }, "_slim_light": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07E", "fontColor": "#cc6d2e" }, "_slim": { - "fontCharacter": "\\E07D", + "fontCharacter": "\\E07E", "fontColor": "#e37933" }, "_smarty_light": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E07F", "fontColor": "#b7b73b" }, "_smarty": { - "fontCharacter": "\\E07E", + "fontCharacter": "\\E07F", "fontColor": "#cbcb41" }, "_spring_light": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E080", "fontColor": "#7fae42" }, "_spring": { - "fontCharacter": "\\E07F", + "fontCharacter": "\\E080", "fontColor": "#8dc149" }, "_stylelint_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E081", "fontColor": "#bfc2c1" }, "_stylelint": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E081", "fontColor": "#d4d7d6" }, "_stylelint_1_light": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E081", "fontColor": "#455155" }, "_stylelint_1": { - "fontCharacter": "\\E080", + "fontCharacter": "\\E081", "fontColor": "#4d5a5e" }, "_stylus_light": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E082", "fontColor": "#7fae42" }, "_stylus": { - "fontCharacter": "\\E081", + "fontCharacter": "\\E082", "fontColor": "#8dc149" }, "_sublime_light": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E083", "fontColor": "#cc6d2e" }, "_sublime": { - "fontCharacter": "\\E082", + "fontCharacter": "\\E083", "fontColor": "#e37933" }, "_svg_light": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E084", "fontColor": "#9068b0" }, "_svg": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E084", "fontColor": "#a074c4" }, "_svg_1_light": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E084", "fontColor": "#498ba7" }, "_svg_1": { - "fontCharacter": "\\E083", + "fontCharacter": "\\E084", "fontColor": "#519aba" }, "_swift_light": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E085", "fontColor": "#cc6d2e" }, "_swift": { - "fontCharacter": "\\E084", + "fontCharacter": "\\E085", "fontColor": "#e37933" }, "_terraform_light": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E086", "fontColor": "#9068b0" }, "_terraform": { - "fontCharacter": "\\E085", + "fontCharacter": "\\E086", "fontColor": "#a074c4" }, "_tex_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#498ba7" }, "_tex": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#519aba" }, "_tex_1_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#b7b73b" }, "_tex_1": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#cbcb41" }, "_tex_2_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#cc6d2e" }, "_tex_2": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#e37933" }, "_tex_3_light": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#bfc2c1" }, "_tex_3": { - "fontCharacter": "\\E086", + "fontCharacter": "\\E087", "fontColor": "#d4d7d6" }, "_todo": { - "fontCharacter": "\\E088" + "fontCharacter": "\\E089" }, "_tsconfig_light": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08A", "fontColor": "#498ba7" }, "_tsconfig": { - "fontCharacter": "\\E089", + "fontCharacter": "\\E08A", "fontColor": "#519aba" }, "_twig_light": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08B", "fontColor": "#7fae42" }, "_twig": { - "fontCharacter": "\\E08A", + "fontCharacter": "\\E08B", "fontColor": "#8dc149" }, "_typescript_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#498ba7" }, "_typescript": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#519aba" }, "_typescript_1_light": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#b7b73b" }, "_typescript_1": { - "fontCharacter": "\\E08B", + "fontCharacter": "\\E08C", "fontColor": "#cbcb41" }, "_vala_light": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#627379" }, "_vala": { - "fontCharacter": "\\E08C", + "fontCharacter": "\\E08D", "fontColor": "#6d8086" }, "_video_light": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08E", "fontColor": "#dd4b78" }, "_video": { - "fontCharacter": "\\E08D", + "fontCharacter": "\\E08E", "fontColor": "#f55385" }, "_vue_light": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E08F", "fontColor": "#7fae42" }, "_vue": { - "fontCharacter": "\\E08E", + "fontCharacter": "\\E08F", "fontColor": "#8dc149" }, "_wasm_light": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E090", "fontColor": "#9068b0" }, "_wasm": { - "fontCharacter": "\\E08F", + "fontCharacter": "\\E090", "fontColor": "#a074c4" }, "_wat_light": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E091", "fontColor": "#9068b0" }, "_wat": { - "fontCharacter": "\\E090", + "fontCharacter": "\\E091", "fontColor": "#a074c4" }, "_webpack_light": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#498ba7" }, "_webpack": { - "fontCharacter": "\\E091", + "fontCharacter": "\\E092", "fontColor": "#519aba" }, "_wgt_light": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E093", "fontColor": "#498ba7" }, "_wgt": { - "fontCharacter": "\\E092", + "fontCharacter": "\\E093", "fontColor": "#519aba" }, "_windows_light": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E094", "fontColor": "#498ba7" }, "_windows": { - "fontCharacter": "\\E093", + "fontCharacter": "\\E094", "fontColor": "#519aba" }, "_word_light": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E095", "fontColor": "#498ba7" }, "_word": { - "fontCharacter": "\\E094", + "fontCharacter": "\\E095", "fontColor": "#519aba" }, "_xls_light": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E096", "fontColor": "#7fae42" }, "_xls": { - "fontCharacter": "\\E095", + "fontCharacter": "\\E096", "fontColor": "#8dc149" }, "_xml_light": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E097", "fontColor": "#cc6d2e" }, "_xml": { - "fontCharacter": "\\E096", + "fontCharacter": "\\E097", "fontColor": "#e37933" }, "_yarn_light": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E098", "fontColor": "#498ba7" }, "_yarn": { - "fontCharacter": "\\E097", + "fontCharacter": "\\E098", "fontColor": "#519aba" }, "_yml_light": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E099", "fontColor": "#9068b0" }, "_yml": { - "fontCharacter": "\\E098", + "fontCharacter": "\\E099", "fontColor": "#a074c4" }, "_zip_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#b8383d" }, "_zip": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#cc3e44" }, "_zip_1_light": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#627379" }, "_zip_1": { - "fontCharacter": "\\E099", + "fontCharacter": "\\E09A", "fontColor": "#6d8086" }, // {{SQL CARBON EDIT}} @@ -1662,6 +1670,7 @@ "ruby": "_ruby", "rust": "_rust", "scss": "_sass", + "search-result": "_code-search", "shellscript": "_shell", "sql": "_db", "swift": "_swift", @@ -1931,6 +1940,7 @@ "ruby": "_ruby_light", "rust": "_rust_light", "scss": "_sass_light", + "search-result": "_code-search_light", "shellscript": "_shell_light", "sql": "_db_light", "swift": "_swift_light", @@ -2017,5 +2027,5 @@ "Schema Compare": "scmp" } }, - "version": "https://github.com/jesseweed/seti-ui/commit/85a222708824c6f19bbecbec71633d2c97077dad" + "version": "https://github.com/jesseweed/seti-ui/commit/4b3e0a3d0ca8999430bc3aa9f2c8324e6922b3de" } \ No newline at end of file diff --git a/extensions/types/lib.textEncoder.d.ts b/extensions/types/lib.textEncoder.d.ts new file mode 100644 index 000000000000..e1b6cc8cdd3e --- /dev/null +++ b/extensions/types/lib.textEncoder.d.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. + *--------------------------------------------------------------------------------------------*/ + +// Define TextEncoder + TextDecoder globals for both browser and node runtimes +// +// Proper fix: https://github.com/microsoft/TypeScript/issues/31535 + +declare var TextDecoder: typeof import('util').TextDecoder; +declare var TextEncoder: typeof import('util').TextEncoder; diff --git a/extensions/types/lib.url.d.ts b/extensions/types/lib.url.d.ts new file mode 100644 index 000000000000..3170181ebd57 --- /dev/null +++ b/extensions/types/lib.url.d.ts @@ -0,0 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Define Url global for both browser and node runtimes +// +// Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/issues/34960 + +declare const URL: typeof import('url').URL; diff --git a/extensions/vscode-account/package.json b/extensions/vscode-account/package.json index 35649821a85c..1cc728195e98 100644 --- a/extensions/vscode-account/package.json +++ b/extensions/vscode-account/package.json @@ -17,14 +17,13 @@ "main": "./out/extension.js", "scripts": { "vscode:prepublish": "npm run compile", - "compile": "tsc -p ./", - "watch": "tsc -watch -p ./" + "compile": "gulp compile-extension:vscode-account", + "watch": "gulp watch-extension:vscode-account" }, "devDependencies": { "typescript": "^3.7.4", "tslint": "^5.12.1", "@types/node": "^10.12.21", - "@types/keytar": "^4.0.1", - "@types/vscode": "^1.41.0" + "@types/keytar": "^4.0.1" } } diff --git a/extensions/vscode-account/src/AADHelper.ts b/extensions/vscode-account/src/AADHelper.ts index 48a313610640..9146510fc9d2 100644 --- a/extensions/vscode-account/src/AADHelper.ts +++ b/extensions/vscode-account/src/AADHelper.ts @@ -18,11 +18,12 @@ const clientId = 'aebc6443-996d-45c2-90f0-388ff96faa56'; const tenant = 'organizations'; interface IToken { - expiresIn: string; // How long access token is valid, in seconds - accessToken: string; + accessToken?: string; // When unable to refresh due to network problems, the access token becomes undefined + + expiresIn?: string; // How long access token is valid, in seconds refreshToken: string; - displayName: string; + accountName: string; scope: string; sessionId: string; // The account id + the scope } @@ -33,6 +34,7 @@ interface ITokenClaims { unique_name?: string; oid?: string; altsecid?: string; + ipd?: string; scp: string; } @@ -40,13 +42,36 @@ interface IStoredSession { id: string; refreshToken: string; scope: string; // Scopes are alphabetized and joined with a space + accountName: string; +} + +function parseQuery(uri: vscode.Uri) { + return uri.query.split('&').reduce((prev: any, current) => { + const queryString = current.split('='); + prev[queryString[0]] = queryString[1]; + return prev; + }, {}); } export const onDidChangeSessions = new vscode.EventEmitter(); +export const REFRESH_NETWORK_FAILURE = 'Network failure'; + +class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { + public handleUri(uri: vscode.Uri) { + this.fire(uri); + } +} + export class AzureActiveDirectoryService { private _tokens: IToken[] = []; private _refreshTimeouts: Map = new Map(); + private _uriHandler: UriEventHandler; + + constructor() { + this._uriHandler = new UriEventHandler(); + vscode.window.registerUriHandler(this._uriHandler); + } public async initialize(): Promise { const storedData = await keychain.getToken(); @@ -57,7 +82,21 @@ export class AzureActiveDirectoryService { try { await this.refreshToken(session.refreshToken, session.scope); } catch (e) { - await this.logout(session.id); + if (e.message === REFRESH_NETWORK_FAILURE) { + const didSucceedOnRetry = await this.handleRefreshNetworkError(session.id, session.refreshToken, session.scope); + if (!didSucceedOnRetry) { + this._tokens.push({ + accessToken: undefined, + refreshToken: session.refreshToken, + accountName: session.accountName, + scope: session.scope, + sessionId: session.id + }); + this.pollForReconnect(session.id, session.refreshToken, session.scope); + } + } else { + await this.logout(session.id); + } } }); @@ -79,7 +118,8 @@ export class AzureActiveDirectoryService { return { id: token.sessionId, refreshToken: token.refreshToken, - scope: token.scope + scope: token.scope, + accountName: token.accountName }; }); @@ -100,7 +140,11 @@ export class AzureActiveDirectoryService { await this.refreshToken(session.refreshToken, session.scope); didChange = true; } catch (e) { - await this.logout(session.id); + if (e.message === REFRESH_NETWORK_FAILURE) { + // Ignore, will automatically retry on next poll. + } else { + await this.logout(session.id); + } } } }); @@ -136,11 +180,11 @@ export class AzureActiveDirectoryService { }, 1000 * 30); } - private convertToSession(token: IToken): vscode.Session { + private convertToSession(token: IToken): vscode.AuthenticationSession { return { id: token.sessionId, - accessToken: token.accessToken, - displayName: token.displayName, + accessToken: () => !token.accessToken ? Promise.reject('Unavailable due to network problems') : Promise.resolve(token.accessToken), + accountName: token.accountName, scopes: token.scope.split(' ') }; } @@ -154,12 +198,18 @@ export class AzureActiveDirectoryService { } } - get sessions(): vscode.Session[] { + get sessions(): vscode.AuthenticationSession[] { return this._tokens.map(token => this.convertToSession(token)); } 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); @@ -206,6 +256,13 @@ export class AzureActiveDirectoryService { res.writeHead(302, { Location: `/?error=${encodeURIComponent(err && err.message || 'Unknown error')}` }); res.end(); } + } 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); + } } finally { setTimeout(() => { server.close(); @@ -213,28 +270,101 @@ export class AzureActiveDirectoryService { } } + private getCallbackEnvironment(callbackUri: vscode.Uri): string { + switch (callbackUri.authority) { + case 'online.visualstudio.com,': + return 'vso'; + case 'online-ppe.core.vsengsaas.visualstudio.com': + return 'vsoppe,'; + case 'online.dev.core.vsengsaas.visualstudio.com': + return 'vsodev,'; + default: + return ''; + } + } + + private async loginWithoutLocalServer(scope: string): Promise { + const callbackUri = await vscode.env.asExternalUri(vscode.Uri.parse(`${vscode.env.uriScheme}://vscode.vscode-account`)); + 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); + const state = `${callbackEnvironment}${port},${encodeURIComponent(nonce)},${encodeURIComponent(callbackUri.query)}`; + const signInUrl = `${loginEndpointUrl}${tenant}/oauth2/v2.0/authorize`; + let uri = vscode.Uri.parse(signInUrl); + const codeVerifier = toBase64UrlEncoding(crypto.randomBytes(32).toString('base64')); + const codeChallenge = toBase64UrlEncoding(crypto.createHash('sha256').update(codeVerifier).digest('base64')); + uri = uri.with({ + query: `response_type=code&client_id=${encodeURIComponent(clientId)}&response_mode=query&redirect_uri=${redirectUrl}&state=${state}&scope=${scope}&prompt=select_account&code_challenge_method=S256&code_challenge=${codeChallenge}` + }); + vscode.env.openExternal(uri); + + const timeoutPromise = new Promise((_: (value: IToken) => void, reject) => { + const wait = setTimeout(() => { + clearTimeout(wait); + reject('Login timed out.'); + }, 1000 * 60 * 5); + }); + + return Promise.race([this.handleCodeResponse(state, codeVerifier, scope), timeoutPromise]); + } + + private async handleCodeResponse(state: string, codeVerifier: string, scope: string) { + let uriEventListener: vscode.Disposable; + return new Promise((resolve: (value: IToken) => void, reject) => { + uriEventListener = this._uriHandler.event(async (uri: vscode.Uri) => { + try { + const query = parseQuery(uri); + const code = query.code; + + if (query.state !== state) { + throw new Error('State does not match.'); + } + + const token = await this.exchangeCodeForToken(code, codeVerifier, scope); + this.setToken(token, scope); + + resolve(token); + } catch (err) { + reject(err); + } + }); + }).then(result => { + uriEventListener.dispose(); + return result; + }).catch(err => { + uriEventListener.dispose(); + throw err; + }); + } + private async setToken(token: IToken, scope: string): Promise { - const existingToken = this._tokens.findIndex(t => t.sessionId === token.sessionId); - if (existingToken) { - this._tokens.splice(existingToken, 1, token); + const existingTokenIndex = this._tokens.findIndex(t => t.sessionId === token.sessionId); + if (existingTokenIndex > -1) { + this._tokens.splice(existingTokenIndex, 1, token); } else { this._tokens.push(token); } - const existingTimeout = this._refreshTimeouts.get(token.sessionId); - if (existingTimeout) { - clearTimeout(existingTimeout); - } + this.clearSessionTimeout(token.sessionId); - this._refreshTimeouts.set(token.sessionId, setTimeout(async () => { - try { - await this.refreshToken(token.refreshToken, scope); - } catch (e) { - await this.logout(token.sessionId); - } finally { - onDidChangeSessions.fire(); - } - }, 1000 * (parseInt(token.expiresIn) - 10))); + if (token.expiresIn) { + this._refreshTimeouts.set(token.sessionId, setTimeout(async () => { + try { + await this.refreshToken(token.refreshToken, scope); + onDidChangeSessions.fire(); + } catch (e) { + if (e.message === REFRESH_NETWORK_FAILURE) { + const didSucceedOnRetry = await this.handleRefreshNetworkError(token.sessionId, token.refreshToken, scope); + if (!didSucceedOnRetry) { + this.pollForReconnect(token.sessionId, token.refreshToken, token.scope); + } + } else { + await this.logout(token.sessionId); + onDidChangeSessions.fire(); + } + } + }, 1000 * (parseInt(token.expiresIn) - 30))); + } this.storeTokenData(); } @@ -247,8 +377,8 @@ export class AzureActiveDirectoryService { accessToken: json.access_token, refreshToken: json.refresh_token, scope, - sessionId: claims.tid + (claims.oid || claims.altsecid) + scope, - displayName: claims.email || claims.unique_name || 'user@example.com' + sessionId: `${claims.tid}/${(claims.oid || (claims.altsecid || '' + claims.ipd || ''))}/${scope}`, + accountName: claims.email || claims.unique_name || 'user@example.com' }; } @@ -282,8 +412,10 @@ export class AzureActiveDirectoryService { }); result.on('end', () => { if (result.statusCode === 200) { + Logger.info('Exchanging login code for token success'); resolve(this.getTokenFromResponse(buffer, scope)); } else { + Logger.error('Exchanging login code for token failed'); reject(new Error('Unable to login.')); } }); @@ -344,29 +476,80 @@ export class AzureActiveDirectoryService { post.end(); post.on('error', err => { Logger.error(err.message); - reject(err); + reject(new Error(REFRESH_NETWORK_FAILURE)); }); }); } - public async logout(sessionId: string) { - Logger.info(`Logging out of session '${sessionId}'`); + private clearSessionTimeout(sessionId: string): void { + const timeout = this._refreshTimeouts.get(sessionId); + if (timeout) { + clearTimeout(timeout); + this._refreshTimeouts.delete(sessionId); + } + } + + private removeInMemorySessionData(sessionId: string) { const tokenIndex = this._tokens.findIndex(token => token.sessionId === sessionId); if (tokenIndex > -1) { this._tokens.splice(tokenIndex, 1); } + this.clearSessionTimeout(sessionId); + } + + private pollForReconnect(sessionId: string, refreshToken: string, scope: string): void { + this.clearSessionTimeout(sessionId); + + this._refreshTimeouts.set(sessionId, setTimeout(async () => { + try { + await this.refreshToken(refreshToken, scope); + } catch (e) { + this.pollForReconnect(sessionId, refreshToken, scope); + } + }, 1000 * 60 * 30)); + } + + private handleRefreshNetworkError(sessionId: string, refreshToken: string, scope: string, attempts: number = 1): Promise { + return new Promise((resolve, _) => { + if (attempts === 3) { + Logger.error('Token refresh failed after 3 attempts'); + return resolve(false); + } + + if (attempts === 1) { + const token = this._tokens.find(token => token.sessionId === sessionId); + if (token) { + token.accessToken = undefined; + } + + onDidChangeSessions.fire(); + } + + const delayBeforeRetry = 5 * attempts * attempts; + + this.clearSessionTimeout(sessionId); + + this._refreshTimeouts.set(sessionId, setTimeout(async () => { + try { + await this.refreshToken(refreshToken, scope); + return resolve(true); + } catch (e) { + return resolve(await this.handleRefreshNetworkError(sessionId, refreshToken, scope, attempts + 1)); + } + }, 1000 * delayBeforeRetry)); + }); + } + + public async logout(sessionId: string) { + Logger.info(`Logging out of session '${sessionId}'`); + this.removeInMemorySessionData(sessionId); + if (this._tokens.length === 0) { await keychain.deleteToken(); } else { this.storeTokenData(); } - - const timeout = this._refreshTimeouts.get(sessionId); - if (timeout) { - clearTimeout(timeout); - this._refreshTimeouts.delete(sessionId); - } } public async clearSessions() { diff --git a/extensions/vscode-account/src/authServer.ts b/extensions/vscode-account/src/authServer.ts index 074b7ddd93ca..e0d91f839c54 100644 --- a/extensions/vscode-account/src/authServer.ts +++ b/extensions/vscode-account/src/authServer.ts @@ -87,8 +87,8 @@ export async function startServer(server: http.Server): Promise { } }); - server.on('error', err => { - reject(err); + server.on('error', _ => { + reject(new Error('Error listening to server')); }); server.on('close', () => { diff --git a/extensions/vscode-account/src/extension.ts b/extensions/vscode-account/src/extension.ts index 3b0007ae495a..1bd358034fdb 100644 --- a/extensions/vscode-account/src/extension.ts +++ b/extensions/vscode-account/src/extension.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { AzureActiveDirectoryService, onDidChangeSessions } from './AADHelper'; -export async function activate(context: vscode.ExtensionContext) { +export async function activate(_: vscode.ExtensionContext) { const loginService = new AzureActiveDirectoryService(); diff --git a/extensions/vscode-account/src/keychain.ts b/extensions/vscode-account/src/keychain.ts index 0e4986403593..d05285eb8494 100644 --- a/extensions/vscode-account/src/keychain.ts +++ b/extensions/vscode-account/src/keychain.ts @@ -7,6 +7,7 @@ // how we load it import * as keytarType from 'keytar'; import { env } from 'vscode'; +import Logger from './logger'; function getKeytar(): Keytar | undefined { try { @@ -44,22 +45,27 @@ export class Keychain { return await this.keytar.setPassword(SERVICE_ID, ACCOUNT_ID, token); } catch (e) { // Ignore + Logger.error(`Setting token failed: ${e}`); } } - async getToken() { + async getToken(): Promise { try { return await this.keytar.getPassword(SERVICE_ID, ACCOUNT_ID); } catch (e) { // Ignore + Logger.error(`Getting token failed: ${e}`); + return Promise.resolve(undefined); } } - async deleteToken() { + async deleteToken(): Promise { try { return await this.keytar.deletePassword(SERVICE_ID, ACCOUNT_ID); } catch (e) { // Ignore + Logger.error(`Deleting token failed: ${e}`); + return Promise.resolve(undefined); } } } diff --git a/extensions/vscode-account/src/typings/refs.d.ts b/extensions/vscode-account/src/typings/refs.d.ts new file mode 100644 index 000000000000..c82a621bfa3b --- /dev/null +++ b/extensions/vscode-account/src/typings/refs.d.ts @@ -0,0 +1,7 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// diff --git a/extensions/vscode-account/src/vscode.proposed.d.ts b/extensions/vscode-account/src/vscode.proposed.d.ts deleted file mode 100644 index d06d51c2c407..000000000000 --- a/extensions/vscode-account/src/vscode.proposed.d.ts +++ /dev/null @@ -1,61 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the Source EULA. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -/** - * This is the place for API experiments and proposals. - * These API are NOT stable and subject to change. They are only available in the Insiders - * distribution and CANNOT be used in published extensions. - * - * To test these API in local environment: - * - Use Insiders release of VS Code. - * - Add `"enableProposedApi": true` to your package.json. - * - Copy this file to your project. - */ - -declare module 'vscode' { - - export interface Session { - id: string; - accessToken: string; - displayName: string; - scopes: string[] - } - - export interface AuthenticationProvider { - readonly id: string; - readonly displayName: string; - readonly onDidChangeSessions: Event; - - /** - * Returns an array of current sessions. - */ - getSessions(): Promise>; - - /** - * Prompts a user to login. - */ - login(scopes: string[]): Promise; - logout(sessionId: string): Promise; - } - - export namespace authentication { - export function registerAuthenticationProvider(provider: AuthenticationProvider): Disposable; - - /** - * Fires with the provider id that was registered or unregistered. - */ - export const onDidRegisterAuthenticationProvider: Event; - export const onDidUnregisterAuthenticationProvider: Event; - - export const providers: ReadonlyArray; - } - - // #region Ben - extension auth flow (desktop+web) - - export namespace env { - - export function asExternalUri(target: Uri): Thenable - } -} diff --git a/extensions/vscode-account/tsconfig.json b/extensions/vscode-account/tsconfig.json index 46be6dc9581a..1225709307b0 100644 --- a/extensions/vscode-account/tsconfig.json +++ b/extensions/vscode-account/tsconfig.json @@ -1,24 +1,13 @@ { + "extends": "../shared.tsconfig.json", "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "out", - "lib": [ - "es6", - "es2016", - "dom" - ], + "outDir": "./out", + "experimentalDecorators": true, "typeRoots": [ - "node_modules/@types", - "src/typings" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true, - "noImplicitAny": true + "./node_modules/@types" + ] }, - "exclude": [ - "node_modules", - ".vscode-test" + "include": [ + "src/**/*" ] } diff --git a/extensions/vscode-account/yarn.lock b/extensions/vscode-account/yarn.lock index 4fc295de4b92..3acdda242e97 100644 --- a/extensions/vscode-account/yarn.lock +++ b/extensions/vscode-account/yarn.lock @@ -30,11 +30,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== -"@types/vscode@^1.41.0": - version "1.41.0" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.41.0.tgz#b0d75920220f84e07093285e59180c0f11d336cd" - integrity sha512-7SfeY5u9jgiELwxyLB3z7l6l/GbN9CqpCQGkcRlB7tKRFBxzbz2PoBfGrLxI1vRfUCIq5+hg5vtDHExwq5j3+A== - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" diff --git a/extensions/vscode-colorize-tests/package.json b/extensions/vscode-colorize-tests/package.json index 152a4c915958..da71634c2e7e 100644 --- a/extensions/vscode-colorize-tests/package.json +++ b/extensions/vscode-colorize-tests/package.json @@ -26,21 +26,25 @@ "vscode": "1.1.5" }, "contributes": { - "tokenTypes": [ + "semanticTokenTypes": [ { "id": "testToken", "description": "A test token" } ], - "tokenModifiers": [ + "semanticTokenModifiers": [ { "id": "testModifier", "description": "A test modifier" } ], - "tokenStyleDefaults": [ + "semanticTokenStyleDefaults": [ { - "selector": "testToken.testModifier", + "selector": "testToken", + "scope": [ "entity.name.function.special" ] + }, + { + "selector": "*.testModifier", "light": { "fontStyle": "bold" }, @@ -50,7 +54,6 @@ "highContrast": { "fontStyle": "bold" } - } ] } diff --git a/extensions/yarn.lock b/extensions/yarn.lock index dc1fa7c2de8b..1d6447cfe6d4 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -typescript@3.7.5: - version "3.7.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae" - integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw== +typescript@^3.8.1-rc: + version "3.8.1-rc" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.1-rc.tgz#f94333c14da70927ccd887be2e91be652a9a09f6" + integrity sha512-aOIe066DyZn2uYIiND6fXMUUJ70nxwu/lKhA92QuQzXyC86fr0ywo1qvO8l2m0EnDcfjprYPuFRgNgDj7U2GlQ== diff --git a/package.json b/package.json index 761a657e2503..41a0cd09afcf 100644 --- a/package.json +++ b/package.json @@ -10,12 +10,13 @@ "private": true, "scripts": { "test": "mocha", + "test-browser": "node test/unit/browser/index.js", "preinstall": "node build/npm/preinstall.js", "postinstall": "node build/npm/postinstall.js", "compile": "gulp compile --max_old_space_size=4095", "watch": "gulp watch --max_old_space_size=4095", "watch-client": "gulp watch-client --max_old_space_size=4095", - "monaco-editor-test": "mocha --only-monaco-editor", + "mocha": "mocha test/unit/node/all.js --delay", "precommit": "node build/gulpfile.hygiene.js", "gulp": "gulp --max_old_space_size=8192", "electron": "node build/lib/electron", @@ -47,7 +48,7 @@ "applicationinsights": "1.0.8", "chart.js": "^2.6.0", "chokidar": "3.2.3", - "graceful-fs": "4.1.11", + "graceful-fs": "4.2.3", "html-query-plan": "git://github.com/anthonydresser/html-query-plan.git#2.6", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", @@ -77,10 +78,11 @@ "vscode-ripgrep": "^1.5.7", "vscode-sqlite3": "4.0.9", "vscode-textmate": "4.4.0", - "xterm": "4.4.0-beta.15", - "xterm-addon-search": "0.4.0-beta4", + "xterm": "4.4.0", + "xterm-addon-search": "0.4.0", + "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", - "xterm-addon-webgl": "0.5.0-beta.7", + "xterm-addon-webgl": "0.5.0", "yauzl": "^2.9.2", "yazl": "^2.4.3", "zone.js": "^0.8.4" @@ -91,6 +93,7 @@ "@types/chart.js": "2.7.57", "@types/chokidar": "2.1.3", "@types/cookie": "^0.3.3", + "@types/debug": "^4.1.5", "@types/graceful-fs": "4.1.2", "@types/http-proxy-agent": "^2.0.1", "@types/iconv-lite": "0.0.1", @@ -117,11 +120,10 @@ "coveralls": "^2.11.11", "cson-parser": "^1.3.3", "debounce": "^1.0.0", - "electron": "6.1.6", + "electron": "7.1.11", "eslint": "6.8.0", "eslint-plugin-jsdoc": "^19.1.0", "event-stream": "3.3.4", - "express": "^4.13.1", "fancy-log": "^1.3.3", "fast-plist": "0.1.2", "glob": "^5.0.13", @@ -163,6 +165,7 @@ "opn": "^6.0.0", "optimist": "0.3.5", "p-all": "^1.0.0", + "playwright": "^0.10.0", "pump": "^1.0.1", "queue": "3.0.6", "rcedit": "^1.1.0", @@ -172,7 +175,7 @@ "temp-write": "^3.4.0", "ts-loader": "^4.4.2", "typemoq": "^0.3.2", - "typescript": "3.8.0-beta", + "typescript": "^3.8.1-rc", "typescript-formatter": "7.1.0", "vinyl": "^2.0.0", "vinyl-fs": "^3.0.0", @@ -181,7 +184,8 @@ "vscode-nls-dev": "^3.3.1", "webpack": "^4.16.5", "webpack-cli": "^3.3.8", - "webpack-stream": "^5.1.1" + "webpack-stream": "^5.1.1", + "yaserver": "^0.2.0" }, "repository": { "type": "git", diff --git a/remote/package.json b/remote/package.json index 95ea409067f7..3481542d70f8 100644 --- a/remote/package.json +++ b/remote/package.json @@ -5,7 +5,7 @@ "applicationinsights": "1.0.8", "chokidar": "3.2.3", "cookie": "^0.4.0", - "graceful-fs": "4.1.11", + "graceful-fs": "4.2.3", "http-proxy-agent": "^2.1.0", "https-proxy-agent": "^2.2.3", "iconv-lite": "0.5.0", @@ -20,10 +20,11 @@ "vscode-proxy-agent": "^0.5.2", "vscode-ripgrep": "^1.5.7", "vscode-textmate": "4.4.0", - "xterm": "4.4.0-beta.15", - "xterm-addon-search": "0.4.0-beta4", + "xterm": "4.4.0", + "xterm-addon-search": "0.4.0", + "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", - "xterm-addon-webgl": "0.5.0-beta.7", + "xterm-addon-webgl": "0.5.0", "yauzl": "^2.9.2", "yazl": "^2.4.3" }, diff --git a/remote/web/package.json b/remote/web/package.json index 5bf2c6c7d9c4..70a55a9074f2 100644 --- a/remote/web/package.json +++ b/remote/web/package.json @@ -5,9 +5,10 @@ "onigasm-umd": "2.2.5", "semver-umd": "^5.5.5", "vscode-textmate": "4.4.0", - "xterm": "4.4.0-beta.15", - "xterm-addon-search": "0.4.0-beta4", + "xterm": "4.4.0", + "xterm-addon-search": "0.4.0", + "xterm-addon-unicode11": "0.1.1", "xterm-addon-web-links": "0.2.1", - "xterm-addon-webgl": "0.5.0-beta.7" + "xterm-addon-webgl": "0.5.0" } } diff --git a/remote/web/yarn.lock b/remote/web/yarn.lock index 12afd3c3d84a..b4c1b7d0b6cd 100644 --- a/remote/web/yarn.lock +++ b/remote/web/yarn.lock @@ -31,22 +31,27 @@ vscode-textmate@4.4.0: dependencies: oniguruma "^7.2.0" -xterm-addon-search@0.4.0-beta4: - version "0.4.0-beta4" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.4.0-beta4.tgz#7762ea342c6b4f5e824d83466bd93793c9d7d779" - integrity sha512-TIbEBVhydGIxcyu/CfKJbD+BKHisMGbkAfaWlCPaWis2Xmw8yE7CKrCPn+lhZYl1MdjDVEmb8lQI6WetbC2OZA== +xterm-addon-search@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.4.0.tgz#a7beadb3caa7330eb31fb1f17d92de25537684a1" + integrity sha512-g07qb/Z4aSfrQ25e6Z6rz6KiExm2DvesQXkx+eA715VABBr5VM/9Jf0INoCiDSYy/nn7rpna+kXiGVJejIffKg== + +xterm-addon-unicode11@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.1.1.tgz#b209ef137db38096f68636af4ef4d0c0acba85ad" + integrity sha512-z6vJTL+dpNljwAYzYoyDjJP8A2XjZuEosl0sRa+FGRf3jEyEVWquDM53MfUd1ztVdAPQ839qR6eYK1BXV04Bhw== xterm-addon-web-links@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== -xterm-addon-webgl@0.5.0-beta.7: - version "0.5.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0-beta.7.tgz#b7b95a362e942ad6f86fa286d7b7bd8ee3e7cf67" - integrity sha512-v6aCvhm1C6mvaurGwUYQfyhb2cAUyuVnzf3Ob/hy5ebtyzUj4wW0N9NbqDEJk67UeMi1lV2xZqrO5gNeTpVqFA== +xterm-addon-webgl@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" + integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0-beta.15: - version "4.4.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0-beta.15.tgz#5897bf79d29d1a2496ccd54665aded28c341b1cc" - integrity sha512-Dvz1CMCYKeoxPF7uIDznbRgUA2Mct49Bq93K2nnrDU0pDMM3Sf1t9fkEyz59wxSx5XEHVdLS80jywsz4sjXBjQ== +xterm@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" + integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== diff --git a/remote/yarn.lock b/remote/yarn.lock index 387226ccd2aa..bc865255b62a 100644 --- a/remote/yarn.lock +++ b/remote/yarn.lock @@ -155,12 +155,7 @@ glob-parent@~5.1.0: dependencies: is-glob "^4.0.1" -graceful-fs@4.1.11: - version "4.1.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" - integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= - -graceful-fs@^4.1.2, graceful-fs@^4.1.6: +graceful-fs@4.2.3, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== @@ -418,25 +413,30 @@ vscode-windows-registry@1.0.2: resolved "https://registry.yarnpkg.com/vscode-windows-registry/-/vscode-windows-registry-1.0.2.tgz#b863e704a6a69c50b3098a55fbddbe595b0c124a" integrity sha512-/CLLvuOSM2Vme2z6aNyB+4Omd7hDxpf4Thrt8ImxnXeQtxzel2bClJpFQvQqK/s4oaXlkBKS7LqVLeZM+uSVIA== -xterm-addon-search@0.4.0-beta4: - version "0.4.0-beta4" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.4.0-beta4.tgz#7762ea342c6b4f5e824d83466bd93793c9d7d779" - integrity sha512-TIbEBVhydGIxcyu/CfKJbD+BKHisMGbkAfaWlCPaWis2Xmw8yE7CKrCPn+lhZYl1MdjDVEmb8lQI6WetbC2OZA== +xterm-addon-search@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.4.0.tgz#a7beadb3caa7330eb31fb1f17d92de25537684a1" + integrity sha512-g07qb/Z4aSfrQ25e6Z6rz6KiExm2DvesQXkx+eA715VABBr5VM/9Jf0INoCiDSYy/nn7rpna+kXiGVJejIffKg== + +xterm-addon-unicode11@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/xterm-addon-unicode11/-/xterm-addon-unicode11-0.1.1.tgz#b209ef137db38096f68636af4ef4d0c0acba85ad" + integrity sha512-z6vJTL+dpNljwAYzYoyDjJP8A2XjZuEosl0sRa+FGRf3jEyEVWquDM53MfUd1ztVdAPQ839qR6eYK1BXV04Bhw== xterm-addon-web-links@0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.2.1.tgz#6d1f2ce613e09870badf17615e7a1170a31542b2" integrity sha512-2KnHtiq0IG7hfwv3jw2/jQeH1RBk2d5CH4zvgwQe00rLofSJqSfgnJ7gwowxxpGHrpbPr6Lv4AmH/joaNw2+HQ== -xterm-addon-webgl@0.5.0-beta.7: - version "0.5.0-beta.7" - resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0-beta.7.tgz#b7b95a362e942ad6f86fa286d7b7bd8ee3e7cf67" - integrity sha512-v6aCvhm1C6mvaurGwUYQfyhb2cAUyuVnzf3Ob/hy5ebtyzUj4wW0N9NbqDEJk67UeMi1lV2xZqrO5gNeTpVqFA== +xterm-addon-webgl@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/xterm-addon-webgl/-/xterm-addon-webgl-0.5.0.tgz#c1031dc7599cce3509824643ab5f15361c928e3e" + integrity sha512-hQrvabKCnwXFaEZ+YtoJM9Pm0CIBXL5KSwoU+RiGStU3KYTAcqYP2GsH3dWdvKX6kTWhWLS81dtDsGkfbOciuA== -xterm@4.4.0-beta.15: - version "4.4.0-beta.15" - resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0-beta.15.tgz#5897bf79d29d1a2496ccd54665aded28c341b1cc" - integrity sha512-Dvz1CMCYKeoxPF7uIDznbRgUA2Mct49Bq93K2nnrDU0pDMM3Sf1t9fkEyz59wxSx5XEHVdLS80jywsz4sjXBjQ== +xterm@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.4.0.tgz#5915d3c4c8800fadbcf555a0a603c672ab9df589" + integrity sha512-JGIpigWM3EBWvnS3rtBuefkiToIILSK1HYMXy4BCsUpO+O4UeeV+/U1AdAXgCB6qJrnPNb7yLgBsVCQUNMteig== yauzl@^2.9.2: version "2.10.0" diff --git a/scripts/test-integration.bat b/scripts/test-integration.bat index 61ba87ef6345..20bbbb03b968 100755 --- a/scripts/test-integration.bat +++ b/scripts/test-integration.bat @@ -3,7 +3,7 @@ setlocal pushd %~dp0\.. -set VSCODEUSERDATADIR=%TMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,5% +set VSCODEUSERDATADIR=%TEMP%\vscodeuserfolder-%RANDOM%-%TIME:~6,2% :: Figure out which Electron to use for running tests if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( @@ -12,7 +12,7 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( set INTEGRATION_TEST_ELECTRON_PATH=.\scripts\code.bat set VSCODE_BUILD_BUILTIN_EXTENSIONS_SILENCE_PLEASE=1 - echo "Running integration tests out of sources." + echo Running integration tests out of sources. ) else ( :: Run from a built: need to compile all test extensions call yarn gulp compile-extension:vscode-api-tests @@ -22,13 +22,14 @@ if "%INTEGRATION_TEST_ELECTRON_PATH%"=="" ( call yarn gulp compile-extension:css-language-features-server call yarn gulp compile-extension:html-language-features-server call yarn gulp compile-extension:json-language-features-server + call yarn gulp compile-extension:git :: Configuration for more verbose output set VSCODE_CLI=1 set ELECTRON_ENABLE_LOGGING=1 set ELECTRON_ENABLE_STACK_DUMPING=1 - echo "Running integration tests with '%INTEGRATION_TEST_ELECTRON_PATH%' as build." + echo Running integration tests with '%INTEGRATION_TEST_ELECTRON_PATH%' as build. ) :: Integration & performance tests in AMD @@ -53,6 +54,12 @@ if %errorlevel% neq 0 exit /b %errorlevel% call "%INTEGRATION_TEST_ELECTRON_PATH%" %~dp0\..\extensions\azurecore\test-fixtures --extensionDevelopmentPath=%~dp0\..\extensions\azurecore --extensionTestsPath=%~dp0\..\extensions\azurecore\out\test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% if %errorlevel% neq 0 exit /b %errorlevel% +for /f "delims=" %%i in ('node -p "require('fs').realpathSync.native(require('os').tmpdir())"') do set TEMPDIR=%%i +set GITWORKSPACE=%TEMPDIR%\git-%RANDOM% +mkdir %GITWORKSPACE% +call "%INTEGRATION_TEST_ELECTRON_PATH%" %GITWORKSPACE% --extensionDevelopmentPath=%~dp0\..\extensions\git --extensionTestsPath=%~dp0\..\extensions\git\out\test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --user-data-dir=%VSCODEUSERDATADIR% +if %errorlevel% neq 0 exit /b %errorlevel% + :: Tests in commonJS (HTML, CSS, JSON language server tests...) call .\scripts\node-electron.bat .\node_modules\mocha\bin\_mocha .\extensions\*\server\out\test\**\*.test.js if %errorlevel% neq 0 exit /b %errorlevel% diff --git a/scripts/test-integration.sh b/scripts/test-integration.sh index 32498a378fc7..691dcf107793 100755 --- a/scripts/test-integration.sh +++ b/scripts/test-integration.sh @@ -4,13 +4,12 @@ set -e if [[ "$OSTYPE" == "darwin"* ]]; then realpath() { [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"; } ROOT=$(dirname $(dirname $(realpath "$0"))) - VSCODEUSERDATADIR=`mktemp -d -t 'myuserdatadir'` else ROOT=$(dirname $(dirname $(readlink -f $0))) - VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` LINUX_NO_SANDBOX="--no-sandbox" # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. fi +VSCODEUSERDATADIR=`mktemp -d 2>/dev/null` cd $ROOT # Figure out which Electron to use for running tests @@ -29,6 +28,7 @@ else yarn gulp compile-extension:css-language-features-server yarn gulp compile-extension:html-language-features-server yarn gulp compile-extension:json-language-features-server + yarn gulp compile-extension:git # Configuration for more verbose output export VSCODE_CLI=1 @@ -47,6 +47,7 @@ fi "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/vscode-colorize-tests/test --extensionDevelopmentPath=$ROOT/extensions/vscode-colorize-tests --extensionTestsPath=$ROOT/extensions/vscode-colorize-tests/out --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/markdown-language-features/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/markdown-language-features --extensionTestsPath=$ROOT/extensions/markdown-language-features/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/azurecore/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/azurecore --extensionTestsPath=$ROOT/extensions/azurecore/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR +# "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $(mktemp -d 2>/dev/null) --extensionDevelopmentPath=$ROOT/extensions/git --extensionTestsPath=$ROOT/extensions/git/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR mkdir -p $ROOT/extensions/emmet/test-fixtures "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX $ROOT/extensions/emmet/test-fixtures --extensionDevelopmentPath=$ROOT/extensions/emmet --extensionTestsPath=$ROOT/extensions/emmet/out/test --disable-telemetry --disable-crash-reporter --disable-updates --disable-extensions --skip-getting-started --user-data-dir=$VSCODEUSERDATADIR diff --git a/scripts/test.bat b/scripts/test.bat index 2906a09f9b58..82295d898735 100644 --- a/scripts/test.bat +++ b/scripts/test.bat @@ -24,7 +24,7 @@ if "%ADS_TEST_GREP%" == "" ( :: Run tests set ELECTRON_ENABLE_LOGGING=1 -%CODE% .\test\electron\index.js %* +%CODE% .\test\unit\electron\index.js %* popd diff --git a/scripts/test.sh b/scripts/test.sh index 3fcc904302e9..31008811e17a 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -36,10 +36,10 @@ if [[ "$OSTYPE" == "darwin"* ]] || [[ "$AGENT_OS" == "Darwin"* ]]; then cd $ROOT ; ulimit -n 4096 ; \ ELECTRON_ENABLE_LOGGING=1 \ "$CODE" \ - test/electron/index.js $PASSED_ARGS + test/unit/electron/index.js "$@" else cd $ROOT ; \ ELECTRON_ENABLE_LOGGING=1 \ "$CODE" \ - test/electron/index.js --no-sandbox $PASSED_ARGS # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. + test/unit/electron/index.js --no-sandbox "$@" # Electron 6 introduces a chrome-sandbox that requires root to run. This can fail. Disable sandbox via --no-sandbox. fi diff --git a/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts b/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts index b8819a2ed96c..c839a3fc34b1 100644 --- a/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts +++ b/src/sql/workbench/contrib/charts/test/browser/chartView.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { ChartView } from 'sql/workbench/contrib/charts/browser/chartView'; import { ContextViewService } from 'vs/platform/contextview/browser/contextViewService'; -import { TestLayoutService } from 'vs/workbench/test/workbenchTestServices'; +import { TestLayoutService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IThemeService } from 'vs/platform/theme/common/themeService'; diff --git a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts index f7da1c77d0bf..a4a919c23fe6 100644 --- a/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts +++ b/src/sql/workbench/contrib/commandLine/test/electron-browser/commandLine.test.ts @@ -22,7 +22,7 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; -import { TestEditorService, TestDialogService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEditorService, TestDialogService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { TestQueryModelService } from 'sql/workbench/services/query/test/common/testQueryModelService'; @@ -393,7 +393,7 @@ suite('commandLineService tests', () => { querymodelService.setup(c => c.onRunQueryComplete).returns(() => Event.None); const instantiationService = new TestInstantiationService(); let uri = URI.file(args._[0]); - const untitledEditorInput = new UntitledTextEditorInput(uri, false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined); + const untitledEditorInput = new UntitledTextEditorInput(uri, false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); const queryInput = new UntitledQueryEditorInput(undefined, untitledEditorInput, undefined, connectionManagementService.object, querymodelService.object, configurationService.object); queryInput.state.connected = true; const editorService: TypeMoq.Mock = TypeMoq.Mock.ofType(TestEditorService, TypeMoq.MockBehavior.Strict); diff --git a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts index 835fd58c299c..b65e123df198 100644 --- a/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts +++ b/src/sql/workbench/contrib/dataExplorer/browser/dataExplorerViewlet.ts @@ -15,7 +15,6 @@ import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewl import { IStorageService } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; import { ConnectionViewletPanel } from 'sql/workbench/contrib/dataExplorer/browser/connectionViewletPanel'; import { Extensions as ViewContainerExtensions, IViewDescriptor, IViewsRegistry, IViewContainersRegistry, ViewContainerLocation, IViewDescriptorService } from 'vs/workbench/common/views'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -146,11 +145,6 @@ export class DataExplorerViewPaneContainer extends ViewPaneContainer { return actions; } - protected onDidAddViews(added: IAddedViewDescriptorRef[]): ViewPane[] { - const addedViews = super.onDidAddViews(added); - return addedViews; - } - protected createView(viewDescriptor: IViewDescriptor, options: IViewletViewOptions): ViewPane { let viewletPanel = this.instantiationService.createInstance(viewDescriptor.ctorDescriptor.ctor, options) as ViewPane; this._register(viewletPanel); diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts index 172ffe2a0996..044bd9d17c36 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookEditorModel.test.ts @@ -28,7 +28,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { TestEnvironmentService, TestLifecycleService, TestStorageService, TestTextFileService, workbenchInstantiationService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEnvironmentService, TestLifecycleService, TestStorageService, TestTextFileService, workbenchInstantiationService, TestTextResourcePropertiesService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Range } from 'vs/editor/common/core/range'; import { nb } from 'azdata'; import { Emitter } from 'vs/base/common/event'; diff --git a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts index 0f81b7e8eba1..a3a213f6a617 100644 --- a/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts +++ b/src/sql/workbench/contrib/notebook/test/browser/notebookInput.test.ts @@ -6,7 +6,7 @@ import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; import { nb } from 'azdata'; -import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/common/models/untitledNotebookInput'; @@ -48,7 +48,7 @@ suite('Notebook Input', function (): void { let untitledNotebookInput: UntitledNotebookInput; setup(() => { - untitledTextInput = new UntitledTextEditorInput(untitledUri, false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined); + untitledTextInput = new UntitledTextEditorInput(untitledUri, false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); untitledNotebookInput = new UntitledNotebookInput( testTitle, untitledUri, untitledTextInput, undefined, instantiationService, mockNotebookService.object, mockExtensionService.object); @@ -169,7 +169,7 @@ suite('Notebook Input', function (): void { assert.ok(untitledNotebookInput.matches(untitledNotebookInput), 'Input should match itself.'); let otherTestUri = URI.from({ scheme: Schemas.untitled, path: 'OtherTestPath' }); - let otherTextInput = new UntitledTextEditorInput(otherTestUri, false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined); + let otherTextInput = new UntitledTextEditorInput(otherTestUri, false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); let otherInput = instantiationService.createInstance(UntitledNotebookInput, 'OtherTestInput', otherTestUri, otherTextInput); assert.strictEqual(untitledNotebookInput.matches(otherInput), false, 'Input should not match different input.'); diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts index 55d31f14f0ed..5af9275334d2 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/contentManagers.test.ts @@ -11,7 +11,7 @@ import * as tempWrite from 'temp-write'; import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager'; import { CellTypes } from 'sql/workbench/contrib/notebook/common/models/contracts'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { TestFileService } from 'vs/workbench/test/workbenchTestServices'; +import { TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IFileService, IReadFileOptions, IFileContent, IWriteFileOptions, IFileStatWithMetadata } from 'vs/platform/files/common/files'; import * as pfs from 'vs/base/node/pfs'; import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer'; diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts index 55692f3615d0..3b68fec9f045 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookFindModel.test.ts @@ -25,7 +25,7 @@ import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/t import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ClientSession } from 'sql/workbench/contrib/notebook/browser/models/clientSession'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import { NotebookRange } from 'sql/workbench/contrib/notebook/find/notebookFindDecorations'; import { NotebookEditorContentManager } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; diff --git a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts index 883c1cbeac13..e88a5138e0b8 100644 --- a/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts +++ b/src/sql/workbench/contrib/notebook/test/electron-browser/notebookModel.test.ts @@ -22,7 +22,7 @@ import { Memento } from 'vs/workbench/common/memento'; import { Emitter } from 'vs/base/common/event'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; diff --git a/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts b/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts index b4cbbb6e35cc..829e986186a3 100644 --- a/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts +++ b/src/sql/workbench/contrib/objectExplorer/test/browser/connectionTreeActions.test.ts @@ -22,7 +22,7 @@ import { TreeNode } from 'sql/workbench/contrib/objectExplorer/common/treeNode'; import { NodeType } from 'sql/workbench/contrib/objectExplorer/common/nodeType'; import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; import { ServerTreeDataSource } from 'sql/workbench/contrib/objectExplorer/browser/serverTreeDataSource'; -import { Emitter } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import { ObjectExplorerActionsContext } from 'sql/workbench/contrib/objectExplorer/browser/objectExplorerActions'; import { IConnectionResult, IConnectionParams } from 'sql/platform/connection/common/connectionManagement'; @@ -31,7 +31,7 @@ import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/t import { UNSAVED_GROUP_ID, mssqlProviderName } from 'sql/platform/connection/common/constants'; import { $ } from 'vs/base/browser/dom'; import { OEManageConnectionAction } from 'sql/workbench/contrib/dashboard/browser/dashboardActions'; -import { IViewsService, IView, ViewContainer, IViewDescriptorCollection } from 'vs/workbench/common/views'; +import { IViewsService, IView } from 'vs/workbench/common/views'; import { ConsoleLogService } from 'vs/platform/log/common/log'; suite('SQL Connection Tree Action tests', () => { @@ -109,18 +109,22 @@ suite('SQL Connection Tree Action tests', () => { }); const viewsService = new class implements IViewsService { - getActiveViewWithId(id: string): IView { + getActiveViewWithId(id: string): T | null { throw new Error('Method not implemented.'); } _serviceBrand: undefined; - openView(id: string, focus?: boolean): Promise { - return Promise.resolve({ + openView(id: string, focus?: boolean): Promise { + return Promise.resolve({ id: '', serversTree: undefined }); } - getViewDescriptors(container: ViewContainer): IViewDescriptorCollection { - throw new Error('Method not implemented.'); + onDidChangeViewVisibility: Event<{ id: string, visible: boolean }> = Event.None; + closeView(id: string): void { + return; + } + isViewVisible(id: string): boolean { + return true; } }; diff --git a/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts b/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts index 677aecef5027..712a4bcfd62b 100644 --- a/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts +++ b/src/sql/workbench/contrib/objectExplorer/test/browser/serverTreeView.test.ts @@ -8,7 +8,7 @@ import { ServerTreeView } from 'sql/workbench/contrib/objectExplorer/browser/ser import { ConnectionManagementService } from 'sql/workbench/services/connection/browser/connectionManagementService'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import * as TypeMoq from 'typemoq'; import { TestCapabilitiesService } from 'sql/platform/capabilities/test/common/testCapabilitiesService'; diff --git a/src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts b/src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts index 4f909c057428..85370428e29f 100644 --- a/src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts +++ b/src/sql/workbench/contrib/query/common/fileQueryEditorInput.ts @@ -11,8 +11,8 @@ import { IQueryModelService } from 'sql/workbench/services/query/common/queryMod import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { EncodingMode } from 'vs/workbench/common/editor'; -import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { BinaryEditorModel } from 'vs/workbench/common/editor/binaryEditorModel'; +import { ITextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles'; type PublicPart = { [K in keyof T]: T[K] }; @@ -31,7 +31,7 @@ export class FileQueryEditorInput extends QueryEditorInput implements PublicPart super(description, text, results, connectionManagementService, queryModelService, configurationService); } - public resolve(): Promise { + public resolve(): Promise { return this.text.resolve(); } diff --git a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts index c8992bcb9699..4ef6980ecc70 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryActions.test.ts @@ -23,7 +23,7 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; -import { TestStorageService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; @@ -70,7 +70,7 @@ suite('SQL QueryAction Tests', () => { connectionManagementService = TypeMoq.Mock.ofType(TestConnectionManagementService); connectionManagementService.setup(q => q.onDisconnect).returns(() => Event.None); const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); // Setup a reusable mock QueryInput testQueryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Strict, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); testQueryInput.setup(x => x.uri).returns(() => testUri); @@ -175,7 +175,7 @@ suite('SQL QueryAction Tests', () => { queryModelService.setup(x => x.onRunQueryStart).returns(() => Event.None); queryModelService.setup(x => x.onRunQueryComplete).returns(() => Event.None); const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); // ... Mock "isSelectionEmpty" in QueryEditor let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Strict, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); @@ -224,7 +224,7 @@ suite('SQL QueryAction Tests', () => { // ... Mock "getSelection" in QueryEditor const instantiationService = new TestInstantiationService(); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); let queryInput = TypeMoq.Mock.ofType(UntitledQueryEditorInput, TypeMoq.MockBehavior.Loose, undefined, fileInput, undefined, connectionManagementService.object, queryModelService.object, configurationService.object); queryInput.setup(x => x.uri).returns(() => testUri); diff --git a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts index 2d41241b479f..bd58cfb784c8 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryEditor.test.ts @@ -18,7 +18,7 @@ import * as TypeMoq from 'typemoq'; import * as assert from 'assert'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; @@ -285,7 +285,7 @@ suite('SQL QueryEditor Tests', () => { return new RunQueryAction(undefined, undefined, undefined); }); - let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, '', '', '', instantiationService.object, undefined, new LabelService(undefined, undefined), undefined, undefined); + let fileInput = new UntitledTextEditorInput(URI.parse('file://testUri'), false, undefined, undefined, undefined, instantiationService.object, undefined, new LabelService(undefined, undefined), undefined, undefined, new TestFileService(), undefined); queryModelService = TypeMoq.Mock.ofType(TestQueryModelService, TypeMoq.MockBehavior.Strict); queryModelService.setup(x => x.disposeQuery(TypeMoq.It.isAny())); queryModelService.setup(x => x.onRunQueryComplete).returns(() => Event.None); diff --git a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts index 23a316e1bb41..0449cbb07fdc 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { URI } from 'vs/base/common/uri'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorInput } from 'vs/workbench/common/editor'; diff --git a/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts b/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts index 5c5060213c68..e7f546964b4c 100644 --- a/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts +++ b/src/sql/workbench/services/accountManagement/test/browser/accountManagementService.test.ts @@ -13,7 +13,7 @@ import { AccountAdditionResult, AccountProviderAddedEventParams, UpdateAccountLi import { IAccountStore } from 'sql/platform/accounts/common/interfaces'; import { AccountProviderStub } from 'sql/platform/accounts/test/common/testAccountManagementService'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import { EventVerifierSingle } from 'sql/base/test/common/event'; // SUITE CONSTANTS ///////////////////////////////////////////////////////// diff --git a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts index 2b8f1888432e..c3baff0ef7b0 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionDialogService.test.ts @@ -10,7 +10,7 @@ import { ConnectionType, IConnectableInput, IConnectionResult, INewConnectionPar import { TestErrorMessageService } from 'sql/platform/errorMessage/test/common/testErrorMessageService'; import * as TypeMoq from 'typemoq'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts index 9b46514a5d79..09fda856d868 100644 --- a/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts +++ b/src/sql/workbench/services/connection/test/browser/connectionManagementService.test.ts @@ -28,7 +28,7 @@ import * as TypeMoq from 'typemoq'; import { IConnectionProfileGroup, ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService'; -import { TestStorageService, TestEnvironmentService, TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService, TestEnvironmentService, TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { TestNotificationService } from 'vs/platform/notification/test/common/testNotificationService'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { NullLogService } from 'vs/platform/log/common/log'; diff --git a/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts b/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts index 8123d1e83d24..c7de35a7b56f 100644 --- a/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts +++ b/src/sql/workbench/services/insights/test/browser/insightsDialogController.test.ts @@ -13,7 +13,7 @@ import { InstantiationService } from 'vs/platform/instantiation/common/instantia import * as azdata from 'azdata'; import { equal } from 'assert'; import { Mock, MockBehavior, It } from 'typemoq'; -import { TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { TestStorageService } from 'vs/workbench/test/browser/workbenchTestServices'; import { Emitter } from 'vs/base/common/event'; import { InsightsDialogModel } from 'sql/workbench/services/insights/browser/insightsDialogModel'; import { IInsightsConfigDetails } from 'sql/platform/dashboard/browser/insightRegistry'; diff --git a/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts b/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts index d8659a56f566..34b59985ad14 100644 --- a/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts +++ b/src/sql/workbench/services/insights/test/electron-browser/insightsUtils.test.ts @@ -12,7 +12,7 @@ import * as path from 'vs/base/common/path'; import { Workspace, toWorkspaceFolder, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ConfigurationResolverService } from 'vs/workbench/services/configurationResolver/browser/configurationResolverService'; -import { TestContextService, TestFileService } from 'vs/workbench/test/workbenchTestServices'; +import { TestContextService, TestFileService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IExtensionHostDebugParams, IDebugParams, ParsedArgs } from 'vs/platform/environment/common/environment'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; diff --git a/src/sql/workbench/test/browser/taskUtilities.test.ts b/src/sql/workbench/test/browser/taskUtilities.test.ts index c979059fffad..56287456c9e3 100644 --- a/src/sql/workbench/test/browser/taskUtilities.test.ts +++ b/src/sql/workbench/test/browser/taskUtilities.test.ts @@ -10,7 +10,7 @@ import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/br import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; -import { TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { assign } from 'vs/base/common/objects'; suite('TaskUtilities', function () { diff --git a/src/sql/workbench/test/common/editorReplacerContribution.test.ts b/src/sql/workbench/test/common/editorReplacerContribution.test.ts index 076c61eab6e5..e303538982e1 100644 --- a/src/sql/workbench/test/common/editorReplacerContribution.test.ts +++ b/src/sql/workbench/test/common/editorReplacerContribution.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { EditorReplacementContribution } from 'sql/workbench/common/editorReplacerContribution'; -import { TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { TestEditorService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; import { Event } from 'vs/base/common/event'; import { IMode, LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; diff --git a/src/sql/workbench/test/electron-browser/api/extHostAccountManagement.test.ts b/src/sql/workbench/test/electron-browser/api/extHostAccountManagement.test.ts index 0ae61eb2fa29..efa0e1049e8e 100644 --- a/src/sql/workbench/test/electron-browser/api/extHostAccountManagement.test.ts +++ b/src/sql/workbench/test/electron-browser/api/extHostAccountManagement.test.ts @@ -8,13 +8,13 @@ import * as azdata from 'azdata'; import * as TypeMoq from 'typemoq'; import { AccountProviderStub, TestAccountManagementService } from 'sql/platform/accounts/test/common/testAccountManagementService'; import { ExtHostAccountManagement } from 'sql/workbench/api/common/extHostAccountManagement'; -import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier'; import { SqlMainContext } from 'sql/workbench/api/common/sqlExtHost.protocol'; import { MainThreadAccountManagement } from 'sql/workbench/api/browser/mainThreadAccountManagement'; import { IAccountManagementService, AzureResource } from 'sql/platform/accounts/common/interfaces'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; const IRPCProtocol = createDecorator('rpcProtocol'); diff --git a/src/sql/workbench/test/electron-browser/api/extHostCredentialManagement.test.ts b/src/sql/workbench/test/electron-browser/api/extHostCredentialManagement.test.ts index 5b36aa977fcc..3320ea40e112 100644 --- a/src/sql/workbench/test/electron-browser/api/extHostCredentialManagement.test.ts +++ b/src/sql/workbench/test/electron-browser/api/extHostCredentialManagement.test.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ExtHostCredentialManagement } from 'sql/workbench/api/common/extHostCredentialManagement'; import { SqlMainContext } from 'sql/workbench/api/common/sqlExtHost.protocol'; @@ -14,6 +13,7 @@ import { ICredentialsService } from 'sql/platform/credentials/common/credentials import { Credential, CredentialProvider } from 'azdata'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { TestCredentialsService, TestCredentialsProvider } from 'sql/platform/credentials/test/common/testCredentialsService'; +import { TestRPCProtocol } from 'vs/workbench/test/browser/api/testRPCProtocol'; const IRPCProtocol = createDecorator('rpcProtocol'); diff --git a/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts b/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts index b0518062a783..3d5c4ea684fa 100644 --- a/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts +++ b/src/sql/workbench/test/electron-browser/api/mainThreadNotebook.test.ts @@ -15,7 +15,7 @@ import { NotebookService } from 'sql/workbench/services/notebook/browser/noteboo import { INotebookProvider } from 'sql/workbench/services/notebook/browser/notebookService'; import { INotebookManagerDetails, INotebookSessionDetails, INotebookKernelDetails, INotebookFutureDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; import { LocalContentManager } from 'sql/workbench/services/notebook/common/localContentManager'; -import { TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/workbenchTestServices'; +import { TestLifecycleService, TestEnvironmentService } from 'vs/workbench/test/browser/workbenchTestServices'; import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; import { ExtHostNotebookShape } from 'sql/workbench/api/common/sqlExtHost.protocol'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; diff --git a/src/sql/workbench/test/workbenchTestServices.ts b/src/sql/workbench/test/workbenchTestServices.ts index 7282a6211119..8dcb5b3b1b7a 100644 --- a/src/sql/workbench/test/workbenchTestServices.ts +++ b/src/sql/workbench/test/workbenchTestServices.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ITestInstantiationService, workbenchInstantiationService as vsworkbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; +import { ITestInstantiationService, workbenchInstantiationService as vsworkbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IQueryModelService } from 'sql/workbench/services/query/common/queryModel'; import { TestQueryModelService } from 'sql/workbench/services/query/test/common/testQueryModelService'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; diff --git a/src/vs/base/browser/browser.ts b/src/vs/base/browser/browser.ts index e5600bb4f4f4..57da73ec8528 100644 --- a/src/vs/base/browser/browser.ts +++ b/src/vs/base/browser/browser.ts @@ -120,6 +120,6 @@ export const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0); export const isChrome = (userAgent.indexOf('Chrome') >= 0); export const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0)); export const isWebkitWebView = (!isChrome && !isSafari && isWebKit); -export const isIPad = (userAgent.indexOf('iPad') >= 0); +export const isIPad = (userAgent.indexOf('iPad') >= 0 || (isSafari && navigator.maxTouchPoints > 0)); export const isEdgeWebView = isEdge && (userAgent.indexOf('WebView/') >= 0); export const isStandalone = (window.matchMedia && window.matchMedia('(display-mode: standalone)').matches); diff --git a/src/vs/base/browser/dom.ts b/src/vs/base/browser/dom.ts index 5fb7fd0ebe1c..e749f1e1bdc3 100644 --- a/src/vs/base/browser/dom.ts +++ b/src/vs/base/browser/dom.ts @@ -1313,7 +1313,22 @@ export function asCSSUrl(uri: URI): string { return `url('${asDomUri(uri).toString(true).replace(/'/g, '%27')}')`; } -export function triggerDownload(uri: URI, name: string): void { + +export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void { + + // If the data is provided as Buffer, we create a + // blog URL out of it to produce a valid link + let url: string; + if (URI.isUri(dataOrUri)) { + url = dataOrUri.toString(true); + } else { + const blob = new Blob([dataOrUri]); + url = URL.createObjectURL(blob); + + // Ensure to free the data from DOM eventually + setTimeout(() => URL.revokeObjectURL(url)); + } + // In order to download from the browser, the only way seems // to be creating a element with download attribute that // points to the file to download. @@ -1321,7 +1336,7 @@ export function triggerDownload(uri: URI, name: string): void { const anchor = document.createElement('a'); document.body.appendChild(anchor); anchor.download = name; - anchor.href = uri.toString(true); + anchor.href = url; anchor.click(); // Ensure to remove the element from DOM eventually diff --git a/src/vs/base/browser/fastDomNode.ts b/src/vs/base/browser/fastDomNode.ts index 8be66b6d902e..021c363526ab 100644 --- a/src/vs/base/browser/fastDomNode.ts +++ b/src/vs/base/browser/fastDomNode.ts @@ -28,6 +28,7 @@ export class FastDomNode { private _backgroundColor: string; private _layerHint: boolean; private _contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'; + private _boxShadow: string; constructor(domNode: T) { this.domNode = domNode; @@ -51,6 +52,7 @@ export class FastDomNode { this._backgroundColor = ''; this._layerHint = false; this._contain = 'none'; + this._boxShadow = ''; } public setMaxWidth(maxWidth: number): void { @@ -218,6 +220,14 @@ export class FastDomNode { this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : ''; } + public setBoxShadow(boxShadow: string): void { + if (this._boxShadow === boxShadow) { + return; + } + this._boxShadow = boxShadow; + this.domNode.style.boxShadow = boxShadow; + } + public setContain(contain: 'none' | 'strict' | 'content' | 'size' | 'layout' | 'style' | 'paint'): void { if (this._contain === contain) { return; diff --git a/src/vs/base/browser/ui/menu/menubar.ts b/src/vs/base/browser/ui/menu/menubar.ts index 0331a871a7a2..5c25b68f7b40 100644 --- a/src/vs/base/browser/ui/menu/menubar.ts +++ b/src/vs/base/browser/ui/menu/menubar.ts @@ -88,7 +88,7 @@ export class MenuBar extends Disposable { private numMenusShown: number = 0; private menuStyle: IMenuStyles | undefined; - private overflowLayoutScheduled: IDisposable | null = null; + private overflowLayoutScheduled: IDisposable | undefined = undefined; constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) { super(); @@ -419,9 +419,8 @@ export class MenuBar extends Disposable { DOM.removeNode(this.overflowMenu.titleElement); DOM.removeNode(this.overflowMenu.buttonElement); - if (this.overflowLayoutScheduled) { - this.overflowLayoutScheduled = dispose(this.overflowLayoutScheduled); - } + dispose(this.overflowLayoutScheduled); + this.overflowLayoutScheduled = undefined; } blur(): void { @@ -561,7 +560,7 @@ export class MenuBar extends Disposable { if (!this.overflowLayoutScheduled) { this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(() => { this.updateOverflowAction(); - this.overflowLayoutScheduled = null; + this.overflowLayoutScheduled = undefined; }); } diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts index 5fa413dc178e..9130435aa4f6 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElement.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElement.ts @@ -287,6 +287,7 @@ export abstract class AbstractScrollableElement extends Widget { this._options.handleMouseWheel = massagedOptions.handleMouseWheel; this._options.mouseWheelScrollSensitivity = massagedOptions.mouseWheelScrollSensitivity; this._options.fastScrollSensitivity = massagedOptions.fastScrollSensitivity; + this._options.scrollPredominantAxis = massagedOptions.scrollPredominantAxis; this._setListeningToMouseWheel(this._options.handleMouseWheel); if (!this._options.lazyRender) { @@ -334,6 +335,14 @@ export abstract class AbstractScrollableElement extends Widget { let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity; let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity; + if (this._options.scrollPredominantAxis) { + if (Math.abs(deltaY) >= Math.abs(deltaX)) { + deltaX = 0; + } else { + deltaY = 0; + } + } + if (this._options.flipAxes) { [deltaY, deltaX] = [deltaX, deltaY]; } @@ -553,6 +562,7 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false), mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1), fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5), + scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true), mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true), arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11), diff --git a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts index 7276a5769a63..10449df397f1 100644 --- a/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts +++ b/src/vs/base/browser/ui/scrollbar/scrollableElementOptions.ts @@ -55,6 +55,13 @@ export interface ScrollableElementCreationOptions { * Defaults to 5. */ fastScrollSensitivity?: number; + /** + * Whether the scrollable will only scroll along the predominant axis when scrolling both + * vertically and horizontally at the same time. + * Prevents horizontal drift when scrolling vertically on a trackpad. + * Defaults to true. + */ + scrollPredominantAxis?: boolean; /** * Height for vertical arrows (top/bottom) and width for horizontal arrows (left/right). * Defaults to 11. @@ -113,6 +120,7 @@ export interface ScrollableElementChangeOptions { handleMouseWheel?: boolean; mouseWheelScrollSensitivity?: number; fastScrollSensitivity: number; + scrollPredominantAxis: boolean; } export interface ScrollableElementResolvedOptions { @@ -125,6 +133,7 @@ export interface ScrollableElementResolvedOptions { alwaysConsumeMouseWheel: boolean; mouseWheelScrollSensitivity: number; fastScrollSensitivity: number; + scrollPredominantAxis: boolean; mouseWheelSmoothScroll: boolean; arrowSize: number; listenOnDomNode: HTMLElement | null; diff --git a/src/vs/base/browser/ui/tree/abstractTree.ts b/src/vs/base/browser/ui/tree/abstractTree.ts index c2fc137dab49..e9c7c135d4a4 100644 --- a/src/vs/base/browser/ui/tree/abstractTree.ts +++ b/src/vs/base/browser/ui/tree/abstractTree.ts @@ -1585,7 +1585,7 @@ export abstract class AbstractTree implements IDisposable } open(elements: TRef[], browserEvent?: UIEvent): void { - const indexes = elements.map(e => this.model.getListIndex(e)); + const indexes = elements.map(e => this.model.getListIndex(e)).filter(i => i >= 0); this.view.open(indexes, browserEvent); } diff --git a/src/vs/base/browser/ui/tree/asyncDataTree.ts b/src/vs/base/browser/ui/tree/asyncDataTree.ts index 8fbae3e6aabc..c2670776ea7e 100644 --- a/src/vs/base/browser/ui/tree/asyncDataTree.ts +++ b/src/vs/base/browser/ui/tree/asyncDataTree.ts @@ -562,6 +562,10 @@ export class AsyncDataTree implements IDisposable const node = this.getDataNode(element); + if (this.tree.hasElement(node) && !this.tree.isCollapsible(node)) { + return false; + } + if (node.refreshPromise) { await this.root.refreshPromise; await Event.toPromise(this._onDidRender.event); diff --git a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts index bc9347e5c253..e624ff634ed0 100644 --- a/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/compressedObjectTreeModel.ts @@ -205,6 +205,10 @@ export class CompressedObjectTreeModel, TFilterData e this.model.setChildren(node, children, _onDidCreateNode, _onDidDeleteNode); } + has(element: T | null): boolean { + return this.nodes.has(element); + } + getListIndex(location: T | null): number { const node = this.getCompressedNode(location); return this.model.getListIndex(node); @@ -421,6 +425,10 @@ export class CompressibleObjectTreeModel, TFilterData this.model.setCompressionEnabled(enabled); } + has(location: T | null): boolean { + return this.model.has(location); + } + getListIndex(location: T | null): number { return this.model.getListIndex(location); } diff --git a/src/vs/base/browser/ui/tree/indexTreeModel.ts b/src/vs/base/browser/ui/tree/indexTreeModel.ts index 61da572b86cb..64947bf0125b 100644 --- a/src/vs/base/browser/ui/tree/indexTreeModel.ts +++ b/src/vs/base/browser/ui/tree/indexTreeModel.ts @@ -199,6 +199,10 @@ export class IndexTreeModel, TFilterData = voi } } + has(location: number[]): boolean { + return this.hasTreeNode(location); + } + getListIndex(location: number[]): number { const { listIndex, visible, revealed } = this.getTreeNodeWithListIndex(location); return visible && revealed ? listIndex : -1; @@ -291,6 +295,8 @@ export class IndexTreeModel, TFilterData = voi if (isCollapsibleStateUpdate(update)) { result = node.collapsible !== update.collapsible; node.collapsible = update.collapsible; + } else if (!node.collapsible) { + result = false; } else { result = node.collapsed !== update.collapsed; node.collapsed = update.collapsed; @@ -516,6 +522,21 @@ export class IndexTreeModel, TFilterData = voi } } + // cheap + private hasTreeNode(location: number[], node: IIndexTreeNode = this.root): boolean { + if (!location || location.length === 0) { + return true; + } + + const [index, ...rest] = location; + + if (index < 0 || index > node.children.length) { + return false; + } + + return this.hasTreeNode(rest, node.children[index]); + } + // cheap private getTreeNode(location: number[], node: IIndexTreeNode = this.root): IIndexTreeNode { if (!location || location.length === 0) { diff --git a/src/vs/base/browser/ui/tree/objectTree.ts b/src/vs/base/browser/ui/tree/objectTree.ts index 19930f9b0029..a21f9b8be446 100644 --- a/src/vs/base/browser/ui/tree/objectTree.ts +++ b/src/vs/base/browser/ui/tree/objectTree.ts @@ -50,6 +50,10 @@ export class ObjectTree, TFilterData = void> extends this.model.resort(element, recursive); } + hasElement(element: T): boolean { + return this.model.has(element); + } + protected createModel(user: string, view: ISpliceable>, options: IObjectTreeOptions): ITreeModel { return new ObjectTreeModel(user, view, options); } diff --git a/src/vs/base/browser/ui/tree/objectTreeModel.ts b/src/vs/base/browser/ui/tree/objectTreeModel.ts index 268d6b4d5046..f0e0abdd47cc 100644 --- a/src/vs/base/browser/ui/tree/objectTreeModel.ts +++ b/src/vs/base/browser/ui/tree/objectTreeModel.ts @@ -195,6 +195,10 @@ export class ObjectTreeModel, TFilterData extends Non return this.model.getLastElementAncestor(location); } + has(element: T | null): boolean { + return this.nodes.has(element); + } + getListIndex(element: T | null): number { const location = this.getElementLocation(element); return this.model.getListIndex(location); diff --git a/src/vs/base/browser/ui/tree/tree.ts b/src/vs/base/browser/ui/tree/tree.ts index c4e257f08426..d9bcf6519fb3 100644 --- a/src/vs/base/browser/ui/tree/tree.ts +++ b/src/vs/base/browser/ui/tree/tree.ts @@ -108,6 +108,8 @@ export interface ITreeModel { readonly onDidChangeCollapseState: Event>; readonly onDidChangeRenderNodeCount: Event>; + has(location: TRef): boolean; + getListIndex(location: TRef): number; getListRenderCount(location: TRef): number; getNode(location?: TRef): ITreeNode; diff --git a/src/vs/base/common/arrays.ts b/src/vs/base/common/arrays.ts index af9e0bf08c60..ee7b34d63aa3 100644 --- a/src/vs/base/common/arrays.ts +++ b/src/vs/base/common/arrays.ts @@ -584,3 +584,7 @@ export function mapArrayOrNot(items: T | T[], fn: (_: T) => U): U | U[] { export function asArray(x: T | T[]): T[] { return Array.isArray(x) ? x : [x]; } + +export function getRandomElement(arr: T[]): T | undefined { + return arr[Math.floor(Math.random() * arr.length)]; +} diff --git a/src/vs/base/common/jsonEdit.ts b/src/vs/base/common/jsonEdit.ts index e8df6c318829..b4e0330c8e1d 100644 --- a/src/vs/base/common/jsonEdit.ts +++ b/src/vs/base/common/jsonEdit.ts @@ -175,7 +175,3 @@ export function applyEdits(text: string, edits: Edit[]): string { } return text; } - -export function isWS(text: string, offset: number) { - return '\r\n \t'.indexOf(text.charAt(offset)) !== -1; -} diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index ac1b7fa14572..d8ad2bb6f0de 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -91,6 +91,9 @@ export function toDisposable(fn: () => void): IDisposable { } export class DisposableStore implements IDisposable { + + static DISABLE_DISPOSED_WARNING = false; + private _toDispose = new Set(); private _isDisposed = false; @@ -127,7 +130,9 @@ export class DisposableStore implements IDisposable { markTracked(t); if (this._isDisposed) { - console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack); + if (!DisposableStore.DISABLE_DISPOSED_WARNING) { + console.warn(new Error('Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!').stack); + } } else { this._toDispose.add(t); } diff --git a/src/vs/base/common/resources.ts b/src/vs/base/common/resources.ts index 4381cc777756..26f23dd95d1d 100644 --- a/src/vs/base/common/resources.ts +++ b/src/vs/base/common/resources.ts @@ -182,7 +182,7 @@ export function hasTrailingPathSeparator(resource: URI, sep: string = paths.sep) 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; // ignore the slash at offset 0 + return (p.length > 1 && p.charCodeAt(p.length - 1) === CharCode.Slash) && !(/^[a-zA-Z]:(\/$|\\$)/.test(resource.fsPath)); // ignore the slash at offset 0 } } @@ -191,6 +191,7 @@ export function hasTrailingPathSeparator(resource: URI, sep: string = paths.sep) * 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) }); } @@ -226,7 +227,7 @@ export function relativePath(from: URI, to: URI, ignoreCase = hasToIgnoreCase(fr return undefined; } if (from.scheme === Schemas.file) { - const relativePath = paths.relative(from.path, to.path); + const relativePath = paths.relative(originalFSPath(from), originalFSPath(to)); return isWindows ? extpath.toSlashes(relativePath) : relativePath; } let fromPath = from.path || '/', toPath = to.path || '/'; diff --git a/src/vs/base/node/config.ts b/src/vs/base/node/config.ts index 8dcb97d41046..41dac28ee2a1 100644 --- a/src/vs/base/node/config.ts +++ b/src/vs/base/node/config.ts @@ -126,8 +126,8 @@ export class ConfigWatcher extends Disposable implements IConfigWatcher { } private async handleSymbolicLink(): Promise { - const { stat, isSymbolicLink } = await statLink(this._path); - if (isSymbolicLink && !stat.isDirectory()) { + const { stat, symbolicLink } = await statLink(this._path); + if (symbolicLink && !stat.isDirectory()) { const realPath = await realpath(this._path); this.watch(realPath, false); diff --git a/src/vs/base/node/pfs.ts b/src/vs/base/node/pfs.ts index 3ec31d9a9e9c..424d2d1ba379 100644 --- a/src/vs/base/node/pfs.ts +++ b/src/vs/base/node/pfs.ts @@ -14,7 +14,18 @@ import { promisify } from 'util'; import { isRootOrDriveLetter } from 'vs/base/common/extpath'; import { generateUuid } from 'vs/base/common/uuid'; import { normalizeNFC } from 'vs/base/common/normalization'; -import { encode, encodeStream } from 'vs/base/node/encoding'; +import { encode } from 'vs/base/node/encoding'; + +// See https://github.com/Microsoft/vscode/issues/30180 +const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB +const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB + +// See https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149 +const WIN32_MAX_HEAP_SIZE = 700 * 1024 * 1024; // 700 MB +const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB + +export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE; +export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE; export enum RimRafMode { @@ -178,30 +189,52 @@ export function stat(path: string): Promise { } export interface IStatAndLink { + + // The stats of the file. If the file is a symbolic + // link, the stats will be of that target file and + // not the link itself. + // If the file is a symbolic link pointing to a non + // existing file, the stat will be of the link and + // the `dangling` flag will indicate this. stat: fs.Stats; - isSymbolicLink: boolean; + + // Will be provided if the resource is a symbolic link + // on disk. Use the `dangling` flag to find out if it + // points to a resource that does not exist on disk. + symbolicLink?: { dangling: boolean }; } export async function statLink(path: string): Promise { // First stat the link - let linkStat: fs.Stats | undefined; - let linkStatError: NodeJS.ErrnoException | undefined; + let lstats: fs.Stats | undefined; try { - linkStat = await lstat(path); + lstats = await lstat(path); + + // Return early if the stat is not a symbolic link at all + if (!lstats.isSymbolicLink()) { + return { stat: lstats }; + } } catch (error) { - linkStatError = error; + /* ignore - use stat() instead */ } - // Then stat the target and return that - const isLink = !!(linkStat && linkStat.isSymbolicLink()); - if (linkStatError || isLink) { - const fileStat = await stat(path); + // If the stat is a symbolic link or failed to stat, use fs.stat() + // which for symbolic links will stat the target they point to + try { + const stats = await stat(path); - return { stat: fileStat, isSymbolicLink: isLink }; - } + return { stat: stats, symbolicLink: lstats?.isSymbolicLink() ? { dangling: false } : undefined }; + } catch (error) { + + // If the link points to a non-existing file we still want + // to return it as result while setting dangling: true flag + if (error.code === 'ENOENT' && lstats) { + return { stat: lstats, symbolicLink: { dangling: true } }; + } - return { stat: linkStat!, isSymbolicLink: false }; + throw error; + } } export function lstat(path: string): Promise { @@ -213,9 +246,7 @@ export function rename(oldPath: string, newPath: string): Promise { } export function renameIgnoreError(oldPath: string, newPath: string): Promise { - return new Promise(resolve => { - fs.rename(oldPath, newPath, () => resolve()); - }); + return new Promise(resolve => fs.rename(oldPath, newPath, () => resolve())); } export function unlink(path: string): Promise { @@ -236,6 +267,10 @@ export function readFile(path: string, encoding?: string): Promise { + return promisify(fs.mkdir)(path, { mode, recursive: true }); +} + // According to node.js docs (https://nodejs.org/docs/v6.5.0/api/fs.html#fs_fs_writefile_file_data_options_callback) // it is not safe to call writeFile() on the same path multiple times without waiting for the callback to return. // Therefor we use a Queue on the path that is given to us to sequentialize calls to the same path properly. @@ -244,12 +279,15 @@ const writeFilePathQueues: Map> = new Map(); export function writeFile(path: string, data: string, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Buffer, options?: IWriteFileOptions): Promise; export function writeFile(path: string, data: Uint8Array, options?: IWriteFileOptions): Promise; -export function writeFile(path: string, data: NodeJS.ReadableStream, options?: IWriteFileOptions): Promise; -export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise; -export function writeFile(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options?: IWriteFileOptions): Promise { +export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise; +export function writeFile(path: string, data: string | Buffer | Uint8Array, options?: IWriteFileOptions): Promise { const queueKey = toQueueKey(path); - return ensureWriteFileQueue(queueKey).queue(() => writeFileAndFlush(path, data, options)); + return ensureWriteFileQueue(queueKey).queue(() => { + const ensuredOptions = ensureWriteOptions(options); + + return new Promise((resolve, reject) => doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve())); + }); } function toQueueKey(path: string): string { @@ -294,103 +332,6 @@ interface IEnsuredWriteFileOptions extends IWriteFileOptions { } let canFlush = true; -function writeFileAndFlush(path: string, data: string | Buffer | NodeJS.ReadableStream | Uint8Array, options: IWriteFileOptions | undefined): Promise { - const ensuredOptions = ensureWriteOptions(options); - - return new Promise((resolve, reject) => { - if (typeof data === 'string' || Buffer.isBuffer(data) || data instanceof Uint8Array) { - doWriteFileAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()); - } else { - doWriteFileStreamAndFlush(path, data, ensuredOptions, error => error ? reject(error) : resolve()); - } - }); -} - -function doWriteFileStreamAndFlush(path: string, reader: NodeJS.ReadableStream, options: IEnsuredWriteFileOptions, callback: (error?: Error) => void): void { - - // finish only once - let finished = false; - const finish = (error?: Error) => { - if (!finished) { - finished = true; - - // in error cases we need to manually close streams - // if the write stream was successfully opened - if (error) { - if (isOpen) { - writer.once('close', () => callback(error)); - writer.destroy(); - } else { - callback(error); - } - } - - // otherwise just return without error - else { - callback(); - } - } - }; - - // create writer to target. we set autoClose: false because we want to use the streams - // file descriptor to call fs.fdatasync to ensure the data is flushed to disk - const writer = fs.createWriteStream(path, { mode: options.mode, flags: options.flag, autoClose: false }); - - // Event: 'open' - // Purpose: save the fd for later use and start piping - // Notes: will not be called when there is an error opening the file descriptor! - let fd: number; - let isOpen: boolean; - writer.once('open', descriptor => { - fd = descriptor; - isOpen = true; - - // if an encoding is provided, we need to pipe the stream through - // an encoder stream and forward the encoding related options - if (options.encoding) { - reader = reader.pipe(encodeStream(options.encoding.charset, { addBOM: options.encoding.addBOM })); - } - - // start data piping only when we got a successful open. this ensures that we do - // not consume the stream when an error happens and helps to fix this issue: - // https://github.com/Microsoft/vscode/issues/42542 - reader.pipe(writer); - }); - - // Event: 'error' - // Purpose: to return the error to the outside and to close the write stream (does not happen automatically) - reader.once('error', error => finish(error)); - writer.once('error', error => finish(error)); - - // Event: 'finish' - // Purpose: use fs.fdatasync to flush the contents to disk - // Notes: event is called when the writer has finished writing to the underlying resource. we must call writer.close() - // because we have created the WriteStream with autoClose: false - writer.once('finish', () => { - - // flush to disk - if (canFlush && isOpen) { - fs.fdatasync(fd, (syncError: Error) => { - - // In some exotic setups it is well possible that node fails to sync - // In that case we disable flushing and warn to the console - if (syncError) { - console.warn('[node.js fs] fdatasync is now disabled for this session because it failed: ', syncError); - canFlush = false; - } - - writer.destroy(); - }); - } else { - writer.destroy(); - } - }); - - // Event: 'close' - // Purpose: signal we are done to the outside - // Notes: event is called when the writer's filedescriptor is closed - writer.once('close', () => finish()); -} // Calls fs.writeFile() followed by a fs.sync() call to flush the changes to disk // We do this in cases where we want to make sure the data is really on disk and @@ -631,18 +572,3 @@ async function doCopyFile(source: string, target: string, mode: number): Promise reader.pipe(writer); }); } - -export async function mkdirp(path: string, mode?: number): Promise { - return promisify(fs.mkdir)(path, { mode, recursive: true }); -} - -// See https://github.com/Microsoft/vscode/issues/30180 -const WIN32_MAX_FILE_SIZE = 300 * 1024 * 1024; // 300 MB -const GENERAL_MAX_FILE_SIZE = 16 * 1024 * 1024 * 1024; // 16 GB - -// See https://github.com/v8/v8/blob/5918a23a3d571b9625e5cce246bdd5b46ff7cd8b/src/heap/heap.cc#L149 -const WIN32_MAX_HEAP_SIZE = 700 * 1024 * 1024; // 700 MB -const GENERAL_MAX_HEAP_SIZE = 700 * 2 * 1024 * 1024; // 1400 MB - -export const MAX_FILE_SIZE = process.arch === 'ia32' ? WIN32_MAX_FILE_SIZE : GENERAL_MAX_FILE_SIZE; -export const MAX_HEAP_SIZE = process.arch === 'ia32' ? WIN32_MAX_HEAP_SIZE : GENERAL_MAX_HEAP_SIZE; diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index e93a9ffd81a0..b489412021e5 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -3,12 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Event, Emitter, Relay } from 'vs/base/common/event'; -import { IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter, Relay, EventMultiplexer } from 'vs/base/common/event'; +import { IDisposable, toDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; 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'; /** * An `IChannel` is an abstraction over a collection of commands. @@ -95,7 +97,8 @@ export interface Client { export interface IConnectionHub { readonly connections: Connection[]; - readonly onDidChangeConnections: Event>; + readonly onDidAddConnection: Event>; + readonly onDidRemoveConnection: Event>; } /** @@ -116,7 +119,7 @@ export interface IClientRouter { * order to pick the right one. */ export interface IRoutingChannelClient { - getChannel(channelName: string, router: IClientRouter): T; + getChannel(channelName: string, router?: IClientRouter): T; } interface IReader { @@ -659,8 +662,11 @@ export class IPCServer implements IChannelServer, I private channels = new Map>(); private _connections = new Set>(); - private readonly _onDidChangeConnections = new Emitter>(); - readonly onDidChangeConnections: Event> = this._onDidChangeConnections.event; + private readonly _onDidAddConnection = new Emitter>(); + readonly onDidAddConnection: Event> = this._onDidAddConnection.event; + + private readonly _onDidRemoveConnection = new Emitter>(); + readonly onDidRemoveConnection: Event> = this._onDidRemoveConnection.event; get connections(): Connection[] { const result: Connection[] = []; @@ -683,30 +689,59 @@ export class IPCServer implements IChannelServer, I const connection: Connection = { channelServer, channelClient, ctx }; this._connections.add(connection); - this._onDidChangeConnections.fire(connection); + this._onDidAddConnection.fire(connection); onDidClientDisconnect(() => { channelServer.dispose(); channelClient.dispose(); this._connections.delete(connection); + this._onDidRemoveConnection.fire(connection); }); }); }); } - getChannel(channelName: string, router: IClientRouter): T { + /** + * Get a channel from a remote client. When passed a router, + * one can specify which client it wants to call and listen to/from. + * Otherwise, when calling without a router, a random client will + * be selected and when listening without a router, every client + * will be listened to. + */ + getChannel(channelName: string, router: IClientRouter): T; + getChannel(channelName: string, clientFilter: (client: Client) => boolean): T; + getChannel(channelName: string, routerOrClientFilter: IClientRouter | ((client: Client) => boolean)): T { const that = this; return { - call(command: string, arg?: any, cancellationToken?: CancellationToken) { - const channelPromise = router.routeCall(that, command, arg) + call(command: string, arg?: any, cancellationToken?: CancellationToken): Promise { + let connectionPromise: Promise>; + + if (isFunction(routerOrClientFilter)) { + // when no router is provided, we go random client picking + let connection = getRandomElement(that.connections.filter(routerOrClientFilter)); + + connectionPromise = connection + // if we found a client, let's call on it + ? Promise.resolve(connection) + // else, let's wait for a client to come along + : Event.toPromise(Event.filter(that.onDidAddConnection, routerOrClientFilter)); + } else { + connectionPromise = routerOrClientFilter.routeCall(that, command, arg); + } + + const channelPromise = connectionPromise .then(connection => (connection as Connection).channelClient.getChannel(channelName)); return getDelayedChannel(channelPromise) .call(command, arg, cancellationToken); }, - listen(event: string, arg: any) { - const channelPromise = router.routeEvent(that, event, arg) + listen(event: string, arg: any): Event { + if (isFunction(routerOrClientFilter)) { + return that.getMulticastEvent(channelName, routerOrClientFilter, event, arg); + } + + const channelPromise = routerOrClientFilter.routeEvent(that, event, arg) .then(connection => (connection as Connection).channelClient.getChannel(channelName)); return getDelayedChannel(channelPromise) @@ -715,6 +750,58 @@ export class IPCServer implements IChannelServer, I } as T; } + private getMulticastEvent(channelName: string, clientFilter: (client: Client) => boolean, eventName: string, arg: any): Event { + const that = this; + let disposables = new DisposableStore(); + + // Create an emitter which hooks up to all clients + // as soon as first listener is added. It also + // disconnects from all clients as soon as the last listener + // is removed. + const emitter = new Emitter({ + onFirstListenerAdd: () => { + disposables = new DisposableStore(); + + // The event multiplexer is useful since the active + // client list is dynamic. We need to hook up and disconnection + // to/from clients as they come and go. + const eventMultiplexer = new EventMultiplexer(); + const map = new Map, IDisposable>(); + + const onDidAddConnection = (connection: Connection) => { + const channel = connection.channelClient.getChannel(channelName); + const event = channel.listen(eventName, arg); + const disposable = eventMultiplexer.add(event); + + map.set(connection, disposable); + }; + + const onDidRemoveConnection = (connection: Connection) => { + const disposable = map.get(connection); + + if (!disposable) { + return; + } + + disposable.dispose(); + map.delete(connection); + }; + + that.connections.filter(clientFilter).forEach(onDidAddConnection); + Event.filter(that.onDidAddConnection, clientFilter)(onDidAddConnection, undefined, disposables); + that.onDidRemoveConnection(onDidRemoveConnection, undefined, disposables); + eventMultiplexer.event(emitter.fire, emitter, disposables); + + disposables.add(eventMultiplexer); + }, + onLastListenerRemove: () => { + disposables.dispose(); + } + }); + + return emitter.event; + } + registerChannel(channelName: string, channel: IServerChannel): void { this.channels.set(channelName, channel); @@ -726,7 +813,8 @@ export class IPCServer implements IChannelServer, I dispose(): void { this.channels.clear(); this._connections.clear(); - this._onDidChangeConnections.dispose(); + this._onDidAddConnection.dispose(); + this._onDidRemoveConnection.dispose(); } } @@ -827,7 +915,7 @@ export class StaticRouter implements IClientRouter } } - await Event.toPromise(hub.onDidChangeConnections); + await Event.toPromise(hub.onDidAddConnection); return await this.route(hub); } } diff --git a/src/vs/base/parts/ipc/test/node/ipc.test.ts b/src/vs/base/parts/ipc/test/node/ipc.test.ts index 227659bf5fe3..1109070d6b0e 100644 --- a/src/vs/base/parts/ipc/test/node/ipc.test.ts +++ b/src/vs/base/parts/ipc/test/node/ipc.test.ts @@ -415,4 +415,80 @@ suite('Base IPC', function () { return assert.equal(r, 'Super Context'); }); }); + + suite('one to many', function () { + test('all clients get pinged', async function () { + const service = new TestService(); + const channel = new TestChannel(service); + const server = new TestIPCServer(); + server.registerChannel('channel', channel); + + let client1GotPinged = false; + const client1 = server.createConnection('client1'); + const ipcService1 = new TestChannelClient(client1.getChannel('channel')); + ipcService1.onPong(() => client1GotPinged = true); + + let client2GotPinged = false; + const client2 = server.createConnection('client2'); + const ipcService2 = new TestChannelClient(client2.getChannel('channel')); + ipcService2.onPong(() => client2GotPinged = true); + + await timeout(1); + service.ping('hello'); + + await timeout(1); + assert(client1GotPinged, 'client 1 got pinged'); + assert(client2GotPinged, 'client 2 got pinged'); + + client1.dispose(); + client2.dispose(); + server.dispose(); + }); + + test('server gets pings from all clients (broadcast channel)', async function () { + const server = new TestIPCServer(); + + const client1 = server.createConnection('client1'); + const clientService1 = new TestService(); + const clientChannel1 = new TestChannel(clientService1); + client1.registerChannel('channel', clientChannel1); + + const pings: string[] = []; + const channel = server.getChannel('channel', () => true); + const service = new TestChannelClient(channel); + service.onPong(msg => pings.push(msg)); + + await timeout(1); + clientService1.ping('hello 1'); + + await timeout(1); + assert.deepEqual(pings, ['hello 1']); + + const client2 = server.createConnection('client2'); + const clientService2 = new TestService(); + const clientChannel2 = new TestChannel(clientService2); + client2.registerChannel('channel', clientChannel2); + + await timeout(1); + clientService2.ping('hello 2'); + + await timeout(1); + assert.deepEqual(pings, ['hello 1', 'hello 2']); + + client1.dispose(); + clientService1.ping('hello 1'); + + await timeout(1); + assert.deepEqual(pings, ['hello 1', 'hello 2']); + + await timeout(1); + clientService2.ping('hello again 2'); + + await timeout(1); + assert.deepEqual(pings, ['hello 1', 'hello 2', 'hello again 2']); + + client2.dispose(); + server.dispose(); + }); + }); }); diff --git a/src/vs/workbench/browser/parts/quickinput/media/arrow-left-dark.svg b/src/vs/base/parts/quickinput/browser/media/arrow-left-dark.svg similarity index 100% rename from src/vs/workbench/browser/parts/quickinput/media/arrow-left-dark.svg rename to src/vs/base/parts/quickinput/browser/media/arrow-left-dark.svg diff --git a/src/vs/workbench/browser/parts/quickinput/media/arrow-left-light.svg b/src/vs/base/parts/quickinput/browser/media/arrow-left-light.svg similarity index 100% rename from src/vs/workbench/browser/parts/quickinput/media/arrow-left-light.svg rename to src/vs/base/parts/quickinput/browser/media/arrow-left-light.svg diff --git a/src/vs/workbench/browser/parts/quickinput/media/quickInput.css b/src/vs/base/parts/quickinput/browser/media/quickInput.css similarity index 100% rename from src/vs/workbench/browser/parts/quickinput/media/quickInput.css rename to src/vs/base/parts/quickinput/browser/media/quickInput.css diff --git a/src/vs/base/parts/quickinput/browser/quickInput.ts b/src/vs/base/parts/quickinput/browser/quickInput.ts new file mode 100644 index 000000000000..27169f6f7f6b --- /dev/null +++ b/src/vs/base/parts/quickinput/browser/quickInput.ts @@ -0,0 +1,1556 @@ +/*--------------------------------------------------------------------------------------------- + * 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!./media/quickInput'; +import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput'; +import * as dom from 'vs/base/browser/dom'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { QuickInputList } from './quickInputList'; +import { QuickInputBox } from './quickInputBox'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { localize } from 'vs/nls'; +import { CountBadge, ICountBadgetyles } from 'vs/base/browser/ui/countBadge/countBadge'; +import { ProgressBar, IProgressBarStyles } from 'vs/base/browser/ui/progressbar/progressbar'; +import { Emitter, Event } from 'vs/base/common/event'; +import { Button, IButtonStyles } from 'vs/base/browser/ui/button/button'; +import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import Severity from 'vs/base/common/severity'; +import { ActionBar, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { Action } from 'vs/base/common/actions'; +import { URI } from 'vs/base/common/uri'; +import { equals } from 'vs/base/common/arrays'; +import { TimeoutTimer } from 'vs/base/common/async'; +import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils'; +import { registerAndGetAmdImageURL } from 'vs/base/common/amd'; +import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; +import { List, IListOptions, IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; +import { Color } from 'vs/base/common/color'; + +export interface IQuickInputOptions { + idPrefix: string; + container: HTMLElement; + ignoreFocusOut(): boolean; + isScreenReaderOptimized(): boolean; + backKeybindingLabel(): string | undefined; + setContextKey(id?: string): void; + returnFocus(): void; + createList( + user: string, + container: HTMLElement, + delegate: IListVirtualDelegate, + renderers: IListRenderer[], + options: IListOptions, + ): List; + styles: IQuickInputStyles; +} + +export interface IQuickInputStyles { + widget: IQuickInputWidgetStyles; + inputBox: IInputBoxStyles; + countBadge: ICountBadgetyles; + button: IButtonStyles; + progressBar: IProgressBarStyles; + list: IListStyles & { listInactiveFocusForeground?: Color; pickerGroupBorder?: Color; pickerGroupForeground?: Color; }; +} + +export interface IQuickInputWidgetStyles { + quickInputBackground?: Color; + quickInputForeground?: Color; + contrastBorder?: Color; + widgetShadow?: Color; + titleColor: string | undefined; +} + +const $ = dom.$; + +type Writeable = { -readonly [P in keyof T]: T[P] }; + +const backButton = { + iconPath: { + dark: URI.parse(registerAndGetAmdImageURL('vs/base/parts/quickinput/browser/media/arrow-left-dark.svg')), + light: URI.parse(registerAndGetAmdImageURL('vs/base/parts/quickinput/browser/media/arrow-left-light.svg')) + }, + tooltip: localize('quickInput.back', "Back"), + handle: -1 // TODO +}; + +interface QuickInputUI { + container: HTMLElement; + styleSheet: HTMLStyleElement; + leftActionBar: ActionBar; + titleBar: HTMLElement; + title: HTMLElement; + description: HTMLElement; + rightActionBar: ActionBar; + checkAll: HTMLInputElement; + filterContainer: HTMLElement; + inputBox: QuickInputBox; + visibleCountContainer: HTMLElement; + visibleCount: CountBadge; + countContainer: HTMLElement; + count: CountBadge; + okContainer: HTMLElement; + ok: Button; + message: HTMLElement; + customButtonContainer: HTMLElement; + customButton: Button; + progressBar: ProgressBar; + list: QuickInputList; + onDidAccept: Event; + onDidCustom: Event; + onDidTriggerButton: Event; + ignoreFocusOut: boolean; + keyMods: Writeable; + isScreenReaderOptimized(): boolean; + show(controller: QuickInput): void; + setVisibilities(visibilities: Visibilities): void; + setComboboxAccessibility(enabled: boolean): void; + setEnabled(enabled: boolean): void; + setContextKey(contextKey?: string): void; + hide(): void; +} + +type Visibilities = { + title?: boolean; + description?: boolean; + checkAll?: boolean; + inputBox?: boolean; + visibleCount?: boolean; + count?: boolean; + message?: boolean; + list?: boolean; + ok?: boolean; + customButton?: boolean; +}; + +class QuickInput extends Disposable implements IQuickInput { + + private _title: string | undefined; + private _description: string | undefined; + private _steps: number | undefined; + private _totalSteps: number | undefined; + protected visible = false; + private _enabled = true; + private _contextKey: string | undefined; + private _busy = false; + private _ignoreFocusOut = false; + private _buttons: IQuickInputButton[] = []; + private buttonsUpdated = false; + private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); + private readonly onDidHideEmitter = this._register(new Emitter()); + + protected readonly visibleDisposables = this._register(new DisposableStore()); + + private busyDelay: TimeoutTimer | undefined; + + constructor( + protected ui: QuickInputUI + ) { + super(); + } + + get title() { + return this._title; + } + + set title(title: string | undefined) { + this._title = title; + this.update(); + } + + get description() { + return this._description; + } + + set description(description: string | undefined) { + this._description = description; + this.update(); + } + + get step() { + return this._steps; + } + + set step(step: number | undefined) { + this._steps = step; + this.update(); + } + + get totalSteps() { + return this._totalSteps; + } + + set totalSteps(totalSteps: number | undefined) { + this._totalSteps = totalSteps; + this.update(); + } + + get enabled() { + return this._enabled; + } + + set enabled(enabled: boolean) { + this._enabled = enabled; + this.update(); + } + + get contextKey() { + return this._contextKey; + } + + set contextKey(contextKey: string | undefined) { + this._contextKey = contextKey; + this.update(); + } + + get busy() { + return this._busy; + } + + set busy(busy: boolean) { + this._busy = busy; + this.update(); + } + + get ignoreFocusOut() { + return this._ignoreFocusOut; + } + + set ignoreFocusOut(ignoreFocusOut: boolean) { + this._ignoreFocusOut = ignoreFocusOut; + this.update(); + } + + get buttons() { + return this._buttons; + } + + set buttons(buttons: IQuickInputButton[]) { + this._buttons = buttons; + this.buttonsUpdated = true; + this.update(); + } + + onDidTriggerButton = this.onDidTriggerButtonEmitter.event; + + show(): void { + if (this.visible) { + return; + } + this.visibleDisposables.add( + this.ui.onDidTriggerButton(button => { + if (this.buttons.indexOf(button) !== -1) { + this.onDidTriggerButtonEmitter.fire(button); + } + }), + ); + this.ui.show(this); + this.visible = true; + this.update(); + } + + hide(): void { + if (!this.visible) { + return; + } + this.ui.hide(); + } + + didHide(): void { + this.visible = false; + this.visibleDisposables.clear(); + this.onDidHideEmitter.fire(); + } + + onDidHide = this.onDidHideEmitter.event; + + protected update() { + if (!this.visible) { + return; + } + const title = this.getTitle(); + if (this.ui.title.textContent !== title) { + this.ui.title.textContent = title; + } + const description = this.getDescription(); + if (this.ui.description.textContent !== description) { + this.ui.description.textContent = description; + } + if (this.busy && !this.busyDelay) { + this.busyDelay = new TimeoutTimer(); + this.busyDelay.setIfNotSet(() => { + if (this.visible) { + this.ui.progressBar.infinite(); + } + }, 800); + } + if (!this.busy && this.busyDelay) { + this.ui.progressBar.stop(); + this.busyDelay.cancel(); + this.busyDelay = undefined; + } + if (this.buttonsUpdated) { + this.buttonsUpdated = false; + this.ui.leftActionBar.clear(); + const leftButtons = this.buttons.filter(button => button === backButton); + this.ui.leftActionBar.push(leftButtons.map((button, index) => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { + this.onDidTriggerButtonEmitter.fire(button); + return Promise.resolve(null); + }); + action.tooltip = button.tooltip || ''; + return action; + }), { icon: true, label: false }); + this.ui.rightActionBar.clear(); + const rightButtons = this.buttons.filter(button => button !== backButton); + this.ui.rightActionBar.push(rightButtons.map((button, index) => { + const action = new Action(`id-${index}`, '', button.iconClass || getIconClass(button.iconPath), true, () => { + this.onDidTriggerButtonEmitter.fire(button); + return Promise.resolve(null); + }); + action.tooltip = button.tooltip || ''; + return action; + }), { icon: true, label: false }); + } + this.ui.ignoreFocusOut = this.ignoreFocusOut; + this.ui.setEnabled(this.enabled); + this.ui.setContextKey(this.contextKey); + } + + private getTitle() { + if (this.title && this.step) { + return `${this.title} (${this.getSteps()})`; + } + if (this.title) { + return this.title; + } + if (this.step) { + return this.getSteps(); + } + return ''; + } + + private getDescription() { + return this.description || ''; + } + + private getSteps() { + if (this.step && this.totalSteps) { + return localize('quickInput.steps', "{0}/{1}", this.step, this.totalSteps); + } + if (this.step) { + return String(this.step); + } + return ''; + } + + protected showMessageDecoration(severity: Severity) { + this.ui.inputBox.showDecoration(severity); + if (severity === Severity.Error) { + const styles = this.ui.inputBox.stylesForType(severity); + this.ui.message.style.backgroundColor = styles.background ? `${styles.background}` : ''; + this.ui.message.style.border = styles.border ? `1px solid ${styles.border}` : ''; + this.ui.message.style.paddingBottom = '4px'; + } else { + this.ui.message.style.backgroundColor = ''; + this.ui.message.style.border = ''; + this.ui.message.style.paddingBottom = ''; + } + } + + public dispose(): void { + this.hide(); + super.dispose(); + } +} + +class QuickPick extends QuickInput implements IQuickPick { + + private static readonly INPUT_BOX_ARIA_LABEL = localize('quickInputBox.ariaLabel', "Type to narrow down results."); + + private _value = ''; + private _placeholder: string | undefined; + private readonly onDidChangeValueEmitter = this._register(new Emitter()); + private readonly onDidAcceptEmitter = this._register(new Emitter()); + private readonly onDidCustomEmitter = this._register(new Emitter()); + private _items: Array = []; + private itemsUpdated = false; + private _canSelectMany = false; + private _matchOnDescription = false; + private _matchOnDetail = false; + private _matchOnLabel = true; + private _sortByLabel = true; + private _autoFocusOnList = true; + private _activeItems: T[] = []; + private activeItemsUpdated = false; + private activeItemsToConfirm: T[] | null = []; + private readonly onDidChangeActiveEmitter = this._register(new Emitter()); + private _selectedItems: T[] = []; + private selectedItemsUpdated = false; + private selectedItemsToConfirm: T[] | null = []; + private readonly onDidChangeSelectionEmitter = this._register(new Emitter()); + private readonly onDidTriggerItemButtonEmitter = this._register(new Emitter>()); + private _valueSelection: Readonly<[number, number]> | undefined; + private valueSelectionUpdated = true; + private _validationMessage: string | undefined; + private _ok = false; + private _customButton = false; + private _customButtonLabel: string | undefined; + private _customButtonHover: string | undefined; + + quickNavigate: IQuickNavigateConfiguration | undefined; + + + get value() { + return this._value; + } + + set value(value: string) { + this._value = value || ''; + this.update(); + } + + get placeholder() { + return this._placeholder; + } + + set placeholder(placeholder: string | undefined) { + this._placeholder = placeholder; + this.update(); + } + + onDidChangeValue = this.onDidChangeValueEmitter.event; + + onDidAccept = this.onDidAcceptEmitter.event; + + onDidCustom = this.onDidCustomEmitter.event; + + get items() { + return this._items; + } + + set items(items: Array) { + this._items = items; + this.itemsUpdated = true; + this.update(); + } + + get canSelectMany() { + return this._canSelectMany; + } + + set canSelectMany(canSelectMany: boolean) { + this._canSelectMany = canSelectMany; + this.update(); + } + + get matchOnDescription() { + return this._matchOnDescription; + } + + set matchOnDescription(matchOnDescription: boolean) { + this._matchOnDescription = matchOnDescription; + this.update(); + } + + get matchOnDetail() { + return this._matchOnDetail; + } + + set matchOnDetail(matchOnDetail: boolean) { + this._matchOnDetail = matchOnDetail; + this.update(); + } + + get matchOnLabel() { + return this._matchOnLabel; + } + + set matchOnLabel(matchOnLabel: boolean) { + this._matchOnLabel = matchOnLabel; + this.update(); + } + + get sortByLabel() { + return this._sortByLabel; + } + + set sortByLabel(sortByLabel: boolean) { + this._sortByLabel = sortByLabel; + this.update(); + } + + + get autoFocusOnList() { + return this._autoFocusOnList; + } + + set autoFocusOnList(autoFocusOnList: boolean) { + this._autoFocusOnList = autoFocusOnList; + this.update(); + } + + get activeItems() { + return this._activeItems; + } + + set activeItems(activeItems: T[]) { + this._activeItems = activeItems; + this.activeItemsUpdated = true; + this.update(); + } + + onDidChangeActive = this.onDidChangeActiveEmitter.event; + + get selectedItems() { + return this._selectedItems; + } + + set selectedItems(selectedItems: T[]) { + this._selectedItems = selectedItems; + this.selectedItemsUpdated = true; + this.update(); + } + + get keyMods() { + return this.ui.keyMods; + } + + set valueSelection(valueSelection: Readonly<[number, number]>) { + this._valueSelection = valueSelection; + this.valueSelectionUpdated = true; + this.update(); + } + + get validationMessage() { + return this._validationMessage; + } + + set validationMessage(validationMessage: string | undefined) { + this._validationMessage = validationMessage; + this.update(); + } + + get customButton() { + return this._customButton; + } + + set customButton(showCustomButton: boolean) { + this._customButton = showCustomButton; + this.update(); + } + + get customLabel() { + return this._customButtonLabel; + } + + set customLabel(label: string | undefined) { + this._customButtonLabel = label; + this.update(); + } + + get customHover() { + return this._customButtonHover; + } + + set customHover(hover: string | undefined) { + this._customButtonHover = hover; + this.update(); + } + + get ok() { + return this._ok; + } + + set ok(showOkButton: boolean) { + this._ok = showOkButton; + this.update(); + } + + public inputHasFocus(): boolean { + return this.visible ? this.ui.inputBox.hasFocus() : false; + } + + public focusOnInput() { + this.ui.inputBox.setFocus(); + } + + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; + + onDidTriggerItemButton = this.onDidTriggerItemButtonEmitter.event; + + private trySelectFirst() { + if (this.autoFocusOnList) { + if (!this.ui.isScreenReaderOptimized() && !this.canSelectMany) { + this.ui.list.focus('First'); + } + } + } + + show() { + if (!this.visible) { + this.visibleDisposables.add( + this.ui.inputBox.onDidChange(value => { + if (value === this.value) { + return; + } + this._value = value; + this.ui.list.filter(this.ui.inputBox.value); + this.trySelectFirst(); + this.onDidChangeValueEmitter.fire(value); + })); + this.visibleDisposables.add(this.ui.inputBox.onMouseDown(event => { + if (!this.autoFocusOnList) { + this.ui.list.clearFocus(); + } + })); + this.visibleDisposables.add(this.ui.inputBox.onKeyDown(event => { + switch (event.keyCode) { + case KeyCode.DownArrow: + this.ui.list.focus('Next'); + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + event.preventDefault(); + break; + case KeyCode.UpArrow: + if (this.ui.list.getFocusedElements().length) { + this.ui.list.focus('Previous'); + } else { + this.ui.list.focus('Last'); + } + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + event.preventDefault(); + break; + case KeyCode.PageDown: + if (this.ui.list.getFocusedElements().length) { + this.ui.list.focus('NextPage'); + } else { + this.ui.list.focus('First'); + } + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + event.preventDefault(); + break; + case KeyCode.PageUp: + if (this.ui.list.getFocusedElements().length) { + this.ui.list.focus('PreviousPage'); + } else { + this.ui.list.focus('Last'); + } + if (this.canSelectMany) { + this.ui.list.domFocus(); + } + event.preventDefault(); + break; + } + })); + this.visibleDisposables.add(this.ui.onDidAccept(() => { + if (!this.canSelectMany && this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + this.onDidChangeSelectionEmitter.fire(this.selectedItems); + } + this.onDidAcceptEmitter.fire(undefined); + })); + this.visibleDisposables.add(this.ui.onDidCustom(() => { + this.onDidCustomEmitter.fire(undefined); + })); + this.visibleDisposables.add(this.ui.list.onDidChangeFocus(focusedItems => { + if (this.activeItemsUpdated) { + return; // Expect another event. + } + if (this.activeItemsToConfirm !== this._activeItems && equals(focusedItems, this._activeItems, (a, b) => a === b)) { + return; + } + this._activeItems = focusedItems as T[]; + this.onDidChangeActiveEmitter.fire(focusedItems as T[]); + })); + this.visibleDisposables.add(this.ui.list.onDidChangeSelection(selectedItems => { + if (this.canSelectMany) { + if (selectedItems.length) { + this.ui.list.setSelectedElements([]); + } + return; + } + if (this.selectedItemsToConfirm !== this._selectedItems && equals(selectedItems, this._selectedItems, (a, b) => a === b)) { + return; + } + this._selectedItems = selectedItems as T[]; + this.onDidChangeSelectionEmitter.fire(selectedItems as T[]); + if (selectedItems.length) { + this.onDidAcceptEmitter.fire(undefined); + } + })); + this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => { + if (!this.canSelectMany) { + return; + } + if (this.selectedItemsToConfirm !== this._selectedItems && equals(checkedItems, this._selectedItems, (a, b) => a === b)) { + return; + } + this._selectedItems = checkedItems as T[]; + this.onDidChangeSelectionEmitter.fire(checkedItems as T[]); + })); + this.visibleDisposables.add(this.ui.list.onButtonTriggered(event => this.onDidTriggerItemButtonEmitter.fire(event as IQuickPickItemButtonEvent))); + this.visibleDisposables.add(this.registerQuickNavigation()); + this.valueSelectionUpdated = true; + } + super.show(); // TODO: Why have show() bubble up while update() trickles down? (Could move setComboboxAccessibility() here.) + } + + private registerQuickNavigation() { + return dom.addDisposableListener(this.ui.container, dom.EventType.KEY_UP, e => { + if (this.canSelectMany || !this.quickNavigate) { + return; + } + + const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e); + const keyCode = keyboardEvent.keyCode; + + // Select element when keys are pressed that signal it + const quickNavKeys = this.quickNavigate.keybindings; + const wasTriggerKeyPressed = quickNavKeys.some(k => { + const [firstPart, chordPart] = k.getParts(); + if (chordPart) { + return false; + } + + if (firstPart.shiftKey && keyCode === KeyCode.Shift) { + if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) { + return false; // this is an optimistic check for the shift key being used to navigate back in quick open + } + + return true; + } + + if (firstPart.altKey && keyCode === KeyCode.Alt) { + return true; + } + + if (firstPart.ctrlKey && keyCode === KeyCode.Ctrl) { + return true; + } + + if (firstPart.metaKey && keyCode === KeyCode.Meta) { + return true; + } + + return false; + }); + + if (wasTriggerKeyPressed && this.activeItems[0]) { + this._selectedItems = [this.activeItems[0]]; + this.onDidChangeSelectionEmitter.fire(this.selectedItems); + this.onDidAcceptEmitter.fire(undefined); + } + }); + } + + protected update() { + if (!this.visible) { + return; + } + this.ui.setVisibilities(this.canSelectMany ? { title: !!this.title || !!this.step, description: !!this.description, checkAll: true, inputBox: true, visibleCount: true, count: true, ok: this.ok, list: true, message: !!this.validationMessage, customButton: this.customButton } : { title: !!this.title || !!this.step, description: !!this.description, inputBox: true, visibleCount: true, list: true, message: !!this.validationMessage, customButton: this.customButton, ok: this.ok }); + super.update(); + if (this.ui.inputBox.value !== this.value) { + this.ui.inputBox.value = this.value; + } + if (this.valueSelectionUpdated) { + this.valueSelectionUpdated = false; + this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] }); + } + if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { + this.ui.inputBox.placeholder = (this.placeholder || ''); + } + if (this.itemsUpdated) { + this.itemsUpdated = false; + this.ui.list.setElements(this.items); + this.ui.list.filter(this.ui.inputBox.value); + this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); + this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()); + this.ui.count.setCount(this.ui.list.getCheckedCount()); + this.trySelectFirst(); + } + if (this.ui.container.classList.contains('show-checkboxes') !== !!this.canSelectMany) { + if (this.canSelectMany) { + this.ui.list.clearFocus(); + } else { + this.trySelectFirst(); + } + } + if (this.activeItemsUpdated) { + this.activeItemsUpdated = false; + this.activeItemsToConfirm = this._activeItems; + this.ui.list.setFocusedElements(this.activeItems); + if (this.activeItemsToConfirm === this._activeItems) { + this.activeItemsToConfirm = null; + } + } + if (this.selectedItemsUpdated) { + this.selectedItemsUpdated = false; + this.selectedItemsToConfirm = this._selectedItems; + if (this.canSelectMany) { + this.ui.list.setCheckedElements(this.selectedItems); + } else { + this.ui.list.setSelectedElements(this.selectedItems); + } + if (this.selectedItemsToConfirm === this._selectedItems) { + this.selectedItemsToConfirm = null; + } + } + if (this.validationMessage) { + this.ui.message.textContent = this.validationMessage; + this.showMessageDecoration(Severity.Error); + } else { + this.ui.message.textContent = null; + this.showMessageDecoration(Severity.Ignore); + } + this.ui.customButton.label = this.customLabel || ''; + this.ui.customButton.element.title = this.customHover || ''; + this.ui.list.matchOnDescription = this.matchOnDescription; + this.ui.list.matchOnDetail = this.matchOnDetail; + this.ui.list.matchOnLabel = this.matchOnLabel; + this.ui.list.sortByLabel = this.sortByLabel; + this.ui.setComboboxAccessibility(true); + this.ui.inputBox.setAttribute('aria-label', QuickPick.INPUT_BOX_ARIA_LABEL); + } +} + +class InputBox extends QuickInput implements IInputBox { + + private static readonly noPromptMessage = localize('inputModeEntry', "Press 'Enter' to confirm your input or 'Escape' to cancel"); + + private _value = ''; + private _valueSelection: Readonly<[number, number]> | undefined; + private valueSelectionUpdated = true; + private _placeholder: string | undefined; + private _password = false; + private _prompt: string | undefined; + private noValidationMessage = InputBox.noPromptMessage; + private _validationMessage: string | undefined; + private readonly onDidValueChangeEmitter = this._register(new Emitter()); + private readonly onDidAcceptEmitter = this._register(new Emitter()); + + get value() { + return this._value; + } + + set value(value: string) { + this._value = value || ''; + this.update(); + } + + set valueSelection(valueSelection: Readonly<[number, number]>) { + this._valueSelection = valueSelection; + this.valueSelectionUpdated = true; + this.update(); + } + + get placeholder() { + return this._placeholder; + } + + set placeholder(placeholder: string | undefined) { + this._placeholder = placeholder; + this.update(); + } + + get password() { + return this._password; + } + + set password(password: boolean) { + this._password = password; + this.update(); + } + + get prompt() { + return this._prompt; + } + + set prompt(prompt: string | undefined) { + this._prompt = prompt; + this.noValidationMessage = prompt + ? localize('inputModeEntryDescription', "{0} (Press 'Enter' to confirm or 'Escape' to cancel)", prompt) + : InputBox.noPromptMessage; + this.update(); + } + + get validationMessage() { + return this._validationMessage; + } + + set validationMessage(validationMessage: string | undefined) { + this._validationMessage = validationMessage; + this.update(); + } + + readonly onDidChangeValue = this.onDidValueChangeEmitter.event; + + readonly onDidAccept = this.onDidAcceptEmitter.event; + + show() { + if (!this.visible) { + this.visibleDisposables.add( + this.ui.inputBox.onDidChange(value => { + if (value === this.value) { + return; + } + this._value = value; + this.onDidValueChangeEmitter.fire(value); + })); + this.visibleDisposables.add(this.ui.onDidAccept(() => this.onDidAcceptEmitter.fire(undefined))); + this.valueSelectionUpdated = true; + } + super.show(); + } + + protected update() { + if (!this.visible) { + return; + } + this.ui.setVisibilities({ title: !!this.title || !!this.step, description: !!this.description || !!this.step, inputBox: true, message: true }); + super.update(); + if (this.ui.inputBox.value !== this.value) { + this.ui.inputBox.value = this.value; + } + if (this.valueSelectionUpdated) { + this.valueSelectionUpdated = false; + this.ui.inputBox.select(this._valueSelection && { start: this._valueSelection[0], end: this._valueSelection[1] }); + } + if (this.ui.inputBox.placeholder !== (this.placeholder || '')) { + this.ui.inputBox.placeholder = (this.placeholder || ''); + } + if (this.ui.inputBox.password !== this.password) { + this.ui.inputBox.password = this.password; + } + if (!this.validationMessage && this.ui.message.textContent !== this.noValidationMessage) { + this.ui.message.textContent = this.noValidationMessage; + this.showMessageDecoration(Severity.Ignore); + } + if (this.validationMessage && this.ui.message.textContent !== this.validationMessage) { + this.ui.message.textContent = this.validationMessage; + this.showMessageDecoration(Severity.Error); + } + } +} + +export class QuickInputController extends Disposable { + private static readonly MAX_WIDTH = 600; // Max total width of quick open widget + + private idPrefix: string; + private ui: QuickInputUI | undefined; + private dimension?: dom.Dimension; + private titleBarOffset?: number; + private comboboxAccessibility = false; + private enabled = true; + private readonly onDidAcceptEmitter = this._register(new Emitter()); + private readonly onDidCustomEmitter = this._register(new Emitter()); + private readonly onDidTriggerButtonEmitter = this._register(new Emitter()); + private keyMods: Writeable = { ctrlCmd: false, alt: false }; + + private controller: QuickInput | null = null; + + private parentElement: HTMLElement; + private styles: IQuickInputStyles; + private onShowEmitter = new Emitter(); + private onHideEmitter = new Emitter(); + public onShow = this.onShowEmitter.event; + public onHide = this.onHideEmitter.event; + + constructor(private options: IQuickInputOptions) { + super(); + this.idPrefix = options.idPrefix; + this.parentElement = options.container; + this.styles = options.styles; + this.registerKeyModsListeners(); + } + + private registerKeyModsListeners() { + this._register(dom.addDisposableListener(this.parentElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Ctrl: + case KeyCode.Meta: + this.keyMods.ctrlCmd = true; + break; + case KeyCode.Alt: + this.keyMods.alt = true; + break; + } + })); + this._register(dom.addDisposableListener(this.parentElement, dom.EventType.KEY_UP, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Ctrl: + case KeyCode.Meta: + this.keyMods.ctrlCmd = false; + break; + case KeyCode.Alt: + this.keyMods.alt = false; + break; + } + })); + } + + private getUI() { + if (this.ui) { + return this.ui; + } + + const container = dom.append(this.parentElement, $('.quick-input-widget.show-file-icons')); + container.tabIndex = -1; + container.style.display = 'none'; + + const styleSheet = dom.createStyleSheet(container); + + const titleBar = dom.append(container, $('.quick-input-titlebar')); + + const leftActionBar = this._register(new ActionBar(titleBar)); + leftActionBar.domNode.classList.add('quick-input-left-action-bar'); + + const title = dom.append(titleBar, $('.quick-input-title')); + + const rightActionBar = this._register(new ActionBar(titleBar)); + rightActionBar.domNode.classList.add('quick-input-right-action-bar'); + + const description = dom.append(container, $('.quick-input-description')); + + const headerContainer = dom.append(container, $('.quick-input-header')); + + const checkAll = dom.append(headerContainer, $('input.quick-input-check-all')); + checkAll.type = 'checkbox'; + this._register(dom.addStandardDisposableListener(checkAll, dom.EventType.CHANGE, e => { + const checked = checkAll.checked; + list.setAllVisibleChecked(checked); + })); + this._register(dom.addDisposableListener(checkAll, dom.EventType.CLICK, e => { + if (e.x || e.y) { // Avoid 'click' triggered by 'space'... + inputBox.setFocus(); + } + })); + + const extraContainer = dom.append(headerContainer, $('.quick-input-and-message')); + const filterContainer = dom.append(extraContainer, $('.quick-input-filter')); + + const inputBox = this._register(new QuickInputBox(filterContainer)); + inputBox.setAttribute('aria-describedby', `${this.idPrefix}message`); + + const visibleCountContainer = dom.append(filterContainer, $('.quick-input-visible-count')); + visibleCountContainer.setAttribute('aria-live', 'polite'); + visibleCountContainer.setAttribute('aria-atomic', 'true'); + const visibleCount = new CountBadge(visibleCountContainer, { countFormat: localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results") }); + + const countContainer = dom.append(filterContainer, $('.quick-input-count')); + countContainer.setAttribute('aria-live', 'polite'); + const count = new CountBadge(countContainer, { countFormat: localize({ key: 'quickInput.countSelected', comment: ['This tells the user how many items are selected in a list of items to select from. The items can be anything.'] }, "{0} Selected") }); + + const okContainer = dom.append(headerContainer, $('.quick-input-action')); + const ok = new Button(okContainer); + ok.label = localize('ok', "OK"); + this._register(ok.onDidClick(e => { + this.onDidAcceptEmitter.fire(); + })); + + const customButtonContainer = dom.append(headerContainer, $('.quick-input-action')); + const customButton = new Button(customButtonContainer); + customButton.label = localize('custom', "Custom"); + this._register(customButton.onDidClick(e => { + this.onDidCustomEmitter.fire(); + })); + + const message = dom.append(extraContainer, $(`#${this.idPrefix}message.quick-input-message`)); + + const progressBar = new ProgressBar(container); + dom.addClass(progressBar.getContainer(), 'quick-input-progress'); + + const list = this._register(new QuickInputList(container, this.idPrefix + 'list', this.options)); + this._register(list.onChangedAllVisibleChecked(checked => { + checkAll.checked = checked; + })); + this._register(list.onChangedVisibleCount(c => { + visibleCount.setCount(c); + })); + this._register(list.onChangedCheckedCount(c => { + count.setCount(c); + })); + this._register(list.onLeave(() => { + // Defer to avoid the input field reacting to the triggering key. + setTimeout(() => { + inputBox.setFocus(); + if (this.controller instanceof QuickPick && this.controller.canSelectMany) { + list.clearFocus(); + } + }, 0); + })); + this._register(list.onDidChangeFocus(() => { + if (this.comboboxAccessibility) { + this.getUI().inputBox.setAttribute('aria-activedescendant', this.getUI().list.getActiveDescendant() || ''); + } + })); + + const focusTracker = dom.trackFocus(container); + this._register(focusTracker); + this._register(focusTracker.onDidBlur(() => { + if (!this.getUI().ignoreFocusOut && !this.options.ignoreFocusOut()) { + this.hide(true); + } + })); + this._register(dom.addDisposableListener(container, dom.EventType.FOCUS, (e: FocusEvent) => { + inputBox.setFocus(); + })); + this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => { + const event = new StandardKeyboardEvent(e); + switch (event.keyCode) { + case KeyCode.Enter: + dom.EventHelper.stop(e, true); + this.onDidAcceptEmitter.fire(); + break; + case KeyCode.Escape: + dom.EventHelper.stop(e, true); + this.hide(); + break; + case KeyCode.Tab: + if (!event.altKey && !event.ctrlKey && !event.metaKey) { + const selectors = ['.action-label.codicon']; + if (container.classList.contains('show-checkboxes')) { + selectors.push('input'); + } else { + selectors.push('input[type=text]'); + } + if (this.getUI().list.isDisplayed()) { + selectors.push('.monaco-list'); + } + const stops = container.querySelectorAll(selectors.join(', ')); + if (event.shiftKey && event.target === stops[0]) { + dom.EventHelper.stop(e, true); + stops[stops.length - 1].focus(); + } else if (!event.shiftKey && event.target === stops[stops.length - 1]) { + dom.EventHelper.stop(e, true); + stops[0].focus(); + } + } + break; + } + })); + + this.ui = { + container, + styleSheet, + leftActionBar, + titleBar, + title, + description, + rightActionBar, + checkAll, + filterContainer, + inputBox, + visibleCountContainer, + visibleCount, + countContainer, + count, + okContainer, + ok, + message, + customButtonContainer, + customButton, + progressBar, + list, + onDidAccept: this.onDidAcceptEmitter.event, + onDidCustom: this.onDidCustomEmitter.event, + onDidTriggerButton: this.onDidTriggerButtonEmitter.event, + ignoreFocusOut: false, + keyMods: this.keyMods, + isScreenReaderOptimized: () => this.options.isScreenReaderOptimized(), + show: controller => this.show(controller), + hide: () => this.hide(), + setVisibilities: visibilities => this.setVisibilities(visibilities), + setComboboxAccessibility: enabled => this.setComboboxAccessibility(enabled), + setEnabled: enabled => this.setEnabled(enabled), + setContextKey: contextKey => this.options.setContextKey(contextKey), + }; + this.updateStyles(); + return this.ui; + } + + pick>(picks: Promise[]> | QuickPickInput[], options: O = {}, token: CancellationToken = CancellationToken.None): Promise { + return new Promise((doResolve, reject) => { + let resolve = (result: any) => { + resolve = doResolve; + if (options.onKeyMods) { + options.onKeyMods(input.keyMods); + } + doResolve(result); + }; + if (token.isCancellationRequested) { + resolve(undefined); + return; + } + const input = this.createQuickPick(); + let activeItem: T | undefined; + const disposables = [ + input, + input.onDidAccept(() => { + if (input.canSelectMany) { + resolve(input.selectedItems.slice()); + input.hide(); + } else { + const result = input.activeItems[0]; + if (result) { + resolve(result); + input.hide(); + } + } + }), + input.onDidChangeActive(items => { + const focused = items[0]; + if (focused && options.onDidFocus) { + options.onDidFocus(focused); + } + }), + input.onDidChangeSelection(items => { + if (!input.canSelectMany) { + const result = items[0]; + if (result) { + resolve(result); + input.hide(); + } + } + }), + input.onDidTriggerItemButton(event => options.onDidTriggerItemButton && options.onDidTriggerItemButton({ + ...event, + removeItem: () => { + const index = input.items.indexOf(event.item); + if (index !== -1) { + const items = input.items.slice(); + items.splice(index, 1); + input.items = items; + } + } + })), + input.onDidChangeValue(value => { + if (activeItem && !value && (input.activeItems.length !== 1 || input.activeItems[0] !== activeItem)) { + input.activeItems = [activeItem]; + } + }), + token.onCancellationRequested(() => { + input.hide(); + }), + input.onDidHide(() => { + dispose(disposables); + resolve(undefined); + }), + ]; + input.canSelectMany = !!options.canPickMany; + input.placeholder = options.placeHolder; + input.ignoreFocusOut = !!options.ignoreFocusLost; + input.matchOnDescription = !!options.matchOnDescription; + input.matchOnDetail = !!options.matchOnDetail; + input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true + input.autoFocusOnList = (options.autoFocusOnList === undefined) || options.autoFocusOnList; // default to true + input.quickNavigate = options.quickNavigate; + input.contextKey = options.contextKey; + input.busy = true; + Promise.all[], T | undefined>([picks, options.activeItem]) + .then(([items, _activeItem]) => { + activeItem = _activeItem; + input.busy = false; + input.items = items; + if (input.canSelectMany) { + input.selectedItems = items.filter(item => item.type !== 'separator' && item.picked) as T[]; + } + if (activeItem) { + input.activeItems = [activeItem]; + } + }); + input.show(); + Promise.resolve(picks).then(undefined, err => { + reject(err); + input.hide(); + }); + }); + } + + input(options: IInputOptions = {}, token: CancellationToken = CancellationToken.None): Promise { + return new Promise((resolve, reject) => { + if (token.isCancellationRequested) { + resolve(undefined); + return; + } + const input = this.createInputBox(); + const validateInput = options.validateInput || (() => >Promise.resolve(undefined)); + const onDidValueChange = Event.debounce(input.onDidChangeValue, (last, cur) => cur, 100); + let validationValue = options.value || ''; + let validation = Promise.resolve(validateInput(validationValue)); + const disposables = [ + input, + onDidValueChange(value => { + if (value !== validationValue) { + validation = Promise.resolve(validateInput(value)); + validationValue = value; + } + validation.then(result => { + if (value === validationValue) { + input.validationMessage = result || undefined; + } + }); + }), + input.onDidAccept(() => { + const value = input.value; + if (value !== validationValue) { + validation = Promise.resolve(validateInput(value)); + validationValue = value; + } + validation.then(result => { + if (!result) { + resolve(value); + input.hide(); + } else if (value === validationValue) { + input.validationMessage = result; + } + }); + }), + token.onCancellationRequested(() => { + input.hide(); + }), + input.onDidHide(() => { + dispose(disposables); + resolve(undefined); + }), + ]; + input.value = options.value || ''; + input.valueSelection = options.valueSelection; + input.prompt = options.prompt; + input.placeholder = options.placeHolder; + input.password = !!options.password; + input.ignoreFocusOut = !!options.ignoreFocusLost; + input.show(); + }); + } + + backButton = backButton; + + createQuickPick(): IQuickPick { + const ui = this.getUI(); + return new QuickPick(ui); + } + + createInputBox(): IInputBox { + const ui = this.getUI(); + return new InputBox(ui); + } + + private show(controller: QuickInput) { + const ui = this.getUI(); + this.onShowEmitter.fire(); + const oldController = this.controller; + this.controller = controller; + if (oldController) { + oldController.didHide(); + } + + this.setEnabled(true); + ui.leftActionBar.clear(); + ui.title.textContent = ''; + ui.description.textContent = ''; + ui.rightActionBar.clear(); + ui.checkAll.checked = false; + // ui.inputBox.value = ''; Avoid triggering an event. + ui.inputBox.placeholder = ''; + ui.inputBox.password = false; + ui.inputBox.showDecoration(Severity.Ignore); + ui.visibleCount.setCount(0); + ui.count.setCount(0); + ui.message.textContent = ''; + ui.progressBar.stop(); + ui.list.setElements([]); + ui.list.matchOnDescription = false; + ui.list.matchOnDetail = false; + ui.list.matchOnLabel = true; + ui.list.sortByLabel = true; + ui.ignoreFocusOut = false; + this.setComboboxAccessibility(false); + ui.inputBox.removeAttribute('aria-label'); + + const backKeybindingLabel = this.options.backKeybindingLabel(); + backButton.tooltip = backKeybindingLabel ? localize('quickInput.backWithKeybinding', "Back ({0})", backKeybindingLabel) : localize('quickInput.back', "Back"); + + ui.container.style.display = ''; + this.updateLayout(); + ui.inputBox.setFocus(); + } + + private setVisibilities(visibilities: Visibilities) { + const ui = this.getUI(); + ui.title.style.display = visibilities.title ? '' : 'none'; + ui.description.style.display = visibilities.description ? '' : 'none'; + ui.checkAll.style.display = visibilities.checkAll ? '' : 'none'; + ui.filterContainer.style.display = visibilities.inputBox ? '' : 'none'; + ui.visibleCountContainer.style.display = visibilities.visibleCount ? '' : 'none'; + ui.countContainer.style.display = visibilities.count ? '' : 'none'; + ui.okContainer.style.display = visibilities.ok ? '' : 'none'; + ui.customButtonContainer.style.display = visibilities.customButton ? '' : 'none'; + ui.message.style.display = visibilities.message ? '' : 'none'; + ui.list.display(!!visibilities.list); + ui.container.classList[visibilities.checkAll ? 'add' : 'remove']('show-checkboxes'); + this.updateLayout(); // TODO + } + + private setComboboxAccessibility(enabled: boolean) { + if (enabled !== this.comboboxAccessibility) { + const ui = this.getUI(); + this.comboboxAccessibility = enabled; + if (this.comboboxAccessibility) { + ui.inputBox.setAttribute('role', 'combobox'); + ui.inputBox.setAttribute('aria-haspopup', 'true'); + ui.inputBox.setAttribute('aria-autocomplete', 'list'); + ui.inputBox.setAttribute('aria-activedescendant', ui.list.getActiveDescendant() || ''); + } else { + ui.inputBox.removeAttribute('role'); + ui.inputBox.removeAttribute('aria-haspopup'); + ui.inputBox.removeAttribute('aria-autocomplete'); + ui.inputBox.removeAttribute('aria-activedescendant'); + } + } + } + + private setEnabled(enabled: boolean) { + if (enabled !== this.enabled) { + this.enabled = enabled; + for (const item of this.getUI().leftActionBar.viewItems) { + (item as ActionViewItem).getAction().enabled = enabled; + } + for (const item of this.getUI().rightActionBar.viewItems) { + (item as ActionViewItem).getAction().enabled = enabled; + } + this.getUI().checkAll.disabled = !enabled; + // this.getUI().inputBox.enabled = enabled; Avoid loosing focus. + this.getUI().ok.enabled = enabled; + this.getUI().list.enabled = enabled; + } + } + + public hide(focusLost?: boolean) { + const controller = this.controller; + if (controller) { + this.controller = null; + this.onHideEmitter.fire(); + this.getUI().container.style.display = 'none'; + if (!focusLost) { + this.options.returnFocus(); + } + controller.didHide(); + } + } + + focus() { + if (this.isDisplayed()) { + this.getUI().inputBox.setFocus(); + } + } + + toggle() { + if (this.isDisplayed() && this.controller instanceof QuickPick && this.controller.canSelectMany) { + this.getUI().list.toggleCheckbox(); + } + } + + navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration) { + if (this.isDisplayed() && this.getUI().list.isDisplayed()) { + this.getUI().list.focus(next ? 'Next' : 'Previous'); + if (quickNavigate && this.controller instanceof QuickPick) { + this.controller.quickNavigate = quickNavigate; + } + } + } + + accept() { + this.onDidAcceptEmitter.fire(); + return Promise.resolve(undefined); + } + + back() { + this.onDidTriggerButtonEmitter.fire(this.backButton); + return Promise.resolve(undefined); + } + + cancel() { + this.hide(); + return Promise.resolve(undefined); + } + + layout(dimension: dom.Dimension, titleBarOffset: number): void { + this.dimension = dimension; + this.titleBarOffset = titleBarOffset; + this.updateLayout(); + } + + private updateLayout() { + if (this.ui) { + this.ui.container.style.top = `${this.titleBarOffset}px`; + + const style = this.ui.container.style; + const width = Math.min(this.dimension!.width * 0.62 /* golden cut */, QuickInputController.MAX_WIDTH); + style.width = width + 'px'; + style.marginLeft = '-' + (width / 2) + 'px'; + + this.ui.inputBox.layout(); + this.ui.list.layout(this.dimension && this.dimension.height * 0.4); + } + } + + public applyStyles(styles: IQuickInputStyles) { + this.styles = styles; + this.updateStyles(); + } + + private updateStyles() { + if (this.ui) { + const { + titleColor, + quickInputBackground, + quickInputForeground, + contrastBorder, + widgetShadow, + } = this.styles.widget; + this.ui.titleBar.style.backgroundColor = titleColor || ''; + this.ui.container.style.backgroundColor = quickInputBackground ? quickInputBackground.toString() : ''; + this.ui.container.style.color = quickInputForeground ? quickInputForeground.toString() : ''; + this.ui.container.style.border = contrastBorder ? `1px solid ${contrastBorder}` : ''; + this.ui.container.style.boxShadow = widgetShadow ? `0 5px 8px ${widgetShadow}` : ''; + this.ui.inputBox.style(this.styles.inputBox); + this.ui.count.style(this.styles.countBadge); + this.ui.ok.style(this.styles.button); + this.ui.customButton.style(this.styles.button); + this.ui.progressBar.style(this.styles.progressBar); + this.ui.list.style(this.styles.list); + + const content: string[] = []; + if (this.styles.list.listInactiveFocusForeground) { + content.push(`.monaco-list .monaco-list-row.focused { color: ${this.styles.list.listInactiveFocusForeground}; }`); + content.push(`.monaco-list .monaco-list-row.focused:hover { color: ${this.styles.list.listInactiveFocusForeground}; }`); // overwrite :hover style in this case! + } + if (this.styles.list.pickerGroupBorder) { + content.push(`.quick-input-list .quick-input-list-entry { border-top-color: ${this.styles.list.pickerGroupBorder}; }`); + } + if (this.styles.list.pickerGroupForeground) { + content.push(`.quick-input-list .quick-input-list-separator { color: ${this.styles.list.pickerGroupForeground}; }`); + } + const newStyles = content.join('\n'); + if (newStyles !== this.ui.styleSheet.innerHTML) { + this.ui.styleSheet.innerHTML = newStyles; + } + } + } + + private isDisplayed() { + return this.ui && this.ui.container.style.display !== 'none'; + } +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/base/parts/quickinput/browser/quickInputBox.ts similarity index 68% rename from src/vs/workbench/browser/parts/quickinput/quickInputBox.ts rename to src/vs/base/parts/quickinput/browser/quickInputBox.ts index 55ca955a7949..d047c885204c 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputBox.ts @@ -5,9 +5,7 @@ import 'vs/css!./media/quickInput'; import * as dom from 'vs/base/browser/dom'; -import { InputBox, IRange, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; -import { inputBackground, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoForeground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningForeground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorForeground, inputValidationErrorBorder } from 'vs/platform/theme/common/colorRegistry'; -import { ITheme } from 'vs/platform/theme/common/themeService'; +import { InputBox, IRange, MessageType, IInputBoxStyles } from 'vs/base/browser/ui/inputbox/inputBox'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import Severity from 'vs/base/common/severity'; @@ -112,20 +110,7 @@ export class QuickInputBox extends Disposable { this.inputBox.layout(); } - style(theme: ITheme) { - this.inputBox.style({ - inputForeground: theme.getColor(inputForeground), - inputBackground: theme.getColor(inputBackground), - inputBorder: theme.getColor(inputBorder), - inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground), - inputValidationInfoForeground: theme.getColor(inputValidationInfoForeground), - inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder), - inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground), - inputValidationWarningForeground: theme.getColor(inputValidationWarningForeground), - inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder), - inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground), - inputValidationErrorForeground: theme.getColor(inputValidationErrorForeground), - inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder), - }); + style(styles: IInputBoxStyles) { + this.inputBox.style(styles); } } diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts b/src/vs/base/parts/quickinput/browser/quickInputList.ts similarity index 87% rename from src/vs/workbench/browser/parts/quickinput/quickInputList.ts rename to src/vs/base/parts/quickinput/browser/quickInputList.ts index 1765f4deb543..aa6e6a7896ac 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts +++ b/src/vs/base/parts/quickinput/browser/quickInputList.ts @@ -7,9 +7,7 @@ import 'vs/css!./media/quickInput'; import { IListVirtualDelegate, IListRenderer } from 'vs/base/browser/ui/list/list'; import * as dom from 'vs/base/browser/dom'; import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { WorkbenchList, IWorkbenchListOptions } from 'vs/platform/list/browser/listService'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput'; import { IMatch } from 'vs/base/common/filters'; import { matchesFuzzyCodiconAware, parseCodicons } from 'vs/base/common/codicon'; import { compareAnything } from 'vs/base/common/comparers'; @@ -22,13 +20,12 @@ import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlighte import { memoize } from 'vs/base/common/decorators'; import { range } from 'vs/base/common/arrays'; import * as platform from 'vs/base/common/platform'; -import { listFocusBackground, pickerGroupBorder, pickerGroupForeground, activeContrastBorder, listFocusForeground } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; -import { getIconClass } from 'vs/workbench/browser/parts/quickinput/quickInputUtils'; +import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils'; import { withNullAsUndefined } from 'vs/base/common/types'; -import { QUICK_INPUT_BACKGROUND } from 'vs/workbench/common/theme'; +import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput'; +import { IListOptions, List, IListStyles } from 'vs/base/browser/ui/list/listWidget'; const $ = dom.$; @@ -215,7 +212,7 @@ export class QuickInputList { readonly id: string; private container: HTMLElement; - private list: WorkbenchList; + private list: List; private inputElements: Array = []; private elements: ListElement[] = []; private elementsToIndexes = new Map(); @@ -242,21 +239,18 @@ export class QuickInputList { constructor( private parent: HTMLElement, id: string, - @IInstantiationService private readonly instantiationService: IInstantiationService + options: IQuickInputOptions, ) { this.id = id; this.container = dom.append(this.parent, $('.quick-input-list')); const delegate = new ListElementDelegate(); - this.list = this.instantiationService.createInstance(WorkbenchList, 'QuickInput', this.container, delegate, [new ListElementRenderer()], { + this.list = options.createList('QuickInput', this.container, delegate, [new ListElementRenderer()], { identityProvider: { getId: element => element.saneLabel }, openController: { shouldOpen: () => false }, // Workaround #58124 setRowLineHeight: false, multipleSelectionSupport: false, horizontalScrolling: false, - overrideStyles: { - listBackground: QUICK_INPUT_BACKGROUND - } - } as IWorkbenchListOptions); + } as IListOptions); this.list.getHTMLElement().id = id; this.disposables.push(this.list); this.disposables.push(this.list.onKeyDown(e => { @@ -577,6 +571,10 @@ export class QuickInputList { private fireButtonTriggered(event: IQuickPickItemButtonEvent) { this._onButtonTriggered.fire(event); } + + style(styles: IListStyles) { + this.list.style(styles); + } } function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: string): number { @@ -593,30 +591,3 @@ function compareEntries(elementA: ListElement, elementB: ListElement, lookFor: s return compareAnything(elementA.saneLabel, elementB.saneLabel, lookFor); } - -registerThemingParticipant((theme, collector) => { - // Override inactive focus foreground with active focus foreground for single-pick case. - const listInactiveFocusForeground = theme.getColor(listFocusForeground); - if (listInactiveFocusForeground) { - collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused { color: ${listInactiveFocusForeground}; }`); - } - // Override inactive focus background with active focus background for single-pick case. - const listInactiveFocusBackground = theme.getColor(listFocusBackground); - if (listInactiveFocusBackground) { - collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused { background-color: ${listInactiveFocusBackground}; }`); - collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused:hover { background-color: ${listInactiveFocusBackground}; }`); - } - // dotted instead of solid (as in listWidget.ts) to match QuickOpen - const activeContrast = theme.getColor(activeContrastBorder); - if (activeContrast) { - collector.addRule(`.quick-input-list .monaco-list .monaco-list-row.focused { outline: 1px dotted ${activeContrast}; outline-offset: -1px; }`); - } - const pickerGroupBorderColor = theme.getColor(pickerGroupBorder); - if (pickerGroupBorderColor) { - collector.addRule(`.quick-input-list .quick-input-list-entry { border-top-color: ${pickerGroupBorderColor}; }`); - } - const pickerGroupForegroundColor = theme.getColor(pickerGroupForeground); - if (pickerGroupForegroundColor) { - collector.addRule(`.quick-input-list .quick-input-list-separator { color: ${pickerGroupForegroundColor}; }`); - } -}); diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts b/src/vs/base/parts/quickinput/browser/quickInputUtils.ts similarity index 100% rename from src/vs/workbench/browser/parts/quickinput/quickInputUtils.ts rename to src/vs/base/parts/quickinput/browser/quickInputUtils.ts diff --git a/src/vs/base/parts/quickinput/common/quickInput.ts b/src/vs/base/parts/quickinput/common/quickInput.ts new file mode 100644 index 000000000000..7228ca06498a --- /dev/null +++ b/src/vs/base/parts/quickinput/common/quickInput.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; +import { URI } from 'vs/base/common/uri'; +import { Event } from 'vs/base/common/event'; + +export interface IQuickPickItem { + type?: 'item'; + id?: string; + label: string; + description?: string; + detail?: string; + iconClasses?: string[]; + buttons?: IQuickInputButton[]; + picked?: boolean; + alwaysShow?: boolean; +} + +export interface IQuickPickSeparator { + type: 'separator'; + label?: string; +} + +export interface IKeyMods { + readonly ctrlCmd: boolean; + readonly alt: boolean; +} + +export interface IQuickNavigateConfiguration { + keybindings: ResolvedKeybinding[]; +} + +export interface IPickOptions { + + /** + * an optional string to show as placeholder in the input box to guide the user what she picks on + */ + placeHolder?: string; + + /** + * an optional flag to include the description when filtering the picks + */ + matchOnDescription?: boolean; + + /** + * an optional flag to include the detail when filtering the picks + */ + matchOnDetail?: boolean; + + /** + * an optional flag to filter the picks based on label. Defaults to true. + */ + matchOnLabel?: boolean; + + /** + * an option flag to control whether focus is always automatically brought to a list item. Defaults to true. + */ + autoFocusOnList?: boolean; + + /** + * an optional flag to not close the picker on focus lost + */ + ignoreFocusLost?: boolean; + + /** + * an optional flag to make this picker multi-select + */ + canPickMany?: boolean; + + /** + * enables quick navigate in the picker to open an element without typing + */ + quickNavigate?: IQuickNavigateConfiguration; + + /** + * a context key to set when this picker is active + */ + contextKey?: string; + + /** + * an optional property for the item to focus initially. + */ + activeItem?: Promise | T; + + onKeyMods?: (keyMods: IKeyMods) => void; + onDidFocus?: (entry: T) => void; + onDidTriggerItemButton?: (context: IQuickPickItemButtonContext) => void; +} + +export interface IInputOptions { + + /** + * the value to prefill in the input box + */ + value?: string; + + /** + * the selection of value, default to the whole word + */ + valueSelection?: [number, number]; + + /** + * the text to display underneath the input box + */ + prompt?: string; + + /** + * an optional string to show as placeholder in the input box to guide the user what to type + */ + placeHolder?: string; + + /** + * set to true to show a password prompt that will not show the typed value + */ + password?: boolean; + + ignoreFocusLost?: boolean; + + /** + * an optional function that is used to validate user input. + */ + validateInput?: (input: string) => Promise; +} + +export interface IQuickInput { + + title: string | undefined; + + description: string | undefined; + + step: number | undefined; + + totalSteps: number | undefined; + + enabled: boolean; + + contextKey: string | undefined; + + busy: boolean; + + ignoreFocusOut: boolean; + + show(): void; + + hide(): void; + + onDidHide: Event; + + dispose(): void; +} + +export interface IQuickPick extends IQuickInput { + + value: string; + + placeholder: string | undefined; + + readonly onDidChangeValue: Event; + + readonly onDidAccept: Event; + + ok: boolean; + + readonly onDidCustom: Event; + + customButton: boolean; + + customLabel: string | undefined; + + customHover: string | undefined; + + buttons: ReadonlyArray; + + readonly onDidTriggerButton: Event; + + readonly onDidTriggerItemButton: Event>; + + items: ReadonlyArray; + + canSelectMany: boolean; + + matchOnDescription: boolean; + + matchOnDetail: boolean; + + matchOnLabel: boolean; + + sortByLabel: boolean; + + autoFocusOnList: boolean; + + quickNavigate: IQuickNavigateConfiguration | undefined; + + activeItems: ReadonlyArray; + + readonly onDidChangeActive: Event; + + selectedItems: ReadonlyArray; + + readonly onDidChangeSelection: Event; + + readonly keyMods: IKeyMods; + + valueSelection: Readonly<[number, number]> | undefined; + + validationMessage: string | undefined; + + inputHasFocus(): boolean; + + focusOnInput(): void; +} + +export interface IInputBox extends IQuickInput { + + value: string; + + valueSelection: Readonly<[number, number]> | undefined; + + placeholder: string | undefined; + + password: boolean; + + readonly onDidChangeValue: Event; + + readonly onDidAccept: Event; + + buttons: ReadonlyArray; + + readonly onDidTriggerButton: Event; + + prompt: string | undefined; + + validationMessage: string | undefined; +} + +export interface IQuickInputButton { + /** iconPath or iconClass required */ + iconPath?: { dark: URI; light?: URI; }; + /** iconPath or iconClass required */ + iconClass?: string; + tooltip?: string; +} + +export interface IQuickPickItemButtonEvent { + button: IQuickInputButton; + item: T; +} + +export interface IQuickPickItemButtonContext extends IQuickPickItemButtonEvent { + removeItem(): void; +} + +export type QuickPickInput = T | IQuickPickSeparator; diff --git a/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts b/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts index a44fc8008f46..b54ac4aeb363 100644 --- a/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts +++ b/src/vs/base/parts/quickopen/browser/quickOpenWidget.ts @@ -754,6 +754,7 @@ export class QuickOpenWidget extends Disposable implements IModelProvider, IThem else if (autoFocus.autoFocusLastEntry) { if (entries.length > 1) { this.tree.focusLast(); + this.tree.reveal(this.tree.getFocus()); } } } diff --git a/src/vs/base/parts/quickopen/browser/quickopen.css b/src/vs/base/parts/quickopen/browser/quickopen.css index 6132b6732947..d7e2f8a2429b 100644 --- a/src/vs/base/parts/quickopen/browser/quickopen.css +++ b/src/vs/base/parts/quickopen/browser/quickopen.css @@ -85,6 +85,10 @@ opacity: 1; } +.monaco-quick-open-widget .quick-open-tree .quick-open-entry .monaco-highlighted-label .codicon { + vertical-align: sub; /* vertically align codicon */ +} + .monaco-quick-open-widget .quick-open-tree .quick-open-entry-meta { opacity: 0.7; line-height: normal; diff --git a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts index bbfc063f2083..c7a8db97769a 100644 --- a/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts +++ b/src/vs/base/test/browser/ui/tree/indexTreeModel.test.ts @@ -356,13 +356,15 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[1].collapsible, false); assert.deepEqual(list[1].collapsed, false); - model.setCollapsed([0], true); - assert.deepEqual(list.length, 1); + assert.deepEqual(model.setCollapsed([0], true), false); assert.deepEqual(list[0].element, 0); assert.deepEqual(list[0].collapsible, false); - assert.deepEqual(list[0].collapsed, true); + assert.deepEqual(list[0].collapsed, false); + assert.deepEqual(list[1].element, 10); + assert.deepEqual(list[1].collapsible, false); + assert.deepEqual(list[1].collapsed, false); - model.setCollapsed([0], false); + assert.deepEqual(model.setCollapsed([0], false), false); assert.deepEqual(list[0].element, 0); assert.deepEqual(list[0].collapsible, false); assert.deepEqual(list[0].collapsed, false); @@ -379,13 +381,13 @@ suite('IndexTreeModel', function () { assert.deepEqual(list[1].collapsible, false); assert.deepEqual(list[1].collapsed, false); - model.setCollapsed([0], true); + assert.deepEqual(model.setCollapsed([0], true), true); assert.deepEqual(list.length, 1); assert.deepEqual(list[0].element, 0); assert.deepEqual(list[0].collapsible, true); assert.deepEqual(list[0].collapsed, true); - model.setCollapsed([0], false); + assert.deepEqual(model.setCollapsed([0], false), true); assert.deepEqual(list[0].element, 0); assert.deepEqual(list[0].collapsible, true); assert.deepEqual(list[0].collapsed, false); diff --git a/src/vs/base/test/common/uri.test.ts b/src/vs/base/test/common/uri.test.ts index a85930d3ef45..253ab63bd88a 100644 --- a/src/vs/base/test/common/uri.test.ts +++ b/src/vs/base/test/common/uri.test.ts @@ -404,7 +404,7 @@ suite('URI', () => { path = 'foo/bar'; assert.equal(URI.file(path).path, '/foo/bar'); path = './foo/bar'; - assert.equal(URI.file(path).path, '/./foo/bar'); // todo@joh missing normalization + assert.equal(URI.file(path).path, '/./foo/bar'); // missing normalization const fileUri1 = URI.parse(`file:foo/bar`); assert.equal(fileUri1.path, '/foo/bar'); diff --git a/src/vs/base/test/common/utils.ts b/src/vs/base/test/common/utils.ts index 18e864443f36..865004bbf7b4 100644 --- a/src/vs/base/test/common/utils.ts +++ b/src/vs/base/test/common/utils.ts @@ -26,35 +26,32 @@ export class DeferredPromise { public complete(value: T) { return new Promise(resolve => { - process.nextTick(() => { - this.completeCallback(value); - resolve(); - }); + this.completeCallback(value); + resolve(); }); } public error(err: any) { return new Promise(resolve => { - process.nextTick(() => { - this.errorCallback(err); - resolve(); - }); + this.errorCallback(err); + resolve(); }); } public cancel() { - process.nextTick(() => { + new Promise(resolve => { this.errorCallback(canceled()); + resolve(); }); } } export function toResource(this: any, path: string) { if (isWindows) { - return URI.file(join('C:\\', Buffer.from(this.test.fullTitle()).toString('base64'), path)); + return URI.file(join('C:\\', btoa(this.test.fullTitle()), path)); } - return URI.file(join('/', Buffer.from(this.test.fullTitle()).toString('base64'), path)); + return URI.file(join('/', btoa(this.test.fullTitle()), path)); } export function suiteRepeat(n: number, description: string, callback: (this: any) => void): void { diff --git a/src/vs/base/test/common/path.test.ts b/src/vs/base/test/node/path.test.ts similarity index 99% rename from src/vs/base/test/common/path.test.ts rename to src/vs/base/test/node/path.test.ts index aca6388008bf..1b25fbe018fb 100644 --- a/src/vs/base/test/common/path.test.ts +++ b/src/vs/base/test/node/path.test.ts @@ -176,8 +176,8 @@ suite('Paths (Node Implementation)', () => { }); test('dirname', () => { - assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-11), - isWindows ? 'test\\common' : 'test/common'); + assert.strictEqual(path.dirname(path.normalize(__filename)).substr(-9), + isWindows ? 'test\\node' : 'test/node'); assert.strictEqual(path.posix.dirname('/a/b/'), '/a'); assert.strictEqual(path.posix.dirname('/a/b'), '/a'); diff --git a/src/vs/base/test/node/pfs/pfs.test.ts b/src/vs/base/test/node/pfs/pfs.test.ts index 7b62153765b4..b597b75324ea 100644 --- a/src/vs/base/test/node/pfs/pfs.test.ts +++ b/src/vs/base/test/node/pfs/pfs.test.ts @@ -7,47 +7,14 @@ import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; import * as fs from 'fs'; -import { Readable } from 'stream'; import * as uuid from 'vs/base/common/uuid'; import * as pfs from 'vs/base/node/pfs'; import { timeout } from 'vs/base/common/async'; import { getPathFromAmdModule } from 'vs/base/common/amd'; -import { isWindows, isLinux } from 'vs/base/common/platform'; +import { isWindows } from 'vs/base/common/platform'; import { canNormalize } from 'vs/base/common/normalization'; import { VSBuffer } from 'vs/base/common/buffer'; -const chunkSize = 64 * 1024; -const readError = 'Error while reading'; -function toReadable(value: string, throwError?: boolean): Readable { - const totalChunks = Math.ceil(value.length / chunkSize); - const stringChunks: string[] = []; - - for (let i = 0, j = 0; i < totalChunks; ++i, j += chunkSize) { - stringChunks[i] = value.substr(j, chunkSize); - } - - let counter = 0; - return new Readable({ - read: function () { - if (throwError) { - this.emit('error', new Error(readError)); - } - - let res!: string; - let canPush = true; - while (canPush && (res = stringChunks[counter++])) { - canPush = this.push(res); - } - - // EOS - if (!res) { - this.push(null); - } - }, - encoding: 'utf8' - }); -} - suite('PFS', function () { // Given issues such as https://github.com/microsoft/vscode/issues/84066 @@ -334,7 +301,7 @@ suite('PFS', function () { test('stat link', async () => { if (isWindows) { - return Promise.resolve(); // Symlinks are not the same on win, and we can not create them programitically without admin privileges + return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges } const id1 = uuid.generateUuid(); @@ -349,12 +316,36 @@ suite('PFS', function () { fs.symlinkSync(directory, symbolicLink); let statAndIsLink = await pfs.statLink(directory); - assert.ok(!statAndIsLink!.isSymbolicLink); + assert.ok(!statAndIsLink?.symbolicLink); statAndIsLink = await pfs.statLink(symbolicLink); - assert.ok(statAndIsLink!.isSymbolicLink); + assert.ok(statAndIsLink?.symbolicLink); + assert.ok(!statAndIsLink?.symbolicLink?.dangling); + + pfs.rimrafSync(directory); + }); + + test('stat link (non existing target)', async () => { + if (isWindows) { + return; // Symlinks are not the same on win, and we can not create them programitically without admin privileges + } + + const id1 = uuid.generateUuid(); + const parentDir = path.join(os.tmpdir(), 'vsctests', id1); + const directory = path.join(parentDir, 'pfs', id1); + + const id2 = uuid.generateUuid(); + const symbolicLink = path.join(parentDir, 'pfs', id2); + + await pfs.mkdirp(directory, 493); + + fs.symlinkSync(directory, symbolicLink); pfs.rimrafSync(directory); + + const statAndIsLink = await pfs.statLink(symbolicLink); + assert.ok(statAndIsLink?.symbolicLink); + assert.ok(statAndIsLink?.symbolicLink?.dangling); }); test('readdir', async () => { @@ -420,17 +411,10 @@ suite('PFS', function () { return testWriteFileAndFlush(VSBuffer.fromString(smallData).buffer, smallData, VSBuffer.fromString(bigData).buffer, bigData); }); - test('writeFile (stream)', async () => { - const smallData = 'Hello World'; - const bigData = (new Array(100 * 1024)).join('Large String\n'); - - return testWriteFileAndFlush(toReadable(smallData), smallData, toReadable(bigData), bigData); - }); - async function testWriteFileAndFlush( - smallData: string | Buffer | NodeJS.ReadableStream | Uint8Array, + smallData: string | Buffer | Uint8Array, smallDataValue: string, - bigData: string | Buffer | NodeJS.ReadableStream | Uint8Array, + bigData: string | Buffer | Uint8Array, bigDataValue: string ): Promise { const id = uuid.generateUuid(); @@ -450,22 +434,6 @@ suite('PFS', function () { await pfs.rimraf(parentDir); } - test('writeFile (file stream)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const sourceFile = getPathFromAmdModule(require, './fixtures/index.html'); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); - - await pfs.writeFile(testFile, fs.createReadStream(sourceFile)); - assert.equal(fs.readFileSync(testFile).toString(), fs.readFileSync(sourceFile).toString()); - - await pfs.rimraf(parentDir); - }); - test('writeFile (string, error handling)', async () => { const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); @@ -490,118 +458,6 @@ suite('PFS', function () { await pfs.rimraf(parentDir); }); - test('writeFile (stream, error handling EISDIR)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! - - const readable = toReadable('Hello World'); - - let expectedError: Error | undefined; - try { - await pfs.writeFile(testFile, readable); - } catch (error) { - expectedError = error; - } - - if (!expectedError || (expectedError).code !== 'EISDIR') { - throw new Error('Expected EISDIR error for writing to folder but got: ' + (expectedError ? (expectedError).code : 'no error')); - } - - // verify that the stream is still consumable (for https://github.com/Microsoft/vscode/issues/42542) - assert.equal(readable.read(), 'Hello World'); - - await pfs.rimraf(parentDir); - }); - - test('writeFile (stream, error handling READERROR)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - await pfs.mkdirp(newDir, 493); - assert.ok(fs.existsSync(newDir)); - - let expectedError: Error | undefined; - try { - await pfs.writeFile(testFile, toReadable('Hello World', true /* throw error */)); - } catch (error) { - expectedError = error; - } - - if (!expectedError || expectedError.message !== readError) { - throw new Error('Expected error for writing to folder'); - } - - await pfs.rimraf(parentDir); - }); - - test('writeFile (stream, error handling EACCES)', async () => { - if (isLinux) { - return Promise.resolve(); // somehow this test fails on Linux in our TFS builds - } - - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - fs.writeFileSync(testFile, ''); - fs.chmodSync(testFile, 33060); // make readonly - - let expectedError: Error | undefined; - try { - await pfs.writeFile(testFile, toReadable('Hello World')); - } catch (error) { - expectedError = error; - } - - if (!expectedError || !((expectedError).code !== 'EACCES' || (expectedError).code !== 'EPERM')) { - throw new Error('Expected EACCES/EPERM error for writing to folder but got: ' + (expectedError ? (expectedError).code : 'no error')); - } - - await pfs.rimraf(parentDir); - }); - - test('writeFile (file stream, error handling)', async () => { - const id = uuid.generateUuid(); - const parentDir = path.join(os.tmpdir(), 'vsctests', id); - const sourceFile = getPathFromAmdModule(require, './fixtures/index.html'); - const newDir = path.join(parentDir, 'pfs', id); - const testFile = path.join(newDir, 'flushed.txt'); - - await pfs.mkdirp(newDir, 493); - - assert.ok(fs.existsSync(newDir)); - - fs.mkdirSync(testFile); // this will trigger an error because testFile is now a directory! - - let expectedError: Error | undefined; - try { - await pfs.writeFile(testFile, fs.createReadStream(sourceFile)); - } catch (error) { - expectedError = error; - } - - if (!expectedError) { - throw new Error('Expected error for writing to folder'); - } - - await pfs.rimraf(parentDir); - }); - test('writeFileSync', async () => { const id = uuid.generateUuid(); const parentDir = path.join(os.tmpdir(), 'vsctests', id); diff --git a/src/vs/code/browser/workbench/workbench-dev.html b/src/vs/code/browser/workbench/workbench-dev.html index af6317d035f8..7355cabbb7cf 100644 --- a/src/vs/code/browser/workbench/workbench-dev.html +++ b/src/vs/code/browser/workbench/workbench-dev.html @@ -31,6 +31,7 @@ 'onigasm-umd': `${window.location.origin}/static/remote/web/node_modules/onigasm-umd/release/main`, 'xterm': `${window.location.origin}/static/remote/web/node_modules/xterm/lib/xterm.js`, 'xterm-addon-search': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, + 'xterm-addon-unicode11': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, 'xterm-addon-web-links': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, 'xterm-addon-webgl': `${window.location.origin}/static/remote/web/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, 'semver-umd': `${window.location.origin}/static/remote/web/node_modules/semver-umd/lib/semver-umd.js`, diff --git a/src/vs/code/browser/workbench/workbench.html b/src/vs/code/browser/workbench/workbench.html index 8a6a9f54e673..db616831643b 100644 --- a/src/vs/code/browser/workbench/workbench.html +++ b/src/vs/code/browser/workbench/workbench.html @@ -35,6 +35,7 @@ 'onigasm-umd': `${window.location.origin}/static/node_modules/onigasm-umd/release/main`, 'xterm': `${window.location.origin}/static/node_modules/xterm/lib/xterm.js`, 'xterm-addon-search': `${window.location.origin}/static/node_modules/xterm-addon-search/lib/xterm-addon-search.js`, + 'xterm-addon-unicode11': `${window.location.origin}/static/node_modules/xterm-addon-unicode11/lib/xterm-addon-unicode11.js`, 'xterm-addon-web-links': `${window.location.origin}/static/node_modules/xterm-addon-web-links/lib/xterm-addon-web-links.js`, 'xterm-addon-webgl': `${window.location.origin}/static/node_modules/xterm-addon-webgl/lib/xterm-addon-webgl.js`, 'semver-umd': `${window.location.origin}/static/node_modules/semver-umd/lib/semver-umd.js`, diff --git a/src/vs/code/electron-browser/issue/issueReporterPage.ts b/src/vs/code/electron-browser/issue/issueReporterPage.ts index d5f704583970..0366e8803f6e 100644 --- a/src/vs/code/electron-browser/issue/issueReporterPage.ts +++ b/src/vs/code/electron-browser/issue/issueReporterPage.ts @@ -35,7 +35,7 @@ export default (): string => ` diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts index 16fa69ac5148..1e702105937b 100644 --- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts @@ -26,7 +26,6 @@ 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 { ActiveWindowManager } from 'vs/code/node/activeWindowTracker'; import { ipcRenderer } from 'electron'; import { ILogService, LogLevel, ILoggerService } from 'vs/platform/log/common/log'; import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc'; @@ -59,7 +58,7 @@ import { LoggerService } from 'vs/platform/log/node/loggerService'; import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog'; import { ICredentialsService } from 'vs/platform/credentials/common/credentials'; import { KeytarCredentialsService } from 'vs/platform/credentials/node/credentialsService'; -import { UserDataAutoSync } from 'vs/platform/userDataSync/electron-browser/userDataAutoSync'; +import { UserDataAutoSyncService } from 'vs/platform/userDataSync/electron-browser/userDataAutoSyncService'; import { SettingsSynchroniser } from 'vs/platform/userDataSync/common/settingsSync'; import { UserDataAuthTokenService } from 'vs/platform/userDataSync/common/userDataAuthTokenService'; @@ -131,9 +130,6 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const electronService = createChannelSender(mainProcessService.getChannel('electron'), { context: configuration.windowId }); services.set(IElectronService, electronService); - const activeWindowManager = new ActiveWindowManager(electronService); - const activeWindowRouter = new StaticRouter(ctx => activeWindowManager.getActiveClientId().then(id => ctx === id)); - // Files const fileService = new FileService(logService); services.set(IFileService, fileService); @@ -184,8 +180,8 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat services.set(ICredentialsService, new SyncDescriptor(KeytarCredentialsService)); services.set(IUserDataAuthTokenService, new SyncDescriptor(UserDataAuthTokenService)); services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService)); - services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', activeWindowRouter))); - services.set(IGlobalExtensionEnablementService, new GlobalExtensionEnablementServiceClient(server.getChannel('globalExtensionEnablement', activeWindowRouter))); + services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main'))); + services.set(IGlobalExtensionEnablementService, new GlobalExtensionEnablementServiceClient(server.getChannel('globalExtensionEnablement', client => client.ctx !== 'main'))); services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService)); services.set(ISettingsSyncService, new SyncDescriptor(SettingsSynchroniser)); services.set(IUserDataSyncService, new SyncDescriptor(UserDataSyncService)); @@ -219,7 +215,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat const userDataSyncChannel = new UserDataSyncChannel(userDataSyncService); server.registerChannel('userDataSync', userDataSyncChannel); - const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSync); + const userDataAutoSync = instantiationService2.createInstance(UserDataAutoSyncService); const userDataAutoSyncChannel = new UserDataAutoSyncChannel(userDataAutoSync); server.registerChannel('userDataAutoSync', userDataAutoSyncChannel); diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index c715f4d51736..e149132b4f22 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -171,7 +171,7 @@ export class CodeApplication extends Disposable { app.on('web-contents-created', (_event: Event, contents) => { contents.on('will-attach-webview', (event: Event, webPreferences, params) => { - const isValidWebviewSource = (source: string): boolean => { + const isValidWebviewSource = (source: string | undefined): boolean => { if (!source) { return false; } @@ -191,11 +191,12 @@ export class CodeApplication extends Disposable { webPreferences.nodeIntegration = false; // Verify URLs being loaded - if (isValidWebviewSource(params.src) && isValidWebviewSource(webPreferences.preloadURL)) { + // https://github.com/electron/electron/issues/21553 + if (isValidWebviewSource(params.src) && isValidWebviewSource((webPreferences as { preloadURL: string }).preloadURL)) { return; } - delete webPreferences.preloadUrl; + delete (webPreferences as { preloadURL: string }).preloadURL; // https://github.com/electron/electron/issues/21553 // Otherwise prevent loading this.logService.error('webContents#web-contents-created: Prevented webview attach'); @@ -497,27 +498,27 @@ export class CodeApplication extends Disposable { this.logService.info(`Tracing: waiting for windows to get ready...`); let recordingStopped = false; - const stopRecording = (timeout: boolean) => { + const stopRecording = async (timeout: boolean) => { if (recordingStopped) { return; } recordingStopped = true; // only once - contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`), path => { - if (!timeout) { - if (this.dialogMainService) { - this.dialogMainService.showMessageBox({ - type: 'info', - message: localize('trace.message', "Successfully created trace."), - detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), - buttons: [localize('trace.ok', "Ok")] - }, withNullAsUndefined(BrowserWindow.getFocusedWindow())); - } - } else { - this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`); + const path = await contentTracing.stopRecording(join(homedir(), `${product.applicationName}-${Math.random().toString(16).slice(-4)}.trace.txt`)); + + if (!timeout) { + if (this.dialogMainService) { + this.dialogMainService.showMessageBox({ + type: 'info', + message: localize('trace.message', "Successfully created trace."), + detail: localize('trace.detail', "Please create an issue and manually attach the following file:\n{0}", path), + buttons: [localize('trace.ok', "Ok")] + }, withNullAsUndefined(BrowserWindow.getFocusedWindow())); } - }); + } else { + this.logService.info(`Tracing: data recorded (after 30s timeout) to ${path}`); + } }; // Wait up to 30s before creating the trace anyways @@ -594,8 +595,6 @@ export class CodeApplication extends Disposable { // Catch file URLs if (uri.authority === Schemas.file && !!uri.path) { const cli = assign(Object.create(null), environmentService.args); - - // hey Ben, we need to convert this `code://file` URI into a `file://` URI const urisToOpen = [{ fileUri: URI.file(uri.fsPath) }]; windowsMainService.open({ context: OpenContext.API, cli, urisToOpen, gotoLineMode: true }); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index b8929ca79252..be27392a3f9c 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -8,7 +8,7 @@ import * as objects from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; -import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment } from 'electron'; +import { screen, BrowserWindow, systemPreferences, app, TouchBar, nativeImage, Rectangle, Display, TouchBarSegmentedControl, NativeImage, BrowserWindowConstructorOptions, SegmentedControlSegment, nativeTheme } from 'electron'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { ILogService } from 'vs/platform/log/common/log'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -347,9 +347,9 @@ export class CodeWindow extends Disposable implements ICodeWindow { }); this._win.webContents.session.webRequest.onHeadersReceived(null!, (details, callback) => { - const responseHeaders = details.responseHeaders as { [key: string]: string[] }; + const responseHeaders = details.responseHeaders as Record; - const contentType: string[] = (responseHeaders['content-type'] || responseHeaders['Content-Type']); + const contentType = (responseHeaders['content-type'] || responseHeaders['Content-Type']); if (contentType && Array.isArray(contentType) && contentType.some(x => x.toLowerCase().indexOf('image/svg') >= 0)) { return callback({ cancel: true }); } @@ -441,7 +441,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { // Inject headers when requests are incoming const urls = ['https://marketplace.visualstudio.com/*', 'https://*.vsassets.io/*']; this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, (details, cb) => - this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) as { [key: string]: string | undefined } }))); + this.marketplaceHeadersPromise.then(headers => cb({ cancel: false, requestHeaders: objects.assign(details.requestHeaders, headers) as Record }))); } private onWindowError(error: WindowError): void { @@ -648,7 +648,7 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (windowConfig?.autoDetectHighContrast === false) { autoDetectHighContrast = false; } - windowConfiguration.highContrast = isWindows && autoDetectHighContrast && systemPreferences.isInvertedColorScheme(); + windowConfiguration.highContrast = isWindows && autoDetectHighContrast && nativeTheme.shouldUseInvertedColorScheme; windowConfiguration.accessibilitySupport = app.accessibilitySupportEnabled; // Title style related @@ -1007,22 +1007,22 @@ export class CodeWindow extends Disposable implements ICodeWindow { switch (visibility) { case ('default'): this._win.setMenuBarVisibility(!isFullscreen); - this._win.setAutoHideMenuBar(isFullscreen); + this._win.autoHideMenuBar = isFullscreen; break; case ('visible'): this._win.setMenuBarVisibility(true); - this._win.setAutoHideMenuBar(false); + this._win.autoHideMenuBar = false; break; case ('toggle'): this._win.setMenuBarVisibility(false); - this._win.setAutoHideMenuBar(true); + this._win.autoHideMenuBar = true; break; case ('hidden'): this._win.setMenuBarVisibility(false); - this._win.setAutoHideMenuBar(false); + this._win.autoHideMenuBar = false; break; } } diff --git a/src/vs/editor/browser/config/elementSizeObserver.ts b/src/vs/editor/browser/config/elementSizeObserver.ts index f26929fe218a..d285162fd4c8 100644 --- a/src/vs/editor/browser/config/elementSizeObserver.ts +++ b/src/vs/editor/browser/config/elementSizeObserver.ts @@ -3,24 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IDimension } from 'vs/editor/common/editorCommon'; +import * as dom from 'vs/base/browser/dom'; export class ElementSizeObserver extends Disposable { private readonly referenceDomElement: HTMLElement | null; - private measureReferenceDomElementToken: any; private readonly changeCallback: () => void; private width: number; private height: number; + private mutationObserver: MutationObserver | null; + private windowSizeListener: IDisposable | null; constructor(referenceDomElement: HTMLElement | null, dimension: IDimension | undefined, changeCallback: () => void) { super(); this.referenceDomElement = referenceDomElement; this.changeCallback = changeCallback; - this.measureReferenceDomElementToken = -1; this.width = -1; this.height = -1; + this.mutationObserver = null; + this.windowSizeListener = null; this.measureReferenceDomElement(false, dimension); } @@ -38,15 +41,25 @@ export class ElementSizeObserver extends Disposable { } public startObserving(): void { - if (this.measureReferenceDomElementToken === -1) { - this.measureReferenceDomElementToken = setInterval(() => this.measureReferenceDomElement(true), 100); + if (!this.mutationObserver && this.referenceDomElement) { + this.mutationObserver = new MutationObserver(() => this._onDidMutate()); + this.mutationObserver.observe(this.referenceDomElement, { + attributes: true, + }); + } + if (!this.windowSizeListener) { + this.windowSizeListener = dom.addDisposableListener(window, 'resize', () => this._onDidResizeWindow()); } } public stopObserving(): void { - if (this.measureReferenceDomElementToken !== -1) { - clearInterval(this.measureReferenceDomElementToken); - this.measureReferenceDomElementToken = -1; + if (this.mutationObserver) { + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + if (this.windowSizeListener) { + this.windowSizeListener.dispose(); + this.windowSizeListener = null; } } @@ -54,6 +67,14 @@ export class ElementSizeObserver extends Disposable { this.measureReferenceDomElement(true, dimension); } + private _onDidMutate(): void { + this.measureReferenceDomElement(true); + } + + private _onDidResizeWindow(): void { + this.measureReferenceDomElement(true); + } + private measureReferenceDomElement(callChangeCallback: boolean, dimension?: IDimension): void { let observedWidth = 0; let observedHeight = 0; diff --git a/src/vs/editor/browser/controller/coreCommands.ts b/src/vs/editor/browser/controller/coreCommands.ts index 41d5b451ae91..6eddab877923 100644 --- a/src/vs/editor/browser/controller/coreCommands.ts +++ b/src/vs/editor/browser/controller/coreCommands.ts @@ -1753,12 +1753,17 @@ registerCommand(new EditorOrNativeTextInputCommand({ kbExpr: null, primary: KeyMod.CtrlCmd | KeyCode.KEY_A }, - menuOpts: { + menuOpts: [{ menuId: MenuId.MenubarEditMenu, // {{SQL CARBON EDIT}} - Put this in the edit menu since we disabled the selection menu group: '4_find_global', // {{SQL CARBON EDIT}} - Put this in the edit menu since we disabled the selection menu title: nls.localize({ key: 'miSelectAll', comment: ['&& denotes a mnemonic'] }, "&&Select All"), order: 1 - } + }, { + menuId: MenuId.CommandPalette, + group: '', + title: nls.localize('selectAll', "Select All"), + order: 1 + }] })); registerCommand(new EditorOrNativeTextInputCommand({ @@ -1771,12 +1776,17 @@ registerCommand(new EditorOrNativeTextInputCommand({ kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.KEY_Z }, - menuOpts: { + menuOpts: [{ menuId: MenuId.MenubarEditMenu, group: '1_do', title: nls.localize({ key: 'miUndo', comment: ['&& denotes a mnemonic'] }, "&&Undo"), order: 1 - } + }, { + menuId: MenuId.CommandPalette, + group: '', + title: nls.localize('undo', "Undo"), + order: 1 + }] })); registerCommand(new EditorHandlerCommand('default:' + Handler.Undo, Handler.Undo)); @@ -1792,12 +1802,17 @@ registerCommand(new EditorOrNativeTextInputCommand({ secondary: [KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z], mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_Z } }, - menuOpts: { + menuOpts: [{ menuId: MenuId.MenubarEditMenu, group: '1_do', title: nls.localize({ key: 'miRedo', comment: ['&& denotes a mnemonic'] }, "&&Redo"), order: 2 - } + }, { + menuId: MenuId.CommandPalette, + group: '', + title: nls.localize('redo', "Redo"), + order: 1 + }] })); registerCommand(new EditorHandlerCommand('default:' + Handler.Redo, Handler.Redo)); diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index 94cacfb550cd..c17496f94064 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -357,8 +357,9 @@ export class TextAreaHandler extends ViewPart { this._accessibilitySupport = options.get(EditorOption.accessibilitySupport); const accessibilityPageSize = options.get(EditorOption.accessibilityPageSize); if (this._accessibilitySupport === AccessibilitySupport.Enabled && accessibilityPageSize === EditorOptions.accessibilityPageSize.defaultValue) { - // If a screen reader is attached and the default value is not set we shuold automatically increase the page size to 1000 for a better experience - this._accessibilityPageSize = 1000; + // If a screen reader is attached and the default value is not set we shuold automatically increase the page size to 160 for a better experience + // If we put more than 160 lines the nvda can not handle this https://github.com/microsoft/vscode/issues/89717 + this._accessibilityPageSize = 160; } else { this._accessibilityPageSize = accessibilityPageSize; } diff --git a/src/vs/editor/browser/controller/textAreaInput.ts b/src/vs/editor/browser/controller/textAreaInput.ts index e3f98ab2e085..00d6b566eea2 100644 --- a/src/vs/editor/browser/controller/textAreaInput.ts +++ b/src/vs/editor/browser/controller/textAreaInput.ts @@ -331,7 +331,7 @@ export class TextAreaInput extends Disposable { this._onType.fire(typeInput); } } else { - if (typeInput.text !== '') { + if (typeInput.text !== '' || typeInput.replaceCharCnt !== 0) { this._firePaste(typeInput.text, null); } this._nextCommand = ReadFromTextArea.Type; @@ -483,6 +483,9 @@ export class TextAreaInput extends Disposable { // Setting this._hasFocus and writing the screen reader content // will result in a focus() and setSelectionRange() in the textarea this._setHasFocus(true); + + // If the editor is off DOM, focus cannot be really set, so let's double check that we have managed to set the focus + this.refreshFocusState(); } public isFocused(): boolean { @@ -490,8 +493,11 @@ export class TextAreaInput extends Disposable { } public refreshFocusState(): void { - if (document.body.contains(this.textArea.domNode) && document.activeElement === this.textArea.domNode) { - this._setHasFocus(true); + const shadowRoot = dom.getShadowRoot(this.textArea.domNode); + if (shadowRoot) { + this._setHasFocus(shadowRoot.activeElement === this.textArea.domNode); + } else if (dom.isInDOM(this.textArea.domNode)) { + this._setHasFocus(document.activeElement === this.textArea.domNode); } else { this._setHasFocus(false); } @@ -696,7 +702,15 @@ class TextAreaWrapper extends Disposable implements ITextAreaWrapper { public setSelectionRange(reason: string, selectionStart: number, selectionEnd: number): void { const textArea = this._actual.domNode; - const currentIsFocused = (document.activeElement === textArea); + let activeElement: Element | null = null; + const shadowRoot = dom.getShadowRoot(textArea); + if (shadowRoot) { + activeElement = shadowRoot.activeElement; + } else { + activeElement = document.activeElement; + } + + const currentIsFocused = (activeElement === textArea); const currentSelectionStart = textArea.selectionStart; const currentSelectionEnd = textArea.selectionEnd; diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts index a0edab2a19dd..e7dc08a8f14a 100644 --- a/src/vs/editor/browser/services/bulkEditService.ts +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -27,6 +27,8 @@ export type IBulkEditPreviewHandler = (edit: WorkspaceEdit, options?: IBulkEditO export interface IBulkEditService { _serviceBrand: undefined; + hasPreviewHandler(): boolean; + setPreviewHandler(handler: IBulkEditPreviewHandler): IDisposable; apply(edit: WorkspaceEdit, options?: IBulkEditOptions): Promise; diff --git a/src/vs/editor/browser/view/domLineBreaksComputer.ts b/src/vs/editor/browser/view/domLineBreaksComputer.ts index d0d182a269df..2fb294eb0661 100644 --- a/src/vs/editor/browser/view/domLineBreaksComputer.ts +++ b/src/vs/editor/browser/view/domLineBreaksComputer.ts @@ -111,6 +111,7 @@ function createLineBreaks(requests: string[], fontInfo: FontInfo, tabSize: numbe containerDomNode.style.position = 'absolute'; containerDomNode.style.top = '10000'; + containerDomNode.style.wordWrap = 'break-word'; document.body.appendChild(containerDomNode); let range = document.createRange(); diff --git a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts index ae516cf5685b..c76e4fee40e3 100644 --- a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts +++ b/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts @@ -316,8 +316,9 @@ class Widget { } private _layoutHorizontalSegmentInPage(windowSize: dom.Dimension, domNodePosition: dom.IDomNodePagePosition, left: number, width: number): [number, number] { - const MIN_LIMIT = (width <= domNodePosition.width - 20 ? domNodePosition.left : 0); - const MAX_LIMIT = (width <= domNodePosition.width - 20 ? domNodePosition.left + domNodePosition.width - 20 : windowSize.width - 20); + // Initially, the limits are defined as the dom node limits + const MIN_LIMIT = Math.max(0, domNodePosition.left - width); + const MAX_LIMIT = Math.min(domNodePosition.left + domNodePosition.width + width, windowSize.width); let absoluteLeft = domNodePosition.left + left - dom.StandardWindow.scrollX; diff --git a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts index 9480b7357892..356d93a11aa8 100644 --- a/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts +++ b/src/vs/editor/browser/viewParts/editorScrollbar/editorScrollbar.ts @@ -34,6 +34,7 @@ export class EditorScrollbar extends ViewPart { const scrollbar = options.get(EditorOption.scrollbar); const mouseWheelScrollSensitivity = options.get(EditorOption.mouseWheelScrollSensitivity); const fastScrollSensitivity = options.get(EditorOption.fastScrollSensitivity); + const scrollPredominantAxis = options.get(EditorOption.scrollPredominantAxis); const scrollbarOptions: ScrollableElementCreationOptions = { listenOnDomNode: viewDomNode.domNode, @@ -54,6 +55,7 @@ export class EditorScrollbar extends ViewPart { arrowSize: scrollbar.arrowSize, mouseWheelScrollSensitivity: mouseWheelScrollSensitivity, fastScrollSensitivity: fastScrollSensitivity, + scrollPredominantAxis: scrollPredominantAxis, }; this.scrollbar = this._register(new SmoothScrollableElement(linesContent.domNode, scrollbarOptions, this._context.viewLayout.getScrollable())); @@ -140,10 +142,12 @@ export class EditorScrollbar extends ViewPart { const scrollbar = options.get(EditorOption.scrollbar); const mouseWheelScrollSensitivity = options.get(EditorOption.mouseWheelScrollSensitivity); const fastScrollSensitivity = options.get(EditorOption.fastScrollSensitivity); + const scrollPredominantAxis = options.get(EditorOption.scrollPredominantAxis); const newOpts: ScrollableElementChangeOptions = { handleMouseWheel: scrollbar.handleMouseWheel, mouseWheelScrollSensitivity: mouseWheelScrollSensitivity, - fastScrollSensitivity: fastScrollSensitivity + fastScrollSensitivity: fastScrollSensitivity, + scrollPredominantAxis: scrollPredominantAxis }; this.scrollbar.updateOptions(newOpts); } diff --git a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css index 680cccb1faf5..54091d864586 100644 --- a/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css +++ b/src/vs/editor/browser/viewParts/lineNumbers/lineNumbers.css @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ .monaco-editor .margin-view-overlays .line-numbers { + font-variant-numeric: tabular-nums; position: absolute; text-align: right; display: inline-block; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.css b/src/vs/editor/browser/viewParts/lines/viewLines.css index 46c57aa0fa2b..94577d765194 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.css +++ b/src/vs/editor/browser/viewParts/lines/viewLines.css @@ -37,6 +37,10 @@ width: 100%; } +.monaco-editor .mtkz { + display: inline-block; +} + /* TODO@tokenization bootstrap fix */ /*.monaco-editor .view-line > span > span { float: none; diff --git a/src/vs/editor/browser/viewParts/lines/viewLines.ts b/src/vs/editor/browser/viewParts/lines/viewLines.ts index 664e0d257537..e98e03ca61c1 100644 --- a/src/vs/editor/browser/viewParts/lines/viewLines.ts +++ b/src/vs/editor/browser/viewParts/lines/viewLines.ts @@ -589,6 +589,14 @@ export class ViewLines extends ViewPart implements IVisibleLinesHost, if (boxEndY - boxStartY > viewportHeight) { // the box is larger than the viewport ... scroll to its top newScrollTop = boxStartY; + } else if (verticalType === viewEvents.VerticalRevealType.NearTop) { + // We want a gap that is 20% of the viewport, but with a minimum of 5 lines + const desiredGapAbove = Math.max(5 * this._lineHeight, viewportHeight * 0.2); + // Try to scroll just above the box with the desired gap + const desiredScrollTop = boxStartY - desiredGapAbove; + // But ensure that the box is not pushed out of viewport + const minScrollTop = boxEndY - viewportHeight; + newScrollTop = Math.max(minScrollTop, desiredScrollTop); } else if (verticalType === viewEvents.VerticalRevealType.Center || verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport) { if (verticalType === viewEvents.VerticalRevealType.CenterIfOutsideViewport && viewportStartY <= boxStartY && boxEndY <= viewportEndY) { // Box is already in the viewport... do nothing diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index fbaf98ef90bf..bf3cea82f943 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -25,8 +25,8 @@ import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/v import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewLineData } from 'vs/editor/common/viewModel/viewModel'; -import { scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, minimapSelection } from 'vs/platform/theme/common/colorRegistry'; -import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { minimapSelection, scrollbarShadow, scrollbarSliderActiveBackground, scrollbarSliderBackground, scrollbarSliderHoverBackground, minimapBackground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant, ITheme } from 'vs/platform/theme/common/themeService'; import { ModelDecorationMinimapOptions } from 'vs/editor/common/model/textModel'; import { Selection } from 'vs/editor/common/core/selection'; import { Color } from 'vs/base/common/color'; @@ -107,7 +107,9 @@ class MinimapOptions { */ public readonly canvasOuterHeight: number; - constructor(configuration: IConfiguration) { + public readonly backgroundColor: RGBA8; + + constructor(configuration: IConfiguration, theme: ITheme, tokensColorTracker: MinimapTokensColorTracker) { const options = configuration.options; const pixelRatio = options.get(EditorOption.pixelRatio); const layoutInfo = options.get(EditorOption.layoutInfo); @@ -131,6 +133,16 @@ class MinimapOptions { this.canvasOuterWidth = this.canvasInnerWidth / pixelRatio; this.canvasOuterHeight = this.canvasInnerHeight / pixelRatio; + + this.backgroundColor = MinimapOptions._getMinimapBackground(theme, tokensColorTracker); + } + + private static _getMinimapBackground(theme: ITheme, tokensColorTracker: MinimapTokensColorTracker): RGBA8 { + const themeColor = theme.getColor(minimapBackground); + if (themeColor) { + return new RGBA8(themeColor.rgba.r, themeColor.rgba.g, themeColor.rgba.b, themeColor.rgba.a); + } + return tokensColorTracker.getColor(ColorId.DefaultBackground); } public equals(other: MinimapOptions): boolean { @@ -148,6 +160,7 @@ class MinimapOptions { && this.canvasInnerHeight === other.canvasInnerHeight && this.canvasOuterWidth === other.canvasOuterWidth && this.canvasOuterHeight === other.canvasOuterHeight + && this.backgroundColor.equals(other.backgroundColor) ); } } @@ -447,13 +460,13 @@ class MinimapBuffers { export class Minimap extends ViewPart { + private readonly _tokensColorTracker: MinimapTokensColorTracker; private readonly _domNode: FastDomNode; private readonly _shadow: FastDomNode; private readonly _canvas: FastDomNode; private readonly _decorationsCanvas: FastDomNode; private readonly _slider: FastDomNode; private readonly _sliderHorizontal: FastDomNode; - private readonly _tokensColorTracker: MinimapTokensColorTracker; private readonly _mouseDownListener: IDisposable; private readonly _sliderMouseMoveMonitor: GlobalMouseMoveMonitor; private readonly _sliderMouseDownListener: IDisposable; @@ -473,7 +486,8 @@ export class Minimap extends ViewPart { constructor(context: ViewContext) { super(context); - this._options = new MinimapOptions(this._context.configuration); + this._tokensColorTracker = MinimapTokensColorTracker.getInstance(); + this._options = new MinimapOptions(this._context.configuration, this._context.theme, this._tokensColorTracker); this._lastRenderData = null; this._buffers = null; this._selectionColor = this._context.theme.getColor(minimapSelection); @@ -512,8 +526,6 @@ export class Minimap extends ViewPart { this._sliderHorizontal.setClassName('minimap-slider-horizontal'); this._slider.appendChild(this._sliderHorizontal); - this._tokensColorTracker = MinimapTokensColorTracker.getInstance(); - this._applyLayout(); this._mouseDownListener = dom.addStandardDisposableListener(this._domNode.domNode, 'mousedown', (e) => { @@ -664,7 +676,7 @@ export class Minimap extends ViewPart { this._canvas.domNode.getContext('2d')!, this._options.canvasInnerWidth, this._options.canvasInnerHeight, - this._tokensColorTracker.getColor(ColorId.DefaultBackground) + this._options.backgroundColor ); } } @@ -672,7 +684,7 @@ export class Minimap extends ViewPart { } private _onOptionsMaybeChanged(): boolean { - const opts = new MinimapOptions(this._context.configuration); + const opts = new MinimapOptions(this._context.configuration, this._context.theme, this._tokensColorTracker); if (this._options.equals(opts)) { return false; } @@ -745,6 +757,7 @@ export class Minimap extends ViewPart { this._context.model.invalidateMinimapColorCache(); this._selectionColor = this._context.theme.getColor(minimapSelection); this._renderDecorations = true; + this._onOptionsMaybeChanged(); return true; } @@ -856,7 +869,7 @@ export class Minimap extends ViewPart { const y = (lineNumber - layout.startLineNumber) * lineHeight; // Skip rendering the line if it's vertically outside our viewport - if (y + height < 0 || y > this._options.canvasOuterHeight) { + if (y + height < 0 || y > this._options.canvasInnerHeight) { return; } @@ -942,7 +955,7 @@ export class Minimap extends ViewPart { // Fetch rendering info from view model for rest of lines that need rendering. const lineInfo = this._context.model.getMinimapLinesRenderingData(startLineNumber, endLineNumber, needed); const tabSize = lineInfo.tabSize; - const background = this._tokensColorTracker.getColor(ColorId.DefaultBackground); + const background = this._options.backgroundColor; const useLighterFont = this._tokensColorTracker.backgroundIsLight(); // Render the rest of lines @@ -1141,6 +1154,10 @@ export class Minimap extends ViewPart { } registerThemingParticipant((theme, collector) => { + const minimapBackgroundValue = theme.getColor(minimapBackground); + if (minimapBackgroundValue) { + collector.addRule(`.monaco-editor .minimap > canvas { opacity: ${minimapBackgroundValue.rgba.a}; will-change: opacity; }`); + } const sliderBackground = theme.getColor(scrollbarSliderBackground); if (sliderBackground) { const halfSliderBackground = sliderBackground.transparent(0.5); diff --git a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts index f4f5144e27a2..9b44e5a7f058 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimapCharRenderer.ts @@ -5,6 +5,7 @@ import { RGBA8 } from 'vs/editor/common/core/rgba'; import { Constants, getCharIndex } from './minimapCharSheet'; +import { toUint8 } from 'vs/base/common/uint'; export class MinimapCharRenderer { _minimapCharRendererBrand: void; @@ -20,7 +21,7 @@ export class MinimapCharRenderer { private static soften(input: Uint8ClampedArray, ratio: number): Uint8ClampedArray { let result = new Uint8ClampedArray(input.length); for (let i = 0, len = input.length; i < len; i++) { - result[i] = input[i] * ratio; + result[i] = toUint8(input[i] * ratio); } return result; } diff --git a/src/vs/editor/browser/viewParts/minimap/minimapCharRendererFactory.ts b/src/vs/editor/browser/viewParts/minimap/minimapCharRendererFactory.ts index 300d51789f4c..09aa705489eb 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimapCharRendererFactory.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimapCharRendererFactory.ts @@ -7,6 +7,7 @@ import { MinimapCharRenderer } from 'vs/editor/browser/viewParts/minimap/minimap import { allCharCodes } from 'vs/editor/browser/viewParts/minimap/minimapCharSheet'; import { prebakedMiniMaps } from 'vs/editor/browser/viewParts/minimap/minimapPreBaked'; import { Constants } from './minimapCharSheet'; +import { toUint8 } from 'vs/base/common/uint'; /** * Creates character renderers. It takes a 'scale' that determines how large @@ -135,7 +136,7 @@ export class MinimapCharRendererFactory { const final = value / samples; brightest = Math.max(brightest, final); - dest[targetIndex++] = final; + dest[targetIndex++] = toUint8(final); } } diff --git a/src/vs/editor/browser/viewParts/rulers/rulers.ts b/src/vs/editor/browser/viewParts/rulers/rulers.ts index 08fe9ec92cc6..faae9d3ad3f1 100644 --- a/src/vs/editor/browser/viewParts/rulers/rulers.ts +++ b/src/vs/editor/browser/viewParts/rulers/rulers.ts @@ -11,13 +11,13 @@ import { RenderingContext, RestrictedRenderingContext } from 'vs/editor/common/v import { ViewContext } from 'vs/editor/common/view/viewContext'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; -import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { EditorOption, IRulerOption } from 'vs/editor/common/config/editorOptions'; export class Rulers extends ViewPart { public domNode: FastDomNode; private readonly _renderedRulers: FastDomNode[]; - private _rulers: number[]; + private _rulers: IRulerOption[]; private _typicalHalfwidthCharacterWidth: number; constructor(context: ViewContext) { @@ -92,9 +92,11 @@ export class Rulers extends ViewPart { for (let i = 0, len = this._rulers.length; i < len; i++) { const node = this._renderedRulers[i]; + const ruler = this._rulers[i]; + node.setBoxShadow(ruler.color ? `1px 0 0 0 ${ruler.color} inset` : ``); node.setHeight(Math.min(ctx.scrollHeight, 1000000)); - node.setLeft(this._rulers[i] * this._typicalHalfwidthCharacterWidth); + node.setLeft(ruler.column * this._typicalHalfwidthCharacterWidth); } } } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index ff0586eb8e57..b816a125acf9 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -557,6 +557,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE this._revealLine(lineNumber, VerticalRevealType.CenterIfOutsideViewport, scrollType); } + public revealLineNearTop(lineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this._revealLine(lineNumber, VerticalRevealType.NearTop, scrollType); + } + private _revealLine(lineNumber: number, revealType: VerticalRevealType, scrollType: editorCommon.ScrollType): void { if (typeof lineNumber !== 'number') { throw new Error('Invalid arguments'); @@ -597,6 +601,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ); } + public revealPositionNearTop(position: IPosition, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this._revealPosition( + position, + VerticalRevealType.NearTop, + true, + scrollType + ); + } + private _revealPosition(position: IPosition, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { if (!Position.isIPosition(position)) { throw new Error('Invalid arguments'); @@ -685,6 +698,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ); } + public revealLinesNearTop(startLineNumber: number, endLineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this._revealLines( + startLineNumber, + endLineNumber, + VerticalRevealType.NearTop, + scrollType + ); + } + private _revealLines(startLineNumber: number, endLineNumber: number, verticalType: VerticalRevealType, scrollType: editorCommon.ScrollType): void { if (typeof startLineNumber !== 'number' || typeof endLineNumber !== 'number') { throw new Error('Invalid arguments'); @@ -725,6 +747,15 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE ); } + public revealRangeNearTop(range: IRange, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this._revealRange( + range, + VerticalRevealType.NearTop, + true, + scrollType + ); + } + public revealRangeAtTop(range: IRange, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { this._revealRange( range, @@ -1613,6 +1644,7 @@ export class BooleanEventEmitter extends Disposable { class EditorContextKeysManager extends Disposable { private readonly _editor: CodeEditorWidget; + private readonly _editorSimpleInput: IContextKey; private readonly _editorFocus: IContextKey; private readonly _textInputFocus: IContextKey; private readonly _editorTextFocus: IContextKey; @@ -1632,6 +1664,8 @@ class EditorContextKeysManager extends Disposable { this._editor = editor; contextKeyService.createKey('editorId', editor.getId()); + + this._editorSimpleInput = EditorContextKeys.editorSimpleInput.bindTo(contextKeyService); this._editorFocus = EditorContextKeys.focus.bindTo(contextKeyService); this._textInputFocus = EditorContextKeys.textInputFocus.bindTo(contextKeyService); this._editorTextFocus = EditorContextKeys.editorTextFocus.bindTo(contextKeyService); @@ -1655,6 +1689,8 @@ class EditorContextKeysManager extends Disposable { this._updateFromSelection(); this._updateFromFocus(); this._updateFromModel(); + + this._editorSimpleInput.set(this._editor.isSimpleWidget); } private _updateFromConfig(): void { diff --git a/src/vs/editor/browser/widget/diffEditorWidget.ts b/src/vs/editor/browser/widget/diffEditorWidget.ts index 42306aa635f7..c2e3889c3d7c 100644 --- a/src/vs/editor/browser/widget/diffEditorWidget.ts +++ b/src/vs/editor/browser/widget/diffEditorWidget.ts @@ -38,7 +38,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { INotificationService } from 'vs/platform/notification/common/notification'; -import { defaultInsertColor, defaultRemoveColor, diffBorder, diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, scrollbarShadow } from 'vs/platform/theme/common/colorRegistry'; +import { defaultInsertColor, defaultRemoveColor, diffBorder, diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, scrollbarShadow, scrollbarSliderBackground, scrollbarSliderHoverBackground, scrollbarSliderActiveBackground } from 'vs/platform/theme/common/colorRegistry'; import { ITheme, IThemeService, getThemeTypeSelector, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { reverseLineChanges } from 'sql/editor/browser/diffEditorHelper'; // {{SQL CARBON EDIT}} import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; @@ -768,6 +768,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this.modifiedEditor.revealLineInCenterIfOutsideViewport(lineNumber, scrollType); } + public revealLineNearTop(lineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this.modifiedEditor.revealLineNearTop(lineNumber, scrollType); + } + public revealPosition(position: IPosition, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { this.modifiedEditor.revealPosition(position, scrollType); } @@ -780,6 +784,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this.modifiedEditor.revealPositionInCenterIfOutsideViewport(position, scrollType); } + public revealPositionNearTop(position: IPosition, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this.modifiedEditor.revealPositionNearTop(position, scrollType); + } + public getSelection(): Selection | null { return this.modifiedEditor.getSelection(); } @@ -812,6 +820,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this.modifiedEditor.revealLinesInCenterIfOutsideViewport(startLineNumber, endLineNumber, scrollType); } + public revealLinesNearTop(startLineNumber: number, endLineNumber: number, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this.modifiedEditor.revealLinesNearTop(startLineNumber, endLineNumber, scrollType); + } + public revealRange(range: IRange, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth, revealVerticalInCenter: boolean = false, revealHorizontal: boolean = true): void { this.modifiedEditor.revealRange(range, scrollType, revealVerticalInCenter, revealHorizontal); } @@ -824,6 +836,10 @@ export class DiffEditorWidget extends Disposable implements editorBrowser.IDiffE this.modifiedEditor.revealRangeInCenterIfOutsideViewport(range, scrollType); } + public revealRangeNearTop(range: IRange, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { + this.modifiedEditor.revealRangeNearTop(range, scrollType); + } + public revealRangeAtTop(range: IRange, scrollType: editorCommon.ScrollType = editorCommon.ScrollType.Smooth): void { this.modifiedEditor.revealRangeAtTop(range, scrollType); } @@ -2227,4 +2243,32 @@ registerThemingParticipant((theme, collector) => { if (border) { collector.addRule(`.monaco-diff-editor.side-by-side .editor.modified { border-left: 1px solid ${border}; }`); } + + const scrollbarSliderBackgroundColor = theme.getColor(scrollbarSliderBackground); + if (scrollbarSliderBackgroundColor) { + collector.addRule(` + .monaco-diff-editor .diffViewport { + background: ${scrollbarSliderBackgroundColor}; + } + `); + } + + const scrollbarSliderHoverBackgroundColor = theme.getColor(scrollbarSliderHoverBackground); + if (scrollbarSliderHoverBackgroundColor) { + collector.addRule(` + .monaco-diff-editor .diffViewport:hover { + background: ${scrollbarSliderHoverBackgroundColor}; + } + `); + } + + const scrollbarSliderActiveBackgroundColor = theme.getColor(scrollbarSliderActiveBackground); + if (scrollbarSliderActiveBackgroundColor) { + collector.addRule(` + .monaco-diff-editor .diffViewport:active { + background: ${scrollbarSliderActiveBackgroundColor}; + } + `); + } + }); diff --git a/src/vs/editor/browser/widget/media/diffEditor.css b/src/vs/editor/browser/widget/media/diffEditor.css index 7765a27a16cd..c4a81bfd5011 100644 --- a/src/vs/editor/browser/widget/media/diffEditor.css +++ b/src/vs/editor/browser/widget/media/diffEditor.css @@ -8,19 +8,14 @@ z-index: 9; } +.monaco-diff-editor .diffOverview .diffViewport { + z-index: 10; +} + /* colors not externalized: using transparancy on background */ .monaco-diff-editor.vs .diffOverview { background: rgba(0, 0, 0, 0.03); } .monaco-diff-editor.vs-dark .diffOverview { background: rgba(255, 255, 255, 0.01); } -.monaco-diff-editor .diffViewport { - box-shadow: inset 0px 0px 1px 0px #B9B9B9; - background: rgba(0, 0, 0, 0.10); -} - -.monaco-diff-editor.vs-dark .diffViewport, -.monaco-diff-editor.hc-black .diffViewport { - background: rgba(255, 255, 255, 0.10); -} .monaco-scrollable-element.modified-in-monaco-diff-editor.vs .scrollbar { background: rgba(0,0,0,0); } .monaco-scrollable-element.modified-in-monaco-diff-editor.vs-dark .scrollbar { background: rgba(0,0,0,0); } .monaco-scrollable-element.modified-in-monaco-diff-editor.hc-black .scrollbar { background: none; } diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 9512311777f9..c56cacffb40e 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -484,6 +484,11 @@ const editorConfiguration: IConfigurationNode = { default: true, description: nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document.") }, + 'editor.semanticHighlighting.enabled': { + type: 'boolean', + default: true, + description: nls.localize('semanticHighlighting.enabled', "Controls whether the semanticHighlighting is shown for the languages that support it.") + }, 'editor.stablePeek': { type: 'boolean', default: false, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index f4961be23b4e..8d2897733bdf 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -58,7 +58,7 @@ export interface IEditorOptions { * Render vertical lines at the specified columns. * Defaults to empty array. */ - rulers?: number[]; + rulers?: (number | IRulerOption)[]; /** * A string containing the word separators used when doing word navigation. * Defaults to `~!@#$%^&*()-=+[{]}\\|;:\'",.<>/? @@ -74,7 +74,7 @@ export interface IEditorOptions { * If it is a function, it will be invoked when rendering a line number and the return value will be rendered. * Otherwise, if it is a truey, line numbers will be rendered normally (equivalent of using an identity function). * Otherwise, line numbers will not be rendered. - * Defaults to true. + * Defaults to `on`. */ lineNumbers?: LineNumbersType; /** @@ -267,10 +267,10 @@ export interface IEditorOptions { */ wrappingIndent?: 'none' | 'same' | 'indent' | 'deepIndent'; /** - * Controls the wrapping algorithm to use. - * Defaults to 'monospace'. + * Controls the wrapping strategy to use. + * Defaults to 'simple'. */ - wrappingAlgorithm?: 'monospace' | 'dom'; + wrappingStrategy?: 'simple' | 'advanced'; /** * Configure word wrapping characters. A break will be introduced before these characters. * Defaults to '([{‘“〈《「『【〔([{「£¥$£¥++'. @@ -319,6 +319,11 @@ export interface IEditorOptions { * Defaults to 5. */ fastScrollSensitivity?: number; + /** + * Enable that the editor scrolls only the predominant axis. Prevents horizontal drift when scrolling vertically on a trackpad. + * Defaults to true. + */ + scrollPredominantAxis?: boolean; /** * The modifier to be used to add multiple cursors with the mouse. * Defaults to 'alt' @@ -385,8 +390,8 @@ export interface IEditorOptions { */ autoSurround?: EditorAutoSurroundStrategy; /** - * Enable auto indentation adjustment. - * Defaults to false. + * Controls whether the editor should automatically adjust the indentation when users type, paste, move or indent lines. + * Defaults to advanced. */ autoIndent?: 'none' | 'keep' | 'brackets' | 'advanced' | 'full'; /** @@ -555,6 +560,11 @@ export interface IEditorOptions { * Defaults to false. */ peekWidgetDefaultFocus?: 'tree' | 'editor'; + /** + * Controls whether the definition link opens element in the peek widget. + * Defaults to false. + */ + definitionLinkOpensInPeek?: boolean; } export interface IEditorConstructionOptions extends IEditorOptions { @@ -625,9 +635,6 @@ export class ConfigurationChangedEvent { constructor(values: boolean[]) { this._values = values; } - /** - * @internal - */ public hasChanged(id: EditorOption): boolean { return this._values[id]; } @@ -1592,55 +1599,6 @@ class EditorHover extends BaseEditorOption>; - -class EditorSemanticHighlighting extends BaseEditorOption { - - constructor() { - const defaults: EditorSemanticHighlightingOptions = { - enabled: true - }; - super( - EditorOption.semanticHighlighting, 'semanticHighlighting', defaults, - { - 'editor.semanticHighlighting.enabled': { - type: 'boolean', - default: defaults.enabled, - description: nls.localize('semanticHighlighting.enabled', "Controls whether the semanticHighlighting is shown for the languages that support it.") - } - } - ); - } - - public validate(_input: any): EditorSemanticHighlightingOptions { - if (typeof _input !== 'object') { - return this.defaultValue; - } - const input = _input as IEditorSemanticHighlightingOptions; - return { - enabled: EditorBooleanOption.boolean(input.enabled, this.defaultValue.enabled) - }; - } -} - -//#endregion - //#region layoutInfo /** @@ -2362,16 +2320,37 @@ export function filterValidationDecorations(options: IComputedEditorOptions): bo //#region rulers -class EditorRulers extends SimpleEditorOption { +export interface IRulerOption { + readonly column: number; + readonly color: string | null; +} + +class EditorRulers extends BaseEditorOption { constructor() { - const defaults: number[] = []; + const defaults: IRulerOption[] = []; + const columnSchema: IJSONSchema = { type: 'number', description: nls.localize('rulers.size', "Number of monospace characters at which this editor ruler will render.") }; super( EditorOption.rulers, 'rulers', defaults, { type: 'array', items: { - type: 'number' + anyOf: [ + columnSchema, + { + type: [ + 'object' + ], + properties: { + column: columnSchema, + color: { + type: 'string', + description: nls.localize('rulers.color', "Color of this editor ruler."), + format: 'color-hex' + } + } + } + ] }, default: defaults, description: nls.localize('rulers', "Render vertical rulers after a certain number of monospace characters. Use multiple values for multiple rulers. No rulers are drawn if array is empty.") @@ -2379,13 +2358,24 @@ class EditorRulers extends SimpleEditorOption { ); } - public validate(input: any): number[] { + public validate(input: any): IRulerOption[] { if (Array.isArray(input)) { - let rulers: number[] = []; - for (let value of input) { - rulers.push(EditorIntOption.clampedInt(value, 0, 0, 10000)); + let rulers: IRulerOption[] = []; + for (let _element of input) { + if (typeof _element === 'number') { + rulers.push({ + column: EditorIntOption.clampedInt(_element, 0, 0, 10000), + color: null + }); + } else if (typeof _element === 'object') { + const element = _element as IRulerOption; + rulers.push({ + column: EditorIntOption.clampedInt(element.column, 0, 0, 10000), + color: element.color + }); + } } - rulers.sort((a, b) => a - b); + rulers.sort((a, b) => a.column - b.column); return rulers; } return this.defaultValue; @@ -2674,6 +2664,10 @@ export interface ISuggestOptions { * Show snippet-suggestions. */ showSnippets?: boolean; + /** + * Controls the visibility of the status bar at the bottom of the suggest widget. + */ + hideStatusBar?: boolean; } export type InternalSuggestOptions = Readonly>; @@ -2683,7 +2677,7 @@ class EditorSuggest extends BaseEditorOption('editorSimpleInput', false); /** * A context key that is set when the editor's text has focus (cursor is blinking). */ diff --git a/src/vs/editor/common/model.ts b/src/vs/editor/common/model.ts index d4003b047b43..dc970cea9880 100644 --- a/src/vs/editor/common/model.ts +++ b/src/vs/editor/common/model.ts @@ -290,6 +290,7 @@ export const enum EndOfLineSequence { /** * An identifier for a single edit operation. + * @internal */ export interface ISingleEditOperationIdentifier { /** diff --git a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts index 4c1fa536a2e6..9815baa5a395 100644 --- a/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts +++ b/src/vs/editor/common/model/pieceTreeTextBuffer/pieceTreeBase.ts @@ -670,7 +670,7 @@ export class PieceTreeBase { if (searcher._wordSeparators) { searchText = buffer.buffer.substring(start, end); offsetInBuffer = (offset: number) => offset + start; - searcher.reset(-1); + searcher.reset(0); } else { searchText = buffer.buffer; offsetInBuffer = (offset: number) => offset; diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index d65b4a00582c..dd438c146e5b 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -2640,14 +2640,16 @@ export class TextModel extends Disposable implements model.ITextModel { let goDown = true; let indent = 0; + let initialIndent = 0; + for (let distance = 0; goUp || goDown; distance++) { const upLineNumber = lineNumber - distance; const downLineNumber = lineNumber + distance; - if (distance !== 0 && (upLineNumber < 1 || upLineNumber < minLineNumber)) { + if (distance > 1 && (upLineNumber < 1 || upLineNumber < minLineNumber)) { goUp = false; } - if (distance !== 0 && (downLineNumber > lineCount || downLineNumber > maxLineNumber)) { + if (distance > 1 && (downLineNumber > lineCount || downLineNumber > maxLineNumber)) { goDown = false; } if (distance > 50000) { @@ -2656,10 +2658,9 @@ export class TextModel extends Disposable implements model.ITextModel { goDown = false; } + let upLineIndentLevel: number = -1; if (goUp) { // compute indent level going up - let upLineIndentLevel: number; - const currentIndent = this._computeIndentLevel(upLineNumber - 1); if (currentIndent >= 0) { // This line has content (besides whitespace) @@ -2671,30 +2672,11 @@ export class TextModel extends Disposable implements model.ITextModel { up_resolveIndents(upLineNumber); upLineIndentLevel = this._getIndentLevelForWhitespaceLine(offSide, up_aboveContentLineIndent, up_belowContentLineIndent); } - - if (distance === 0) { - // This is the initial line number - startLineNumber = upLineNumber; - endLineNumber = downLineNumber; - indent = upLineIndentLevel; - if (indent === 0) { - // No need to continue - return { startLineNumber, endLineNumber, indent }; - } - continue; - } - - if (upLineIndentLevel >= indent) { - startLineNumber = upLineNumber; - } else { - goUp = false; - } } + let downLineIndentLevel = -1; if (goDown) { // compute indent level going down - let downLineIndentLevel: number; - const currentIndent = this._computeIndentLevel(downLineNumber - 1); if (currentIndent >= 0) { // This line has content (besides whitespace) @@ -2706,7 +2688,50 @@ export class TextModel extends Disposable implements model.ITextModel { down_resolveIndents(downLineNumber); downLineIndentLevel = this._getIndentLevelForWhitespaceLine(offSide, down_aboveContentLineIndent, down_belowContentLineIndent); } + } + + if (distance === 0) { + initialIndent = upLineIndentLevel; + continue; + } + + if (distance === 1) { + if (downLineNumber <= lineCount && downLineIndentLevel >= 0 && initialIndent + 1 === downLineIndentLevel) { + // This is the beginning of a scope, we have special handling here, since we want the + // child scope indent to be active, not the parent scope + goUp = false; + startLineNumber = downLineNumber; + endLineNumber = downLineNumber; + indent = downLineIndentLevel; + continue; + } + if (upLineNumber >= 1 && upLineIndentLevel >= 0 && upLineIndentLevel - 1 === initialIndent) { + // This is the end of a scope, just like above + goDown = false; + startLineNumber = upLineNumber; + endLineNumber = upLineNumber; + indent = upLineIndentLevel; + continue; + } + + startLineNumber = lineNumber; + endLineNumber = lineNumber; + indent = initialIndent; + if (indent === 0) { + // No need to continue + return { startLineNumber, endLineNumber, indent }; + } + } + + if (goUp) { + if (upLineIndentLevel >= indent) { + startLineNumber = upLineNumber; + } else { + goUp = false; + } + } + if (goDown) { if (downLineIndentLevel >= indent) { endLineNumber = downLineNumber; } else { diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index 95a179c73577..dcb0e807ecb5 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -80,6 +80,7 @@ export interface IModelDecorationsChangedEvent { /** * An event describing that some ranges of lines have been tokenized (their tokens have changed). + * @internal */ export interface IModelTokensChangedEvent { readonly tokenizationSupportChanged: boolean; diff --git a/src/vs/editor/common/model/tokensStore.ts b/src/vs/editor/common/model/tokensStore.ts index 5f80d4252b8b..d329f0b0ff98 100644 --- a/src/vs/editor/common/model/tokensStore.ts +++ b/src/vs/editor/common/model/tokensStore.ts @@ -786,6 +786,15 @@ export class TokensStore2 { const bEndCharacter = bTokens.getEndCharacter(bIndex); const bMetadata = bTokens.getMetadata(bIndex); + const bMask = ( + ((bMetadata & MetadataConsts.SEMANTIC_USE_ITALIC) ? MetadataConsts.ITALIC_MASK : 0) + | ((bMetadata & MetadataConsts.SEMANTIC_USE_BOLD) ? MetadataConsts.BOLD_MASK : 0) + | ((bMetadata & MetadataConsts.SEMANTIC_USE_UNDERLINE) ? MetadataConsts.UNDERLINE_MASK : 0) + | ((bMetadata & MetadataConsts.SEMANTIC_USE_FOREGROUND) ? MetadataConsts.FOREGROUND_MASK : 0) + | ((bMetadata & MetadataConsts.SEMANTIC_USE_BACKGROUND) ? MetadataConsts.BACKGROUND_MASK : 0) + ) >>> 0; + const aMask = (~bMask) >>> 0; + // push any token from `a` that is before `b` while (aIndex < aLen && aTokens.getEndOffset(aIndex) <= bStartCharacter) { result[resultLen++] = aTokens.getEndOffset(aIndex); @@ -800,21 +809,24 @@ export class TokensStore2 { } // skip any tokens from `a` that are contained inside `b` - while (aIndex < aLen && aTokens.getEndOffset(aIndex) <= bEndCharacter) { + while (aIndex < aLen && aTokens.getEndOffset(aIndex) < bEndCharacter) { + result[resultLen++] = aTokens.getEndOffset(aIndex); + result[resultLen++] = (aTokens.getMetadata(aIndex) & aMask) | (bMetadata & bMask); aIndex++; } - const aMetadata = aTokens.getMetadata(Math.min(Math.max(0, aIndex - 1), aLen - 1)); - const languageId = TokenMetadata.getLanguageId(aMetadata); - const tokenType = TokenMetadata.getTokenType(aMetadata); - - // push the token from `b` - result[resultLen++] = bEndCharacter; - result[resultLen++] = ( - (bMetadata & MetadataConsts.LANG_TTYPE_CMPL) - | ((languageId << MetadataConsts.LANGUAGEID_OFFSET) >>> 0) - | ((tokenType << MetadataConsts.TOKEN_TYPE_OFFSET) >>> 0) - ); + if (aIndex < aLen && aTokens.getEndOffset(aIndex) === bEndCharacter) { + // `a` ends exactly at the same spot as `b`! + result[resultLen++] = aTokens.getEndOffset(aIndex); + result[resultLen++] = (aTokens.getMetadata(aIndex) & aMask) | (bMetadata & bMask); + aIndex++; + } else { + const aMergeIndex = Math.min(Math.max(0, aIndex - 1), aLen - 1); + + // push the token from `b` + result[resultLen++] = bEndCharacter; + result[resultLen++] = (aTokens.getMetadata(aMergeIndex) & aMask) | (bMetadata & bMask); + } } // push the remaining tokens from `a` diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 56042681e725..3beda583a2fc 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -125,7 +125,15 @@ export const enum MetadataConsts { FOREGROUND_MASK = 0b00000000011111111100000000000000, BACKGROUND_MASK = 0b11111111100000000000000000000000, - LANG_TTYPE_CMPL = 0b11111111111111111111100000000000, + ITALIC_MASK = 0b00000000000000000000100000000000, + BOLD_MASK = 0b00000000000000000001000000000000, + UNDERLINE_MASK = 0b00000000000000000010000000000000, + + SEMANTIC_USE_ITALIC = 0b00000000000000000000000000000001, + SEMANTIC_USE_BOLD = 0b00000000000000000000000000000010, + SEMANTIC_USE_UNDERLINE = 0b00000000000000000000000000000100, + SEMANTIC_USE_FOREGROUND = 0b00000000000000000000000000001000, + SEMANTIC_USE_BACKGROUND = 0b00000000000000000000000000010000, LANGUAGEID_OFFSET = 0, TOKEN_TYPE_OFFSET = 8, @@ -1330,10 +1338,10 @@ export interface RenameProvider { /** * @internal */ -export interface Session { +export interface AuthenticationSession { id: string; - accessToken: string; - displayName: string; + accessToken(): Promise; + accountName: string; } export interface Command { diff --git a/src/vs/editor/common/modes/linkComputer.ts b/src/vs/editor/common/modes/linkComputer.ts index a446446b8d71..b2e6c6b8264b 100644 --- a/src/vs/editor/common/modes/linkComputer.ts +++ b/src/vs/editor/common/modes/linkComputer.ts @@ -223,6 +223,7 @@ export class LinkComputer { let state = State.Start; let hasOpenParens = false; let hasOpenSquareBracket = false; + let inSquareBrackets = false; let hasOpenCurlyBracket = false; while (j < len) { @@ -241,10 +242,12 @@ export class LinkComputer { chClass = (hasOpenParens ? CharacterClass.None : CharacterClass.ForceTermination); break; case CharCode.OpenSquareBracket: + inSquareBrackets = true; hasOpenSquareBracket = true; chClass = CharacterClass.None; break; case CharCode.CloseSquareBracket: + inSquareBrackets = false; chClass = (hasOpenSquareBracket ? CharacterClass.None : CharacterClass.ForceTermination); break; case CharCode.OpenCurlyBrace: @@ -272,6 +275,10 @@ export class LinkComputer { // `|` terminates a link if the link began with `|` chClass = (linkBeginChCode === CharCode.Pipe) ? CharacterClass.ForceTermination : CharacterClass.None; break; + case CharCode.Space: + // ` ` allow space in between [ and ] + chClass = (inSquareBrackets ? CharacterClass.None : CharacterClass.ForceTermination); + break; default: chClass = classifier.get(chCode); } diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index ce61d1d056e5..a50f51209beb 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -45,11 +45,13 @@ function canSyncModel(modelService: IModelService, resource: URI): boolean { } export class EditorWorkerServiceImpl extends Disposable implements IEditorWorkerService { - public _serviceBrand: undefined; + + _serviceBrand: undefined; private readonly _modelService: IModelService; private readonly _workerManager: WorkerManager; private readonly _logService: ILogService; + constructor( @IModelService modelService: IModelService, @ITextResourceConfigurationService configurationService: ITextResourceConfigurationService, @@ -60,7 +62,7 @@ export class EditorWorkerServiceImpl extends Disposable implements IEditorWorker this._workerManager = this._register(new WorkerManager(this._modelService)); this._logService = logService; - // todo@joh make sure this happens only once + // register default link-provider and default completions-provider this._register(modes.LinkProviderRegistry.register('*', { provideLinks: (model, token) => { if (!canSyncModel(this._modelService, model.uri)) { diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index b8243ef9c796..7b460d7737bb 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -8,13 +8,13 @@ import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecyc import * as platform from 'vs/base/common/platform'; import * as errors from 'vs/base/common/errors'; import { URI } from 'vs/base/common/uri'; -import { EDITOR_MODEL_DEFAULTS, IEditorSemanticHighlightingOptions } from 'vs/editor/common/config/editorOptions'; +import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, IIdentifiedSingleEditOperation, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { IModelLanguageChangedEvent, IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata } from 'vs/editor/common/modes'; +import { LanguageIdentifier, DocumentSemanticTokensProviderRegistry, DocumentSemanticTokensProvider, SemanticTokensLegend, SemanticTokens, SemanticTokensEdits, TokenMetadata, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; import { PLAINTEXT_LANGUAGE_IDENTIFIER } from 'vs/editor/common/modes/modesRegistry'; import { ILanguageSelection } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; @@ -26,6 +26,10 @@ import { SparseEncodedTokens, MultilineTokens2 } from 'vs/editor/common/model/to import { IThemeService } from 'vs/platform/theme/common/themeService'; import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +export interface IEditorSemanticHighlightingOptions { + enabled?: boolean; +} + function MODEL_ID(resource: URI): string { return resource.toString(); } @@ -629,7 +633,7 @@ class SemanticColoringProviderStyling { public getMetadata(tokenTypeIndex: number, tokenModifierSet: number): number { const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet); - let metadata: number | undefined; + let metadata: number; if (entry) { metadata = entry.metadata; } else { @@ -643,9 +647,31 @@ class SemanticColoringProviderStyling { modifierSet = modifierSet >> 1; } - metadata = this._themeService.getTheme().getTokenStyleMetadata(tokenType, tokenModifiers); - if (typeof metadata === 'undefined') { + const tokenStyle = this._themeService.getTheme().getTokenStyleMetadata(tokenType, tokenModifiers); + if (typeof tokenStyle === 'undefined') { metadata = Constants.NO_STYLING; + } else { + metadata = 0; + if (typeof tokenStyle.italic !== 'undefined') { + const italicBit = (tokenStyle.italic ? FontStyle.Italic : 0) << MetadataConsts.FONT_STYLE_OFFSET; + metadata |= italicBit | MetadataConsts.SEMANTIC_USE_ITALIC; + } + if (typeof tokenStyle.bold !== 'undefined') { + const boldBit = (tokenStyle.bold ? FontStyle.Bold : 0) << MetadataConsts.FONT_STYLE_OFFSET; + metadata |= boldBit | MetadataConsts.SEMANTIC_USE_BOLD; + } + if (typeof tokenStyle.underline !== 'undefined') { + const underlineBit = (tokenStyle.underline ? FontStyle.Underline : 0) << MetadataConsts.FONT_STYLE_OFFSET; + metadata |= underlineBit | MetadataConsts.SEMANTIC_USE_UNDERLINE; + } + if (tokenStyle.foreground) { + const foregroundBits = (tokenStyle.foreground) << MetadataConsts.FOREGROUND_OFFSET; + metadata |= foregroundBits | MetadataConsts.SEMANTIC_USE_FOREGROUND; + } + if (metadata === 0) { + // Nothing! + metadata = Constants.NO_STYLING; + } } this._hashTable.add(tokenTypeIndex, tokenModifierSet, metadata); } @@ -763,10 +789,21 @@ class ModelSemanticColoring extends Disposable { contentChangeListener.dispose(); this._setSemanticTokens(provider, res || null, styling, pendingChanges); }, (err) => { - errors.onUnexpectedError(err); + if (!err || typeof err.message !== 'string' || err.message.indexOf('busy') === -1) { + errors.onUnexpectedError(err); + } + + // Semantic tokens eats up all errors and considers errors to mean that the result is temporarily not available + // The API does not have a special error kind to express this... this._currentRequestCancellationTokenSource = null; contentChangeListener.dispose(); - this._setSemanticTokens(provider, null, styling, pendingChanges); + + if (pendingChanges.length > 0) { + // More changes occurred while the request was running + if (!this._fetchSemanticTokens.isScheduled()) { + this._fetchSemanticTokens.schedule(); + } + } }); } diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index da44ff3fa527..c876882ebe33 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -231,50 +231,51 @@ export enum EditorOption { overviewRulerLanes = 63, parameterHints = 64, peekWidgetDefaultFocus = 65, - quickSuggestions = 66, - quickSuggestionsDelay = 67, - readOnly = 68, - renderControlCharacters = 69, - renderIndentGuides = 70, - renderFinalNewline = 71, - renderLineHighlight = 72, - renderValidationDecorations = 73, - renderWhitespace = 74, - revealHorizontalRightPadding = 75, - roundedSelection = 76, - rulers = 77, - scrollbar = 78, - scrollBeyondLastColumn = 79, - scrollBeyondLastLine = 80, - selectionClipboard = 81, - selectionHighlight = 82, - selectOnLineNumbers = 83, - semanticHighlighting = 84, - showFoldingControls = 85, - showUnused = 86, - snippetSuggestions = 87, - smoothScrolling = 88, - stopRenderingLineAfter = 89, - suggest = 90, - suggestFontSize = 91, - suggestLineHeight = 92, - suggestOnTriggerCharacters = 93, - suggestSelection = 94, - tabCompletion = 95, - useTabStops = 96, - wordSeparators = 97, - wordWrap = 98, - wordWrapBreakAfterCharacters = 99, - wordWrapBreakBeforeCharacters = 100, - wordWrapColumn = 101, - wordWrapMinified = 102, - wrappingIndent = 103, - wrappingAlgorithm = 104, - editorClassName = 105, - pixelRatio = 106, - tabFocusMode = 107, - layoutInfo = 108, - wrappingInfo = 109 + definitionLinkOpensInPeek = 66, + quickSuggestions = 67, + quickSuggestionsDelay = 68, + readOnly = 69, + renderControlCharacters = 70, + renderIndentGuides = 71, + renderFinalNewline = 72, + renderLineHighlight = 73, + renderValidationDecorations = 74, + renderWhitespace = 75, + revealHorizontalRightPadding = 76, + roundedSelection = 77, + rulers = 78, + scrollbar = 79, + scrollBeyondLastColumn = 80, + scrollBeyondLastLine = 81, + scrollPredominantAxis = 82, + selectionClipboard = 83, + selectionHighlight = 84, + selectOnLineNumbers = 85, + showFoldingControls = 86, + showUnused = 87, + snippetSuggestions = 88, + smoothScrolling = 89, + stopRenderingLineAfter = 90, + suggest = 91, + suggestFontSize = 92, + suggestLineHeight = 93, + suggestOnTriggerCharacters = 94, + suggestSelection = 95, + tabCompletion = 96, + useTabStops = 97, + wordSeparators = 98, + wordWrap = 99, + wordWrapBreakAfterCharacters = 100, + wordWrapBreakBeforeCharacters = 101, + wordWrapColumn = 102, + wordWrapMinified = 103, + wrappingIndent = 104, + wrappingStrategy = 105, + editorClassName = 106, + pixelRatio = 107, + tabFocusMode = 108, + layoutInfo = 109, + wrappingInfo = 110 } /** diff --git a/src/vs/editor/common/view/editorColorRegistry.ts b/src/vs/editor/common/view/editorColorRegistry.ts index 6b30ef7d27ea..76afd2655df1 100644 --- a/src/vs/editor/common/view/editorColorRegistry.ts +++ b/src/vs/editor/common/view/editorColorRegistry.ts @@ -87,6 +87,7 @@ registerThemingParticipant((theme, collector) => { const invisibles = theme.getColor(editorWhitespaces); if (invisibles) { - collector.addRule(`.vs-whitespace { color: ${invisibles} !important; }`); + collector.addRule(`.monaco-editor .mtkw { color: ${invisibles} !important; }`); + collector.addRule(`.monaco-editor .mtkz { color: ${invisibles} !important; }`); } }); diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 7715c163fdfd..4db5ffb766c8 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -183,7 +183,8 @@ export const enum VerticalRevealType { Center = 1, CenterIfOutsideViewport = 2, Top = 3, - Bottom = 4 + Bottom = 4, + NearTop = 5, } export class ViewRevealRangeRequestEvent { diff --git a/src/vs/editor/common/viewLayout/linesLayout.ts b/src/vs/editor/common/viewLayout/linesLayout.ts index be71fcb11634..ca7ca0cf7033 100644 --- a/src/vs/editor/common/viewLayout/linesLayout.ts +++ b/src/vs/editor/common/viewLayout/linesLayout.ts @@ -262,7 +262,6 @@ export class LinesLayout { private _checkPendingChanges(): void { if (this._pendingChanges.mustCommit()) { - console.warn(`Commiting pending changes before change accessor leaves due to read access.`); this._pendingChanges.commit(this); } } diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index 45c8e3b017b8..e7238286d381 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -300,13 +300,23 @@ export class ViewLayout extends Disposable implements IViewLayout { private _computeContentWidth(maxLineWidth: number): number { const options = this._configuration.options; const wrappingInfo = options.get(EditorOption.wrappingInfo); - let isViewportWrapping = wrappingInfo.isViewportWrapping; - if (!isViewportWrapping) { - const extraHorizontalSpace = options.get(EditorOption.scrollBeyondLastColumn) * options.get(EditorOption.fontInfo).typicalHalfwidthCharacterWidth; + const fontInfo = options.get(EditorOption.fontInfo); + if (wrappingInfo.isViewportWrapping) { + const layoutInfo = options.get(EditorOption.layoutInfo); + const minimap = options.get(EditorOption.minimap); + if (maxLineWidth > layoutInfo.contentWidth + fontInfo.typicalHalfwidthCharacterWidth) { + // This is a case where viewport wrapping is on, but the line extends above the viewport + if (minimap.enabled && minimap.side === 'right') { + // We need to accomodate the scrollbar width + return maxLineWidth + layoutInfo.verticalScrollbarWidth; + } + } + return maxLineWidth; + } else { + const extraHorizontalSpace = options.get(EditorOption.scrollBeyondLastColumn) * fontInfo.typicalHalfwidthCharacterWidth; const whitespaceMinWidth = this._linesLayout.getWhitespaceMinWidth(); return Math.max(maxLineWidth + extraHorizontalSpace, whitespaceMinWidth); } - return maxLineWidth; } public onMaxLineWidthChanged(maxLineWidth: number): void { diff --git a/src/vs/editor/common/viewLayout/viewLineRenderer.ts b/src/vs/editor/common/viewLayout/viewLineRenderer.ts index 4f4f76bce472..ff8c4568b208 100644 --- a/src/vs/editor/common/viewLayout/viewLineRenderer.ts +++ b/src/vs/editor/common/viewLayout/viewLineRenderer.ts @@ -549,7 +549,7 @@ function splitLargeTokens(lineContent: string, tokens: LinePart[], onlyAtSpaces: } /** - * Whitespace is rendered by "replacing" tokens with a special-purpose `vs-whitespace` type that is later recognized in the rendering phase. + * Whitespace is rendered by "replacing" tokens with a special-purpose `mtkw` type that is later recognized in the rendering phase. * Moreover, a token is created for every visual indent because on some fonts the glyphs used for rendering whitespace (→ or ·) do not have the same width as  . * The rendering phase will generate `style="width:..."` for these tokens. */ @@ -616,7 +616,7 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW // was in whitespace token if (!isInWhitespace || (!useMonospaceOptimizations && tmpIndent >= tabSize)) { // leaving whitespace token or entering a new indent - result[resultLen++] = new LinePart(charIndex, 'vs-whitespace'); + result[resultLen++] = new LinePart(charIndex, 'mtkw'); tmpIndent = tmpIndent % tabSize; } } else { @@ -661,7 +661,7 @@ function _applyRenderWhitespace(lineContent: string, len: number, continuesWithW } } - result[resultLen++] = new LinePart(len, generateWhitespace ? 'vs-whitespace' : tokenType); + result[resultLen++] = new LinePart(len, generateWhitespace ? 'mtkw' : tokenType); return result; } @@ -763,11 +763,12 @@ function _renderLine(input: ResolvedRenderLineInput, sb: IStringBuilder): Render const part = parts[partIndex]; const partEndIndex = part.endIndex; const partType = part.type; - const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && (partType.indexOf('vs-whitespace') >= 0)); + const partRendersWhitespace = (renderWhitespace !== RenderWhitespace.None && (partType.indexOf('mtkw') >= 0)); + const partRendersWhitespaceWithWidth = partRendersWhitespace && !fontIsMonospace && (partType === 'mtkw'/*only whitespace*/ || !containsForeignElements); charOffsetInPart = 0; sb.appendASCIIString(' { const foldBackground = theme.getColor(foldBackgroundBackground); diff --git a/src/vs/editor/contrib/format/format.ts b/src/vs/editor/contrib/format/format.ts index 8baeacfae49b..fa7492a850ef 100644 --- a/src/vs/editor/contrib/format/format.ts +++ b/src/vs/editor/contrib/format/format.ts @@ -178,10 +178,8 @@ export async function formatDocumentRangeWithProvider( if (isCodeEditor(editorOrModel)) { // use editor to apply edits - FormattingEdit.execute(editorOrModel, edits); + FormattingEdit.execute(editorOrModel, edits, true); alertFormattingEdits(edits); - editorOrModel.pushUndoStop(); - editorOrModel.focus(); editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); } else { @@ -266,12 +264,10 @@ export async function formatDocumentWithProvider( if (isCodeEditor(editorOrModel)) { // use editor to apply edits - FormattingEdit.execute(editorOrModel, edits); + FormattingEdit.execute(editorOrModel, edits, mode !== FormattingMode.Silent); if (mode !== FormattingMode.Silent) { alertFormattingEdits(edits); - editorOrModel.pushUndoStop(); - editorOrModel.focus(); editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate); } diff --git a/src/vs/editor/contrib/format/formatActions.ts b/src/vs/editor/contrib/format/formatActions.ts index 30afe933819a..cb5281b24f04 100644 --- a/src/vs/editor/contrib/format/formatActions.ts +++ b/src/vs/editor/contrib/format/formatActions.ts @@ -138,7 +138,7 @@ class FormatOnType implements IEditorContribution { } if (isNonEmptyArray(edits)) { - FormattingEdit.execute(this._editor, edits); + FormattingEdit.execute(this._editor, edits, true); alertFormattingEdits(edits); } diff --git a/src/vs/editor/contrib/format/formattingEdit.ts b/src/vs/editor/contrib/format/formattingEdit.ts index 431adda97c17..9b44a46d6432 100644 --- a/src/vs/editor/contrib/format/formattingEdit.ts +++ b/src/vs/editor/contrib/format/formattingEdit.ts @@ -43,8 +43,10 @@ export class FormattingEdit { return fullModelRange.equalsRange(editRange); } - static execute(editor: ICodeEditor, _edits: TextEdit[]) { - editor.pushUndoStop(); + static execute(editor: ICodeEditor, _edits: TextEdit[], addUndoStops: boolean) { + if (addUndoStops) { + editor.pushUndoStop(); + } const edits = FormattingEdit._handleEolEdits(editor, _edits); if (edits.length === 1 && FormattingEdit._isFullModelReplaceEdit(editor, edits[0])) { // We use replace semantics and hope that markers stay put... @@ -52,6 +54,8 @@ export class FormattingEdit { } else { editor.executeEdits('formatEditsCommand', edits.map(edit => EditOperation.replaceMove(Range.lift(edit.range), edit.text))); } - editor.pushUndoStop(); + if (addUndoStops) { + editor.pushUndoStop(); + } } } diff --git a/src/vs/editor/contrib/gotoError/gotoError.ts b/src/vs/editor/contrib/gotoError/gotoError.ts index 7fe02c5bd1b1..e8180d367d6f 100644 --- a/src/vs/editor/contrib/gotoError/gotoError.ts +++ b/src/vs/editor/contrib/gotoError/gotoError.ts @@ -28,6 +28,7 @@ import { Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { isEqual } from 'vs/base/common/resources'; import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; class MarkerModel { @@ -396,7 +397,7 @@ class MarkerNavigationAction extends EditorAction { return editorService.openCodeEditor({ resource: newMarker.resource, - options: { pinned: false, revealIfOpened: true, revealInCenterIfOutsideViewport: true, selection: newMarker } + options: { pinned: false, revealIfOpened: true, selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport, selection: newMarker } }, editor).then(editor => { if (!editor) { return undefined; diff --git a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts index 832247fb8acc..11173005662e 100644 --- a/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts +++ b/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts @@ -317,6 +317,7 @@ export class MarkerNavigationWidget extends PeekViewWidget { this._icon.className = `codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(this._severity))}`; this.editor.revealPositionInCenter(position, ScrollType.Smooth); + this.editor.focus(); } updateMarker(marker: IMarker): void { diff --git a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts index e129d659e59e..676e4ed82314 100644 --- a/src/vs/editor/contrib/gotoSymbol/goToCommands.ts +++ b/src/vs/editor/contrib/gotoSymbol/goToCommands.ts @@ -36,6 +36,8 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ScrollType, IEditorAction } from 'vs/editor/common/editorCommon'; import { assertType } from 'vs/base/common/types'; +import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; MenuRegistry.appendMenuItem(MenuId.EditorContext, { @@ -129,7 +131,7 @@ abstract class SymbolNavigationAction extends EditorAction { private async _onResult(editorService: ICodeEditorService, symbolNavService: ISymbolNavigationService, editor: IActiveCodeEditor, model: ReferencesModel): Promise { const gotoLocation = this._getGoToPreference(editor); - if (this._configuration.openInPeek || (gotoLocation === 'peek' && model.references.length > 1)) { + if (!(editor instanceof EmbeddedCodeEditorWidget) && (this._configuration.openInPeek || (gotoLocation === 'peek' && model.references.length > 1))) { this._openInPeek(editor, model); } else { @@ -165,7 +167,7 @@ abstract class SymbolNavigationAction extends EditorAction { resource: reference.uri, options: { selection: Range.collapseToStart(range), - revealInCenterIfOutsideViewport: true + selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport } }, editor, sideBySide); diff --git a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts index 24629d038a76..6a26beb2bede 100644 --- a/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts +++ b/src/vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition.ts @@ -27,6 +27,10 @@ import { IWordAtPosition, IModelDeltaDecoration, ITextModel, IFoundBracket } fro import { Position } from 'vs/editor/common/core/position'; import { withNullAsUndefined } from 'vs/base/common/types'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { PeekContext } from 'vs/editor/contrib/peekView/peekView'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution { @@ -334,8 +338,17 @@ export class GotoDefinitionAtPositionEditorContribution implements IEditorContri private gotoDefinition(position: Position, openToSide: boolean): Promise { this.editor.setPosition(position); - const action = new DefinitionAction({ openToSide, openInPeek: false, muteMessage: true }, { alias: '', label: '', id: '', precondition: undefined }); - return this.editor.invokeWithinContext(accessor => action.run(accessor, this.editor)); + const definitionLinkOpensInPeek = this.editor.getOption(EditorOption.definitionLinkOpensInPeek); + return this.editor.invokeWithinContext((accessor) => { + const canPeek = definitionLinkOpensInPeek && !this.isInPeekEditor(accessor); + const action = new DefinitionAction({ openToSide, openInPeek: canPeek, muteMessage: true }, { alias: '', label: '', id: '', precondition: undefined }); + return action.run(accessor, this.editor); + }); + } + + private isInPeekEditor(accessor: ServicesAccessor): boolean | undefined { + const contextKeyService = accessor.get(IContextKeyService); + return PeekContext.inPeekEditor.getValue(contextKeyService); } public dispose(): void { diff --git a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts index 2a32c3b2b73d..3958e96b8c06 100644 --- a/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts +++ b/src/vs/editor/contrib/gotoSymbol/peek/referencesController.ts @@ -216,14 +216,16 @@ export abstract class ReferencesController implements IEditorContribution { } } - closeWidget(): void { + closeWidget(focusEditor = true): void { this._referenceSearchVisible.reset(); this._disposables.clear(); dispose(this._widget); dispose(this._model); this._widget = undefined; this._model = undefined; - this._editor.focus(); + if (focusEditor) { + this._editor.focus(); + } this._requestIdPool += 1; // Cancel pending requests } @@ -300,7 +302,7 @@ function withController(accessor: ServicesAccessor, fn: (controller: ReferencesC } KeybindingsRegistry.registerCommandAndKeybindingRule({ - id: 'changePeekFocus', + id: 'togglePeekWidgetFocus', weight: KeybindingWeight.EditorContrib, primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.F2), when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor), diff --git a/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts b/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts index a6bba6e922b9..d8e54eb47d42 100644 --- a/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts +++ b/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts @@ -19,6 +19,7 @@ import { localize } from 'vs/nls'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { isEqual } from 'vs/base/common/resources'; +import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor'; export const ctxHasSymbols = new RawContextKey('hasSymbols', false); @@ -127,7 +128,7 @@ class SymbolNavigationService implements ISymbolNavigationService { resource: reference.uri, options: { selection: Range.collapseToStart(reference.range), - revealInCenterIfOutsideViewport: true + selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport } }, source).finally(() => { this._ignoreEditorChange = false; diff --git a/src/vs/editor/contrib/linesOperations/linesOperations.ts b/src/vs/editor/contrib/linesOperations/linesOperations.ts index 087cb8d30375..80e3d2ae25bd 100644 --- a/src/vs/editor/contrib/linesOperations/linesOperations.ts +++ b/src/vs/editor/contrib/linesOperations/linesOperations.ts @@ -37,12 +37,36 @@ abstract class AbstractCopyLinesAction extends EditorAction { } public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { + if (!editor.hasModel()) { + return; + } - const commands: ICommand[] = []; - const selections = editor.getSelections() || []; + const selections = editor.getSelections().map((selection, index) => ({ selection, index, ignore: false })); + selections.sort((a, b) => Range.compareRangesUsingStarts(a.selection, b.selection)); + + // Remove selections that would result in copying the same line + let prev = selections[0]; + for (let i = 1; i < selections.length; i++) { + const curr = selections[i]; + if (prev.selection.endLineNumber === curr.selection.startLineNumber) { + // these two selections would copy the same line + if (prev.index < curr.index) { + // prev wins + curr.ignore = true; + } else { + // curr wins + prev.ignore = true; + prev = curr; + } + } + } + const commands: ICommand[] = []; for (const selection of selections) { - commands.push(new CopyLinesCommand(selection, this.down)); + if (selection.ignore) { + continue; + } + commands.push(new CopyLinesCommand(selection.selection, this.down)); } editor.pushUndoStop(); diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 2d0897bf63f3..13e857c2314f 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -179,7 +179,7 @@ class RenameController implements IEditorContribution { selectionEnd = Math.min(loc.range.endColumn, selection.endColumn) - loc.range.startColumn; } - const supportPreview = this._configService.getValue(this.editor.getModel().uri, 'editor.rename.enablePreview'); + 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); // no result, only hint to focus the editor or not diff --git a/src/vs/editor/contrib/suggest/media/suggest.css b/src/vs/editor/contrib/suggest/media/suggest.css index e5c418b3548e..852b9f65d425 100644 --- a/src/vs/editor/contrib/suggest/media/suggest.css +++ b/src/vs/editor/contrib/suggest/media/suggest.css @@ -111,6 +111,44 @@ font-weight: bold; } +/** Status Bar **/ + +.monaco-editor .suggest-widget > .suggest-status-bar { + visibility: hidden; + + position: absolute; + left: 0; + + box-sizing: border-box; + + display: flex; + flex-flow: row nowrap; + justify-content: space-between; + + width: 100%; + + font-size: 80%; + + border-left-width: 1px; + border-left-style: solid; + border-right-width: 1px; + border-right-style: solid; + border-bottom-width: 1px; + border-bottom-style: solid; + + padding: 0 8px 0 4px; + + box-shadow: 0 -.5px 3px #ddd; +} + +.monaco-editor .suggest-widget.list-right.docs-side > .suggest-status-bar { + left: auto; + right: 0; +} +.monaco-editor .suggest-widget.docs-side > .suggest-status-bar { + width: 50%; +} + /** ReadMore Icon styles **/ .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .header > .codicon-close, @@ -190,7 +228,7 @@ overflow: hidden; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .left > .monaco-icon-label { - flex-shrink: 0; + flex-shrink: 1; } .monaco-editor .suggest-widget .monaco-list .monaco-list-row > .contents > .main > .right { overflow: hidden; @@ -324,6 +362,7 @@ .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs { padding: 0; white-space: initial; + min-height: calc(1rem + 8px); } .monaco-editor .suggest-widget .details > .monaco-scrollable-element > .body > .docs.markdown-docs > div, diff --git a/src/vs/editor/contrib/suggest/media/suggestStatusBar.css b/src/vs/editor/contrib/suggest/media/suggestStatusBar.css new file mode 100644 index 000000000000..eb05ba733abf --- /dev/null +++ b/src/vs/editor/contrib/suggest/media/suggestStatusBar.css @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar { + visibility: visible; +} +.monaco-editor .suggest-widget.with-status-bar > .tree { + margin-bottom: 18px; +} + +.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-label { + min-height: 18px; + opacity: 0.5; + color: inherit; +} + +.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-item:not(:last-of-type) .action-label { + margin-right: 0; +} + +.monaco-editor .suggest-widget.with-status-bar .suggest-status-bar .action-item:not(:last-of-type) .action-label::after { + content: ', '; + margin-right: 0.3em; +} + +.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row > .contents > .main > .right > .readMore, +.monaco-editor .suggest-widget.with-status-bar .monaco-list .monaco-list-row.focused > .contents > .main > .right:not(.always-show-details) > .readMore { + display: none; +} + +.monaco-editor .suggest-widget.with-status-bar:not(.docs-side) .monaco-list .monaco-list-row:hover > .contents > .main > .right.can-expand-details > .details-label { + width: 100%; +} diff --git a/src/vs/editor/contrib/suggest/suggest.ts b/src/vs/editor/contrib/suggest/suggest.ts index f4bb3fd65b79..da63de0b9051 100644 --- a/src/vs/editor/contrib/suggest/suggest.ts +++ b/src/vs/editor/contrib/suggest/suggest.ts @@ -17,14 +17,19 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { Range } from 'vs/editor/common/core/range'; import { FuzzyScore } from 'vs/base/common/filters'; import { isDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { MenuId } from 'vs/platform/actions/common/actions'; export const Context = { Visible: new RawContextKey('suggestWidgetVisible', false), + DetailsVisible: new RawContextKey('suggestWidgetDetailsVisible', false), MultipleSuggestions: new RawContextKey('suggestWidgetMultipleSuggestions', false), MakesTextEdit: new RawContextKey('suggestionMakesTextEdit', true), - AcceptSuggestionsOnEnter: new RawContextKey('acceptSuggestionOnEnter', true) + AcceptSuggestionsOnEnter: new RawContextKey('acceptSuggestionOnEnter', true), + HasInsertAndReplaceRange: new RawContextKey('suggestionHasInsertAndReplaceRange', false), }; +export const suggestWidgetStatusbarMenu = new MenuId('suggestWidgetStatusBar'); + export class CompletionItem { _brand!: 'ISuggestionItem'; diff --git a/src/vs/editor/contrib/suggest/suggestController.ts b/src/vs/editor/contrib/suggest/suggestController.ts index 9a706b214c92..691ae91a3f78 100644 --- a/src/vs/editor/contrib/suggest/suggestController.ts +++ b/src/vs/editor/contrib/suggest/suggestController.ts @@ -23,7 +23,7 @@ import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/c import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; -import { Context as SuggestContext, CompletionItem } from './suggest'; +import { Context as SuggestContext, CompletionItem, suggestWidgetStatusbarMenu } from './suggest'; import { SuggestAlternatives } from './suggestAlternatives'; import { State, SuggestModel } from './suggestModel'; import { ISelectedSuggestion, SuggestWidget } from './suggestWidget'; @@ -33,17 +33,16 @@ import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerServ import { IdleValue } from 'vs/base/common/async'; import { isObject, assertType } from 'vs/base/common/types'; import { CommitCharacterController } from './suggestCommitCharacters'; -import { IPosition } from 'vs/editor/common/core/position'; +import { IPosition, Position } from 'vs/editor/common/core/position'; 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 { SuggestRangeHighlighter } from 'vs/editor/contrib/suggest/suggestRangeHighlighter'; +import { MenuRegistry } from 'vs/platform/actions/common/actions'; -/** - * Stop suggest widget from disappearing when clicking into other areas - * For development purpose only - */ -const _sticky = false; +// sticky suggest widget which doesn't disappear on focus out and such +let _sticky = false; +// _sticky = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this class LineSuffix { @@ -138,9 +137,17 @@ export class SuggestController implements IEditorContribution { })); // Wire up makes text edit context key - let makesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService); + const ctxMakesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService); + const ctxHasInsertAndReplace = SuggestContext.HasInsertAndReplaceRange.bindTo(this._contextKeyService); + + this._toDispose.add(toDisposable(() => { + ctxMakesTextEdit.reset(); + ctxHasInsertAndReplace.reset(); + })); + this._toDispose.add(widget.onDidFocus(({ item }) => { + // (ctx: makesTextEdit) const position = this.editor.getPosition()!; const startColumn = item.editStart.column; const endColumn = position.column; @@ -161,9 +168,11 @@ export class SuggestController implements IEditorContribution { }); value = oldText !== item.completion.insertText; } - makesTextEdit.set(value); + ctxMakesTextEdit.set(value); + + // (ctx: hasInsertAndReplaceRange) + ctxHasInsertAndReplace.set(!Position.equals(item.editInsertEnd, item.editReplaceEnd)); })); - this._toDispose.add(toDisposable(() => makesTextEdit.reset())); this._toDispose.add(widget.onDetailsKeyDown(e => { // cmd + c on macOS, ctrl + c on Win / Linux @@ -435,7 +444,6 @@ export class SuggestController implements IEditorContribution { } this._insertSuggestion(item, flags); } - acceptNextSuggestion() { this._alternatives.getValue().next(); } @@ -528,11 +536,8 @@ const SuggestCommand = EditorCommand.bindToContribution(Sugge registerEditorCommand(new SuggestCommand({ id: 'acceptSelectedSuggestion', precondition: SuggestContext.Visible, - handler(x, args) { - const alternative: boolean = typeof args === 'object' && typeof args.alternative === 'boolean' - ? args.alternative - : false; - x.acceptSelectedSuggestion(true, alternative); + handler(x) { + x.acceptSelectedSuggestion(true, false); } })); @@ -549,20 +554,56 @@ KeybindingsRegistry.registerKeybindingRule({ id: 'acceptSelectedSuggestion', when: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus, SuggestContext.AcceptSuggestionsOnEnter, SuggestContext.MakesTextEdit), primary: KeyCode.Enter, - weight + weight, }); -// shift+enter and shift+tab use the alternative-flag so that the suggest controller -// is doing the opposite of the editor.suggest.overwriteOnAccept-configuration -KeybindingsRegistry.registerKeybindingRule({ - id: 'acceptSelectedSuggestion', - when: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus), - primary: KeyMod.Shift | KeyCode.Tab, - secondary: [KeyMod.Shift | KeyCode.Enter], - args: { alternative: true }, - weight +MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, { + command: { id: 'acceptSelectedSuggestion', title: nls.localize({ key: 'accept.accept', comment: ['{0} will be a keybinding, e.g "Enter to insert"'] }, "{0} to insert") }, + group: 'left', + order: 1, + when: SuggestContext.HasInsertAndReplaceRange.toNegated() +}); +MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, { + command: { id: 'acceptSelectedSuggestion', title: nls.localize({ key: 'accept.insert', comment: ['{0} will be a keybinding, e.g "Enter to insert"'] }, "{0} to insert") }, + group: 'left', + order: 1, + when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'insert')) +}); +MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, { + command: { id: 'acceptSelectedSuggestion', title: nls.localize({ key: 'accept.replace', comment: ['{0} will be a keybinding, e.g "Enter to replace"'] }, "{0} to replace") }, + group: 'left', + order: 1, + when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'replace')) }); +registerEditorCommand(new SuggestCommand({ + id: 'acceptAlternativeSelectedSuggestion', + precondition: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus), + kbOpts: { + weight: weight, + kbExpr: EditorContextKeys.textInputFocus, + primary: KeyMod.Shift | KeyCode.Enter, + secondary: [KeyMod.Shift | KeyCode.Tab], + }, + handler(x) { + x.acceptSelectedSuggestion(false, true); + }, + menuOpts: [{ + menuId: suggestWidgetStatusbarMenu, + group: 'left', + order: 2, + when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'insert')), + title: nls.localize({ key: 'accept.replace', comment: ['{0} will be a keybinding, e.g "Enter to replace"'] }, "{0} to replace") + }, { + menuId: suggestWidgetStatusbarMenu, + group: 'left', + order: 2, + when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, ContextKeyExpr.equals('config.editor.suggest.insertMode', 'replace')), + title: nls.localize({ key: 'accept.insert', comment: ['{0} will be a keybinding, e.g "Enter to insert"'] }, "{0} to insert") + }] +})); + + // continue to support the old command CommandsRegistry.registerCommandAlias('acceptSelectedSuggestionOnEnter', 'acceptSelectedSuggestion'); @@ -649,7 +690,20 @@ registerEditorCommand(new SuggestCommand({ kbExpr: EditorContextKeys.textInputFocus, primary: KeyMod.CtrlCmd | KeyCode.Space, mac: { primary: KeyMod.WinCtrl | KeyCode.Space } - } + }, + menuOpts: [{ + menuId: suggestWidgetStatusbarMenu, + group: 'right', + order: 1, + when: SuggestContext.DetailsVisible, + title: nls.localize('detail.more', "show less") + }, { + menuId: suggestWidgetStatusbarMenu, + group: 'right', + order: 1, + when: SuggestContext.DetailsVisible.toNegated(), + title: nls.localize('detail.less', "show more") + }] })); registerEditorCommand(new SuggestCommand({ diff --git a/src/vs/editor/contrib/suggest/suggestWidget.ts b/src/vs/editor/contrib/suggest/suggestWidget.ts index ae5a36dc2d8c..c61ed33c3fe0 100644 --- a/src/vs/editor/contrib/suggest/suggestWidget.ts +++ b/src/vs/editor/contrib/suggest/suggestWidget.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/suggest'; +import 'vs/css!./media/suggestStatusBar'; import 'vs/base/browser/ui/codiconLabel/codiconLabel'; // The codicon symbol styles are defined here and must be loaded import 'vs/editor/contrib/documentSymbols/outlineTree'; // The codicon symbol colors are defined here and must be loaded import * as nls from 'vs/nls'; @@ -20,7 +21,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; -import { Context as SuggestContext, CompletionItem } from './suggest'; +import { Context as SuggestContext, CompletionItem, suggestWidgetStatusbarMenu } from './suggest'; import { CompletionModel } from './completionModel'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { attachListStyler } from 'vs/platform/theme/common/styler'; @@ -39,8 +40,11 @@ import { URI } from 'vs/base/common/uri'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileKind } from 'vs/platform/files/common/files'; import { MarkdownString } from 'vs/base/common/htmlContent'; -import { flatten } from 'vs/base/common/arrays'; +import { flatten, isFalsyOrEmpty } from 'vs/base/common/arrays'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { IMenuService } from 'vs/platform/actions/common/actions'; +import { ActionBar, IActionViewItemProvider, ActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; +import { IAction } from 'vs/base/common/actions'; const expandSuggestionDocsByDefault = false; @@ -49,8 +53,8 @@ interface ISuggestionTemplateData { /** * Flexbox - * < ------- left ------- > < -------- right -------- > - *