diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index addf36f4339..4b385f04183 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -2,7 +2,7 @@ name: Bug report about: Create a report to help us improve --- - + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index a820760fc48..2dc1460b16f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -4,6 +4,7 @@ about: Suggest an idea for this project --- + diff --git a/.github/commands.yml b/.github/commands.yml index ea0895b3dd2..24ac951d6f0 100644 --- a/.github/commands.yml +++ b/.github/commands.yml @@ -131,5 +131,11 @@ action: 'updateLabels', addLabel: 'a11ymas' }, + { + type: 'label', + name: '*off-topic', + action: 'close', + comment: "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting](https://aka.ms/vscodeissuereporting) guidelines.\n\nHappy Coding!" + } ] } diff --git a/.github/copycat.yml b/.github/copycat.yml index eccccc16b00..192f225684f 100644 --- a/.github/copycat.yml +++ b/.github/copycat.yml @@ -1,5 +1,5 @@ { - perform: true, + perform: false, target_owner: 'chrmarti', target_repo: 'testissues' -} \ No newline at end of file +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55415410f7c..e9a2a3e71bd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,8 @@ jobs: name: Run TSLint Checks - run: yarn monaco-compile-check name: Run Monaco Editor Checks + - run: yarn valid-globals-check + name: Run Valid Globals Checks - run: yarn compile name: Compile Sources - run: yarn download-builtin-extensions @@ -73,6 +75,8 @@ jobs: name: Run TSLint Checks - run: yarn monaco-compile-check name: Run Monaco Editor Checks + - run: yarn valid-globals-check + name: Run Valid Globals Checks - run: yarn compile name: Compile Sources - run: yarn download-builtin-extensions @@ -102,6 +106,8 @@ jobs: name: Run TSLint Checks - run: yarn monaco-compile-check name: Run Monaco Editor Checks + - run: yarn valid-globals-check + name: Run Valid Globals Checks - run: yarn compile name: Compile Sources - run: yarn download-builtin-extensions diff --git a/.vscode/launch.json b/.vscode/launch.json index 7e112fc94e1..ccbf4a70ae3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,10 @@ "restart": true, "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "hidden": true, + } }, { "type": "chrome", @@ -35,7 +38,10 @@ "port": 5876, "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "hidden": true, + } }, { "type": "node", @@ -53,7 +59,10 @@ "port": 5875, "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "hidden": true, + } }, { "type": "extensionHost", @@ -67,7 +76,11 @@ ], "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 6 + } }, { "type": "extensionHost", @@ -82,7 +95,11 @@ ], "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 3 + } }, { "type": "extensionHost", @@ -96,7 +113,11 @@ ], "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 4 + } }, { "type": "extensionHost", @@ -110,13 +131,20 @@ ], "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 5 + } }, { "type": "chrome", "request": "attach", "name": "Attach to VS Code", - "port": 9222 + "port": 9222, + "presentation": { + "hidden": true + } }, { "type": "chrome", @@ -142,7 +170,10 @@ "--inspect=5875", "--no-cached-data" ], - "webRoot": "${workspaceFolder}" + "webRoot": "${workspaceFolder}", + "presentation": { + "hidden": true + } }, { "type": "node", @@ -157,7 +188,11 @@ ], "outFiles": [ "${workspaceFolder}/out/**/*.js" - ] + ], + "presentation": { + "group": "2_launch", + "order": 1 + } }, { "type": "node", @@ -167,13 +202,21 @@ "runtimeArgs": [ "web" ], + "presentation": { + "group": "2_launch", + "order": 2 + } }, { "type": "chrome", "request": "launch", "name": "Launch VS Code (Web, Chrome)", "url": "http://localhost:8080", - "preLaunchTask": "Run web" + "preLaunchTask": "Run web", + "presentation": { + "group": "2_launch", + "order": 3 + } }, { "type": "node", @@ -184,7 +227,11 @@ "cwd": "${workspaceFolder}/extensions/git", "outFiles": [ "${workspaceFolder}/extensions/git/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 10 + } }, { "type": "extensionHost", @@ -198,7 +245,11 @@ ], "outFiles": [ "${workspaceFolder}/extensions/markdown-language-features/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 7 + } }, { "type": "extensionHost", @@ -212,7 +263,11 @@ ], "outFiles": [ "${workspaceFolder}/extensions/typescript-language-features/out/**/*.js" - ] + ], + "presentation": { + "group": "5_tests", + "order": 8 + } }, { "type": "node", @@ -236,6 +291,9 @@ ], "env": { "MOCHA_COLORS": "true" + }, + "presentation": { + "hidden": true } }, { @@ -276,28 +334,44 @@ "Launch VS Code", "Attach to Main Process", "Attach to Extension Host" - ] + ], + "presentation": { + "group": "1_vscode", + "order": 1 + } }, { "name": "Search and Renderer processes", "configurations": [ "Launch VS Code", "Attach to Search Process" - ] + ], + "presentation": { + "group": "1_vscode", + "order": 4 + } }, { "name": "Renderer and Extension Host processes", "configurations": [ "Launch VS Code", "Attach to Extension Host" - ] + ], + "presentation": { + "group": "1_vscode", + "order": 3 + } }, { "name": "Debug Unit Tests", "configurations": [ "Attach to VS Code", "Run Unit Tests" - ] - }, + ], + "presentation": { + "group": "1_vscode", + "order": 2 + } + } ] } diff --git a/build/azure-pipelines/darwin/continuous-build-darwin.yml b/build/azure-pipelines/darwin/continuous-build-darwin.yml index c98aa3770de..1609e4a0027 100644 --- a/build/azure-pipelines/darwin/continuous-build-darwin.yml +++ b/build/azure-pipelines/darwin/continuous-build-darwin.yml @@ -32,6 +32,9 @@ steps: - script: | yarn monaco-compile-check displayName: Run Monaco Editor Checks +- script: | + yarn valid-globals-check + displayName: Run Valid Globals Checks - script: | yarn compile displayName: Compile Sources diff --git a/build/azure-pipelines/linux/continuous-build-linux.yml b/build/azure-pipelines/linux/continuous-build-linux.yml index 0f611bd439d..649c12593da 100644 --- a/build/azure-pipelines/linux/continuous-build-linux.yml +++ b/build/azure-pipelines/linux/continuous-build-linux.yml @@ -40,6 +40,9 @@ steps: - script: | yarn monaco-compile-check displayName: Run Monaco Editor Checks +- script: | + yarn valid-globals-check + displayName: Run Valid Globals Checks - script: | yarn compile displayName: Compile Sources diff --git a/build/azure-pipelines/product-compile.yml b/build/azure-pipelines/product-compile.yml index 8029f8a5661..9ec01fd5db1 100644 --- a/build/azure-pipelines/product-compile.yml +++ b/build/azure-pipelines/product-compile.yml @@ -91,7 +91,8 @@ steps: yarn gulp hygiene --skip-tslint yarn gulp tslint yarn monaco-compile-check - displayName: Run hygiene, tslint and monaco compile checks + yarn valid-globals-check + displayName: Run hygiene, tslint, monaco compile & valid globals checks condition: and(succeeded(), ne(variables['CacheExists-Compilation'], 'true'), eq(variables['VSCODE_STEP_ON_IT'], 'false')) - script: | diff --git a/build/azure-pipelines/win32/continuous-build-win32.yml b/build/azure-pipelines/win32/continuous-build-win32.yml index 9351bafa0bd..050a967629b 100644 --- a/build/azure-pipelines/win32/continuous-build-win32.yml +++ b/build/azure-pipelines/win32/continuous-build-win32.yml @@ -37,6 +37,9 @@ steps: - powershell: | yarn monaco-compile-check displayName: Run Monaco Editor Checks +- script: | + yarn valid-globals-check + displayName: Run Valid Globals Checks - powershell: | yarn compile displayName: Compile Sources diff --git a/build/lib/globalsLinter.js b/build/lib/globalsLinter.js new file mode 100644 index 00000000000..cfcb05c2689 --- /dev/null +++ b/build/lib/globalsLinter.js @@ -0,0 +1,176 @@ +"use strict"; +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +Object.defineProperty(exports, "__esModule", { value: true }); +const ts = require("typescript"); +const fs_1 = require("fs"); +const path_1 = require("path"); +const minimatch_1 = require("minimatch"); +// +// ############################################################################################# +// +// A custom typescript linter for the specific task of detecting the use of certain globals in a +// layer that does not allow the use. For example: +// - using DOM globals in common/node/electron-main layer (e.g. HTMLElement) +// - using node.js globals in common/browser layer (e.g. process) +// +// Make changes to below RULES to lift certain files from these checks only if absolutely needed +// +// ############################################################################################# +// +const RULES = { + "no-nodejs-globals": [ + { + "target": "**/vs/**/test/{common,browser}/**", + "allowed": [ + "process", + "Buffer", + "__filename", + "__dirname" + ] + }, + { + "target": "**/vs/workbench/api/common/extHostExtensionService.ts", + "allowed": [ + "global" // -> safe access to 'global' + ] + }, + { + "target": "**/vs/**/{common,browser}/**", + "allowed": [ /* none */] + } + ], + "no-dom-globals": [ + { + "target": "**/vs/base/parts/quickopen/common/quickOpen.ts", + "allowed": [ + "HTMLElement" // quick open will be replaced with a different widget soon + ] + }, + { + "target": "**/vs/**/test/{common,node,electron-main}/**", + "allowed": [ + "document", + "HTMLElement", + "createElement" + ] + }, + { + "target": "**/vs/**/{common,node,electron-main}/**", + "allowed": [ /* none */] + } + ] +}; +const TS_CONFIG_PATH = path_1.join(__dirname, '../../', 'src', 'tsconfig.json'); +const DOM_GLOBALS_DEFINITION = 'lib.dom.d.ts'; +const DISALLOWED_DOM_GLOBALS = [ + "window", + "document", + "HTMLElement", + "createElement" +]; +const NODE_GLOBALS_DEFINITION = '@types/node'; +const DISALLOWED_NODE_GLOBALS = [ + // https://nodejs.org/api/globals.html#globals_global_objects + "NodeJS", + "Buffer", + "__dirname", + "__filename", + "clearImmediate", + "exports", + "global", + "module", + "process", + "setImmediate" +]; +let hasErrors = false; +function checkFile(program, sourceFile, rule) { + checkNode(sourceFile); + function checkNode(node) { + if (node.kind !== ts.SyntaxKind.Identifier) { + return ts.forEachChild(node, checkNode); // recurse down + } + const text = node.getText(sourceFile); + if (!rule.disallowedGlobals.some(disallowedGlobal => disallowedGlobal === text)) { + return; // only if disallowed + } + if (rule.allowedGlobals.some(allowed => allowed === text)) { + return; // override + } + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (symbol) { + const declarations = symbol.declarations; + if (Array.isArray(declarations) && symbol.declarations.some(declaration => { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const sourceFile = parent.getSourceFile(); + if (sourceFile) { + const fileName = sourceFile.fileName; + if (fileName && fileName.indexOf(rule.definition) >= 0) { + return true; + } + } + } + } + return false; + })) { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + console.log(`build/lib/globalsLinter.ts: Cannot use global '${text}' in ${sourceFile.fileName} (${line + 1},${character + 1})`); + hasErrors = true; + } + } + } +} +function createProgram(tsconfigPath) { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => fs_1.readFileSync(file, "utf8"), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, path_1.resolve(path_1.dirname(tsconfigPath)), { noEmit: true }); + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} +// +// Create program and start checking +// +const program = createProgram(TS_CONFIG_PATH); +for (const sourceFile of program.getSourceFiles()) { + let noDomGlobalsLinter = undefined; + let noNodeJSGlobalsLinter = undefined; + for (const rules of RULES["no-dom-globals"]) { + if (minimatch_1.match([sourceFile.fileName], rules.target).length > 0) { + noDomGlobalsLinter = { allowed: rules.allowed }; + break; + } + } + for (const rules of RULES["no-nodejs-globals"]) { + if (minimatch_1.match([sourceFile.fileName], rules.target).length > 0) { + noNodeJSGlobalsLinter = { allowed: rules.allowed }; + break; + } + } + if (!noDomGlobalsLinter && !noNodeJSGlobalsLinter) { + continue; // no rule to run + } + // No DOM Globals + if (noDomGlobalsLinter) { + checkFile(program, sourceFile, { + definition: DOM_GLOBALS_DEFINITION, + disallowedGlobals: DISALLOWED_DOM_GLOBALS, + allowedGlobals: noDomGlobalsLinter.allowed + }); + } + // No node.js Globals + if (noNodeJSGlobalsLinter) { + checkFile(program, sourceFile, { + definition: NODE_GLOBALS_DEFINITION, + disallowedGlobals: DISALLOWED_NODE_GLOBALS, + allowedGlobals: noNodeJSGlobalsLinter.allowed + }); + } +} +if (hasErrors) { + process.exit(1); +} diff --git a/build/lib/globalsLinter.ts b/build/lib/globalsLinter.ts new file mode 100644 index 00000000000..1f931ec6e0e --- /dev/null +++ b/build/lib/globalsLinter.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as ts from "typescript"; +import { readFileSync, existsSync } from "fs"; +import { resolve, dirname, join } from "path"; +import { match } from 'minimatch'; + +// +// ############################################################################################# +// +// A custom typescript linter for the specific task of detecting the use of certain globals in a +// layer that does not allow the use. For example: +// - using DOM globals in common/node/electron-main layer (e.g. HTMLElement) +// - using node.js globals in common/browser layer (e.g. process) +// +// Make changes to below RULES to lift certain files from these checks only if absolutely needed +// +// ############################################################################################# +// + +const RULES = { + "no-nodejs-globals": [ + { + "target": "**/vs/**/test/{common,browser}/**", + "allowed": [ // -> less strict for test files + "process", + "Buffer", + "__filename", + "__dirname" + ] + }, + { + "target": "**/vs/workbench/api/common/extHostExtensionService.ts", + "allowed": [ + "global" // -> safe access to 'global' + ] + }, + { + "target": "**/vs/**/{common,browser}/**", + "allowed": [ /* none */] + } + ], + "no-dom-globals": [ + { + "target": "**/vs/base/parts/quickopen/common/quickOpen.ts", + "allowed": [ + "HTMLElement" // quick open will be replaced with a different widget soon + ] + }, + { + "target": "**/vs/**/test/{common,node,electron-main}/**", + "allowed": [ // -> less strict for test files + "document", + "HTMLElement", + "createElement" + ] + }, + { + "target": "**/vs/**/{common,node,electron-main}/**", + "allowed": [ /* none */] + } + ] +}; + +const TS_CONFIG_PATH = join(__dirname, '../../', 'src', 'tsconfig.json'); + +const DOM_GLOBALS_DEFINITION = 'lib.dom.d.ts'; + +const DISALLOWED_DOM_GLOBALS = [ + "window", + "document", + "HTMLElement", + "createElement" +]; + +const NODE_GLOBALS_DEFINITION = '@types/node'; + +const DISALLOWED_NODE_GLOBALS = [ + // https://nodejs.org/api/globals.html#globals_global_objects + "NodeJS", + "Buffer", + "__dirname", + "__filename", + "clearImmediate", + "exports", + "global", + "module", + "process", + "setImmediate" +]; + +interface IRule { + definition: string; + disallowedGlobals: string[]; + allowedGlobals: string[]; +} + +let hasErrors = false; + +function checkFile(program: ts.Program, sourceFile: ts.SourceFile, rule: IRule) { + checkNode(sourceFile); + + function checkNode(node: ts.Node): void { + if (node.kind !== ts.SyntaxKind.Identifier) { + return ts.forEachChild(node, checkNode); // recurse down + } + + const text = node.getText(sourceFile); + + if (!rule.disallowedGlobals.some(disallowedGlobal => disallowedGlobal === text)) { + return; // only if disallowed + } + + if (rule.allowedGlobals.some(allowed => allowed === text)) { + return; // override + } + + const checker = program.getTypeChecker(); + const symbol = checker.getSymbolAtLocation(node); + if (symbol) { + const declarations = symbol.declarations; + if (Array.isArray(declarations) && symbol.declarations.some(declaration => { + if (declaration) { + const parent = declaration.parent; + if (parent) { + const sourceFile = parent.getSourceFile(); + if (sourceFile) { + const fileName = sourceFile.fileName; + if (fileName && fileName.indexOf(rule.definition) >= 0) { + return true; + } + } + } + } + + return false; + })) { + const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); + console.log(`build/lib/globalsLinter.ts: Cannot use global '${text}' in ${sourceFile.fileName} (${line + 1},${character + 1})`); + + hasErrors = true; + } + } + } +} + +function createProgram(tsconfigPath: string): ts.Program { + const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); + + const configHostParser: ts.ParseConfigHost = { fileExists: existsSync, readDirectory: ts.sys.readDirectory, readFile: file => readFileSync(file, "utf8"), useCaseSensitiveFileNames: process.platform === 'linux' }; + const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, resolve(dirname(tsconfigPath)), { noEmit: true }); + + const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); + + return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); +} + +// +// Create program and start checking +// +const program = createProgram(TS_CONFIG_PATH); + +for (const sourceFile of program.getSourceFiles()) { + let noDomGlobalsLinter: { allowed: string[] } | undefined = undefined; + let noNodeJSGlobalsLinter: { allowed: string[] } | undefined = undefined; + + for (const rules of RULES["no-dom-globals"]) { + if (match([sourceFile.fileName], rules.target).length > 0) { + noDomGlobalsLinter = { allowed: rules.allowed }; + break; + } + } + + for (const rules of RULES["no-nodejs-globals"]) { + if (match([sourceFile.fileName], rules.target).length > 0) { + noNodeJSGlobalsLinter = { allowed: rules.allowed }; + break; + } + } + + if (!noDomGlobalsLinter && !noNodeJSGlobalsLinter) { + continue; // no rule to run + } + + // No DOM Globals + if (noDomGlobalsLinter) { + checkFile(program, sourceFile, { + definition: DOM_GLOBALS_DEFINITION, + disallowedGlobals: DISALLOWED_DOM_GLOBALS, + allowedGlobals: noDomGlobalsLinter.allowed + }); + } + + // No node.js Globals + if (noNodeJSGlobalsLinter) { + checkFile(program, sourceFile, { + definition: NODE_GLOBALS_DEFINITION, + disallowedGlobals: DISALLOWED_NODE_GLOBALS, + allowedGlobals: noNodeJSGlobalsLinter.allowed + }); + } +} + +if (hasErrors) { + process.exit(1); +} diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index e9a4f279631..d5fd9c166b5 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -58,6 +58,10 @@ "name": "vs/workbench/contrib/emmet", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/experiments", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/extensions", "project": "vscode-workbench" @@ -266,6 +270,10 @@ "name": "vs/workbench/services/remote", "project": "vscode-workbench" }, + { + "name": "vs/workbench/services/search", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/textfile", "project": "vscode-workbench" diff --git a/build/lib/tslint/abstractGlobalsRule.js b/build/lib/tslint/abstractGlobalsRule.js deleted file mode 100644 index 1566c0aa576..00000000000 --- a/build/lib/tslint/abstractGlobalsRule.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -class AbstractGlobalsRuleWalker extends Lint.RuleWalker { - constructor(file, program, opts, _config) { - super(file, opts); - this.program = program; - this._config = _config; - } - visitIdentifier(node) { - if (this.getDisallowedGlobals().some(disallowedGlobal => disallowedGlobal === node.text)) { - if (this._config.allowed && this._config.allowed.some(allowed => allowed === node.text)) { - return; // override - } - const checker = this.program.getTypeChecker(); - const symbol = checker.getSymbolAtLocation(node); - if (symbol) { - const declarations = symbol.declarations; - if (Array.isArray(declarations) && symbol.declarations.some(declaration => { - if (declaration) { - const parent = declaration.parent; - if (parent) { - const sourceFile = parent.getSourceFile(); - if (sourceFile) { - const fileName = sourceFile.fileName; - if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { - return true; - } - } - } - } - return false; - })) { - this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); - } - } - } - super.visitIdentifier(node); - } -} -exports.AbstractGlobalsRuleWalker = AbstractGlobalsRuleWalker; diff --git a/build/lib/tslint/abstractGlobalsRule.ts b/build/lib/tslint/abstractGlobalsRule.ts deleted file mode 100644 index 543720455c3..00000000000 --- a/build/lib/tslint/abstractGlobalsRule.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; - -interface AbstractGlobalsRuleConfig { - target: string; - allowed: string[]; -} - -export abstract class AbstractGlobalsRuleWalker extends Lint.RuleWalker { - - constructor(file: ts.SourceFile, private program: ts.Program, opts: Lint.IOptions, private _config: AbstractGlobalsRuleConfig) { - super(file, opts); - } - - protected abstract getDisallowedGlobals(): string[]; - - protected abstract getDefinitionPattern(): string; - - visitIdentifier(node: ts.Identifier) { - if (this.getDisallowedGlobals().some(disallowedGlobal => disallowedGlobal === node.text)) { - if (this._config.allowed && this._config.allowed.some(allowed => allowed === node.text)) { - return; // override - } - - const checker = this.program.getTypeChecker(); - const symbol = checker.getSymbolAtLocation(node); - if (symbol) { - const declarations = symbol.declarations; - if (Array.isArray(declarations) && symbol.declarations.some(declaration => { - if (declaration) { - const parent = declaration.parent; - if (parent) { - const sourceFile = parent.getSourceFile(); - if (sourceFile) { - const fileName = sourceFile.fileName; - if (fileName && fileName.indexOf(this.getDefinitionPattern()) >= 0) { - return true; - } - } - } - } - - return false; - })) { - this.addFailureAtNode(node, `Cannot use global '${node.text}' in '${this._config.target}'`); - } - } - } - - super.visitIdentifier(node); - } -} diff --git a/build/lib/tslint/noDomGlobalsRule.js b/build/lib/tslint/noDomGlobalsRule.js deleted file mode 100644 index a83ac8f7f59..00000000000 --- a/build/lib/tslint/noDomGlobalsRule.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const minimatch = require("minimatch"); -const abstractGlobalsRule_1 = require("./abstractGlobalsRule"); -class Rule extends Lint.Rules.TypedRule { - applyWithProgram(sourceFile, program) { - const configs = this.getOptions().ruleArguments; - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoDOMGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - return []; - } -} -exports.Rule = Rule; -class NoDOMGlobalsRuleWalker extends abstractGlobalsRule_1.AbstractGlobalsRuleWalker { - getDefinitionPattern() { - return 'lib.dom.d.ts'; - } - getDisallowedGlobals() { - // intentionally not complete - return [ - "window", - "document", - "HTMLElement" - ]; - } -} diff --git a/build/lib/tslint/noDomGlobalsRule.ts b/build/lib/tslint/noDomGlobalsRule.ts deleted file mode 100644 index df9e67bf78b..00000000000 --- a/build/lib/tslint/noDomGlobalsRule.ts +++ /dev/null @@ -1,45 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as minimatch from 'minimatch'; -import { AbstractGlobalsRuleWalker } from './abstractGlobalsRule'; - -interface NoDOMGlobalsRuleConfig { - target: string; - allowed: string[]; -} - -export class Rule extends Lint.Rules.TypedRule { - - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - const configs = this.getOptions().ruleArguments; - - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoDOMGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - - return []; - } -} - -class NoDOMGlobalsRuleWalker extends AbstractGlobalsRuleWalker { - - getDefinitionPattern(): string { - return 'lib.dom.d.ts'; - } - - getDisallowedGlobals(): string[] { - // intentionally not complete - return [ - "window", - "document", - "HTMLElement" - ]; - } -} diff --git a/build/lib/tslint/noNodejsGlobalsRule.js b/build/lib/tslint/noNodejsGlobalsRule.js deleted file mode 100644 index 8c36fa342c2..00000000000 --- a/build/lib/tslint/noNodejsGlobalsRule.js +++ /dev/null @@ -1,41 +0,0 @@ -"use strict"; -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -Object.defineProperty(exports, "__esModule", { value: true }); -const Lint = require("tslint"); -const minimatch = require("minimatch"); -const abstractGlobalsRule_1 = require("./abstractGlobalsRule"); -class Rule extends Lint.Rules.TypedRule { - applyWithProgram(sourceFile, program) { - const configs = this.getOptions().ruleArguments; - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoNodejsGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - return []; - } -} -exports.Rule = Rule; -class NoNodejsGlobalsRuleWalker extends abstractGlobalsRule_1.AbstractGlobalsRuleWalker { - getDefinitionPattern() { - return '@types/node'; - } - getDisallowedGlobals() { - // https://nodejs.org/api/globals.html#globals_global_objects - return [ - "NodeJS", - "Buffer", - "__dirname", - "__filename", - "clearImmediate", - "exports", - "global", - "module", - "process", - "setImmediate" - ]; - } -} diff --git a/build/lib/tslint/noNodejsGlobalsRule.ts b/build/lib/tslint/noNodejsGlobalsRule.ts deleted file mode 100644 index 7e5767d8570..00000000000 --- a/build/lib/tslint/noNodejsGlobalsRule.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as ts from 'typescript'; -import * as Lint from 'tslint'; -import * as minimatch from 'minimatch'; -import { AbstractGlobalsRuleWalker } from './abstractGlobalsRule'; - -interface NoNodejsGlobalsConfig { - target: string; - allowed: string[]; -} - -export class Rule extends Lint.Rules.TypedRule { - - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] { - const configs = this.getOptions().ruleArguments; - - for (const config of configs) { - if (minimatch(sourceFile.fileName, config.target)) { - return this.applyWithWalker(new NoNodejsGlobalsRuleWalker(sourceFile, program, this.getOptions(), config)); - } - } - - return []; - } -} - -class NoNodejsGlobalsRuleWalker extends AbstractGlobalsRuleWalker { - - getDefinitionPattern(): string { - return '@types/node'; - } - - getDisallowedGlobals(): string[] { - // https://nodejs.org/api/globals.html#globals_global_objects - return [ - "NodeJS", - "Buffer", - "__dirname", - "__filename", - "clearImmediate", - "exports", - "global", - "module", - "process", - "setImmediate" - ]; - } -} diff --git a/build/package.json b/build/package.json index bafb409c97b..7d72317ff5e 100644 --- a/build/package.json +++ b/build/package.json @@ -36,6 +36,7 @@ "gulp-uglify": "^3.0.0", "iconv-lite": "0.4.23", "mime": "^1.3.4", + "minimatch": "3.0.4", "minimist": "^1.2.0", "request": "^2.85.0", "terser": "4.3.8", diff --git a/extensions/configuration-editing/package.json b/extensions/configuration-editing/package.json index 5defc86cf5d..be2d2e8ebae 100644 --- a/extensions/configuration-editing/package.json +++ b/extensions/configuration-editing/package.json @@ -88,6 +88,10 @@ "fileMatch": "/.vscode/tasks.json", "url": "vscode://schemas/tasks" }, + { + "fileMatch": "%APP_SETTINGS_HOME%/tasks.json", + "url": "vscode://schemas/tasks" + }, { "fileMatch": "%APP_SETTINGS_HOME%/snippets/*.json", "url": "vscode://schemas/snippets" diff --git a/extensions/emmet/src/extension.ts b/extensions/emmet/src/extension.ts index a5195af577c..4fe40db638b 100644 --- a/extensions/emmet/src/extension.ts +++ b/extensions/emmet/src/extension.ts @@ -165,7 +165,7 @@ function registerCompletionProviders(context: vscode.ExtensionContext) { completionProvidersMapping.delete(language); } - const provider = vscode.languages.registerCompletionItemProvider([{ language, scheme: 'file' }, { language, scheme: 'untitled' }], completionProvider, ...LANGUAGE_MODES[includedLanguages[language]]); + const provider = vscode.languages.registerCompletionItemProvider({ language, scheme: '*' }, completionProvider, ...LANGUAGE_MODES[includedLanguages[language]]); context.subscriptions.push(provider); languageMappingForCompletionProviders.set(language, includedLanguages[language]); @@ -174,7 +174,7 @@ function registerCompletionProviders(context: vscode.ExtensionContext) { Object.keys(LANGUAGE_MODES).forEach(language => { if (!languageMappingForCompletionProviders.has(language)) { - const provider = vscode.languages.registerCompletionItemProvider([{ language, scheme: 'file' }, { language, scheme: 'untitled' }], completionProvider, ...LANGUAGE_MODES[language]); + const provider = vscode.languages.registerCompletionItemProvider({ language, scheme: '*' }, completionProvider, ...LANGUAGE_MODES[language]); context.subscriptions.push(provider); languageMappingForCompletionProviders.set(language, language); diff --git a/extensions/go/cgmanifest.json b/extensions/go/cgmanifest.json index a26813ed47e..f47768ba861 100644 --- a/extensions/go/cgmanifest.json +++ b/extensions/go/cgmanifest.json @@ -4,14 +4,14 @@ "component": { "type": "git", "git": { - "name": "language-go", - "repositoryUrl": "https://github.com/atom/language-go", - "commitHash": "b6fd68f74efa109679e31fe6f4a41ac105262d0e" + "name": "better-go-syntax", + "repositoryUrl": "https://github.com/jeff-hykin/better-go-syntax/ ", + "commitHash": "54ff898316f8647d77ffcf83880a9556445326f1" } }, "license": "MIT", - "description": "The file syntaxes/go.json was derived from the Atom package https://atom.io/packages/language-go.", - "version": "0.44.3" + "description": "The file syntaxes/go.tmLanguage.json is from https://github.com/jeff-hykin/better-go-syntax/ .", + "version": "1.0.0" } ], "version": 1 diff --git a/extensions/go/package.json b/extensions/go/package.json index a91d729adaf..121ce0c33e5 100644 --- a/extensions/go/package.json +++ b/extensions/go/package.json @@ -5,26 +5,36 @@ "version": "1.0.0", "publisher": "vscode", "license": "MIT", - "engines": { "vscode": "*" }, + "engines": { + "vscode": "*" + }, "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js atom/language-go grammars/go.cson ./syntaxes/go.tmLanguage.json" + "update-grammar": "node ../../build/npm/update-grammar.js jeff-hykin/better-go-syntax source/generated.tmLanguage.json ./syntaxes/go.tmLanguage.json" }, "contributes": { - "languages": [{ - "id": "go", - "extensions": [ ".go" ], - "aliases": [ "Go" ], - "configuration": "./language-configuration.json" - }], - "grammars": [{ - "language": "go", - "scopeName": "source.go", - "path": "./syntaxes/go.tmLanguage.json" - }], + "languages": [ + { + "id": "go", + "extensions": [ + ".go" + ], + "aliases": [ + "Go" + ], + "configuration": "./language-configuration.json" + } + ], + "grammars": [ + { + "language": "go", + "scopeName": "source.go", + "path": "./syntaxes/go.tmLanguage.json" + } + ], "configurationDefaults": { "[go]": { "editor.insertSpaces": false } } } -} \ No newline at end of file +} diff --git a/extensions/go/syntaxes/go.tmLanguage.json b/extensions/go/syntaxes/go.tmLanguage.json index bc73a1d91bd..ea726a30206 100644 --- a/extensions/go/syntaxes/go.tmLanguage.json +++ b/extensions/go/syntaxes/go.tmLanguage.json @@ -1,14 +1,16 @@ { "information_for_contributors": [ - "This file has been converted from https://github.com/atom/language-go/blob/master/grammars/go.cson", + "This file has been converted from https://github.com/jeff-hykin/better-go-syntax/blob/master/source/generated.tmLanguage.json", "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/atom/language-go/commit/b6fd68f74efa109679e31fe6f4a41ac105262d0e", + "version": "https://github.com/jeff-hykin/better-go-syntax/commit/54ff898316f8647d77ffcf83880a9556445326f1", "name": "Go", "scopeName": "source.go", - "comment": "Go language", "patterns": [ + { + "include": "#comments" + }, { "include": "#comments" }, @@ -350,24 +352,29 @@ "comments": { "patterns": [ { - "begin": "/\\*", - "end": "\\*/", - "captures": { - "0": { + "name": "comment.block.go", + "begin": "(\\/\\*)", + "beginCaptures": { + "1": { "name": "punctuation.definition.comment.go" } }, - "name": "comment.block.go" + "end": "(\\*\\/)", + "endCaptures": { + "1": { + "name": "punctuation.definition.comment.go" + } + } }, { - "begin": "//", + "name": "comment.line.double-slash.go", + "begin": "(\\/\\/)", "beginCaptures": { - "0": { + "1": { "name": "punctuation.definition.comment.go" } }, - "end": "$", - "name": "comment.line.double-slash.go" + "end": "(?:\\n|$)" } ] }, diff --git a/extensions/html-language-features/client/src/htmlMain.ts b/extensions/html-language-features/client/src/htmlMain.ts index e8a9a2fa22b..2df5e7f5c13 100644 --- a/extensions/html-language-features/client/src/htmlMain.ts +++ b/extensions/html-language-features/client/src/htmlMain.ts @@ -8,8 +8,15 @@ import * as fs from 'fs'; import * as nls from 'vscode-nls'; const localize = nls.loadMessageBundle(); -import { languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList } from 'vscode'; -import { LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, DocumentRangeFormattingRequest, ProvideCompletionItemsSignature } from 'vscode-languageclient'; +import { + languages, ExtensionContext, IndentAction, Position, TextDocument, Range, CompletionItem, CompletionItemKind, SnippetString, workspace, + Disposable, FormattingOptions, CancellationToken, ProviderResult, TextEdit, CompletionContext, CompletionList, SemanticTokensLegend, + SemanticTokensProvider, SemanticTokens +} from 'vscode'; +import { + LanguageClient, LanguageClientOptions, ServerOptions, TransportKind, RequestType, TextDocumentPositionParams, DocumentRangeFormattingParams, + DocumentRangeFormattingRequest, ProvideCompletionItemsSignature, TextDocumentIdentifier, RequestType0, Range as LspRange +} from 'vscode-languageclient'; import { EMPTY_ELEMENTS } from './htmlEmptyTagsShared'; import { activateTagClosing } from './tagClosing'; import TelemetryReporter from 'vscode-extension-telemetry'; @@ -23,6 +30,18 @@ namespace MatchingTagPositionRequest { export const type: RequestType = new RequestType('html/matchingTagPosition'); } +// experimental: semantic tokens +interface SemanticTokenParams { + textDocument: TextDocumentIdentifier; + ranges?: LspRange[]; +} +namespace SemanticTokenRequest { + export const type: RequestType = new RequestType('html/semanticTokens'); +} +namespace SemanticTokenLegendRequest { + export const type: RequestType0<{ types: string[]; modifiers: string[] } | null, any, any> = new RequestType0('html/semanticTokenLegend'); +} + interface IPackageInfo { name: string; version: string; @@ -132,6 +151,24 @@ export function activate(context: ExtensionContext) { updateFormatterRegistration(); toDispose.push({ dispose: () => rangeFormatting && rangeFormatting.dispose() }); toDispose.push(workspace.onDidChangeConfiguration(e => e.affectsConfiguration('html.format.enable') && updateFormatterRegistration())); + + client.sendRequest(SemanticTokenLegendRequest.type).then(legend => { + if (legend) { + const provider: SemanticTokensProvider = { + provideSemanticTokens(doc, opts) { + const params: SemanticTokenParams = { + textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(doc), + ranges: opts.ranges?.map(r => client.code2ProtocolConverter.asRange(r)) + }; + return client.sendRequest(SemanticTokenRequest.type, params).then(data => { + return data && new SemanticTokens(new Uint32Array(data)); + }); + } + }; + toDispose.push(languages.registerSemanticTokensProvider(documentSelector, provider, new SemanticTokensLegend(legend.types, legend.modifiers))); + } + }); + }); function updateFormatterRegistration() { diff --git a/extensions/html-language-features/client/src/mirrorCursor.ts b/extensions/html-language-features/client/src/mirrorCursor.ts index 43ea3169c97..5c9ae446087 100644 --- a/extensions/html-language-features/client/src/mirrorCursor.ts +++ b/extensions/html-language-features/client/src/mirrorCursor.ts @@ -54,6 +54,10 @@ export function activateMirrorCursor( return; } + if (event.textEditor.document?.languageId !== 'html' && event.textEditor.document?.languageId !== 'handlebars') { + return; + } + prevCursors = cursors; cursors = event.selections; @@ -66,13 +70,13 @@ export function activateMirrorCursor( if (event.selections[0].isEmpty) { matchingTagPositionProvider(event.textEditor.document, event.selections[0].active).then(matchingTagPosition => { if (matchingTagPosition && window.activeTextEditor) { - const charBeforeAndAfterPositionsRoughtlyEqual = isCharBeforeAndAfterPositionsRoughtlyEqual( + const charBeforeAndAfterPositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( event.textEditor.document, event.selections[0].anchor, new Position(matchingTagPosition.line, matchingTagPosition.character) ); - if (charBeforeAndAfterPositionsRoughtlyEqual) { + if (charBeforeAndAfterPositionsRoughlyEqual) { inMirrorMode = true; const newCursor = new Selection( matchingTagPosition.line, @@ -93,6 +97,9 @@ export function activateMirrorCursor( }; if (cursors.length === 2 && inMirrorMode) { + /** + * Both cursors are positions + */ if (event.selections[0].isEmpty && event.selections[1].isEmpty) { if ( prevCursors.length === 2 && @@ -103,13 +110,13 @@ export function activateMirrorCursor( return; } - const charBeforeAndAfterPositionsRoughtlyEqual = isCharBeforeAndAfterPositionsRoughtlyEqual( + const charBeforeAndAfterPositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( event.textEditor.document, event.selections[0].anchor, event.selections[1].anchor ); - if (!charBeforeAndAfterPositionsRoughtlyEqual) { + if (!charBeforeAndAfterPositionsRoughlyEqual) { exitMirrorMode(); return; } else { @@ -128,6 +135,25 @@ export function activateMirrorCursor( workspace.applyEdit(cleanupEdit); } } + } else { + /** + * Both cursors are selections + */ + const charBeforeAndAfterAnchorPositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( + event.textEditor.document, + event.selections[0].anchor, + event.selections[1].anchor + ); + + const charBeforeAndAfterActivePositionsRoughlyEqual = isCharBeforeAndAfterPositionsRoughlyEqual( + event.textEditor.document, + event.selections[0].active, + event.selections[1].active + ); + + if (!charBeforeAndAfterAnchorPositionsRoughlyEqual || !charBeforeAndAfterActivePositionsRoughlyEqual) { + exitMirrorMode(); + } } } } @@ -154,8 +180,8 @@ function getCharAfter(document: TextDocument, position: Position) { } // Check if chars before and after the two positions are equal -// For the chars before, `<` and `/` are consiered equal to handle the case of `<|>` -function isCharBeforeAndAfterPositionsRoughtlyEqual(document: TextDocument, firstPos: Position, secondPos: Position) { +// For the chars before, `<` and `/` are considered equal to handle the case of `<|>` +function isCharBeforeAndAfterPositionsRoughlyEqual(document: TextDocument, firstPos: Position, secondPos: Position) { const charBeforePrimarySelection = getCharBefore(document, firstPos); const charAfterPrimarySelection = getCharAfter(document, firstPos); const charBeforeSecondarySelection = getCharBefore(document, secondPos); diff --git a/extensions/html-language-features/server/src/htmlServerMain.ts b/extensions/html-language-features/server/src/htmlServerMain.ts index ba4d4b8e1aa..2289c38c950 100644 --- a/extensions/html-language-features/server/src/htmlServerMain.ts +++ b/extensions/html-language-features/server/src/htmlServerMain.ts @@ -6,11 +6,13 @@ import { createConnection, IConnection, TextDocuments, InitializeParams, InitializeResult, RequestType, DocumentRangeFormattingRequest, Disposable, DocumentSelector, TextDocumentPositionParams, ServerCapabilities, - Position, ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, - WorkspaceFolder, DocumentColorRequest, ColorInformation, ColorPresentationRequest, TextDocumentSyncKind + ConfigurationRequest, ConfigurationParams, DidChangeWorkspaceFoldersNotification, + DocumentColorRequest, ColorPresentationRequest, TextDocumentSyncKind } from 'vscode-languageserver'; -import { TextDocument, Diagnostic, DocumentLink, SymbolInformation } from 'vscode-html-languageservice'; -import { getLanguageModes, LanguageModes, Settings } from './modes/languageModes'; +import { + getLanguageModes, LanguageModes, Settings, TextDocument, Position, Diagnostic, WorkspaceFolder, ColorInformation, + Range, DocumentLink, SymbolInformation, TextDocumentIdentifier +} from './modes/languageModes'; import { format } from './modes/formatting'; import { pushAll } from './utils/arrays'; @@ -29,6 +31,18 @@ namespace MatchingTagPositionRequest { export const type: RequestType = new RequestType('html/matchingTagPosition'); } +// experimental: semantic tokens +interface SemanticTokenParams { + textDocument: TextDocumentIdentifier; + ranges?: Range[]; +} +namespace SemanticTokenRequest { + export const type: RequestType = new RequestType('html/semanticTokens'); +} +namespace SemanticTokenLegendRequest { + export const type: RequestType = new RequestType('html/semanticTokenLegend'); +} + // Create a connection for the server const connection: IConnection = createConnection(); @@ -500,5 +514,45 @@ connection.onRequest(MatchingTagPositionRequest.type, (params, token) => { }, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token); }); +connection.onRequest(MatchingTagPositionRequest.type, (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + const pos = params.position; + if (pos.character > 0) { + const mode = languageModes.getModeAtPosition(document, Position.create(pos.line, pos.character - 1)); + if (mode && mode.findMatchingTagPosition) { + return mode.findMatchingTagPosition(document, pos); + } + } + } + return null; + }, null, `Error while computing matching tag position for ${params.textDocument.uri}`, token); +}); + +connection.onRequest(SemanticTokenRequest.type, (params, token) => { + return runSafe(() => { + const document = documents.get(params.textDocument.uri); + if (document) { + const jsMode = languageModes.getMode('javascript'); + if (jsMode && jsMode.getSemanticTokens) { + return jsMode.getSemanticTokens(document, params.ranges); + } + } + return null; + }, null, `Error while computing semantic tokens for ${params.textDocument.uri}`, token); +}); + +connection.onRequest(SemanticTokenLegendRequest.type, (_params, token) => { + return runSafe(() => { + const jsMode = languageModes.getMode('javascript'); + if (jsMode && jsMode.getSemanticTokenLegend) { + return jsMode.getSemanticTokenLegend(); + } + return null; + }, null, `Error while computing semantic tokens legend`, token); +}); + + // Listen on the connection connection.listen(); diff --git a/extensions/html-language-features/server/src/languageModelCache.ts b/extensions/html-language-features/server/src/languageModelCache.ts index c022b8047a9..06d331bca8a 100644 --- a/extensions/html-language-features/server/src/languageModelCache.ts +++ b/extensions/html-language-features/server/src/languageModelCache.ts @@ -18,10 +18,10 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime let cleanupInterval: NodeJS.Timer | undefined = undefined; if (cleanupIntervalTimeInSec > 0) { cleanupInterval = setInterval(() => { - let cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; - let uris = Object.keys(languageModels); - for (let uri of uris) { - let languageModelInfo = languageModels[uri]; + const cutoffTime = Date.now() - cleanupIntervalTimeInSec * 1000; + const uris = Object.keys(languageModels); + for (const uri of uris) { + const languageModelInfo = languageModels[uri]; if (languageModelInfo.cTime < cutoffTime) { delete languageModels[uri]; nModels--; @@ -32,14 +32,14 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime return { get(document: TextDocument): T { - let version = document.version; - let languageId = document.languageId; - let languageModelInfo = languageModels[document.uri]; + const version = document.version; + const languageId = document.languageId; + const languageModelInfo = languageModels[document.uri]; if (languageModelInfo && languageModelInfo.version === version && languageModelInfo.languageId === languageId) { languageModelInfo.cTime = Date.now(); return languageModelInfo.languageModel; } - let languageModel = parse(document); + const languageModel = parse(document); languageModels[document.uri] = { languageModel, version, languageId, cTime: Date.now() }; if (!languageModelInfo) { nModels++; @@ -48,8 +48,8 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime if (nModels === maxEntries) { let oldestTime = Number.MAX_VALUE; let oldestUri = null; - for (let uri in languageModels) { - let languageModelInfo = languageModels[uri]; + for (const uri in languageModels) { + const languageModelInfo = languageModels[uri]; if (languageModelInfo.cTime < oldestTime) { oldestUri = uri; oldestTime = languageModelInfo.cTime; @@ -64,7 +64,7 @@ export function getLanguageModelCache(maxEntries: number, cleanupIntervalTime }, onDocumentRemoved(document: TextDocument) { - let uri = document.uri; + const uri = document.uri; if (languageModels[uri]) { delete languageModels[uri]; nModels--; diff --git a/extensions/html-language-features/server/src/modes/cssMode.ts b/extensions/html-language-features/server/src/modes/cssMode.ts index 490f7ea2fa4..e705f44bbab 100644 --- a/extensions/html-language-features/server/src/modes/cssMode.ts +++ b/extensions/html-language-features/server/src/modes/cssMode.ts @@ -4,11 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache'; -import { TextDocument, Position, Range, CompletionList } from 'vscode-html-languageservice'; -import { Stylesheet, FoldingRange, LanguageService as CSSLanguageService } from 'vscode-css-languageservice'; -import { LanguageMode, Workspace } from './languageModes'; +import { Stylesheet, LanguageService as CSSLanguageService } from 'vscode-css-languageservice'; +import { FoldingRange, LanguageMode, Workspace, Color, TextDocument, Position, Range, CompletionList } from './languageModes'; import { HTMLDocumentRegions, CSS_STYLE_RULE } from './embeddedSupport'; -import { Color } from 'vscode-languageserver'; export function getCSSMode(cssLanguageService: CSSLanguageService, documentRegions: LanguageModelCache, workspace: Workspace): LanguageMode { let embeddedCSSDocuments = getLanguageModelCache(10, 60, document => documentRegions.get(document).getEmbeddedDocument('css')); diff --git a/extensions/html-language-features/server/src/modes/embeddedSupport.ts b/extensions/html-language-features/server/src/modes/embeddedSupport.ts index df47be3ed97..837842c6405 100644 --- a/extensions/html-language-features/server/src/modes/embeddedSupport.ts +++ b/extensions/html-language-features/server/src/modes/embeddedSupport.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Position, LanguageService, TokenType, Range } from 'vscode-html-languageservice'; +import { TextDocument, Position, LanguageService, TokenType, Range } from './languageModes'; export interface LanguageRange extends Range { languageId: string | undefined; diff --git a/extensions/html-language-features/server/src/modes/formatting.ts b/extensions/html-language-features/server/src/modes/formatting.ts index a9df4b4a30b..4acb60a9b9a 100644 --- a/extensions/html-language-features/server/src/modes/formatting.ts +++ b/extensions/html-language-features/server/src/modes/formatting.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, Range, TextEdit, FormattingOptions, Position } from 'vscode-html-languageservice'; -import { LanguageModes, Settings, LanguageModeRange } from './languageModes'; +import { LanguageModes, Settings, LanguageModeRange, TextDocument, Range, TextEdit, FormattingOptions, Position } from './languageModes'; import { pushAll } from '../utils/arrays'; import { isEOL } from '../utils/strings'; diff --git a/extensions/html-language-features/server/src/modes/htmlFolding.ts b/extensions/html-language-features/server/src/modes/htmlFolding.ts index aacdcc604fc..5eccc0ef876 100644 --- a/extensions/html-language-features/server/src/modes/htmlFolding.ts +++ b/extensions/html-language-features/server/src/modes/htmlFolding.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TextDocument, CancellationToken, Position, Range } from 'vscode-languageserver'; -import { FoldingRange } from 'vscode-html-languageservice'; -import { LanguageModes, LanguageMode } from './languageModes'; +import { TextDocument, FoldingRange, Position, Range, LanguageModes, LanguageMode } from './languageModes'; +import { CancellationToken } from 'vscode-languageserver'; export function getFoldingRanges(languageModes: LanguageModes, document: TextDocument, maxRanges: number | undefined, _cancellationToken: CancellationToken | null): FoldingRange[] { let htmlMode = languageModes.getMode('html'); diff --git a/extensions/html-language-features/server/src/modes/htmlMode.ts b/extensions/html-language-features/server/src/modes/htmlMode.ts index 6f2461d6c20..251821d1272 100644 --- a/extensions/html-language-features/server/src/modes/htmlMode.ts +++ b/extensions/html-language-features/server/src/modes/htmlMode.ts @@ -7,9 +7,9 @@ import { getLanguageModelCache } from '../languageModelCache'; import { LanguageService as HTMLLanguageService, HTMLDocument, DocumentContext, FormattingOptions, HTMLFormatConfiguration, SelectionRange, - TextDocument, Position, Range, CompletionItem, FoldingRange -} from 'vscode-html-languageservice'; -import { LanguageMode, Workspace } from './languageModes'; + TextDocument, Position, Range, CompletionItem, FoldingRange, + LanguageMode, Workspace +} from './languageModes'; import { getPathCompletionParticipant } from './pathCompletion'; export function getHTMLMode(htmlLanguageService: HTMLLanguageService, workspace: Workspace): LanguageMode { diff --git a/extensions/html-language-features/server/src/modes/javascriptMode.ts b/extensions/html-language-features/server/src/modes/javascriptMode.ts index 0bd4259f883..d9c1b1623ad 100644 --- a/extensions/html-language-features/server/src/modes/javascriptMode.ts +++ b/extensions/html-language-features/server/src/modes/javascriptMode.ts @@ -7,16 +7,18 @@ import { LanguageModelCache, getLanguageModelCache } from '../languageModelCache import { SymbolInformation, SymbolKind, CompletionItem, Location, SignatureHelp, SignatureInformation, ParameterInformation, Definition, TextEdit, TextDocument, Diagnostic, DiagnosticSeverity, Range, CompletionItemKind, Hover, MarkedString, - DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange -} from 'vscode-html-languageservice'; -import { LanguageMode, Settings } from './languageModes'; + DocumentHighlight, DocumentHighlightKind, CompletionList, Position, FormattingOptions, FoldingRange, FoldingRangeKind, SelectionRange, + LanguageMode, Settings +} from './languageModes'; import { getWordAtText, startsWith, isWhitespaceOnly, repeat } from '../utils/strings'; import { HTMLDocumentRegions } from './embeddedSupport'; import * as ts from 'typescript'; import { join } from 'path'; +import { getSemanticTokens, getSemanticTokenLegend } from './javascriptSemanticTokens'; const FILE_NAME = 'vscode://javascript/1'; // the same 'file' is used for all contents +const TS_FILE_NAME = 'vscode://javascript/2.ts'; const JS_WORD_REGEX = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; let jquery_d_ts = join(__dirname, '../lib/jquery.d.ts'); // when packaged @@ -38,10 +40,10 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache compilerOptions, - getScriptFileNames: () => [FILE_NAME, jquery_d_ts], - getScriptKind: () => ts.ScriptKind.JS, + getScriptFileNames: () => [FILE_NAME, TS_FILE_NAME, jquery_d_ts], + getScriptKind: (fileName) => fileName === TS_FILE_NAME ? ts.ScriptKind.TS : ts.ScriptKind.JS, getScriptVersion: (fileName: string) => { - if (fileName === FILE_NAME) { + if (fileName === FILE_NAME || fileName === TS_FILE_NAME) { return String(scriptFileVersion); } return '1'; // default lib an jquery.d.ts are static @@ -49,7 +51,7 @@ export function getJavaScriptMode(documentRegions: LanguageModelCache { let text = ''; if (startsWith(fileName, 'vscode:')) { - if (fileName === FILE_NAME) { + if (fileName === FILE_NAME || fileName === TS_FILE_NAME) { text = currentTextDocument.getText(); } } else { @@ -314,6 +316,16 @@ export function getJavaScriptMode(documentRegions: LanguageModelCachenode.parent).name === node) { + modifierSet = TokenModifier.declaration; + } + } + const modifiers = ts.getCombinedModifierFlags(decl); + if (modifiers & ts.ModifierFlags.Static) { + modifierSet |= TokenModifier.static; + } + if (modifiers & ts.ModifierFlags.Async) { + modifierSet |= TokenModifier.async; + } + if (typeIdx !== undefined) { + resultTokens.push({ offset: node.getStart(), length: node.getWidth(), typeIdx, modifierSet }); + } + } + } + } + + ts.forEachChild(node, visit); + } + const sourceFile = program.getSourceFile(fileName); + if (sourceFile) { + visit(sourceFile); + } + } + + + resultTokens = resultTokens.sort((d1, d2) => d1.offset - d2.offset); + const offsetRanges = ranges.map(r => ({ startOffset: currentTextDocument.offsetAt(r.start), endOffset: currentTextDocument.offsetAt(r.end) })).sort((d1, d2) => d1.startOffset - d2.startOffset); + + let rangeIndex = 0; + let currRange = offsetRanges[rangeIndex++]; + + let prefLine = 0; + let prevChar = 0; + + let encodedResult: number[] = []; + + for (let k = 0; k < resultTokens.length && currRange; k++) { + const curr = resultTokens[k]; + if (currRange.startOffset <= curr.offset && curr.offset + curr.length <= currRange.endOffset) { + // token inside a range + + const startPos = currentTextDocument.positionAt(curr.offset); + if (prefLine !== startPos.line) { + prevChar = 0; + } + encodedResult.push(startPos.line - prefLine); // line delta + encodedResult.push(startPos.character - prevChar); // line delta + encodedResult.push(curr.length); // length + encodedResult.push(curr.typeIdx); // tokenType + encodedResult.push(curr.modifierSet); // tokenModifier + + prefLine = startPos.line; + prevChar = startPos.character; + + } else if (currRange.endOffset >= curr.offset) { + currRange = offsetRanges[rangeIndex++]; + } + } + return encodedResult; +} + + +export function getSemanticTokenLegend() { + return { types: tokenTypes, modifiers: tokenModifiers }; +} + + +const tokenTypes: string[] = ['class', 'enum', 'interface', 'namespace', 'parameterType', 'type', 'parameter', 'variable', 'property', 'constant', 'function', 'member']; +const tokenModifiers: string[] = ['declaration', 'static', 'async']; + +enum TokenType { + 'class' = 0, + 'enum' = 1, + 'interface' = 2, + 'namespace' = 3, + 'parameterType' = 4, + 'type' = 5, + 'parameter' = 6, + 'variable' = 7, + 'property' = 8, + 'constant' = 9, + 'function' = 10, + 'member' = 11 +} + +enum TokenModifier { + 'declaration' = 0x01, + 'static' = 0x02, + 'async' = 0x04, +} + +// const tokenFromClassificationMapping: { [name: string]: TokenType } = { +// [ts.ClassificationTypeNames.className]: TokenType.class, +// [ts.ClassificationTypeNames.enumName]: TokenType.enum, +// [ts.ClassificationTypeNames.interfaceName]: TokenType.interface, +// [ts.ClassificationTypeNames.moduleName]: TokenType.namespace, +// [ts.ClassificationTypeNames.typeParameterName]: TokenType.parameterType, +// [ts.ClassificationTypeNames.typeAliasName]: TokenType.type, +// [ts.ClassificationTypeNames.parameterName]: TokenType.parameter +// }; + +const tokenFromDeclarationMapping: { [name: string]: TokenType } = { + [ts.SyntaxKind.VariableDeclaration]: TokenType.variable, + [ts.SyntaxKind.Parameter]: TokenType.parameter, + [ts.SyntaxKind.PropertyDeclaration]: TokenType.property, + [ts.SyntaxKind.ModuleDeclaration]: TokenType.namespace, + [ts.SyntaxKind.EnumDeclaration]: TokenType.enum, + [ts.SyntaxKind.EnumMember]: TokenType.property, + [ts.SyntaxKind.ClassDeclaration]: TokenType.class, + [ts.SyntaxKind.MethodDeclaration]: TokenType.member, + [ts.SyntaxKind.FunctionDeclaration]: TokenType.function, + [ts.SyntaxKind.MethodSignature]: TokenType.member, + [ts.SyntaxKind.GetAccessor]: TokenType.property, + [ts.SyntaxKind.PropertySignature]: TokenType.property, +}; diff --git a/extensions/html-language-features/server/src/modes/languageModes.ts b/extensions/html-language-features/server/src/modes/languageModes.ts index 51e1cfc7cfc..e522de5cb86 100644 --- a/extensions/html-language-features/server/src/modes/languageModes.ts +++ b/extensions/html-language-features/server/src/modes/languageModes.ts @@ -17,7 +17,8 @@ import { getDocumentRegions, HTMLDocumentRegions } from './embeddedSupport'; import { getHTMLMode } from './htmlMode'; import { getJavaScriptMode } from './javascriptMode'; -export { ColorInformation, ColorPresentation, Color }; +export * from 'vscode-html-languageservice'; +export { WorkspaceFolder } from 'vscode-languageserver'; export interface Settings { css?: any; @@ -51,6 +52,8 @@ export interface LanguageMode { findMatchingTagPosition?: (document: TextDocument, position: Position) => Position | null; getFoldingRanges?: (document: TextDocument) => FoldingRange[]; onDocumentRemoved(document: TextDocument): void; + getSemanticTokens?(document: TextDocument, ranges: Range[] | undefined): number[]; + getSemanticTokenLegend?(): { types: string[], modifiers: string[] }; dispose(): void; } diff --git a/extensions/html-language-features/server/src/modes/pathCompletion.ts b/extensions/html-language-features/server/src/modes/pathCompletion.ts index 278d967bd95..d522efdc0df 100644 --- a/extensions/html-language-features/server/src/modes/pathCompletion.ts +++ b/extensions/html-language-features/server/src/modes/pathCompletion.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { WorkspaceFolder } from 'vscode-languageserver'; import * as path from 'path'; import * as fs from 'fs'; import { URI } from 'vscode-uri'; -import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position } from 'vscode-html-languageservice'; +import { ICompletionParticipant, TextDocument, CompletionItemKind, CompletionItem, TextEdit, Range, Position, WorkspaceFolder } from './languageModes'; import { startsWith } from '../utils/strings'; import { contains } from '../utils/arrays'; diff --git a/extensions/html-language-features/server/src/modes/selectionRanges.ts b/extensions/html-language-features/server/src/modes/selectionRanges.ts index ab6cbdc0f36..6c0627f6356 100644 --- a/extensions/html-language-features/server/src/modes/selectionRanges.ts +++ b/extensions/html-language-features/server/src/modes/selectionRanges.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { LanguageModes } from './languageModes'; -import { TextDocument, Position, Range, SelectionRange } from 'vscode-html-languageservice'; +import { LanguageModes, TextDocument, Position, Range, SelectionRange } from './languageModes'; export function getSelectionRanges(languageModes: LanguageModes, document: TextDocument, positions: Position[]) { const htmlMode = languageModes.getMode('html'); diff --git a/extensions/html-language-features/server/src/test/completions.test.ts b/extensions/html-language-features/server/src/test/completions.test.ts index e0e3a7e22ca..9c3b8f0831b 100644 --- a/extensions/html-language-features/server/src/test/completions.test.ts +++ b/extensions/html-language-features/server/src/test/completions.test.ts @@ -6,10 +6,7 @@ import 'mocha'; import * as assert from 'assert'; import * as path from 'path'; import { URI } from 'vscode-uri'; -import { getLanguageModes } from '../modes/languageModes'; -import { WorkspaceFolder } from 'vscode-languageserver'; -import { TextDocument, CompletionList, CompletionItemKind, ClientCapabilities } from 'vscode-html-languageservice'; - +import { getLanguageModes, WorkspaceFolder, TextDocument, CompletionList, CompletionItemKind, ClientCapabilities} from '../modes/languageModes'; export interface ItemDescription { label: string; documentation?: string; diff --git a/extensions/html-language-features/server/src/test/embedded.test.ts b/extensions/html-language-features/server/src/test/embedded.test.ts index 4191cfcb9b5..525d5a59c11 100644 --- a/extensions/html-language-features/server/src/test/embedded.test.ts +++ b/extensions/html-language-features/server/src/test/embedded.test.ts @@ -5,31 +5,32 @@ import 'mocha'; import * as assert from 'assert'; import * as embeddedSupport from '../modes/embeddedSupport'; -import { getLanguageService, TextDocument } from 'vscode-html-languageservice'; +import { getLanguageService } from 'vscode-html-languageservice'; +import { TextDocument } from '../modes/languageModes'; suite('HTML Embedded Support', () => { - var htmlLanguageService = getLanguageService(); + const htmlLanguageService = getLanguageService(); function assertLanguageId(value: string, expectedLanguageId: string | undefined): void { - let offset = value.indexOf('|'); + const offset = value.indexOf('|'); value = value.substr(0, offset) + value.substr(offset + 1); - let document = TextDocument.create('test://test/test.html', 'html', 0, value); + const document = TextDocument.create('test://test/test.html', 'html', 0, value); - let position = document.positionAt(offset); + const position = document.positionAt(offset); - let docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); - let languageId = docRegions.getLanguageAtPosition(position); + const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); + const languageId = docRegions.getLanguageAtPosition(position); assert.equal(languageId, expectedLanguageId); } function assertEmbeddedLanguageContent(value: string, languageId: string, expectedContent: string): void { - let document = TextDocument.create('test://test/test.html', 'html', 0, value); + const document = TextDocument.create('test://test/test.html', 'html', 0, value); - let docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); - let content = docRegions.getEmbeddedDocument(languageId); + const docRegions = embeddedSupport.getDocumentRegions(htmlLanguageService, document); + const content = docRegions.getEmbeddedDocument(languageId); assert.equal(content.getText(), expectedContent); } diff --git a/extensions/html-language-features/server/src/test/folding.test.ts b/extensions/html-language-features/server/src/test/folding.test.ts index f732a6cd72c..c86f3e40f98 100644 --- a/extensions/html-language-features/server/src/test/folding.test.ts +++ b/extensions/html-language-features/server/src/test/folding.test.ts @@ -5,9 +5,8 @@ import 'mocha'; import * as assert from 'assert'; -import { TextDocument } from 'vscode-html-languageservice'; import { getFoldingRanges } from '../modes/htmlFolding'; -import { getLanguageModes } from '../modes/languageModes'; +import { TextDocument, getLanguageModes } from '../modes/languageModes'; import { ClientCapabilities } from 'vscode-css-languageservice'; interface ExpectedIndentRange { @@ -17,13 +16,13 @@ interface ExpectedIndentRange { } function assertRanges(lines: string[], expected: ExpectedIndentRange[], message?: string, nRanges?: number): void { - let document = TextDocument.create('test://foo/bar.json', 'json', 1, lines.join('\n')); - let workspace = { + const document = TextDocument.create('test://foo/bar.html', 'html', 1, lines.join('\n')); + const workspace = { settings: {}, folders: [{ name: 'foo', uri: 'test://foo' }] }; - let languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST); - let actual = getFoldingRanges(languageModes, document, nRanges, null); + const languageModes = getLanguageModes({ css: true, javascript: true }, workspace, ClientCapabilities.LATEST); + const actual = getFoldingRanges(languageModes, document, nRanges, null); let actualRanges = []; for (let i = 0; i < actual.length; i++) { @@ -40,7 +39,7 @@ function r(startLine: number, endLine: number, kind?: string): ExpectedIndentRan suite('HTML Folding', () => { test('Embedded JavaScript', () => { - let input = [ + const input = [ /*0*/'', /*1*/'', /*2*/'', + /*10*/'', + /*11*/'', + ]; + assertTokens(input, [ + t(3, 6, 1, 'variable.declaration'), t(3, 13, 2, 'variable.declaration'), t(3, 19, 1, 'variable'), + t(5, 15, 1, 'variable.declaration'), t(5, 20, 2, 'variable'), + t(6, 11, 1, 'variable.declaration'), + t(7, 10, 2, 'variable') + ]); + }); + + test('function', () => { + const input = [ + /*0*/'', + /*1*/'', + /*2*/'', + /*8*/'', + /*9*/'', + ]; + assertTokens(input, [ + t(3, 11, 3, 'function.declaration'), t(3, 15, 2, 'parameter.declaration'), + t(4, 11, 3, 'function'), t(4, 15, 4, 'variable'), t(4, 20, 3, 'member'), t(4, 24, 2, 'parameter'), + t(6, 6, 6, 'variable'), t(6, 13, 8, 'property'), t(6, 24, 5, 'member'), t(6, 35, 7, 'member'), t(6, 43, 1, 'parameter.declaration'), t(6, 48, 3, 'function'), t(6, 52, 1, 'parameter') + ]); + }); + + test('members', () => { + const input = [ + /*0*/'', + /*1*/'', + /*2*/'', + /*12*/'', + /*13*/'', + ]; + + + assertTokens(input, [ + t(3, 8, 1, 'class.declaration'), + t(4, 11, 1, 'property.declaration.static'), + t(5, 4, 1, 'property.declaration'), + t(6, 10, 1, 'member.declaration.async'), t(6, 23, 1, 'class'), t(6, 25, 1, 'property.static'), t(6, 40, 1, 'member.async'), + t(7, 8, 1, 'property.declaration'), t(7, 26, 1, 'property'), + t(8, 11, 1, 'member.declaration.static'), t(8, 28, 1, 'class'), t(8, 32, 1, 'property'), + ]); + }); + + + +}); diff --git a/extensions/html-language-features/server/src/utils/documentContext.ts b/extensions/html-language-features/server/src/utils/documentContext.ts index 6c391064fae..0f728860de8 100644 --- a/extensions/html-language-features/server/src/utils/documentContext.ts +++ b/extensions/html-language-features/server/src/utils/documentContext.ts @@ -3,10 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DocumentContext } from 'vscode-html-languageservice'; +import { DocumentContext, WorkspaceFolder } from '../modes/languageModes'; import { endsWith, startsWith } from '../utils/strings'; import * as url from 'url'; -import { WorkspaceFolder } from 'vscode-languageserver'; export function getDocumentContext(documentUri: string, workspaceFolders: WorkspaceFolder[]): DocumentContext { function getRootFolder(): string | undefined { diff --git a/extensions/html/build/update-grammar.js b/extensions/html/build/update-grammar.js new file mode 100644 index 00000000000..3277cb16cca --- /dev/null +++ b/extensions/html/build/update-grammar.js @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +'use strict'; + +var updateGrammar = require('../../../build/npm/update-grammar'); + +function patchGrammar(grammar) { + let patchCount = 0; + + let visit = function (rule, parent) { + if (rule.name === 'source.js' || rule.name === 'source.css') { + if (parent.parent && parent.parent.property === 'endCaptures') { + rule.name = rule.name + '-ignored-vscode'; + patchCount++; + } + } + for (let property in rule) { + let value = rule[property]; + if (typeof value === 'object') { + visit(value, { node: rule, property: property, parent: parent }); + } + } + }; + + let repository = grammar.repository; + for (let key in repository) { + visit(repository[key], { node: repository, property: key, parent: undefined }); + } + if (patchCount !== 6) { + console.warn(`Expected to patch 6 occurrences of source.js & source.css: Was ${patchCount}`); + } + + + return grammar; +} + +const tsGrammarRepo = 'textmate/html.tmbundle'; +const grammarPath = 'Syntaxes/HTML.plist'; +updateGrammar.update(tsGrammarRepo, grammarPath, './syntaxes/html.tmLanguage.json', grammar => patchGrammar(grammar)); + + diff --git a/extensions/html/package.json b/extensions/html/package.json index 1b28ff2e5eb..065eb26669b 100644 --- a/extensions/html/package.json +++ b/extensions/html/package.json @@ -9,7 +9,7 @@ "vscode": "0.10.x" }, "scripts": { - "update-grammar": "node ../../build/npm/update-grammar.js textmate/html.tmbundle Syntaxes/HTML.plist ./syntaxes/html.tmLanguage.json Syntaxes/HTML%20%28Derivative%29.tmLanguage ./syntaxes/html-derivative.tmLanguage.json" + "update-grammar": "node ./build/update-grammar.js" }, "contributes": { "languages": [ diff --git a/extensions/html/syntaxes/html.tmLanguage.json b/extensions/html/syntaxes/html.tmLanguage.json index a071d90a9e3..1e1c85c899f 100644 --- a/extensions/html/syntaxes/html.tmLanguage.json +++ b/extensions/html/syntaxes/html.tmLanguage.json @@ -108,7 +108,7 @@ "name": "punctuation.definition.string.end.html" }, "1": { - "name": "source.css" + "name": "source.css-ignored-vscode" } }, "name": "string.quoted.double.html", @@ -132,7 +132,7 @@ "name": "punctuation.definition.string.end.html" }, "1": { - "name": "source.css" + "name": "source.css-ignored-vscode" } }, "name": "string.quoted.single.html", @@ -207,7 +207,7 @@ "name": "punctuation.definition.string.end.html" }, "1": { - "name": "source.js" + "name": "source.js-ignored-vscode" } }, "name": "string.quoted.double.html", @@ -265,7 +265,7 @@ "name": "punctuation.definition.string.end.html" }, "1": { - "name": "source.js" + "name": "source.js-ignored-vscode" } }, "name": "string.quoted.single.html", @@ -1785,7 +1785,7 @@ "name": "punctuation.definition.tag.begin.html" }, "2": { - "name": "source.css" + "name": "source.css-ignored-vscode" }, "3": { "name": "entity.name.tag.html" @@ -1892,7 +1892,7 @@ "name": "punctuation.definition.tag.begin.html" }, "2": { - "name": "source.js" + "name": "source.js-ignored-vscode" } }, "patterns": [ diff --git a/extensions/html/test/colorize-fixtures/test-embedding.html b/extensions/html/test/colorize-fixtures/test-embedding.html new file mode 100644 index 00000000000..87f92b6264b --- /dev/null +++ b/extensions/html/test/colorize-fixtures/test-embedding.html @@ -0,0 +1,6 @@ + + + +
+
+
diff --git a/extensions/html/test/colorize-results/12750_html.json b/extensions/html/test/colorize-results/12750_html.json index c70c7afdb8b..a4f0184b51c 100644 --- a/extensions/html/test/colorize-results/12750_html.json +++ b/extensions/html/test/colorize-results/12750_html.json @@ -210,7 +210,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -397,7 +397,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", diff --git a/extensions/html/test/colorize-results/25920_html.json b/extensions/html/test/colorize-results/25920_html.json index 4f4b04e21f5..73c94747231 100644 --- a/extensions/html/test/colorize-results/25920_html.json +++ b/extensions/html/test/colorize-results/25920_html.json @@ -518,7 +518,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", diff --git a/extensions/html/test/colorize-results/test-embedding_html.json b/extensions/html/test/colorize-results/test-embedding_html.json new file mode 100644 index 00000000000..1931950a02c --- /dev/null +++ b/extensions/html/test/colorize-results/test-embedding_html.json @@ -0,0 +1,1003 @@ +[ + { + "c": "<", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.start.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "script", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.start.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.start.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "console", + "t": "text.html.derivative meta.embedded.block.html source.js meta.function-call.js support.class.console.js", + "r": { + "dark_plus": "support.class: #4EC9B0", + "light_plus": "support.class: #267F99", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "support.class: #4EC9B0" + } + }, + { + "c": ".", + "t": "text.html.derivative meta.embedded.block.html source.js meta.function-call.js punctuation.accessor.js", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "log", + "t": "text.html.derivative meta.embedded.block.html source.js meta.function-call.js support.function.console.js", + "r": { + "dark_plus": "support.function: #DCDCAA", + "light_plus": "support.function: #795E26", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "support.function: #DCDCAA" + } + }, + { + "c": "(", + "t": "text.html.derivative meta.embedded.block.html source.js meta.brace.round.js", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "'", + "t": "text.html.derivative meta.embedded.block.html source.js string.quoted.single.js punctuation.definition.string.begin.js", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "x", + "t": "text.html.derivative meta.embedded.block.html source.js string.quoted.single.js", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": "'", + "t": "text.html.derivative meta.embedded.block.html source.js string.quoted.single.js punctuation.definition.string.end.js", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string: #A31515", + "dark_vs": "string: #CE9178", + "light_vs": "string: #A31515", + "hc_black": "string: #CE9178" + } + }, + { + "c": ")", + "t": "text.html.derivative meta.embedded.block.html source.js meta.brace.round.js", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "/", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "script", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.start.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "style", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.start.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.start.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "test", + "t": "text.html.derivative meta.embedded.block.html source.css meta.selector.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.embedded.block.html source.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "{", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css punctuation.section.property-list.begin.bracket.curly.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "display", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css meta.property-name.css support.type.property-name.css", + "r": { + "dark_plus": "support.type.property-name: #9CDCFE", + "light_plus": "support.type.property-name: #FF0000", + "dark_vs": "support.type.property-name: #9CDCFE", + "light_vs": "support.type.property-name: #FF0000", + "hc_black": "support.type.property-name: #D4D4D4" + } + }, + { + "c": ":", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css punctuation.separator.key-value.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "none", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css meta.property-value.css support.constant.property-value.css", + "r": { + "dark_plus": "support.constant.property-value: #CE9178", + "light_plus": "support.constant.property-value: #0451A5", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "support.constant.property-value: #0451A5", + "hc_black": "support.constant.property-value: #CE9178" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "}", + "t": "text.html.derivative meta.embedded.block.html source.css meta.property-list.css punctuation.section.property-list.end.bracket.curly.css", + "r": { + "dark_plus": "meta.embedded: #D4D4D4", + "light_plus": "meta.embedded: #000000", + "dark_vs": "meta.embedded: #D4D4D4", + "light_vs": "meta.embedded: #000000", + "hc_black": "meta.embedded: #FFFFFF" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.begin.html source.css-ignored-vscode", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "/", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "style", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.end.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.tag.inline.a.start.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "a", + "t": "text.html.derivative meta.tag.inline.a.start.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.tag.inline.a.start.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "onblur", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.blur.html entity.other.attribute-name.html", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" + } + }, + { + "c": "=", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.blur.html punctuation.separator.key-value.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "\"", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.blur.html meta.embedded.line.js string.quoted.double.html punctuation.definition.string.begin.html", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.double.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "doBlur", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.blur.html meta.embedded.line.js string.quoted.double.html source.js meta.function-call.js entity.name.function.js", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "()", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.blur.html meta.embedded.line.js string.quoted.double.html source.js meta.brace.round.js", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.double.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "\"", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.blur.html meta.embedded.line.js string.quoted.double.html punctuation.definition.string.end.html source.js-ignored-vscode", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.double.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.tag.inline.a.start.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "onclick", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.click.html entity.other.attribute-name.html", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" + } + }, + { + "c": "=", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.click.html punctuation.separator.key-value.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "'", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.click.html meta.embedded.line.js string.quoted.single.html punctuation.definition.string.begin.html", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "doClick", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.click.html meta.embedded.line.js string.quoted.single.html source.js meta.function-call.js entity.name.function.js", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "()", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.click.html meta.embedded.line.js string.quoted.single.html source.js meta.brace.round.js", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "'", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.click.html meta.embedded.line.js string.quoted.single.html punctuation.definition.string.end.html source.js-ignored-vscode", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.tag.inline.a.start.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "ondrag", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.drag.html entity.other.attribute-name.html", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" + } + }, + { + "c": "=", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.drag.html punctuation.separator.key-value.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "doDrag", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.drag.html meta.embedded.line.js string.unquoted.html meta.function-call.js entity.name.function.js", + "r": { + "dark_plus": "entity.name.function: #DCDCAA", + "light_plus": "entity.name.function: #795E26", + "dark_vs": "string: #CE9178", + "light_vs": "string.unquoted.html: #0000FF", + "hc_black": "entity.name.function: #DCDCAA" + } + }, + { + "c": "()", + "t": "text.html.derivative meta.tag.inline.a.start.html meta.attribute.event-handler.drag.html meta.embedded.line.js string.unquoted.html meta.brace.round.js", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.unquoted.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.unquoted.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.tag.inline.a.start.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "", + "t": "text.html.derivative meta.tag.inline.a.end.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.tag.structure.div.start.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "div", + "t": "text.html.derivative meta.tag.structure.div.start.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.tag.structure.div.start.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "style", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html entity.other.attribute-name.html", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" + } + }, + { + "c": "=", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html punctuation.separator.key-value.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "\"", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.quoted.double.html punctuation.definition.string.begin.html", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.double.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "x { }", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.quoted.double.html source.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.double.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "\"", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.quoted.double.html punctuation.definition.string.end.html source.css-ignored-vscode", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.double.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.double.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.tag.structure.div.start.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "", + "t": "text.html.derivative meta.tag.structure.div.end.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.tag.structure.div.start.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "div", + "t": "text.html.derivative meta.tag.structure.div.start.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.tag.structure.div.start.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "style", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html entity.other.attribute-name.html", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" + } + }, + { + "c": "=", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html punctuation.separator.key-value.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "'", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.quoted.single.html punctuation.definition.string.begin.html", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "y { }", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.quoted.single.html source.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": "'", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.quoted.single.html punctuation.definition.string.end.html source.css-ignored-vscode", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.quoted.single.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.quoted.single.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.tag.structure.div.start.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "", + "t": "text.html.derivative meta.tag.structure.div.end.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "<", + "t": "text.html.derivative meta.tag.structure.div.start.html punctuation.definition.tag.begin.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "div", + "t": "text.html.derivative meta.tag.structure.div.start.html entity.name.tag.html", + "r": { + "dark_plus": "entity.name.tag: #569CD6", + "light_plus": "entity.name.tag: #800000", + "dark_vs": "entity.name.tag: #569CD6", + "light_vs": "entity.name.tag: #800000", + "hc_black": "entity.name.tag: #569CD6" + } + }, + { + "c": " ", + "t": "text.html.derivative meta.tag.structure.div.start.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "style", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html entity.other.attribute-name.html", + "r": { + "dark_plus": "entity.other.attribute-name: #9CDCFE", + "light_plus": "entity.other.attribute-name: #FF0000", + "dark_vs": "entity.other.attribute-name: #9CDCFE", + "light_vs": "entity.other.attribute-name: #FF0000", + "hc_black": "entity.other.attribute-name: #9CDCFE" + } + }, + { + "c": "=", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html punctuation.separator.key-value.html", + "r": { + "dark_plus": "default: #D4D4D4", + "light_plus": "default: #000000", + "dark_vs": "default: #D4D4D4", + "light_vs": "default: #000000", + "hc_black": "default: #FFFFFF" + } + }, + { + "c": "z{}", + "t": "text.html.derivative meta.tag.structure.div.start.html meta.attribute.style.html meta.embedded.line.css string.unquoted.html source.css", + "r": { + "dark_plus": "string: #CE9178", + "light_plus": "string.unquoted.html: #0000FF", + "dark_vs": "string: #CE9178", + "light_vs": "string.unquoted.html: #0000FF", + "hc_black": "string: #CE9178" + } + }, + { + "c": ">", + "t": "text.html.derivative meta.tag.structure.div.start.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + }, + { + "c": "", + "t": "text.html.derivative meta.tag.structure.div.end.html punctuation.definition.tag.end.html", + "r": { + "dark_plus": "punctuation.definition.tag: #808080", + "light_plus": "punctuation.definition.tag: #800000", + "dark_vs": "punctuation.definition.tag: #808080", + "light_vs": "punctuation.definition.tag: #800000", + "hc_black": "punctuation.definition.tag: #808080" + } + } +] \ No newline at end of file diff --git a/extensions/html/test/colorize-results/test_html.json b/extensions/html/test/colorize-results/test_html.json index 970d4214a74..dd8a4aa2043 100644 --- a/extensions/html/test/colorize-results/test_html.json +++ b/extensions/html/test/colorize-results/test_html.json @@ -782,7 +782,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.begin.html source.css", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.begin.html source.css-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -1200,7 +1200,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -1354,7 +1354,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -2212,7 +2212,7 @@ }, { "c": "<", - "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.derivative meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", @@ -3189,4 +3189,4 @@ "hc_black": "punctuation.definition.tag: #808080" } } -] +] \ No newline at end of file diff --git a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json index a8dff6ef901..db94d219f4f 100644 --- a/extensions/javascript/syntaxes/JavaScript.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/2e0fded599d7b85b78692dee3d96e8a4710a5c57", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/c9c955af17ed0c86ebce32e0f26c72f0a2925937", "name": "JavaScript (with React support)", "scopeName": "source.js", "patterns": [ @@ -3161,6 +3161,17 @@ "name": "keyword.operator.relational.js", "match": "<=|>=|<>|<|>" }, + { + "match": "(\\!)\\s*(/)(?![/*])", + "captures": { + "1": { + "name": "keyword.operator.logical.js" + }, + "2": { + "name": "keyword.operator.arithmetic.js" + } + } + }, { "name": "keyword.operator.logical.js", "match": "\\!|&&|\\|\\||\\?\\?" diff --git a/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json b/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json index 178660a674b..8703a62331d 100644 --- a/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json +++ b/extensions/javascript/syntaxes/JavaScriptReact.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/2e0fded599d7b85b78692dee3d96e8a4710a5c57", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/c9c955af17ed0c86ebce32e0f26c72f0a2925937", "name": "JavaScript (with React support)", "scopeName": "source.js.jsx", "patterns": [ @@ -3161,6 +3161,17 @@ "name": "keyword.operator.relational.js.jsx", "match": "<=|>=|<>|<|>" }, + { + "match": "(\\!)\\s*(/)(?![/*])", + "captures": { + "1": { + "name": "keyword.operator.logical.js.jsx" + }, + "2": { + "name": "keyword.operator.arithmetic.js.jsx" + } + } + }, { "name": "keyword.operator.logical.js.jsx", "match": "\\!|&&|\\|\\||\\?\\?" diff --git a/extensions/json-language-features/extension.webpack.config.js b/extensions/json-language-features/extension.webpack.config.js index a4d4ff955d7..39e5e567a7a 100644 --- a/extensions/json-language-features/extension.webpack.config.js +++ b/extensions/json-language-features/extension.webpack.config.js @@ -9,7 +9,7 @@ const withDefaults = require('../shared.webpack.config'); const path = require('path'); -var webpack = require('webpack'); +const webpack = require('webpack'); const config = withDefaults({ context: path.join(__dirname, 'client'), @@ -25,4 +25,4 @@ const config = withDefaults({ // add plugin, don't replace inherited config.plugins.push(new webpack.IgnorePlugin(/vertx/)); // request-light dependency -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/extensions/json-language-features/server/extension.webpack.config.js b/extensions/json-language-features/server/extension.webpack.config.js index 22b23c1d94b..2c90a0b0751 100644 --- a/extensions/json-language-features/server/extension.webpack.config.js +++ b/extensions/json-language-features/server/extension.webpack.config.js @@ -9,7 +9,7 @@ const withDefaults = require('../../shared.webpack.config'); const path = require('path'); -var webpack = require('webpack'); +const webpack = require('webpack'); const config = withDefaults({ context: path.join(__dirname), diff --git a/extensions/markdown-basics/test/colorize-results/test_md.json b/extensions/markdown-basics/test/colorize-results/test_md.json index 9bc9aa7595f..6963f66e797 100644 --- a/extensions/markdown-basics/test/colorize-results/test_md.json +++ b/extensions/markdown-basics/test/colorize-results/test_md.json @@ -1134,7 +1134,7 @@ }, { "c": "<", - "t": "text.html.markdown meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.begin.html source.css", + "t": "text.html.markdown meta.embedded.block.html meta.tag.metadata.style.end.html punctuation.definition.tag.begin.html source.css-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 80595ff26f7..0bcc8473745 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -49,10 +49,7 @@ function registerMarkdownLanguageFeatures( symbolProvider: MDDocumentSymbolProvider, engine: MarkdownEngine ): vscode.Disposable { - const selector: vscode.DocumentSelector = [ - { language: 'markdown', scheme: 'file' }, - { language: 'markdown', scheme: 'untitled' } - ]; + const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' }; const charPattern = '(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})'; diff --git a/extensions/php/test/colorize-results/issue-28354_php.json b/extensions/php/test/colorize-results/issue-28354_php.json index 3a34ef9deb8..27983076fa7 100644 --- a/extensions/php/test/colorize-results/issue-28354_php.json +++ b/extensions/php/test/colorize-results/issue-28354_php.json @@ -1,4 +1,5 @@ -[{ +[ + { "c": "<", "t": "text.html.php meta.embedded.block.html meta.tag.metadata.script.start.html punctuation.definition.tag.begin.html", "r": { @@ -495,7 +496,7 @@ }, { "c": "<", - "t": "text.html.php meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js", + "t": "text.html.php meta.embedded.block.html meta.tag.metadata.script.end.html punctuation.definition.tag.begin.html source.js-ignored-vscode", "r": { "dark_plus": "punctuation.definition.tag: #808080", "light_plus": "punctuation.definition.tag: #800000", diff --git a/extensions/python/package.json b/extensions/python/package.json index 645cba2bae3..c500b586f45 100644 --- a/extensions/python/package.json +++ b/extensions/python/package.json @@ -14,7 +14,7 @@ "extensions": [ ".py", ".rpy", ".pyw", ".cpy", ".gyp", ".gypi", ".pyi", ".ipy"], "aliases": [ "Python", "py" ], "filenames": [ "Snakefile" ], - "firstLine": "^#!\\s*/.*\\bpython[0-9.-]*\\b", + "firstLine": "^#!\\s*/?.*\\bpython[0-9.-]*\\b", "configuration": "./language-configuration.json" }], "grammars": [{ diff --git a/extensions/typescript-basics/cgmanifest.json b/extensions/typescript-basics/cgmanifest.json index 5aed2047051..f75427c65b2 100644 --- a/extensions/typescript-basics/cgmanifest.json +++ b/extensions/typescript-basics/cgmanifest.json @@ -6,7 +6,7 @@ "git": { "name": "TypeScript-TmLanguage", "repositoryUrl": "https://github.com/Microsoft/TypeScript-TmLanguage", - "commitHash": "2e0fded599d7b85b78692dee3d96e8a4710a5c57" + "commitHash": "c9c955af17ed0c86ebce32e0f26c72f0a2925937" } }, "license": "MIT", diff --git a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json index 2edc66ac5ed..b858d4758e9 100644 --- a/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScript.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/2e0fded599d7b85b78692dee3d96e8a4710a5c57", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/c9c955af17ed0c86ebce32e0f26c72f0a2925937", "name": "TypeScript", "scopeName": "source.ts", "patterns": [ @@ -3210,6 +3210,17 @@ "name": "keyword.operator.relational.ts", "match": "<=|>=|<>|<|>" }, + { + "match": "(\\!)\\s*(/)(?![/*])", + "captures": { + "1": { + "name": "keyword.operator.logical.ts" + }, + "2": { + "name": "keyword.operator.arithmetic.ts" + } + } + }, { "name": "keyword.operator.logical.ts", "match": "\\!|&&|\\|\\||\\?\\?" diff --git a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json b/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json index 86dfac819a7..32071749fc1 100644 --- a/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json +++ b/extensions/typescript-basics/syntaxes/TypeScriptReact.tmLanguage.json @@ -4,7 +4,7 @@ "If you want to provide a fix or improvement, please create a pull request against the original repository.", "Once accepted there, we are happy to receive an update request." ], - "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/2e0fded599d7b85b78692dee3d96e8a4710a5c57", + "version": "https://github.com/Microsoft/TypeScript-TmLanguage/commit/c9c955af17ed0c86ebce32e0f26c72f0a2925937", "name": "TypeScriptReact", "scopeName": "source.tsx", "patterns": [ @@ -3161,6 +3161,17 @@ "name": "keyword.operator.relational.tsx", "match": "<=|>=|<>|<|>" }, + { + "match": "(\\!)\\s*(/)(?![/*])", + "captures": { + "1": { + "name": "keyword.operator.logical.tsx" + }, + "2": { + "name": "keyword.operator.arithmetic.tsx" + } + } + }, { "name": "keyword.operator.logical.tsx", "match": "\\!|&&|\\|\\||\\?\\?" diff --git a/extensions/typescript-language-features/src/features/completions.ts b/extensions/typescript-language-features/src/features/completions.ts index da13f97f3db..5fdefe1b2ad 100644 --- a/extensions/typescript-language-features/src/features/completions.ts +++ b/extensions/typescript-language-features/src/features/completions.ts @@ -161,6 +161,7 @@ class MyCompletionItem extends vscode.CompletionItem { case PConst.Kind.memberSetAccessor: return vscode.CompletionItemKind.Field; case PConst.Kind.function: + case PConst.Kind.localFunction: return vscode.CompletionItemKind.Function; case PConst.Kind.memberFunction: case PConst.Kind.constructSignature: diff --git a/extensions/typescript-language-features/src/features/jsDocCompletions.ts b/extensions/typescript-language-features/src/features/jsDocCompletions.ts index 82aacc8927a..d4d59c8d6d5 100644 --- a/extensions/typescript-language-features/src/features/jsDocCompletions.ts +++ b/extensions/typescript-language-features/src/features/jsDocCompletions.ts @@ -27,9 +27,8 @@ class JsDocCompletionItem extends vscode.CompletionItem { const prefix = line.slice(0, position.character).match(/\/\**\s*$/); const suffix = line.slice(position.character).match(/^\s*\**\//); const start = position.translate(0, prefix ? -prefix[0].length : 0); - this.range = new vscode.Range( - start, - position.translate(0, suffix ? suffix[0].length : 0)); + const range = new vscode.Range(start, position.translate(0, suffix ? suffix[0].length : 0)); + this.range2 = { inserting: range, replacing: range }; } } diff --git a/extensions/typescript-language-features/src/features/task.ts b/extensions/typescript-language-features/src/features/task.ts index 59044029b69..48f9ca27d4a 100644 --- a/extensions/typescript-language-features/src/features/task.ts +++ b/extensions/typescript-language-features/src/features/task.ts @@ -20,7 +20,8 @@ type AutoDetect = 'on' | 'off' | 'build' | 'watch'; const exists = async (resource: vscode.Uri): Promise => { try { const stat = await vscode.workspace.fs.stat(resource); - return stat.type === vscode.FileType.File; + // stat.type is an enum flag + return !!(stat.type & vscode.FileType.File); } catch { return false; } diff --git a/extensions/typescript-language-features/src/test/index.ts b/extensions/typescript-language-features/src/test/index.ts index ba0fd6d7663..1a5f7419059 100644 --- a/extensions/typescript-language-features/src/test/index.ts +++ b/extensions/typescript-language-features/src/test/index.ts @@ -23,7 +23,6 @@ testRunner.configure({ ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'), // colored output from test results (only windows cannot handle) timeout: 60000, - grep: 'References' }); export = testRunner; diff --git a/package.json b/package.json index d55272e9772..6645783674b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "code-oss-dev", "version": "1.42.0", - "distro": "e49812b31f1c71911d0557284874d07c7b790f13", + "distro": "ec0412876eac04d73be9bcc6919aecfaffe2ab64", "author": { "name": "Microsoft Corporation" }, @@ -25,6 +25,7 @@ "smoketest": "cd test/smoke && node test/index.js", "download-builtin-extensions": "node build/lib/builtInExtensions.js", "monaco-compile-check": "tsc -p src/tsconfig.monaco.json --noEmit", + "valid-globals-check": "node build/lib/globalsLinter.js", "strict-function-types-watch": "tsc --watch -p src/tsconfig.json --noEmit --strictFunctionTypes", "update-distro": "node build/npm/update-distro.js", "web": "node scripts/code-web.js" @@ -70,8 +71,9 @@ "@types/iconv-lite": "0.0.1", "@types/keytar": "^4.4.0", "@types/mocha": "2.2.39", - "@types/node": "^10.12.12", + "@types/node": "^12.11.7", "@types/sinon": "^1.16.36", + "@types/vscode-windows-registry": "^1.0.0", "@types/webpack": "^4.4.10", "@types/windows-foreground-love": "^0.3.0", "@types/windows-mutex": "^0.4.0", diff --git a/src/tsconfig.json b/src/tsconfig.json index 8917d57336d..7e1e6c6d108 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -24,6 +24,7 @@ "./vs" ], "exclude": [ + "./typings/es6-promise.d.ts", "./typings/require-monaco.d.ts", "./typings/xterm.d.ts", "./typings/xterm-addon-search.d.ts", diff --git a/src/typings/lib.ie11_safe_es6.d.ts b/src/typings/lib.ie11_safe_es6.d.ts index 43bde6f7c64..547ba246917 100644 --- a/src/typings/lib.ie11_safe_es6.d.ts +++ b/src/typings/lib.ie11_safe_es6.d.ts @@ -25,7 +25,7 @@ interface Map { interface MapConstructor { new (): Map; - prototype: Map; + readonly prototype: Map; // not supported on IE11: // new (iterable: Iterable<[K, V]>): Map; @@ -51,7 +51,7 @@ interface Set { interface SetConstructor { new (): Set; - prototype: Set; + readonly prototype: Set; // not supported on IE11: // new (iterable: Iterable): Set; diff --git a/src/typings/vscode-windows-registry.d.ts b/src/typings/vscode-windows-registry.d.ts deleted file mode 100644 index 9be14daa4b2..00000000000 --- a/src/typings/vscode-windows-registry.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -declare module 'vscode-windows-registry' { - export type HKEY = "HKEY_CURRENT_USER" | "HKEY_LOCAL_MACHINE" | "HKEY_CLASSES_ROOT" | "HKEY_USERS" | "HKEY_CURRENT_CONFIG"; - export function GetStringRegKey(hive: HKEY, path: string, name: string): string | undefined; -} \ No newline at end of file diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css index b60241663e1..3cb3a855ded 100644 --- a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css +++ b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.css @@ -5,7 +5,7 @@ @font-face { font-family: "codicon"; - src: url("./codicon.ttf?072cd8445a025297c265f9d008123381") format("truetype"); + src: url("./codicon.ttf?17db7f5e5f31fd546e62218bb0823c0c") format("truetype"); } .codicon[class*='codicon-'] { diff --git a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf index fd2bf2e192d..8788b6e97da 100644 Binary files a/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf and b/src/vs/base/browser/ui/codiconLabel/codicon/codicon.ttf differ diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index cb0a39611e7..a2b6766b209 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -34,7 +34,7 @@ export function createCancelablePromise(callback: (token: CancellationToken) }); }); - return new class implements CancelablePromise { + return >new class { cancel() { source.cancel(); } diff --git a/src/vs/base/common/buffer.ts b/src/vs/base/common/buffer.ts index 25371f5ec79..1126f89c7d8 100644 --- a/src/vs/base/common/buffer.ts +++ b/src/vs/base/common/buffer.ts @@ -6,7 +6,7 @@ import * as strings from 'vs/base/common/strings'; import * as streams from 'vs/base/common/stream'; -declare var Buffer: any; +declare const Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); const hasTextEncoder = (typeof TextEncoder !== 'undefined'); diff --git a/src/vs/base/common/lifecycle.ts b/src/vs/base/common/lifecycle.ts index 651c5fa22a4..06e3746750c 100644 --- a/src/vs/base/common/lifecycle.ts +++ b/src/vs/base/common/lifecycle.ts @@ -163,7 +163,7 @@ export abstract class Disposable implements IDisposable { /** * Manages the lifecycle of a disposable value that may be changed. * - * This ensures that when the the disposable value is changed, the previously held disposable is disposed of. You can + * This ensures that when the disposable value is changed, the previously held disposable is disposed of. You can * also register a `MutableDisposable` on a `Disposable` to ensure it is automatically cleaned up. */ export class MutableDisposable implements IDisposable { diff --git a/src/vs/base/node/decoder.ts b/src/vs/base/node/decoder.ts index 0e313a5715a..767cf6d89c1 100644 --- a/src/vs/base/node/decoder.ts +++ b/src/vs/base/node/decoder.ts @@ -15,7 +15,7 @@ import { CharCode } from 'vs/base/common/charCode'; * - forEach() over the result to get the lines */ export class LineDecoder { - private stringDecoder: sd.NodeStringDecoder; + private stringDecoder: sd.StringDecoder; private remaining: string | null; constructor(encoding: string = 'utf8') { diff --git a/src/vs/base/node/encoding.ts b/src/vs/base/node/encoding.ts index 8da85d8ce39..3b459fa4037 100644 --- a/src/vs/base/node/encoding.ts +++ b/src/vs/base/node/encoding.ts @@ -4,8 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as iconv from 'iconv-lite'; -import { isLinux, isMacintosh } from 'vs/base/common/platform'; -import { exec } from 'child_process'; import { Readable, Writable } from 'stream'; import { VSBuffer } from 'vs/base/common/buffer'; @@ -353,87 +351,3 @@ export function detectEncodingFromBuffer({ buffer, bytesRead }: IReadResult, aut return { seemsBinary, encoding }; } - -// https://ss64.com/nt/chcp.html -const windowsTerminalEncodings = { - '437': 'cp437', // United States - '850': 'cp850', // Multilingual(Latin I) - '852': 'cp852', // Slavic(Latin II) - '855': 'cp855', // Cyrillic(Russian) - '857': 'cp857', // Turkish - '860': 'cp860', // Portuguese - '861': 'cp861', // Icelandic - '863': 'cp863', // Canadian - French - '865': 'cp865', // Nordic - '866': 'cp866', // Russian - '869': 'cp869', // Modern Greek - '936': 'cp936', // Simplified Chinese - '1252': 'cp1252' // West European Latin -}; - -export async function resolveTerminalEncoding(verbose?: boolean): Promise { - let rawEncodingPromise: Promise; - - // Support a global environment variable to win over other mechanics - const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; - if (cliEncodingEnv) { - if (verbose) { - console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`); - } - - rawEncodingPromise = Promise.resolve(cliEncodingEnv); - } - - // Linux/Mac: use "locale charmap" command - else if (isLinux || isMacintosh) { - rawEncodingPromise = new Promise(resolve => { - if (verbose) { - console.log('Running "locale charmap" to detect terminal encoding...'); - } - - exec('locale charmap', (err, stdout, stderr) => resolve(stdout)); - }); - } - - // Windows: educated guess - else { - rawEncodingPromise = new Promise(resolve => { - if (verbose) { - console.log('Running "chcp" to detect terminal encoding...'); - } - - exec('chcp', (err, stdout, stderr) => { - if (stdout) { - const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; - for (const key of windowsTerminalEncodingKeys) { - if (stdout.indexOf(key) >= 0) { - return resolve(windowsTerminalEncodings[key]); - } - } - } - - return resolve(undefined); - }); - }); - } - - const rawEncoding = await rawEncodingPromise; - if (verbose) { - console.log(`Detected raw terminal encoding: ${rawEncoding}`); - } - - if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) { - return UTF8; - } - - const iconvEncoding = toIconvLiteEncoding(rawEncoding); - if (iconv.encodingExists(iconvEncoding)) { - return iconvEncoding; - } - - if (verbose) { - console.log('Unsupported terminal encoding, falling back to UTF-8.'); - } - - return UTF8; -} diff --git a/src/vs/base/node/terminalEncoding.ts b/src/vs/base/node/terminalEncoding.ts new file mode 100644 index 00000000000..3077f3a9633 --- /dev/null +++ b/src/vs/base/node/terminalEncoding.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small. + */ +import { exec } from 'child_process'; +import * as os from 'os'; + +const windowsTerminalEncodings = { + '437': 'cp437', // United States + '850': 'cp850', // Multilingual(Latin I) + '852': 'cp852', // Slavic(Latin II) + '855': 'cp855', // Cyrillic(Russian) + '857': 'cp857', // Turkish + '860': 'cp860', // Portuguese + '861': 'cp861', // Icelandic + '863': 'cp863', // Canadian - French + '865': 'cp865', // Nordic + '866': 'cp866', // Russian + '869': 'cp869', // Modern Greek + '936': 'cp936', // Simplified Chinese + '1252': 'cp1252' // West European Latin +}; + +function toIconvLiteEncoding(encodingName: string): string { + const normalizedEncodingName = encodingName.replace(/[^a-zA-Z0-9]/g, '').toLowerCase(); + const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName]; + + return mapped || normalizedEncodingName; +} + +const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = { + 'ibm866': 'cp866', + 'big5': 'cp950' +}; + +const UTF8 = 'utf8'; + + +export async function resolveTerminalEncoding(verbose?: boolean): Promise { + let rawEncodingPromise: Promise; + + // Support a global environment variable to win over other mechanics + const cliEncodingEnv = process.env['VSCODE_CLI_ENCODING']; + if (cliEncodingEnv) { + if (verbose) { + console.log(`Found VSCODE_CLI_ENCODING variable: ${cliEncodingEnv}`); + } + + rawEncodingPromise = Promise.resolve(cliEncodingEnv); + } + + // Windows: educated guess + else if (os.platform() === 'win32') { + rawEncodingPromise = new Promise(resolve => { + if (verbose) { + console.log('Running "chcp" to detect terminal encoding...'); + } + + exec('chcp', (err, stdout, stderr) => { + if (stdout) { + const windowsTerminalEncodingKeys = Object.keys(windowsTerminalEncodings) as Array; + for (const key of windowsTerminalEncodingKeys) { + if (stdout.indexOf(key) >= 0) { + return resolve(windowsTerminalEncodings[key]); + } + } + } + + return resolve(undefined); + }); + }); + } + // Linux/Mac: use "locale charmap" command + else { + rawEncodingPromise = new Promise(resolve => { + if (verbose) { + console.log('Running "locale charmap" to detect terminal encoding...'); + } + + exec('locale charmap', (err, stdout, stderr) => resolve(stdout)); + }); + } + + const rawEncoding = await rawEncodingPromise; + if (verbose) { + console.log(`Detected raw terminal encoding: ${rawEncoding}`); + } + + if (!rawEncoding || rawEncoding.toLowerCase() === 'utf-8' || rawEncoding.toLowerCase() === UTF8) { + return UTF8; + } + + return toIconvLiteEncoding(rawEncoding); +} diff --git a/src/vs/base/parts/ipc/common/ipc.ts b/src/vs/base/parts/ipc/common/ipc.ts index 603414de3aa..662a29a9d86 100644 --- a/src/vs/base/parts/ipc/common/ipc.ts +++ b/src/vs/base/parts/ipc/common/ipc.ts @@ -187,7 +187,7 @@ const BufferPresets = { Object: createOneByteBuffer(DataType.Object), }; -declare var Buffer: any; +declare const Buffer: any; const hasBuffer = (typeof Buffer !== 'undefined'); function serialize(writer: IWriter, data: any): void { diff --git a/src/vs/base/parts/quickopen/test/browser/quickopen.test.ts b/src/vs/base/parts/quickopen/test/browser/quickopen.test.ts index f53d0b4e1ed..f857c0b64a7 100644 --- a/src/vs/base/parts/quickopen/test/browser/quickopen.test.ts +++ b/src/vs/base/parts/quickopen/test/browser/quickopen.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { QuickOpenModel, QuickOpenEntry, QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { DataSource } from 'vs/base/parts/quickopen/browser/quickOpenViewer'; @@ -28,7 +29,7 @@ suite('QuickOpen', () => { assert.equal(entry2, model.getEntries(true)[0]); }); - test('QuickOpenDataSource', () => { + test('QuickOpenDataSource', async () => { const model = new QuickOpenModel(); const entry1 = new QuickOpenEntry(); @@ -42,8 +43,7 @@ suite('QuickOpen', () => { assert.equal(true, ds.hasChildren(null!, model)); assert.equal(false, ds.hasChildren(null!, entry1)); - ds.getChildren(null!, model).then((children: any[]) => { - assert.equal(3, children.length); - }); + const children = await ds.getChildren(null!, model); + assert.equal(3, children.length); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts b/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts index 548cc9489f0..a7bbe6fb2a0 100644 --- a/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts +++ b/src/vs/base/parts/quickopen/test/common/quickOpenScorer.test.ts @@ -833,4 +833,4 @@ suite('Quick Open Scorer', () => { assert.equal(scorer.prepareQuery('ModelTester.ts').containsPathSeparator, false); assert.equal(scorer.prepareQuery('Model' + sep + 'Tester.ts').containsPathSeparator, true); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/browser/progressBar.test.ts b/src/vs/base/test/browser/progressBar.test.ts index 03e0a061419..f43082a0bc6 100644 --- a/src/vs/base/test/browser/progressBar.test.ts +++ b/src/vs/base/test/browser/progressBar.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar'; @@ -28,4 +29,4 @@ suite('ProgressBar', () => { bar.dispose(); }); -}); \ No newline at end of file +}); diff --git a/src/vs/base/test/common/assert.test.ts b/src/vs/base/test/common/assert.test.ts index c7d3343ba29..a925cd0437a 100644 --- a/src/vs/base/test/common/assert.test.ts +++ b/src/vs/base/test/common/assert.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { ok } from 'vs/base/common/assert'; diff --git a/src/vs/base/test/common/collections.test.ts b/src/vs/base/test/common/collections.test.ts index 353b5d0147d..e5449f46333 100644 --- a/src/vs/base/test/common/collections.test.ts +++ b/src/vs/base/test/common/collections.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import * as collections from 'vs/base/common/collections'; - suite('Collections', () => { test('forEach', () => { diff --git a/src/vs/base/test/common/extpath.test.ts b/src/vs/base/test/common/extpath.test.ts index da6da32873f..eb3d8da7a46 100644 --- a/src/vs/base/test/common/extpath.test.ts +++ b/src/vs/base/test/common/extpath.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import * as extpath from 'vs/base/common/extpath'; import * as platform from 'vs/base/common/platform'; @@ -28,7 +29,6 @@ suite('Paths', () => { assert.equal(extpath.getRoot('http://www/'), 'http://www/'); assert.equal(extpath.getRoot('file:///foo'), 'file:///'); assert.equal(extpath.getRoot('file://foo'), ''); - }); test('isUNC', () => { diff --git a/src/vs/base/test/common/mime.test.ts b/src/vs/base/test/common/mime.test.ts index 7c99c964c75..3d163580a6d 100644 --- a/src/vs/base/test/common/mime.test.ts +++ b/src/vs/base/test/common/mime.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import { guessMimeTypes, registerTextMime, suggestFilename } from 'vs/base/common/mime'; import { URI } from 'vs/base/common/uri'; diff --git a/src/vs/base/test/common/types.test.ts b/src/vs/base/test/common/types.test.ts index 90e174dd000..0bec27fcd84 100644 --- a/src/vs/base/test/common/types.test.ts +++ b/src/vs/base/test/common/types.test.ts @@ -2,10 +2,12 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import * as types from 'vs/base/common/types'; suite('Types', () => { + test('isFunction', () => { assert(!types.isFunction(undefined)); assert(!types.isFunction(null)); diff --git a/src/vs/base/test/node/encoding/encoding.test.ts b/src/vs/base/test/node/encoding/encoding.test.ts index a78f2ed6b46..1931c76aa40 100644 --- a/src/vs/base/test/node/encoding/encoding.test.ts +++ b/src/vs/base/test/node/encoding/encoding.test.ts @@ -6,6 +6,7 @@ import * as assert from 'assert'; import * as fs from 'fs'; import * as encoding from 'vs/base/node/encoding'; +import * as terminalEncoding from 'vs/base/node/terminalEncoding'; import { Readable } from 'stream'; import { getPathFromAmdModule } from 'vs/base/common/amd'; @@ -118,14 +119,14 @@ suite('Encoding', () => { }); test('resolve terminal encoding (detect)', async function () { - const enc = await encoding.resolveTerminalEncoding(); - assert.ok(encoding.encodingExists(enc)); + const enc = await terminalEncoding.resolveTerminalEncoding(); + assert.ok(enc.length > 0); }); test('resolve terminal encoding (environment)', async function () { process.env['VSCODE_CLI_ENCODING'] = 'utf16le'; - const enc = await encoding.resolveTerminalEncoding(); + const enc = await terminalEncoding.resolveTerminalEncoding(); assert.ok(encoding.encodingExists(enc)); assert.equal(enc, 'utf16le'); }); diff --git a/src/vs/code/electron-main/window.ts b/src/vs/code/electron-main/window.ts index b0a839d5b9d..d0ea8752aae 100644 --- a/src/vs/code/electron-main/window.ts +++ b/src/vs/code/electron-main/window.ts @@ -130,11 +130,6 @@ export class CodeWindow extends Disposable implements ICodeWindow { show: !isFullscreenOrMaximized, title: product.nameLong, webPreferences: { - // By default if Code is in the background, intervals and timeouts get throttled, so we - // want to enforce that Code stays in the foreground. This triggers a disable_hidden_ - // flag that Electron provides via patch: - // https://github.com/electron/libchromiumcontent/blob/master/patches/common/chromium/disable_hidden.patch - backgroundThrottling: false, nodeIntegration: true, nodeIntegrationInWorker: RUN_TEXTMATE_IN_WORKER, webviewTag: true @@ -805,15 +800,24 @@ export class CodeWindow extends Disposable implements ICodeWindow { if (displayWorkingArea) { this.logService.trace('window#validateWindowState: 1 monitor working area', displayWorkingArea); - if (state.x < displayWorkingArea.x) { - // prevent window from falling out of the screen to the left - state.x = displayWorkingArea.x; + function ensureStateInDisplayWorkingArea(): void { + if (!state || typeof state.x !== 'number' || typeof state.y !== 'number' || !displayWorkingArea) { + return; + } + + if (state.x < displayWorkingArea.x) { + // prevent window from falling out of the screen to the left + state.x = displayWorkingArea.x; + } + + if (state.y < displayWorkingArea.y) { + // prevent window from falling out of the screen to the top + state.y = displayWorkingArea.y; + } } - if (state.y < displayWorkingArea.y) { - // prevent window from falling out of the screen to the top - state.y = displayWorkingArea.y; - } + // ensure state is not outside display working area (top, left) + ensureStateInDisplayWorkingArea(); if (state.width > displayWorkingArea.width) { // prevent window from exceeding display bounds width @@ -838,6 +842,10 @@ export class CodeWindow extends Disposable implements ICodeWindow { // the screen state.y = displayWorkingArea.y + displayWorkingArea.height - state.height; } + + // again ensure state is not outside display working area + // (it may have changed from the previous validation step) + ensureStateInDisplayWorkingArea(); } return state; @@ -857,17 +865,16 @@ export class CodeWindow extends Disposable implements ICodeWindow { } } - // Multi Monitor (non-fullscreen): be less strict because metrics can be crazy - const bounds = { x: state.x, y: state.y, width: state.width, height: state.height }; - const display = screen.getDisplayMatching(bounds); + // Multi Monitor (non-fullscreen): ensure window is within display bounds + const display = screen.getDisplayMatching({ x: state.x, y: state.y, width: state.width, height: state.height }); const displayWorkingArea = this.getWorkingArea(display); if ( display && // we have a display matching the desired bounds displayWorkingArea && // we have valid working area bounds - bounds.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right - bounds.y < displayWorkingArea.y + displayWorkingArea.height && // prevent window from falling out of the screen to the bottom - bounds.x + bounds.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left - bounds.y + bounds.height > displayWorkingArea.y // prevent window from falling out of the scree nto the top + state.x + state.width > displayWorkingArea.x && // prevent window from falling out of the screen to the left + state.y + state.height > displayWorkingArea.y && // prevent window from falling out of the screen to the top + state.x < displayWorkingArea.x + displayWorkingArea.width && // prevent window from falling out of the screen to the right + state.y < displayWorkingArea.y + displayWorkingArea.height // prevent window from falling out of the screen to the bottom ) { this.logService.trace('window#validateWindowState: multi-monitor working area', displayWorkingArea); diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index 5a6c573b36d..609395b535a 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -14,10 +14,10 @@ import product from 'vs/platform/product/common/product'; import * as paths from 'vs/base/common/path'; import { whenDeleted, writeFileSync } from 'vs/base/node/pfs'; import { findFreePort, randomPort } from 'vs/base/node/ports'; -import { resolveTerminalEncoding } from 'vs/base/node/encoding'; import { isWindows, isLinux } from 'vs/base/common/platform'; import { ProfilingSession, Target } from 'v8-inspect-profiler'; import { isString } from 'vs/base/common/types'; +import { hasStdinWithoutTty, stdinDataListener, getStdinFilePath, readFromStdin } from 'vs/platform/environment/node/stdin'; function shouldSpawnCliProcess(argv: ParsedArgs): boolean { return !!argv['install-source'] @@ -142,91 +142,55 @@ export async function main(argv: string[]): Promise { }); } - let stdinWithoutTty: boolean = false; - try { - stdinWithoutTty = !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 - } catch (error) { - // Windows workaround for https://github.com/nodejs/node/issues/11656 - } - - const readFromStdin = args._.some(a => a === '-'); - if (readFromStdin) { + const hasReadStdinArg = args._.some(a => a === '-'); + if (hasReadStdinArg) { // remove the "-" argument when we read from stdin args._ = args._.filter(a => a !== '-'); argv = argv.filter(a => a !== '-'); } - let stdinFilePath: string; - if (stdinWithoutTty) { + let stdinFilePath: string | undefined; + if (hasStdinWithoutTty()) { // Read from stdin: we require a single "-" argument to be passed in order to start reading from // stdin. We do this because there is no reliable way to find out if data is piped to stdin. Just // checking for stdin being connected to a TTY is not enough (https://github.com/Microsoft/vscode/issues/40351) - if (args._.length === 0 && readFromStdin) { - // prepare temp file to read stdin to - stdinFilePath = paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`); + if (args._.length === 0) { + if (hasReadStdinArg) { + stdinFilePath = getStdinFilePath(); - // open tmp file for writing - let stdinFileError: Error | undefined; - let stdinFileStream: fs.WriteStream; - try { - stdinFileStream = fs.createWriteStream(stdinFilePath); - } catch (error) { - stdinFileError = error; - } + // returns a file path where stdin input is written into (write in progress). + try { + readFromStdin(stdinFilePath, !!verbose); // throws error if file can not be written - if (!stdinFileError) { + // Make sure to open tmp file + addArg(argv, stdinFilePath); - // Pipe into tmp file using terminals encoding - resolveTerminalEncoding(verbose).then(async encoding => { - const iconv = await import('iconv-lite'); - const converterStream = iconv.decodeStream(encoding); - process.stdin.pipe(converterStream).pipe(stdinFileStream); - }); + // Enable --wait to get all data and ignore adding this to history + addArg(argv, '--wait'); + addArg(argv, '--skip-add-to-recently-opened'); + args.wait = true; - // Make sure to open tmp file - addArg(argv, stdinFilePath); - - // Enable --wait to get all data and ignore adding this to history - addArg(argv, '--wait'); - addArg(argv, '--skip-add-to-recently-opened'); - args.wait = true; - } - - if (verbose) { - if (stdinFileError) { - console.error(`Failed to create file to read via stdin: ${stdinFileError.toString()}`); - } else { console.log(`Reading from stdin via: ${stdinFilePath}`); + } catch (e) { + console.log(`Failed to create file to read via stdin: ${e.toString()}`); + stdinFilePath = undefined; } - } - } + } else { - // If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message - // if we detect that data flows into via stdin after a certain timeout. - else if (args._.length === 0) { - processCallbacks.push(child => new Promise(c => { - const dataListener = () => { - if (isWindows) { - console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`); - } else { - console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`); + // If the user pipes data via stdin but forgot to add the "-" argument, help by printing a message + // if we detect that data flows into via stdin after a certain timeout. + processCallbacks.push(_ => stdinDataListener(1000).then(dataReceived => { + if (dataReceived) { + if (isWindows) { + console.log(`Run with '${product.applicationName} -' to read output from another program (e.g. 'echo Hello World | ${product.applicationName} -').`); + } else { + console.log(`Run with '${product.applicationName} -' to read from stdin (e.g. 'ps aux | grep code | ${product.applicationName} -').`); + } } - - c(undefined); - }; - - // wait for 1s maximum... - setTimeout(() => { - process.stdin.removeListener('data', dataListener); - - c(undefined); - }, 1000); - - // ...but finish early if we detect data - process.stdin.once('data', dataListener); - })); + })); + } } } diff --git a/src/vs/code/test/electron-main/windowsStateStorage.test.ts b/src/vs/code/test/electron-main/windowsStateStorage.test.ts index 6094456c6f5..419caee0d71 100644 --- a/src/vs/code/test/electron-main/windowsStateStorage.test.ts +++ b/src/vs/code/test/electron-main/windowsStateStorage.test.ts @@ -2,6 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import * as assert from 'assert'; import * as os from 'os'; import * as path from 'vs/base/common/path'; diff --git a/src/vs/editor/browser/controller/mouseTarget.ts b/src/vs/editor/browser/controller/mouseTarget.ts index 7e7e84a613e..c38191a19c6 100644 --- a/src/vs/editor/browser/controller/mouseTarget.ts +++ b/src/vs/editor/browser/controller/mouseTarget.ts @@ -79,7 +79,7 @@ interface IETextRange { setEndPoint(how: string, SourceRange: IETextRange): void; } -declare var IETextRange: { +declare const IETextRange: { prototype: IETextRange; new(): IETextRange; }; diff --git a/src/vs/editor/common/core/stringBuilder.ts b/src/vs/editor/common/core/stringBuilder.ts index 36cbe87f54e..661fd6642e2 100644 --- a/src/vs/editor/common/core/stringBuilder.ts +++ b/src/vs/editor/common/core/stringBuilder.ts @@ -5,7 +5,7 @@ import * as strings from 'vs/base/common/strings'; -declare var TextDecoder: any; // TODO@TypeScript +declare const TextDecoder: any; // TODO@TypeScript interface TextDecoder { decode(view: Uint16Array): string; } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 1b0a5dd0fea..9db95948d24 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -436,7 +436,7 @@ export interface CompletionItem { preselect?: boolean; /** * A string or snippet that should be inserted in a document when selecting - * this completion. + * this completion. When `falsy` the [label](#CompletionItem.label) * is used. */ insertText: string; diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index 01ca792e497..4960aa3984a 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -322,7 +322,7 @@ export interface IForeignModuleFactory { (ctx: IWorkerContext, createData: any): any; } -declare var require: any; +declare const require: any; /** * @internal diff --git a/src/vs/editor/common/services/editorWorkerServiceImpl.ts b/src/vs/editor/common/services/editorWorkerServiceImpl.ts index a4a25cf7865..e1e5804fdc0 100644 --- a/src/vs/editor/common/services/editorWorkerServiceImpl.ts +++ b/src/vs/editor/common/services/editorWorkerServiceImpl.ts @@ -236,7 +236,7 @@ class WorkerManager extends Disposable { public withWorker(): Promise { this._lastWorkerUsedTime = (new Date()).getTime(); if (!this._editorWorkerClient) { - this._editorWorkerClient = new EditorWorkerClient(this._modelService, 'editorWorkerService'); + this._editorWorkerClient = new EditorWorkerClient(this._modelService, false, 'editorWorkerService'); } return Promise.resolve(this._editorWorkerClient); } @@ -374,13 +374,15 @@ export class EditorWorkerHost { export class EditorWorkerClient extends Disposable { private readonly _modelService: IModelService; + private readonly _keepIdleModels: boolean; private _worker: IWorkerClient | null; private readonly _workerFactory: DefaultWorkerFactory; private _modelManager: EditorModelManager | null; - constructor(modelService: IModelService, label: string | undefined) { + constructor(modelService: IModelService, keepIdleModels: boolean, label: string | undefined) { super(); this._modelService = modelService; + this._keepIdleModels = keepIdleModels; this._workerFactory = new DefaultWorkerFactory(label); this._worker = null; this._modelManager = null; @@ -417,7 +419,7 @@ export class EditorWorkerClient extends Disposable { private _getOrCreateModelManager(proxy: EditorSimpleWorker): EditorModelManager { if (!this._modelManager) { - this._modelManager = this._register(new EditorModelManager(proxy, this._modelService, false)); + this._modelManager = this._register(new EditorModelManager(proxy, this._modelService, this._keepIdleModels)); } return this._modelManager; } diff --git a/src/vs/editor/common/services/modelServiceImpl.ts b/src/vs/editor/common/services/modelServiceImpl.ts index c0280a3c379..95b967ab215 100644 --- a/src/vs/editor/common/services/modelServiceImpl.ts +++ b/src/vs/editor/common/services/modelServiceImpl.ts @@ -592,30 +592,34 @@ class SemanticColoringProviderStyling { public getMetadata(tokenTypeIndex: number, tokenModifierSet: number): number { const entry = this._hashTable.get(tokenTypeIndex, tokenModifierSet); + let metadata: number | undefined; if (entry) { - return entry.metadata; - } - - const tokenType = this._legend.tokenTypes[tokenTypeIndex]; - const tokenModifiers: string[] = []; - for (let modifierIndex = 0; tokenModifierSet !== 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) { - if (tokenModifierSet & 1) { - tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]); + metadata = entry.metadata; + } else { + const tokenType = this._legend.tokenTypes[tokenTypeIndex]; + const tokenModifiers: string[] = []; + for (let modifierIndex = 0; tokenModifierSet !== 0 && modifierIndex < this._legend.tokenModifiers.length; modifierIndex++) { + if (tokenModifierSet & 1) { + tokenModifiers.push(this._legend.tokenModifiers[modifierIndex]); + } + tokenModifierSet = tokenModifierSet >> 1; } - tokenModifierSet = tokenModifierSet >> 1; - } - let metadata = this._themeService.getTheme().getTokenStyleMetadata(tokenType, tokenModifiers); - if (typeof metadata === 'undefined') { - metadata = Constants.NO_STYLING; + metadata = this._themeService.getTheme().getTokenStyleMetadata(tokenType, tokenModifiers); + if (typeof metadata === 'undefined') { + metadata = Constants.NO_STYLING; + } + this._hashTable.add(tokenTypeIndex, tokenModifierSet, metadata); } if (this._logService.getLevel() === LogLevel.Trace) { - this._logService.trace(`getTokenStyleMetadata(${tokenType}${tokenModifiers.length ? ', ' + tokenModifiers.join(' ') : ''}): foreground: ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); + const type = this._legend.tokenTypes[tokenTypeIndex]; + const modifiers = tokenModifierSet ? ' ' + this._legend.tokenModifiers.filter((_, i) => tokenModifierSet & (1 << i)).join(' ') : ''; + this._logService.trace(`tokenStyleMetadata ${entry ? '[CACHED] ' : ''}${type}${modifiers}: foreground ${TokenMetadata.getForeground(metadata)}, fontStyle ${TokenMetadata.getFontStyle(metadata).toString(2)}`); } - - this._hashTable.add(tokenTypeIndex, tokenModifierSet, metadata); return metadata; } + + } const enum SemanticColoringConstants { diff --git a/src/vs/editor/common/services/webWorker.ts b/src/vs/editor/common/services/webWorker.ts index 0ae55dffaa0..55dfe84af39 100644 --- a/src/vs/editor/common/services/webWorker.ts +++ b/src/vs/editor/common/services/webWorker.ts @@ -53,6 +53,11 @@ export interface IWebWorkerOptions { * An object that can be used by the web worker to make calls back to the main thread. */ host?: any; + /** + * Keep idle models. + * Defaults to false, which means that idle models will stop syncing after a while. + */ + keepIdleModels?: boolean; } class MonacoWebWorkerImpl extends EditorWorkerClient implements MonacoWebWorker { @@ -63,7 +68,7 @@ class MonacoWebWorkerImpl extends EditorWorkerClient implements MonacoWebWork private _foreignProxy: Promise | null; constructor(modelService: IModelService, opts: IWebWorkerOptions) { - super(modelService, opts.label); + super(modelService, opts.keepIdleModels || false, opts.label); this._foreignModuleId = opts.moduleId; this._foreignModuleCreateData = opts.createData || null; this._foreignModuleHost = opts.host || null; diff --git a/src/vs/editor/contrib/codelens/codelensWidget.css b/src/vs/editor/contrib/codelens/codelensWidget.css index d949272a106..79eae29cea9 100644 --- a/src/vs/editor/contrib/codelens/codelensWidget.css +++ b/src/vs/editor/contrib/codelens/codelensWidget.css @@ -23,17 +23,16 @@ } .monaco-editor .codelens-decoration > a:hover { - text-decoration: underline; cursor: pointer; } .monaco-editor .codelens-decoration .codicon { line-height: inherit; - font-size: inherit; + font-size: 110%; + vertical-align: inherit; } .monaco-editor .codelens-decoration > a:hover .codicon::before { - text-decoration: underline; cursor: pointer; } diff --git a/src/vs/editor/contrib/folding/folding.ts b/src/vs/editor/contrib/folding/folding.ts index 86f04e0db86..1af69c1cfb5 100644 --- a/src/vs/editor/contrib/folding/folding.ts +++ b/src/vs/editor/contrib/folding/folding.ts @@ -421,9 +421,18 @@ export class FoldingController extends Disposable implements IEditorContribution if (region && region.startLineNumber === lineNumber) { let isCollapsed = region.isCollapsed; if (iconClicked || isCollapsed) { - let toToggle = [region]; - if (e.event.middleButton || e.event.shiftKey) { - toToggle.push(...foldingModel.getRegionsInside(region, (r: FoldingRegion) => r.isCollapsed === isCollapsed)); + let toToggle = []; + let recursive = e.event.middleButton || e.event.shiftKey; + if (recursive) { + for (const r of foldingModel.getRegionsInside(region)) { + if (r.isCollapsed === isCollapsed) { + toToggle.push(r); + } + } + } + // when recursive, first only collapse all children. If all are already folded or there are no children, also fold parent. + if (isCollapsed || !recursive || toToggle.length === 0) { + toToggle.push(region); } foldingModel.toggleCollapseState(toToggle); this.reveal({ lineNumber, column: 1 }); diff --git a/src/vs/editor/contrib/folding/foldingDecorations.ts b/src/vs/editor/contrib/folding/foldingDecorations.ts index 333ad459db0..e3bbec8ae67 100644 --- a/src/vs/editor/contrib/folding/foldingDecorations.ts +++ b/src/vs/editor/contrib/folding/foldingDecorations.ts @@ -13,6 +13,8 @@ export class FoldingDecorationProvider implements IDecorationProvider { private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({ stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, afterContentClassName: 'inline-folded', + className: 'folded-background', + isWholeLine: true, linesDecorationsClassName: 'codicon codicon-chevron-right' }); diff --git a/src/vs/editor/contrib/folding/foldingModel.ts b/src/vs/editor/contrib/folding/foldingModel.ts index 93a52a14728..386c923f834 100644 --- a/src/vs/editor/contrib/folding/foldingModel.ts +++ b/src/vs/editor/contrib/folding/foldingModel.ts @@ -6,6 +6,9 @@ import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model'; import { Event, Emitter } from 'vs/base/common/event'; import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { registerColor, editorSelectionBackground, darken, lighten } from 'vs/platform/theme/common/colorRegistry'; +import * as nls from 'vs/nls'; export interface IDecorationProvider { getDecorationOption(isCollapsed: boolean): IModelDecorationOptions; @@ -90,7 +93,6 @@ export class FoldingModel { }; newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed) }); }; - let i = 0; let nextCollapsed = () => { while (i < this._regions.length) { @@ -355,3 +357,12 @@ export function setCollapseStateForType(foldingModel: FoldingModel, type: string } foldingModel.toggleCollapseState(toToggle); } + +export const foldBackgroundBackground = registerColor('editor.foldBackground', { light: lighten(editorSelectionBackground, 0.5), dark: darken(editorSelectionBackground, 0.5), hc: null }, nls.localize('editorSelectionBackground', "Color of the editor selection.")); + +registerThemingParticipant((theme, collector) => { + const foldBackground = theme.getColor(foldBackgroundBackground); + if (foldBackground) { + collector.addRule(`.monaco-editor .folded-background { background-color: ${foldBackground}; }`); + } +}); diff --git a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts index 7724699eded..157a018ac0d 100644 --- a/src/vs/editor/contrib/multicursor/test/multicursor.test.ts +++ b/src/vs/editor/contrib/multicursor/test/multicursor.test.ts @@ -69,7 +69,8 @@ suite('Multicursor selection', () => { store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); }, remove: (key) => undefined, logStorage: () => undefined, - migrate: (toWorkspace) => Promise.resolve(undefined) + migrate: (toWorkspace) => Promise.resolve(undefined), + flush: () => undefined } as IStorageService); test('issue #8817: Cursor position changes when you cancel multicursor', () => { diff --git a/src/vs/editor/contrib/suggest/test/suggestController.test.ts b/src/vs/editor/contrib/suggest/test/suggestController.test.ts new file mode 100644 index 00000000000..e3c00dbb01f --- /dev/null +++ b/src/vs/editor/contrib/suggest/test/suggestController.test.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; +import { createTestCodeEditor, TestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; +import { TextModel } from 'vs/editor/common/model/textModel'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { IStorageService, InMemoryStorageService } from 'vs/platform/storage/common/storage'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MockKeybindingService } from 'vs/platform/keybinding/test/common/mockKeybindingService'; +import { ISuggestMemoryService } from 'vs/editor/contrib/suggest/suggestMemory'; +import { DisposableStore } from 'vs/base/common/lifecycle'; +import { URI } from 'vs/base/common/uri'; +import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService'; +import { mock } from 'vs/editor/contrib/suggest/test/suggestModel.test'; +import { Selection } from 'vs/editor/common/core/selection'; +import { CompletionProviderRegistry, CompletionItemKind, CompletionItemInsertTextRule } from 'vs/editor/common/modes'; +import { Event } from 'vs/base/common/event'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2'; + +suite('SuggestController', function () { + + const disposables = new DisposableStore(); + + let controller: SuggestController; + let editor: TestCodeEditor; + let model: TextModel; + + setup(function () { + disposables.clear(); + + const serviceCollection = new ServiceCollection( + [ITelemetryService, NullTelemetryService], + [IStorageService, new InMemoryStorageService()], + [IKeybindingService, new MockKeybindingService()], + [IEditorWorkerService, new class extends mock() { + computeWordRanges() { + return Promise.resolve({}); + } + }], + [ISuggestMemoryService, new class extends mock() { + memorize(): void { } + select(): number { return 0; } + }] + ); + + model = TextModel.createFromString('', undefined, undefined, URI.from({ scheme: 'test-ctrl', path: '/path.tst' })); + editor = createTestCodeEditor({ + model, + serviceCollection, + }); + + editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); + controller = editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController); + }); + + test('postfix completion reports incorrect position #86984', async function () { + disposables.add(CompletionProviderRegistry.register({ scheme: 'test-ctrl' }, { + provideCompletionItems(doc, pos) { + return { + suggestions: [{ + kind: CompletionItemKind.Snippet, + label: 'let', + insertText: 'let ${1:name} = foo$0', + insertTextRules: CompletionItemInsertTextRule.InsertAsSnippet, + range: { startLineNumber: 1, startColumn: 9, endLineNumber: 1, endColumn: 11 }, + additionalTextEdits: [{ + text: '', + range: { startLineNumber: 1, startColumn: 5, endLineNumber: 1, endColumn: 9 } + }] + }] + }; + } + })); + + editor.setValue(' foo.le'); + editor.setSelection(new Selection(1, 11, 1, 11)); + + // trigger + let p1 = Event.toPromise(controller.model.onDidSuggest); + controller.triggerSuggest(); + await p1; + + // + let p2 = Event.toPromise(controller.model.onDidCancel); + controller.acceptSelectedSuggestion(false, false); + await p2; + + assert.equal(editor.getValue(), ' let name = foo'); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 1f40e30e414..27cece82f20 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -996,6 +996,11 @@ declare namespace monaco.editor { * An object that can be used by the web worker to make calls back to the main thread. */ host?: any; + /** + * Keep idle models. + * Defaults to false, which means that idle models will stop syncing after a while. + */ + keepIdleModels?: boolean; } /** @@ -4752,7 +4757,7 @@ declare namespace monaco.languages { preselect?: boolean; /** * A string or snippet that should be inserted in a document when selecting - * this completion. + * this completion. When `falsy` the [label](#CompletionItem.label) * is used. */ insertText: string; diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 8eb6380e0f9..13b05bd3949 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -70,6 +70,8 @@ export const enum MenuId { EditorTitleContext, EmptyEditorGroupContext, ExplorerContext, + ExtensionContext, + GlobalActivity, MenubarAppearanceMenu, MenubarDebugMenu, MenubarEditMenu, @@ -106,7 +108,6 @@ export const enum MenuId { CommentThreadActions, CommentTitle, CommentActions, - GlobalActivity } export interface IMenuActionOptions { diff --git a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts index b4dc3452beb..0bffaa82992 100644 --- a/src/vs/platform/backup/test/electron-main/backupMainService.test.ts +++ b/src/vs/platform/backup/test/electron-main/backupMainService.test.ts @@ -44,16 +44,16 @@ suite('BackupMainService', () => { this.workspacesJsonPath = backupWorkspacesPath; } - public toBackupPath(arg: URI | string): string { + toBackupPath(arg: URI | string): string { const id = arg instanceof URI ? super.getFolderHash(arg) : arg; return path.join(this.backupHome, id); } - public getFolderHash(folderUri: URI): string { + getFolderHash(folderUri: URI): string { return super.getFolderHash(folderUri); } - public toLegacyBackupPath(folderPath: string): string { + toLegacyBackupPath(folderPath: string): string { return path.join(this.backupHome, super.getLegacyFolderHash(folderPath)); } } @@ -119,17 +119,16 @@ suite('BackupMainService', () => { let service: TestBackupMainService; let configService: TestConfigurationService; - setup(() => { + setup(async () => { // Delete any existing backups completely and then re-create it. - return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE).then(() => { - return pfs.mkdirp(backupHome); - }).then(() => { - configService = new TestConfigurationService(); - service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService); + await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + await pfs.mkdirp(backupHome); - return service.initialize(); - }); + configService = new TestConfigurationService(); + service = new TestBackupMainService(backupHome, backupWorkspacesPath, configService); + + return service.initialize(); }); teardown(() => { @@ -591,71 +590,71 @@ suite('BackupMainService', () => { }); }); - test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', () => { + test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (folder workspace)', async () => { service.registerFolderBackupSync(URI.file(fooFile.fsPath.toUpperCase())); assertEqualUris(service.getFolderBackupPaths(), [URI.file(fooFile.fsPath.toUpperCase())]); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { - const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); - }); + + const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json = JSON.parse(buffer); + assert.deepEqual(json.folderURIWorkspaces, [URI.file(fooFile.fsPath.toUpperCase()).toString()]); }); - test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', () => { + test('should always store the workspace path in workspaces.json using the case given, regardless of whether the file system is case-sensitive (root workspace)', async () => { const upperFooPath = fooFile.fsPath.toUpperCase(); service.registerWorkspaceBackupSync(toWorkspaceBackupInfo(upperFooPath)); assertEqualUris(service.getWorkspaceBackups().map(b => b.workspace.configPath), [URI.file(upperFooPath)]); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { - const json = JSON.parse(buffer); - assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); - }); + + const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json = (JSON.parse(buffer)); + assert.deepEqual(json.rootURIWorkspaces.map(b => b.configURIPath), [URI.file(upperFooPath).toString()]); }); suite('removeBackupPathSync', () => { - test('should remove folder workspaces from workspaces.json (folder workspace)', () => { + test('should remove folder workspaces from workspaces.json (folder workspace)', async () => { service.registerFolderBackupSync(fooFile); service.registerFolderBackupSync(barFile); service.unregisterFolderBackupSync(fooFile); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { - const json = JSON.parse(buffer); - assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); - service.unregisterFolderBackupSync(barFile); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { - const json2 = JSON.parse(content); - assert.deepEqual(json2.folderURIWorkspaces, []); - }); - }); + + const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json = (JSON.parse(buffer)); + assert.deepEqual(json.folderURIWorkspaces, [barFile.toString()]); + service.unregisterFolderBackupSync(barFile); + + const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json2 = (JSON.parse(content)); + assert.deepEqual(json2.folderURIWorkspaces, []); }); - test('should remove folder workspaces from workspaces.json (root workspace)', () => { + test('should remove folder workspaces from workspaces.json (root workspace)', async () => { const ws1 = toWorkspaceBackupInfo(fooFile.fsPath); service.registerWorkspaceBackupSync(ws1); const ws2 = toWorkspaceBackupInfo(barFile.fsPath); service.registerWorkspaceBackupSync(ws2); service.unregisterWorkspaceBackupSync(ws1.workspace); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { - const json = JSON.parse(buffer); - assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); - service.unregisterWorkspaceBackupSync(ws2.workspace); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { - const json2 = JSON.parse(content); - assert.deepEqual(json2.rootURIWorkspaces, []); - }); - }); + + const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json = (JSON.parse(buffer)); + assert.deepEqual(json.rootURIWorkspaces.map(r => r.configURIPath), [barFile.toString()]); + service.unregisterWorkspaceBackupSync(ws2.workspace); + + const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json2 = (JSON.parse(content)); + assert.deepEqual(json2.rootURIWorkspaces, []); }); - test('should remove empty workspaces from workspaces.json', () => { + test('should remove empty workspaces from workspaces.json', async () => { service.registerEmptyWindowBackupSync('foo'); service.registerEmptyWindowBackupSync('bar'); service.unregisterEmptyWindowBackupSync('foo'); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(buffer => { - const json = JSON.parse(buffer); - assert.deepEqual(json.emptyWorkspaces, ['bar']); - service.unregisterEmptyWindowBackupSync('bar'); - return pfs.readFile(backupWorkspacesPath, 'utf-8').then(content => { - const json2 = JSON.parse(content); - assert.deepEqual(json2.emptyWorkspaces, []); - }); - }); + + const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json = (JSON.parse(buffer)); + assert.deepEqual(json.emptyWorkspaces, ['bar']); + service.unregisterEmptyWindowBackupSync('bar'); + + const content = await pfs.readFile(backupWorkspacesPath, 'utf-8'); + const json2 = (JSON.parse(content)); + assert.deepEqual(json2.emptyWorkspaces, []); }); test('should fail gracefully when removing a path that doesn\'t exist', async () => { diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 81615b1c9e3..68f8ac9c6d4 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -257,9 +257,8 @@ export const enum ConfirmResult { } const MAX_CONFIRM_FILES = 10; -export function getConfirmMessage(start: string, fileNamesOrResources: readonly (string | URI)[]): string { - const message = [start]; - message.push(''); +export function getFileNamesMessage(fileNamesOrResources: readonly (string | URI)[]): string { + const message: string[] = []; message.push(...fileNamesOrResources.slice(0, MAX_CONFIRM_FILES).map(fileNameOrResource => typeof fileNameOrResource === 'string' ? fileNameOrResource : basename(fileNameOrResource))); if (fileNamesOrResources.length > MAX_CONFIRM_FILES) { diff --git a/src/vs/platform/environment/node/stdin.ts b/src/vs/platform/environment/node/stdin.ts new file mode 100644 index 00000000000..2cd928e2507 --- /dev/null +++ b/src/vs/platform/environment/node/stdin.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +/** + * This code is also used by standalone cli's. Avoid adding dependencies to keep the size of the cli small. + */ +import * as paths from 'vs/base/common/path'; +import * as fs from 'fs'; +import * as os from 'os'; +import { resolveTerminalEncoding } from 'vs/base/node/terminalEncoding'; + +export function hasStdinWithoutTty() { + try { + return !process.stdin.isTTY; // Via https://twitter.com/MylesBorins/status/782009479382626304 + } catch (error) { + // Windows workaround for https://github.com/nodejs/node/issues/11656 + } + return false; +} + +export function stdinDataListener(durationinMs: number): Promise { + return new Promise(c => { + const dataListener = () => c(true); + + // wait for 1s maximum... + setTimeout(() => { + process.stdin.removeListener('data', dataListener); + + c(false); + }, durationinMs); + + // ...but finish early if we detect data + process.stdin.once('data', dataListener); + }); +} + +export function getStdinFilePath(): string { + return paths.join(os.tmpdir(), `code-stdin-${Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 3)}.txt`); +} + +export function readFromStdin(targetPath: string, verbose: boolean): Promise { + // open tmp file for writing + const stdinFileStream = fs.createWriteStream(targetPath); + // Pipe into tmp file using terminals encoding + return resolveTerminalEncoding(verbose).then(async encoding => { + + const iconv = await import('iconv-lite'); + if (!iconv.encodingExists(encoding)) { + console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`); + encoding = 'utf8'; + } + const converterStream = iconv.decodeStream(encoding); + process.stdin.pipe(converterStream).pipe(stdinFileStream); + }); +} diff --git a/src/vs/platform/files/common/fileService.ts b/src/vs/platform/files/common/fileService.ts index 6edd9ff882a..268f00245ac 100644 --- a/src/vs/platform/files/common/fileService.ts +++ b/src/vs/platform/files/common/fileService.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError } from 'vs/platform/files/common/files'; +import { IFileService, IResolveFileOptions, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata, IWatchOptions, IWriteFileOptions, IReadFileOptions, IFileStreamContent, IFileContent, ETAG_DISABLED, hasFileReadStreamCapability, IFileSystemProviderWithFileReadStreamCapability, ensureFileSystemProviderError, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { isAbsolutePath, dirname, basename, joinPath, isEqual, isEqualOrParent } from 'vs/base/common/resources'; @@ -33,11 +33,14 @@ export class FileService extends Disposable implements IFileService { //#region File System Provider - private _onDidChangeFileSystemProviderRegistrations: Emitter = this._register(new Emitter()); - readonly onDidChangeFileSystemProviderRegistrations: Event = this._onDidChangeFileSystemProviderRegistrations.event; + private _onDidChangeFileSystemProviderRegistrations = this._register(new Emitter()); + readonly onDidChangeFileSystemProviderRegistrations = this._onDidChangeFileSystemProviderRegistrations.event; - private _onWillActivateFileSystemProvider: Emitter = this._register(new Emitter()); - readonly onWillActivateFileSystemProvider: Event = this._onWillActivateFileSystemProvider.event; + private _onWillActivateFileSystemProvider = this._register(new Emitter()); + readonly onWillActivateFileSystemProvider = this._onWillActivateFileSystemProvider.event; + + private _onDidChangeFileSystemProviderCapabilities = this._register(new Emitter()); + readonly onDidChangeFileSystemProviderCapabilities = this._onDidChangeFileSystemProviderCapabilities.event; private readonly provider = new Map(); @@ -53,6 +56,7 @@ export class FileService extends Disposable implements IFileService { // Forward events from provider const providerDisposables = new DisposableStore(); providerDisposables.add(provider.onDidChangeFile(changes => this._onFileChanges.fire(new FileChangesEvent(changes)))); + providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme }))); if (typeof provider.onDidErrorOccur === 'function') { providerDisposables.add(provider.onDidErrorOccur(error => this._onError.fire(new Error(error)))); } @@ -113,7 +117,7 @@ export class FileService extends Disposable implements IFileService { if (!provider) { const error = new Error(); error.name = 'ENOPRO'; - error.message = localize('noProviderFound', "No file system provider found for {0}", resource.toString()); + error.message = localize('noProviderFound', "No file system provider found for resource '{0}'", resource.toString()); throw error; } @@ -128,7 +132,7 @@ export class FileService extends Disposable implements IFileService { return provider; } - throw new Error('Provider neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.'); + throw new Error(`Provider for scheme '${resource.scheme}' neither has FileReadWrite, FileReadStream nor FileOpenReadWriteClose capability which is needed for the read operation.`); } private async withWriteProvider(resource: URI): Promise { @@ -138,7 +142,7 @@ export class FileService extends Disposable implements IFileService { return provider; } - throw new Error('Provider neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.'); + throw new Error(`Provider for scheme '${resource.scheme}' neither has FileReadWrite nor FileOpenReadWriteClose capability which is needed for the write operation.`); } //#endregion @@ -160,10 +164,7 @@ export class FileService extends Disposable implements IFileService { // Specially handle file not found case as file operation result if (toFileSystemProviderErrorCode(error) === FileSystemProviderErrorCode.FileNotFound) { - throw new FileOperationError( - localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), - FileOperationResult.FILE_NOT_FOUND - ); + throw new FileOperationError(localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Bubble up any other error as is @@ -304,7 +305,7 @@ export class FileService extends Disposable implements IFileService { } async writeFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise { - const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource)); + const provider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(resource), resource); try { @@ -338,7 +339,7 @@ export class FileService extends Disposable implements IFileService { await this.doWriteBuffered(provider, resource, bufferOrReadableOrStream instanceof VSBuffer ? bufferToReadable(bufferOrReadableOrStream) : bufferOrReadableOrStream); } } catch (error) { - throw new FileOperationError(localize('err.write', "Unable to write file ({0})", ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.write', "Unable to write file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); } return this.resolve(resource, { resolveMetadata: true }); @@ -354,7 +355,7 @@ export class FileService extends Disposable implements IFileService { // file cannot be directory if ((stat.type & FileType.Directory) !== 0) { - throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); + throw new FileOperationError(localize('fileIsDirectoryError', "Expected file '{0}' is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } // Dirty write prevention: if the file on disk has been changed and does not match our expected @@ -453,14 +454,14 @@ export class FileService extends Disposable implements IFileService { value: fileStream }; } catch (error) { - throw new FileOperationError(localize('err.read', "Unable to read file ({0})", ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); + throw new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options); } } private readFileStreamed(provider: IFileSystemProviderWithFileReadStreamCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream { const fileStream = provider.readFileStream(resource, options, token); - return this.transformFileReadStream(fileStream, options); + return this.transformFileReadStream(resource, fileStream, options); } private readFileBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, token: CancellationToken, options: IReadFileOptions = Object.create(null)): VSBufferReadableStream { @@ -469,13 +470,13 @@ export class FileService extends Disposable implements IFileService { bufferSize: this.BUFFER_SIZE }, token); - return this.transformFileReadStream(fileStream, options); + return this.transformFileReadStream(resource, fileStream, options); } - private transformFileReadStream(stream: ReadableStreamEvents, options: IReadFileOptions): VSBufferReadableStream { + private transformFileReadStream(resource: URI, stream: ReadableStreamEvents, options: IReadFileOptions): VSBufferReadableStream { return transform(stream, { data: data => data instanceof VSBuffer ? data : VSBuffer.wrap(data), - error: error => new FileOperationError(localize('err.read', "Unable to read file ({0})", ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options) + error: error => new FileOperationError(localize('err.read', "Unable to read file '{0}' ({1})", this.resourceForError(resource), ensureFileSystemProviderError(error).toString()), toFileOperationResult(error), options) }, data => VSBuffer.concat(data)); } @@ -493,7 +494,7 @@ export class FileService extends Disposable implements IFileService { } // Throw if file is too large to load - this.validateReadFileLimits(buffer.byteLength, options); + this.validateReadFileLimits(resource, buffer.byteLength, options); return bufferToStream(VSBuffer.wrap(buffer)); } @@ -503,7 +504,7 @@ export class FileService extends Disposable implements IFileService { // Throw if resource is a directory if (stat.isDirectory) { - throw new FileOperationError(localize('fileIsDirectoryError', "Expected file {0} is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); + throw new FileOperationError(localize('fileIsDirectoryError', "Expected file '{0}' is actually a directory", this.resourceForError(resource)), FileOperationResult.FILE_IS_DIRECTORY, options); } // Throw if file not modified since (unless disabled) @@ -512,12 +513,12 @@ export class FileService extends Disposable implements IFileService { } // Throw if file is too large to load - this.validateReadFileLimits(stat.size, options); + this.validateReadFileLimits(resource, stat.size, options); return stat; } - private validateReadFileLimits(size: number, options?: IReadFileOptions): void { + private validateReadFileLimits(resource: URI, size: number, options?: IReadFileOptions): void { if (options?.limits) { let tooLargeErrorResult: FileOperationResult | undefined = undefined; @@ -530,7 +531,7 @@ export class FileService extends Disposable implements IFileService { } if (typeof tooLargeErrorResult === 'number') { - throw new FileOperationError(localize('fileTooLargeError', "File is too large to open"), tooLargeErrorResult); + throw new FileOperationError(localize('fileTooLargeError', "File '{0}' is too large to open", this.resourceForError(resource)), tooLargeErrorResult); } } } @@ -540,8 +541,8 @@ export class FileService extends Disposable implements IFileService { //#region Move/Copy/Delete/Create Folder async move(source: URI, target: URI, overwrite?: boolean): Promise { - const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source)); - const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target)); + const sourceProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(source), source); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); // move const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'move', !!overwrite); @@ -555,7 +556,7 @@ export class FileService extends Disposable implements IFileService { async copy(source: URI, target: URI, overwrite?: boolean): Promise { const sourceProvider = await this.withReadProvider(source); - const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target)); + const targetProvider = this.throwIfFileSystemIsReadonly(await this.withWriteProvider(target), target); // copy const mode = await this.doMoveCopy(sourceProvider, source, targetProvider, target, 'copy', !!overwrite); @@ -678,11 +679,11 @@ export class FileService extends Disposable implements IFileService { } if (isSameResourceWithDifferentPathCase && mode === 'copy') { - throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source is same as target with different path case on a case insensitive file system")); + throw new Error(localize('unableToMoveCopyError1', "Unable to copy when source '{0}' is same as target '{1}' with different path case on a case insensitive file system", this.resourceForError(source), this.resourceForError(target))); } if (!isSameResourceWithDifferentPathCase && isEqualOrParent(target, source, !isPathCaseSensitive)) { - throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source is parent of target.")); + throw new Error(localize('unableToMoveCopyError2', "Unable to move/copy when source '{0}' is parent of target '{1}'.", this.resourceForError(source), this.resourceForError(target))); } } @@ -709,7 +710,7 @@ export class FileService extends Disposable implements IFileService { } async createFolder(resource: URI): Promise { - const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); // mkdir recursively await this.mkdirp(provider, resource); @@ -729,7 +730,7 @@ export class FileService extends Disposable implements IFileService { try { const stat = await provider.stat(directory); if ((stat.type & FileType.Directory) === 0) { - throw new Error(localize('mkdirExistsError', "{0} exists, but is not a directory", this.resourceForError(directory))); + throw new Error(localize('mkdirExistsError', "Path '{0}' already exists, but is not a directory", this.resourceForError(directory))); } break; // we have hit a directory that exists -> good @@ -756,21 +757,18 @@ export class FileService extends Disposable implements IFileService { } async del(resource: URI, options?: { useTrash?: boolean; recursive?: boolean; }): Promise { - const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource)); + const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource); // Validate trash support const useTrash = !!options?.useTrash; if (useTrash && !(provider.capabilities & FileSystemProviderCapabilities.Trash)) { - throw new Error(localize('err.trash', "Provider does not support trash.")); + throw new Error(localize('err.trash', "Provider for scheme '{0}' does not support trash.", resource.scheme)); } // Validate delete const exists = await this.exists(resource); if (!exists) { - throw new FileOperationError( - localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), - FileOperationResult.FILE_NOT_FOUND - ); + throw new FileOperationError(localize('fileNotFoundError', "File not found ({0})", this.resourceForError(resource)), FileOperationResult.FILE_NOT_FOUND); } // Validate recursive @@ -1059,9 +1057,9 @@ export class FileService extends Disposable implements IFileService { await this.doWriteUnbuffered(targetProvider, target, buffer); } - protected throwIfFileSystemIsReadonly(provider: T): T { + protected throwIfFileSystemIsReadonly(provider: T, resource: URI): T { if (provider.capabilities & FileSystemProviderCapabilities.Readonly) { - throw new FileOperationError(localize('err.readonly', "Resource can not be modified."), FileOperationResult.FILE_PERMISSION_DENIED); + throw new FileOperationError(localize('err.readonly', "Resource '{0}' can not be modified.", this.resourceForError(resource)), FileOperationResult.FILE_PERMISSION_DENIED); } return provider; diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index c00e9b17fef..a45eb2016cf 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -28,6 +28,11 @@ export interface IFileService { */ readonly onDidChangeFileSystemProviderRegistrations: Event; + /** + * An even that is fired when a registered file system provider changes it's capabilities. + */ + readonly onDidChangeFileSystemProviderCapabilities: Event; + /** * An event that is fired when a file system provider is about to be activated. Listeners * can join this event with a long running promise to help in the activation process. @@ -409,6 +414,11 @@ export interface IFileSystemProviderRegistrationEvent { provider?: IFileSystemProvider; } +export interface IFileSystemProviderCapabilitiesChangeEvent { + provider: IFileSystemProvider; + scheme: string; +} + export interface IFileSystemProviderActivationEvent { scheme: string; join(promise: Promise): void; diff --git a/src/vs/platform/files/test/browser/fileService.test.ts b/src/vs/platform/files/test/browser/fileService.test.ts index 2eb366a65f0..edc32f0a3b4 100644 --- a/src/vs/platform/files/test/browser/fileService.test.ts +++ b/src/vs/platform/files/test/browser/fileService.test.ts @@ -6,7 +6,7 @@ import * as assert from 'assert'; import { FileService } from 'vs/platform/files/common/fileService'; import { URI } from 'vs/base/common/uri'; -import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileSystemProviderRegistrationEvent, FileSystemProviderCapabilities, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { NullLogService } from 'vs/platform/log/common/log'; import { timeout } from 'vs/base/common/async'; @@ -17,6 +17,7 @@ suite('File Service', () => { test('provider registration', async () => { const service = new FileService(new NullLogService()); const resource = URI.parse('test://foo/bar'); + const provider = new NullFileSystemProvider(); assert.equal(service.canHandleResource(resource), false); @@ -25,6 +26,11 @@ suite('File Service', () => { registrations.push(e); }); + const capabilityChanges: IFileSystemProviderCapabilitiesChangeEvent[] = []; + service.onDidChangeFileSystemProviderCapabilities(e => { + capabilityChanges.push(e); + }); + let registrationDisposable: IDisposable | undefined = undefined; let callCount = 0; service.onWillActivateFileSystemProvider(e => { @@ -32,7 +38,7 @@ suite('File Service', () => { if (e.scheme === 'test' && callCount === 1) { e.join(new Promise(resolve => { - registrationDisposable = service.registerProvider('test', new NullFileSystemProvider()); + registrationDisposable = service.registerProvider('test', provider); resolve(); })); @@ -48,6 +54,13 @@ suite('File Service', () => { assert.equal(registrations[0].added, true); assert.ok(registrationDisposable); + assert.equal(capabilityChanges.length, 0); + + provider.setCapabilities(FileSystemProviderCapabilities.FileFolderCopy); + assert.equal(capabilityChanges.length, 1); + provider.setCapabilities(FileSystemProviderCapabilities.Readonly); + assert.equal(capabilityChanges.length, 2); + await service.activateProvider('test'); assert.equal(callCount, 2); // activation is called again @@ -109,4 +122,4 @@ suite('File Service', () => { watcher3Disposable2.dispose(); assert.equal(disposeCounter, 2); }); -}); \ No newline at end of file +}); diff --git a/src/vs/platform/files/test/common/nullFileSystemProvider.ts b/src/vs/platform/files/test/common/nullFileSystemProvider.ts index 59ba8e873cd..27250d0a7d1 100644 --- a/src/vs/platform/files/test/common/nullFileSystemProvider.ts +++ b/src/vs/platform/files/test/common/nullFileSystemProvider.ts @@ -6,14 +6,22 @@ import { URI } from 'vs/base/common/uri'; import { FileSystemProviderCapabilities, IFileSystemProvider, IWatchOptions, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileOpenOptions, IFileChange } from 'vs/platform/files/common/files'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; export class NullFileSystemProvider implements IFileSystemProvider { capabilities: FileSystemProviderCapabilities = FileSystemProviderCapabilities.Readonly; - onDidChangeCapabilities: Event = Event.None; - onDidChangeFile: Event = Event.None; + private readonly _onDidChangeCapabilities = new Emitter(); + readonly onDidChangeCapabilities: Event = this._onDidChangeCapabilities.event; + + setCapabilities(capabilities: FileSystemProviderCapabilities): void { + this.capabilities = capabilities; + + this._onDidChangeCapabilities.fire(); + } + + readonly onDidChangeFile: Event = Event.None; constructor(private disposableFactory: () => IDisposable = () => Disposable.None) { } diff --git a/src/vs/platform/instantiation/common/instantiationService.ts b/src/vs/platform/instantiation/common/instantiationService.ts index f4b681204c8..86f21d18e76 100644 --- a/src/vs/platform/instantiation/common/instantiationService.ts +++ b/src/vs/platform/instantiation/common/instantiationService.ts @@ -16,7 +16,7 @@ const _enableTracing = false; // PROXY // Ghetto-declare of the global Proxy object. This isn't the proper way // but allows us to run this code in the browser without IE11. -declare var Proxy: any; +declare const Proxy: any; const _canUseProxy = typeof Proxy === 'function'; class CyclicDependencyError extends Error { diff --git a/src/vs/platform/remote/common/remoteAuthorityResolver.ts b/src/vs/platform/remote/common/remoteAuthorityResolver.ts index 8d0e2617a34..7cd49599314 100644 --- a/src/vs/platform/remote/common/remoteAuthorityResolver.ts +++ b/src/vs/platform/remote/common/remoteAuthorityResolver.ts @@ -18,7 +18,7 @@ export interface ResolvedOptions { } export interface TunnelInformation { - detectedTunnels?: { remote: { port: number, host: string }, localAddress: string }[]; + environmentTunnels?: { remoteAddress: { port: number, host: string }, localAddress: string }[]; } export interface ResolverResult { diff --git a/src/vs/platform/remote/common/tunnel.ts b/src/vs/platform/remote/common/tunnel.ts index 7693ad596c6..154df6e884e 100644 --- a/src/vs/platform/remote/common/tunnel.ts +++ b/src/vs/platform/remote/common/tunnel.ts @@ -19,9 +19,9 @@ export interface RemoteTunnel { } export interface TunnelOptions { - remote: { port: number, host: string }; + remoteAddress: { port: number, host: string }; localPort?: number; - name?: string; + label?: string; } export interface ITunnelProvider { @@ -33,10 +33,10 @@ export interface ITunnelService { readonly tunnels: Promise; readonly onTunnelOpened: Event; - readonly onTunnelClosed: Event; + readonly onTunnelClosed: Event<{ host: string, port: number }>; - openTunnel(remotePort: number, localPort?: number): Promise | undefined; - closeTunnel(remotePort: number): Promise; + openTunnel(remoteHost: string | undefined, remotePort: number, localPort?: number): Promise | undefined; + closeTunnel(remoteHost: string, remotePort: number): Promise; setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable; } diff --git a/src/vs/platform/remote/common/tunnelService.ts b/src/vs/platform/remote/common/tunnelService.ts index 2501ebc90f1..a5fa07ac174 100644 --- a/src/vs/platform/remote/common/tunnelService.ts +++ b/src/vs/platform/remote/common/tunnelService.ts @@ -13,12 +13,12 @@ export class NoOpTunnelService implements ITunnelService { public readonly tunnels: Promise = Promise.resolve([]); private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter = new Emitter(); - public onTunnelClosed: Event = this._onTunnelClosed.event; - openTunnel(_remotePort: number): Promise | undefined { + private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; + openTunnel(_remoteHost: string, _remotePort: number): Promise | undefined { return undefined; } - async closeTunnel(_remotePort: number): Promise { + async closeTunnel(_remoteHost: string, _remotePort: number): Promise { } setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable { throw new Error('Method not implemented.'); diff --git a/src/vs/platform/storage/browser/storageService.ts b/src/vs/platform/storage/browser/storageService.ts index e1da1af90f8..c1fa72a27c6 100644 --- a/src/vs/platform/storage/browser/storageService.ts +++ b/src/vs/platform/storage/browser/storageService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Event, Emitter } from 'vs/base/common/event'; import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -21,11 +21,11 @@ export class BrowserStorageService extends Disposable implements IStorageService _serviceBrand: undefined; - private readonly _onDidChangeStorage: Emitter = this._register(new Emitter()); - readonly onDidChangeStorage: Event = this._onDidChangeStorage.event; + private readonly _onDidChangeStorage = this._register(new Emitter()); + readonly onDidChangeStorage = this._onDidChangeStorage.event; - private readonly _onWillSaveState: Emitter = this._register(new Emitter()); - readonly onWillSaveState: Event = this._onWillSaveState.event; + private readonly _onWillSaveState = this._register(new Emitter()); + readonly onWillSaveState = this._onWillSaveState.event; private globalStorage: IStorage | undefined; private workspaceStorage: IStorage | undefined; @@ -37,45 +37,15 @@ export class BrowserStorageService extends Disposable implements IStorageService private workspaceStorageFile: URI | undefined; private initializePromise: Promise | undefined; - private periodicSaveScheduler = this._register(new RunOnceScheduler(() => this.collectState(), 5000)); - get hasPendingUpdate(): boolean { - return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate); - } + private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 5000 /* every 5s */)); + private runWhenIdleDisposable: IDisposable | undefined = undefined; constructor( @IEnvironmentService private readonly environmentService: IEnvironmentService, @IFileService private readonly fileService: IFileService ) { super(); - - // In the browser we do not have support for long running unload sequences. As such, - // we cannot ask for saving state in that moment, because that would result in a - // long running operation. - // Instead, periodically ask customers to save save. The library will be clever enough - // to only save state that has actually changed. - this.periodicSaveScheduler.schedule(); - } - - private collectState(): void { - runWhenIdle(() => { - - // this event will potentially cause new state to be stored - // since new state will only be created while the document - // has focus, one optimization is to not run this when the - // document has no focus, assuming that state has not changed - // - // another optimization is to not collect more state if we - // have a pending update already running which indicates - // that the connection is either slow or disconnected and - // thus unhealthy. - if (document.hasFocus() && !this.hasPendingUpdate) { - this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE }); - } - - // repeat - this.periodicSaveScheduler.schedule(); - }); } initialize(payload: IWorkspaceInitializationPayload): Promise { @@ -109,6 +79,13 @@ export class BrowserStorageService extends Disposable implements IStorageService this.workspaceStorage.init(), this.globalStorage.init() ]); + + // In the browser we do not have support for long running unload sequences. As such, + // we cannot ask for saving state in that moment, because that would result in a + // long running operation. + // Instead, periodically ask customers to save save. The library will be clever enough + // to only save state that has actually changed. + this.periodicFlushScheduler.schedule(); } get(key: string, scope: StorageScope, fallbackValue: string): string; @@ -156,6 +133,40 @@ export class BrowserStorageService extends Disposable implements IStorageService throw new Error('Migrating storage is currently unsupported in Web'); } + private doFlushWhenIdle(): void { + + // Dispose any previous idle runner + this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable); + + // Run when idle + this.runWhenIdleDisposable = runWhenIdle(() => { + + // this event will potentially cause new state to be stored + // since new state will only be created while the document + // has focus, one optimization is to not run this when the + // document has no focus, assuming that state has not changed + // + // another optimization is to not collect more state if we + // have a pending update already running which indicates + // that the connection is either slow or disconnected and + // thus unhealthy. + if (document.hasFocus() && !this.hasPendingUpdate) { + this.flush(); + } + + // repeat + this.periodicFlushScheduler.schedule(); + }); + } + + get hasPendingUpdate(): boolean { + return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate); + } + + flush(): void { + this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE }); + } + close(): void { // We explicitly do not close our DBs because writing data onBeforeUnload() // can result in unexpected results. Namely, it seems that - even though this @@ -167,6 +178,12 @@ export class BrowserStorageService extends Disposable implements IStorageService // get triggered in this phase. this.dispose(); } + + dispose(): void { + this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable); + + super.dispose(); + } } export class FileStorageDatabase extends Disposable implements IStorageDatabase { diff --git a/src/vs/platform/storage/common/storage.ts b/src/vs/platform/storage/common/storage.ts index 0094786e95b..8a97b4be30c 100644 --- a/src/vs/platform/storage/common/storage.ts +++ b/src/vs/platform/storage/common/storage.ts @@ -102,6 +102,13 @@ export interface IStorageService { * Migrate the storage contents to another workspace. */ migrate(toWorkspace: IWorkspaceInitializationPayload): Promise; + + /** + * Allows to flush state, e.g. in cases where a shutdown is + * imminent. This will send out the onWillSaveState to ask + * everyone for latest state. + */ + flush(): void; } export const enum StorageScope { @@ -126,10 +133,11 @@ export class InMemoryStorageService extends Disposable implements IStorageServic _serviceBrand: undefined; - private readonly _onDidChangeStorage: Emitter = this._register(new Emitter()); - readonly onDidChangeStorage: Event = this._onDidChangeStorage.event; + private readonly _onDidChangeStorage = this._register(new Emitter()); + readonly onDidChangeStorage = this._onDidChangeStorage.event; - readonly onWillSaveState = Event.None; + protected readonly _onWillSaveState = this._register(new Emitter()); + readonly onWillSaveState = this._onWillSaveState.event; private globalCache: Map = new Map(); private workspaceCache: Map = new Map(); @@ -215,6 +223,10 @@ export class InMemoryStorageService extends Disposable implements IStorageServic async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise { // not supported } + + flush(): void { + this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE }); + } } export async function logStorage(global: Map, workspace: Map, globalPath: string, workspacePath: string): Promise { diff --git a/src/vs/platform/storage/node/storageMainService.ts b/src/vs/platform/storage/node/storageMainService.ts index 81c9511a239..3c34616f373 100644 --- a/src/vs/platform/storage/node/storageMainService.ts +++ b/src/vs/platform/storage/node/storageMainService.ts @@ -89,11 +89,11 @@ export class StorageMainService extends Disposable implements IStorageMainServic private static readonly STORAGE_NAME = 'state.vscdb'; - private readonly _onDidChangeStorage: Emitter = this._register(new Emitter()); - readonly onDidChangeStorage: Event = this._onDidChangeStorage.event; + private readonly _onDidChangeStorage = this._register(new Emitter()); + readonly onDidChangeStorage = this._onDidChangeStorage.event; - private readonly _onWillSaveState: Emitter = this._register(new Emitter()); - readonly onWillSaveState: Event = this._onWillSaveState.event; + private readonly _onWillSaveState = this._register(new Emitter()); + readonly onWillSaveState = this._onWillSaveState.event; get items(): Map { return this.storage.items; } diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index bdd7b454af1..db8c578e75b 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -16,6 +16,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment' import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces'; import { onUnexpectedError } from 'vs/base/common/errors'; import { assertIsDefined, assertAllDefined } from 'vs/base/common/types'; +import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; export class NativeStorageService extends Disposable implements IStorageService { @@ -38,6 +39,9 @@ export class NativeStorageService extends Disposable implements IStorageService private initializePromise: Promise | undefined; + private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 60000 /* every minute */)); + private runWhenIdleDisposable: IDisposable | undefined = undefined; + constructor( globalStorageDatabase: IStorageDatabase, @ILogService private readonly logService: ILogService, @@ -63,10 +67,17 @@ export class NativeStorageService extends Disposable implements IStorageService } private async doInitialize(payload: IWorkspaceInitializationPayload): Promise { + + // Init all storage locations await Promise.all([ this.initializeGlobalStorage(), this.initializeWorkspaceStorage(payload) ]); + + // On some OS we do not get enough time to persist state on shutdown (e.g. when + // Windows restarts after applying updates). In other cases, VSCode might crash, + // so we periodically save state to reduce the chance of loosing any state. + this.periodicFlushScheduler.schedule(); } private initializeGlobalStorage(): Promise { @@ -185,8 +196,32 @@ export class NativeStorageService extends Disposable implements IStorageService this.getStorage(scope).delete(key); } + private doFlushWhenIdle(): void { + + // Dispose any previous idle runner + this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable); + + // Run when idle + this.runWhenIdleDisposable = runWhenIdle(() => { + + // send event to collect state + this.flush(); + + // repeat + this.periodicFlushScheduler.schedule(); + }); + } + + flush(): void { + this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE }); + } + async close(): Promise { + // Stop periodic scheduler and idle runner as we now collect state normally + this.periodicFlushScheduler.dispose(); + this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable); + // Signal as event so that clients can still store data this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); diff --git a/src/vs/platform/storage/test/node/storageService.test.ts b/src/vs/platform/storage/test/node/storageService.test.ts index 5f9271b43c5..67eabfb6b66 100644 --- a/src/vs/platform/storage/test/node/storageService.test.ts +++ b/src/vs/platform/storage/test/node/storageService.test.ts @@ -28,11 +28,11 @@ suite('StorageService', () => { function removeData(scope: StorageScope): void { const storage = new InMemoryStorageService(); - storage.store('Monaco.IDE.Core.Storage.Test.remove', 'foobar', scope); - strictEqual('foobar', storage.get('Monaco.IDE.Core.Storage.Test.remove', scope, (undefined)!)); + storage.store('test.remove', 'foobar', scope); + strictEqual('foobar', storage.get('test.remove', scope, (undefined)!)); - storage.remove('Monaco.IDE.Core.Storage.Test.remove', scope); - ok(!storage.get('Monaco.IDE.Core.Storage.Test.remove', scope, (undefined)!)); + storage.remove('test.remove', scope); + ok(!storage.get('test.remove', scope, (undefined)!)); } test('Get Data, Integer, Boolean (global, in-memory)', () => { @@ -46,34 +46,34 @@ suite('StorageService', () => { function storeData(scope: StorageScope): void { const storage = new InMemoryStorageService(); - strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, 'foobar'), 'foobar'); - strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, ''), ''); - strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, 5), 5); - strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, 0), 0); - strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, true), true); - strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, false), false); + strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar'); + strictEqual(storage.get('test.get', scope, ''), ''); + strictEqual(storage.getNumber('test.getNumber', scope, 5), 5); + strictEqual(storage.getNumber('test.getNumber', scope, 0), 0); + strictEqual(storage.getBoolean('test.getBoolean', scope, true), true); + strictEqual(storage.getBoolean('test.getBoolean', scope, false), false); - storage.store('Monaco.IDE.Core.Storage.Test.get', 'foobar', scope); - strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, (undefined)!), 'foobar'); + storage.store('test.get', 'foobar', scope); + strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar'); - storage.store('Monaco.IDE.Core.Storage.Test.get', '', scope); - strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.get', scope, (undefined)!), ''); + storage.store('test.get', '', scope); + strictEqual(storage.get('test.get', scope, (undefined)!), ''); - storage.store('Monaco.IDE.Core.Storage.Test.getNumber', 5, scope); - strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, (undefined)!), 5); + storage.store('test.getNumber', 5, scope); + strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5); - storage.store('Monaco.IDE.Core.Storage.Test.getNumber', 0, scope); - strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumber', scope, (undefined)!), 0); + storage.store('test.getNumber', 0, scope); + strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0); - storage.store('Monaco.IDE.Core.Storage.Test.getBoolean', true, scope); - strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, (undefined)!), true); + storage.store('test.getBoolean', true, scope); + strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true); - storage.store('Monaco.IDE.Core.Storage.Test.getBoolean', false, scope); - strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBoolean', scope, (undefined)!), false); + storage.store('test.getBoolean', false, scope); + strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false); - strictEqual(storage.get('Monaco.IDE.Core.Storage.Test.getDefault', scope, 'getDefault'), 'getDefault'); - strictEqual(storage.getNumber('Monaco.IDE.Core.Storage.Test.getNumberDefault', scope, 5), 5); - strictEqual(storage.getBoolean('Monaco.IDE.Core.Storage.Test.getBooleanDefault', scope, true), true); + strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault'); + strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5); + strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true); } function uniqueStorageDir(): string { diff --git a/src/vs/platform/theme/common/tokenClassificationRegistry.ts b/src/vs/platform/theme/common/tokenClassificationRegistry.ts index a9355a0eb83..a67211cf7ed 100644 --- a/src/vs/platform/theme/common/tokenClassificationRegistry.ts +++ b/src/vs/platform/theme/common/tokenClassificationRegistry.ts @@ -381,12 +381,13 @@ function registerDefaultClassifications(): void { registerTokenType('parameterType', nls.localize('parameterType', "Style for parameter types."), undefined, 'type'); registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]); + registerTokenType('member', nls.localize('member', "Style for member"), [['entity.name.function'], ['support.function']]); registerTokenType('macro', nls.localize('macro', "Style for macros."), undefined, 'function'); registerTokenType('variable', nls.localize('variable', "Style for variables."), [['variable'], ['entity.name.variable']]); registerTokenType('constant', nls.localize('constant', "Style for constants."), undefined, 'variable'); registerTokenType('parameter', nls.localize('parameter', "Style for parameters."), undefined, 'variable'); - registerTokenType('property', nls.localize('propertie', "Style for properties."), undefined, 'variable'); + registerTokenType('property', nls.localize('property', "Style for properties."), undefined, 'variable'); registerTokenType('label', nls.localize('labels', "Style for labels. "), undefined); @@ -394,7 +395,7 @@ function registerDefaultClassifications(): void { tokenClassificationRegistry.registerTokenModifier('declaration', nls.localize('declaration', "Style for all symbol declarations."), undefined); tokenClassificationRegistry.registerTokenModifier('documentation', nls.localize('documentation', "Style to use for references in documentation."), undefined); - tokenClassificationRegistry.registerTokenModifier('member', nls.localize('member', "Style to use for member functions, variables (fields) and types."), undefined); + //tokenClassificationRegistry.registerTokenModifier('member', nls.localize('member', "Style to use for member functions, variables (fields) and types."), undefined); tokenClassificationRegistry.registerTokenModifier('static', nls.localize('static', "Style to use for symbols that are static."), undefined); tokenClassificationRegistry.registerTokenModifier('abstract', nls.localize('abstract', "Style to use for symbols that are abstract."), undefined); tokenClassificationRegistry.registerTokenModifier('deprecated', nls.localize('deprecated', "Style to use for symbols that are deprecated."), undefined); diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index db118026b58..b906b66aafa 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -47,7 +47,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ if (this.userDataSyncStoreService.userDataSyncStore) { this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus())); - this._register(authTokenService.onDidChangeStatus(() => this.onDidChangeAuthTokenStatus())); } this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => s.onDidChangeLocal)); @@ -118,11 +117,4 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ } return null; } - - private onDidChangeAuthTokenStatus(): void { - if (this.authTokenService.status === AuthTokenStatus.SignedOut) { - this.stop(); - } - } - } diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index 101dd334ad3..ca87b9be80c 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -13,14 +13,14 @@ import { IEmptyWindowBackupInfo } from 'vs/platform/backup/node/backup'; import { IEnvironmentService, ParsedArgs } from 'vs/platform/environment/common/environment'; import { IStateService } from 'vs/platform/state/node/state'; import { CodeWindow, defaultWindowState } from 'vs/code/electron-main/window'; -import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display } from 'electron'; +import { ipcMain as ipc, screen, BrowserWindow, systemPreferences, MessageBoxOptions, Display, app } from 'electron'; import { parseLineAndColumnAware } from 'vs/code/node/paths'; import { ILifecycleMainService, UnloadReason, LifecycleMainService, LifecycleMainPhase } from 'vs/platform/lifecycle/electron-main/lifecycleMainService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ILogService } from 'vs/platform/log/common/log'; import { IWindowSettings, OpenContext, IPath, IWindowConfiguration, IPathsToWaitFor, isFileToOpen, isWorkspaceToOpen, isFolderToOpen, IWindowOpenable, IOpenEmptyWindowOptions, IAddFoldersRequest } from 'vs/platform/windows/common/windows'; import { getLastActiveWindow, findBestWindowOrFolderForFile, findWindowOnWorkspace, findWindowOnExtensionDevelopmentPath, findWindowOnWorkspaceOrFolderUri } from 'vs/platform/windows/node/window'; -import { Event as CommonEvent, Emitter } from 'vs/base/common/event'; +import { Emitter } from 'vs/base/common/event'; import product from 'vs/platform/product/common/product'; import { IWindowsMainService, IOpenConfiguration, IWindowsCountChangedEvent, ICodeWindow, IWindowState as ISingleWindowState, WindowMode } from 'vs/platform/windows/electron-main/windows'; import { IWorkspacesHistoryMainService } from 'vs/platform/workspaces/electron-main/workspacesHistoryMainService'; @@ -160,14 +160,16 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic private readonly windowsState: IWindowsState; private lastClosedWindowState?: IWindowState; + private shuttingDown = false; + private readonly _onWindowReady = this._register(new Emitter()); - readonly onWindowReady: CommonEvent = this._onWindowReady.event; + readonly onWindowReady = this._onWindowReady.event; private readonly _onWindowClose = this._register(new Emitter()); - readonly onWindowClose: CommonEvent = this._onWindowClose.event; + readonly onWindowClose = this._onWindowClose.event; private readonly _onWindowsCountChanged = this._register(new Emitter()); - readonly onWindowsCountChanged: CommonEvent = this._onWindowsCountChanged.event; + readonly onWindowsCountChanged = this._onWindowsCountChanged.event; constructor( private readonly machineId: string, @@ -236,6 +238,15 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic systemPreferences.on('high-contrast-color-scheme-changed', () => onHighContrastChange()); } + // When a window looses focus, save all windows state. This allows to + // prevent loss of window-state data when OS is restarted without properly + // shutting down the application (https://github.com/microsoft/vscode/issues/87171) + app.on('browser-window-blur', () => { + if (!this.shuttingDown) { + this.saveWindowsState(); + } + }); + // Handle various lifecycle events around windows this.lifecycleMainService.onBeforeWindowClose(window => this.onBeforeWindowClose(window)); this.lifecycleMainService.onBeforeShutdown(() => this.onBeforeShutdown()); @@ -292,6 +303,12 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // - closeAll(2): onBeforeWindowClose(2, false), onBeforeWindowClose(2, false), onBeforeShutdown(0) // private onBeforeShutdown(): void { + this.shuttingDown = true; + + this.saveWindowsState(); + } + + private saveWindowsState(): void { const currentWindowsState: IWindowsState = { openedWindows: [], lastPluginDevelopmentHostWindow: this.windowsState.lastPluginDevelopmentHostWindow, @@ -327,8 +344,11 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic // Persist const state = getWindowsStateStoreData(currentWindowsState); - this.logService.trace('onBeforeShutdown', state); this.stateService.setItem(WindowsMainService.windowsStateStorageKey, state); + + if (this.shuttingDown) { + this.logService.trace('onBeforeShutdown', state); + } } // See note on #onBeforeShutdown() for details how these events are flowing @@ -999,10 +1019,10 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic restoreWindows = 'all'; // always reopen all windows when an update was applied } else { const windowConfig = this.configurationService.getValue('window'); - restoreWindows = windowConfig?.restoreWindows || 'one'; + restoreWindows = windowConfig?.restoreWindows || 'all'; // by default restore all windows if (['all', 'folders', 'one', 'none'].indexOf(restoreWindows) === -1) { - restoreWindows = 'one'; + restoreWindows = 'all'; // by default restore all windows } } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index c3a6c7ff176..f4c8fa12dba 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -790,7 +790,8 @@ declare module 'vscode' { } /** - * A reference to a named icon. Currently only [File](#ThemeIcon.File) and [Folder](#ThemeIcon.Folder) are supported. + * A reference to a named icon. Currently, [File](#ThemeIcon.File), [Folder](#ThemeIcon.Folder), + * and [codicons](https://microsoft.github.io/vscode-codicons/dist/codicon.html) are supported. * Using a theme icon is preferred over a custom icon as it gives theme authors the possibility to change the icons. */ export class ThemeIcon { @@ -804,7 +805,11 @@ declare module 'vscode' { */ static readonly Folder: ThemeIcon; - private constructor(id: string); + /** + * Creates a reference to a theme icon. + * @param id id of the icon. The avaiable icons are listed in https://microsoft.github.io/vscode-codicons/dist/codicon.html. + */ + constructor(id: string); } /** @@ -1574,17 +1579,17 @@ declare module 'vscode' { export interface QuickPickItem { /** - * A human readable string which is rendered prominent. + * A human-readable string which is rendered prominent. */ label: string; /** - * A human readable string which is rendered less prominent. + * A human-readable string which is rendered less prominent. */ description?: string; /** - * A human readable string which is rendered less prominent. + * A human-readable string which is rendered less prominent. */ detail?: string; @@ -1688,7 +1693,7 @@ declare module 'vscode' { canSelectMany?: boolean; /** - * A set of file filters that are used by the dialog. Each entry is a human readable label, + * A set of file filters that are used by the dialog. Each entry is a human-readable label, * like "TypeScript", and an array of extensions, e.g. * ```ts * { @@ -1715,7 +1720,7 @@ declare module 'vscode' { saveLabel?: string; /** - * A set of file filters that are used by the dialog. Each entry is a human readable label, + * A set of file filters that are used by the dialog. Each entry is a human-readable label, * like "TypeScript", and an array of extensions, e.g. * ```ts * { @@ -1810,7 +1815,7 @@ declare module 'vscode' { * to the user. * * @param value The current value of the input box. - * @return A human readable string which is presented as diagnostic message. + * @return A human-readable string which is presented as diagnostic message. * Return `undefined`, `null`, or the empty string when 'value' is valid. */ validateInput?(value: string): string | undefined | null | Thenable; @@ -2313,7 +2318,7 @@ declare module 'vscode' { /** * The declaration of a symbol representation as one or many [locations](#Location) - * or [location links][#LocationLink]. + * or [location links](#LocationLink). */ export type Declaration = Location | Location[] | LocationLink[]; @@ -2336,7 +2341,7 @@ declare module 'vscode' { } /** - * The MarkdownString represents human readable text that supports formatting via the + * The MarkdownString represents human-readable text that supports formatting via the * markdown syntax. Standard markdown is supported, also tables, but no embedded html. */ export class MarkdownString { @@ -2381,7 +2386,7 @@ declare module 'vscode' { } /** - * ~~MarkedString can be used to render human readable text. It is either a markdown string + * ~~MarkedString can be used to render human-readable text. It is either a markdown string * or a code-block that provides a language and a code snippet. Note that * markdown strings will be sanitized - that means html will be escaped.~~ * @@ -2678,7 +2683,7 @@ declare module 'vscode' { */ export interface DocumentSymbolProviderMetadata { /** - * A human readable string that is shown when multiple outlines trees show for one document. + * A human-readable string that is shown when multiple outlines trees show for one document. */ label?: string; } @@ -4009,7 +4014,7 @@ declare module 'vscode' { * @returns A call hierarchy item or a thenable that resolves to such. The lack of a result can be * signaled by returning `undefined` or `null`. */ - prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + prepareCallHierarchy(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; /** * Provide all incoming calls for an item, e.g all callers for a method. In graph terms this describes directed @@ -4293,7 +4298,7 @@ declare module 'vscode' { * * The *effective* value (returned by [`get`](#WorkspaceConfiguration.get)) * is computed like this: `defaultValue` overridden by `globalValue`, - * `globalValue` overridden by `workspaceValue`. `workspaceValue` overwridden by `workspaceFolderValue`. + * `globalValue` overridden by `workspaceValue`. `workspaceValue` overridden by `workspaceFolderValue`. * Refer to [Settings](https://code.visualstudio.com/docs/getstarted/settings) * for more information. * @@ -5711,7 +5716,7 @@ declare module 'vscode' { /** * Enumeration of file types. The types `File` and `Directory` can also be - * a symbolic links, in that use `FileType.File | FileType.SymbolicLink` and + * a symbolic links, in that case use `FileType.File | FileType.SymbolicLink` and * `FileType.Directory | FileType.SymbolicLink`. */ export enum FileType { @@ -5740,6 +5745,8 @@ declare module 'vscode' { /** * The type of the file, e.g. is a regular file, a directory, or symbolic link * to a file. + * + * *Note:* This value might be a bitmask, e.g. `FileType.File | FileType.SymbolicLink`. */ type: FileType; /** @@ -7369,7 +7376,7 @@ declare module 'vscode' { iconPath?: string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon; /** - * A human readable string which is rendered less prominent. + * A human-readable string which is rendered less prominent. * When `true`, it is derived from [resourceUri](#TreeItem.resourceUri) and when `falsy`, it is not shown. */ description?: string | boolean; @@ -9578,7 +9585,45 @@ declare module 'vscode' { constructor(port: number, host?: string); } - export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer; + /** + * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. + */ + export interface DebugAdapter extends Disposable { + + /** + * An event which fires after the debug adapter has sent a Debug Adapter Protocol message to VS Code. + * Messages can be requests, responses, or events. + */ + readonly onDidSendMessage: Event; + + /** + * Handle a Debug Adapter Protocol message. + * Messages can be requests, responses, or events. + * Results or errors are returned via onSendMessage events. + * @param message A Debug Adapter Protocol message + */ + handleMessage(message: DebugProtocolMessage): void; + } + + /** + * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. + */ + export interface DebugProtocolMessage { + // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). + } + + /** + * A debug adapter descriptor for an inline implementation. + */ + export class DebugAdapterInlineImplementation { + + /** + * Create a descriptor for an inline implementation of a debug adapter. + */ + constructor(implementation: DebugAdapter); + } + + export type DebugAdapterDescriptor = DebugAdapterExecutable | DebugAdapterServer | DebugAdapterInlineImplementation; export interface DebugAdapterDescriptorFactory { /** diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index b04aeb64807..a36026d282f 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -34,15 +34,18 @@ declare module 'vscode' { } export interface TunnelOptions { - remote: { port: number, host: string }; - localPort?: number; - name?: string; + remoteAddress: { port: number, host: string }; + // The desired local port. If this port can't be used, then another will be chosen. + localAddressPort?: number; + label?: string; } export interface Tunnel { - remote: { port: number, host: string }; + remoteAddress: { port: number, host: string }; + //The complete local address(ex. localhost:1234) localAddress: string; - onDispose: Event; + // Implementers of Tunnel should fire onDidDispose when dispose is called. + onDidDispose: Event; dispose(): void; } @@ -52,10 +55,10 @@ declare module 'vscode' { export interface TunnelInformation { /** * Tunnels that are detected by the extension. The remotePort is used for display purposes. - * The localAddress should be the complete local address(ex. localhost:1234) for connecting to the port. Tunnels provided through + * The localAddress should be the complete local address (ex. localhost:1234) for connecting to the port. Tunnels provided through * detected are read-only from the forwarded ports UI. */ - detectedTunnels?: { remote: { port: number, host: string }, localAddress: string }[]; + environmentTunnels?: { remoteAddress: { port: number, host: string }, localAddress: string }[]; } export type ResolverResult = ResolvedAuthority & ResolvedOptions & TunnelInformation; @@ -74,15 +77,16 @@ declare module 'vscode' { * When not implemented, the core will use its default forwarding logic. * When implemented, the core will use this to forward ports. */ - forwardPort?(tunnelOptions: TunnelOptions): Thenable | undefined; + tunnelFactory?: (tunnelOptions: TunnelOptions) => Thenable | undefined; } export namespace workspace { /** - * Forwards a port. Currently only works for a remote host of localhost. - * @param forward The `localPort` is a suggestion only. If that port is not available another will be chosen. + * Forwards a port. If the current resolver implements RemoteAuthorityResolver:forwardPort then that will be used to make the tunnel. + * By default, openTunnel only support localhost; however, RemoteAuthorityResolver:tunnelFactory can be used to support other ips. + * @param tunnelOptions The `localPort` is a suggestion only. If that port is not available another will be chosen. */ - export function makeTunnel(forward: TunnelOptions): Thenable; + export function openTunnel(tunnelOptions: TunnelOptions): Thenable; } export interface ResourceLabelFormatter { @@ -784,44 +788,24 @@ declare module 'vscode' { //#endregion - //#region André: debug API for inline debug adapters https://github.com/microsoft/vscode/issues/85544 + //#region Debug: - /** - * A DebugProtocolMessage is an opaque stand-in type for the [ProtocolMessage](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage) type defined in the Debug Adapter Protocol. - */ - export interface DebugProtocolMessage { - // Properties: see details [here](https://microsoft.github.io/debug-adapter-protocol/specification#Base_Protocol_ProtocolMessage). - } - - /** - * A debug adapter that implements the Debug Adapter Protocol can be registered with VS Code if it implements the DebugAdapter interface. - */ - export interface DebugAdapter extends Disposable { + export interface DebugConfigurationProvider { /** - * An event which fires after the debug adapter has sent a Debug Adapter Protocol message to VS Code. - * Messages can be requests, responses, or events. + * This hook is directly called after 'resolveDebugConfiguration' but with all variables substituted. + * It can be used to resolve or verify a [debug configuration](#DebugConfiguration) by filling in missing values or by adding/changing/removing attributes. + * If more than one debug configuration provider is registered for the same type, the 'resolveDebugConfigurationWithSubstitutedVariables' calls are chained + * in arbitrary order and the initial debug configuration is piped through the chain. + * Returning the value 'undefined' prevents the debug session from starting. + * Returning the value 'null' prevents the debug session from starting and opens the underlying debug configuration instead. + * + * @param folder The workspace folder from which the configuration originates from or `undefined` for a folderless setup. + * @param debugConfiguration The [debug configuration](#DebugConfiguration) to resolve. + * @param token A cancellation token. + * @return The resolved debug configuration or undefined or null. */ - readonly onDidSendMessage: Event; - - /** - * Handle a Debug Adapter Protocol message. - * Messages can be requests, responses, or events. - * Results or errors are returned via onSendMessage events. - * @param message A Debug Adapter Protocol message - */ - handleMessage(message: DebugProtocolMessage): void; - } - - /** - * A debug adapter descriptor for an inline implementation. - */ - export class DebugAdapterInlineImplementation { - - /** - * Create a descriptor for an inline implementation of a debug adapter. - */ - constructor(implementation: DebugAdapter); + resolveDebugConfigurationWithSubstitutedVariables?(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult; } // deprecated @@ -1344,4 +1328,204 @@ declare module 'vscode' { } //#endregion + + //#region Language specific settings: https://github.com/microsoft/vscode/issues/26707 + + export type ConfigurationScope = Uri | TextDocument | WorkspaceFolder | { resource: Uri, languageId: string }; + + /** + * An event describing the change in Configuration + */ + export interface ConfigurationChangeEvent { + + /** + * Returns `true` if the given section is affected in the provided scope. + * + * @param section Configuration name, supports _dotted_ names. + * @param scope A scope in which to check. + * @return `true` if the given section is affected in the provided scope. + */ + affectsConfiguration(section: string, scope?: ConfigurationScope): boolean; + } + + export namespace workspace { + + /** + * Get a workspace configuration object. + * + * When a section-identifier is provided only that part of the configuration + * is returned. Dots in the section-identifier are interpreted as child-access, + * like `{ myExt: { setting: { doIt: true }}}` and `getConfiguration('myExt.setting').get('doIt') === true`. + * + * When a scope is provided configuraiton confined to that scope is returned. Scope can be a resource or a language identifier or both. + * + * @param section A dot-separated identifier. + * @return The full configuration or a subset. + */ + export function getConfiguration(section?: string | undefined, scope?: ConfigurationScope | null): WorkspaceConfiguration; + + } + + /** + * Represents the configuration. It is a merged view of + * + * - *Default Settings* + * - *Global (User) Settings* + * - *Workspace settings* + * - *Workspace Folder settings* - From one of the [Workspace Folders](#workspace.workspaceFolders) under which requested resource belongs to. + * - *Language settings* - Settings defined under requested language. + * + * The *effective* value (returned by [`get`](#WorkspaceConfiguration.get)) is computed by overriding or merging the values in the following order. + * + * ``` + * `defaultValue` + * `globalValue` (if defined) + * `workspaceValue` (if defined) + * `workspaceFolderValue` (if defined) + * `defaultLanguageValue` (if defined) + * `globalLanguageValue` (if defined) + * `workspaceLanguageValue` (if defined) + * `workspaceLanguageValue` (if defined) + * ``` + * **Note:** Only `object` value types are merged and all other value types are overridden. + * + * Example 1: Overriding + * + * ```ts + * defaultValue = 'on'; + * globalValue = 'relative' + * workspaceFolderValue = 'off' + * value = 'off' + * ``` + * + * Example 2: Language Values + * + * ```ts + * defaultValue = 'on'; + * globalValue = 'relative' + * workspaceFolderValue = 'off' + * globalLanguageValue = 'on' + * value = 'on' + * ``` + * + * Example 3: Object Values + * + * ```ts + * defaultValue = { "a": 1, "b": 2 }; + * globalValue = { "b": 3, "c": 4 }; + * value = { "a": 1, "b": 3, "c": 4 }; + * ``` + * + * *Note:* Workspace and Workspace Folder configurations contains `launch` and `tasks` settings. Their basename will be + * part of the section identifier. The following snippets shows how to retrieve all configurations + * from `launch.json`: + * + * ```ts + * // launch.json configuration + * const config = workspace.getConfiguration('launch', vscode.workspace.workspaceFolders[0].uri); + * + * // retrieve values + * const values = config.get('configurations'); + * ``` + * + * Refer to [Settings](https://code.visualstudio.com/docs/getstarted/settings) for more information. + */ + export interface WorkspaceConfiguration { + + /** + * Retrieve all information about a configuration setting. A configuration value + * often consists of a *default* value, a global or installation-wide value, + * a workspace-specific value, folder-specific value + * and language-specific values (if [WorkspaceConfiguration](#WorkspaceConfiguration) is scoped to a language). + * + * *Note:* The configuration name must denote a leaf in the configuration tree + * (`editor.fontSize` vs `editor`) otherwise no result is returned. + * + * @param section Configuration name, supports _dotted_ names. + * @return Information about a configuration setting or `undefined`. + */ + inspect(section: string): { + key: string; + + defaultValue?: T; + globalValue?: T; + workspaceValue?: T, + workspaceFolderValue?: T, + + defaultLanguageValue?: T; + userLanguageValue?: T; + workspaceLanguageValue?: T; + workspaceFolderLanguageValue?: T; + + } | undefined; + + /** + * Update a configuration value. The updated configuration values are persisted. + * + * A value can be changed in + * + * - [Global settings](#ConfigurationTarget.Global): Changes the value for all instances of the editor. + * - [Workspace settings](#ConfigurationTarget.Workspace): Changes the value for current workspace, if available. + * - [Workspace folder settings](#ConfigurationTarget.WorkspaceFolder): Changes the value for settings from one of the [Workspace Folders](#workspace.workspaceFolders) under which the requested resource belongs to. + * - Language settings: Changes the value for the requested languageId. + * + * *Note:* To remove a configuration value use `undefined`, like so: `config.update('somekey', undefined)` + * + * @param section Configuration name, supports _dotted_ names. + * @param value The new value. + * @param configurationTarget The [configuration target](#ConfigurationTarget) or a boolean value. + * - If `true` updates [Global settings](#ConfigurationTarget.Global). + * - If `false` updates [Workspace settings](#ConfigurationTarget.Workspace). + * - If `undefined` or `null` updates to [Workspace folder settings](#ConfigurationTarget.WorkspaceFolder) if configuration is resource specific, + * otherwise to [Workspace settings](#ConfigurationTarget.Workspace). + * @param scopeToLanguage Whether to update the value in the scope of requested languageId or not. + * - If `true` updates the value under the requested languageId. + * - If `undefined` updates the value under the requested languageId only if the configuration is defined for the language. + * @throws error while updating + * - configuration which is not registered. + * - window configuration to workspace folder + * - configuration to workspace or workspace folder when no workspace is opened. + * - configuration to workspace folder when there is no workspace folder settings. + * - configuration to workspace folder when [WorkspaceConfiguration](#WorkspaceConfiguration) is not scoped to a resource. + */ + update(section: string, value: any, configurationTarget?: ConfigurationTarget | boolean, scopeToLanguage?: boolean): Thenable; + } + + //#endregion + + //#region color theme access + + /** + * Represents a color theme kind. + */ + export enum ColorThemeKind { + Light = 1, + Dark = 2, + HighContrast = 3 + } + + /** + * Represents a color theme. + */ + export interface ColorTheme { + + /** + * The kind of this color theme: light, dark or high contrast. + */ + readonly kind: ColorThemeKind; + } + + export namespace window { + /** + * The currently active color theme as configured in the settings. The active + * theme can be changed via the `workbench.colorTheme` setting. + */ + export let activeColorTheme: ColorTheme; + + /** + * An [event](#Event) which fires when the active theme changes or one of it's colors chnage. + */ + export const onDidChangeActiveColorTheme: Event; + } + } diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 77e7bdf3987..5560678f607 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -48,6 +48,7 @@ import './mainThreadStatusBar'; import './mainThreadStorage'; import './mainThreadTelemetry'; import './mainThreadTerminalService'; +import './mainThreadTheming'; import './mainThreadTreeViews'; import './mainThreadDownloadService'; import './mainThreadUrls'; diff --git a/src/vs/workbench/api/browser/mainThreadConfiguration.ts b/src/vs/workbench/api/browser/mainThreadConfiguration.ts index 2871b8d4ca7..5c1dd842f6d 100644 --- a/src/vs/workbench/api/browser/mainThreadConfiguration.ts +++ b/src/vs/workbench/api/browser/mainThreadConfiguration.ts @@ -3,14 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URI, UriComponents } from 'vs/base/common/uri'; +import { URI } from 'vs/base/common/uri'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope, getScopes } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { MainThreadConfigurationShape, MainContext, ExtHostContext, IExtHostContext, IConfigurationInitData } from '../common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; -import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @extHostNamedCustomer(MainContext.MainThreadConfiguration) @@ -45,23 +45,43 @@ export class MainThreadConfiguration implements MainThreadConfigurationShape { this._configurationListener.dispose(); } - $updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, resourceUriComponenets: UriComponents | undefined): Promise { - const resource = resourceUriComponenets ? URI.revive(resourceUriComponenets) : null; - return this.writeConfiguration(target, key, value, resource); + $updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise { + overrides = { resource: overrides?.resource ? URI.revive(overrides.resource) : undefined, overrideIdentifier: overrides?.overrideIdentifier }; + return this.writeConfiguration(target, key, value, overrides, scopeToLanguage); } - $removeConfigurationOption(target: ConfigurationTarget | null, key: string, resourceUriComponenets: UriComponents | undefined): Promise { - const resource = resourceUriComponenets ? URI.revive(resourceUriComponenets) : null; - return this.writeConfiguration(target, key, undefined, resource); + $removeConfigurationOption(target: ConfigurationTarget | null, key: string, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise { + overrides = { resource: overrides?.resource ? URI.revive(overrides.resource) : undefined, overrideIdentifier: overrides?.overrideIdentifier }; + return this.writeConfiguration(target, key, undefined, overrides, scopeToLanguage); } - private writeConfiguration(target: ConfigurationTarget | null, key: string, value: any, resource: URI | null): Promise { - target = target !== null && target !== undefined ? target : this.deriveConfigurationTarget(key, resource); - return this.configurationService.updateValue(key, value, { resource }, target, true); + private writeConfiguration(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides, scopeToLanguage: boolean | undefined): Promise { + target = target !== null && target !== undefined ? target : this.deriveConfigurationTarget(key, overrides); + const configurationValue = this.configurationService.inspect(key, overrides); + switch (target) { + case ConfigurationTarget.MEMORY: + return this._updateValue(key, value, target, configurationValue?.memory?.override, overrides, scopeToLanguage); + case ConfigurationTarget.WORKSPACE_FOLDER: + return this._updateValue(key, value, target, configurationValue?.workspaceFolder?.override, overrides, scopeToLanguage); + case ConfigurationTarget.WORKSPACE: + return this._updateValue(key, value, target, configurationValue?.workspace?.override, overrides, scopeToLanguage); + case ConfigurationTarget.USER_REMOTE: + return this._updateValue(key, value, target, configurationValue?.userRemote?.override, overrides, scopeToLanguage); + default: + return this._updateValue(key, value, target, configurationValue?.userLocal?.override, overrides, scopeToLanguage); + } } - private deriveConfigurationTarget(key: string, resource: URI | null): ConfigurationTarget { - if (resource && this._workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { + private _updateValue(key: string, value: any, configurationTarget: ConfigurationTarget, overriddenValue: any | undefined, overrides: IConfigurationOverrides, scopeToLanguage: boolean | undefined): Promise { + overrides = scopeToLanguage === true ? overrides + : scopeToLanguage === false ? { resource: overrides.resource } + : overrides.overrideIdentifier && overriddenValue !== undefined ? overrides + : { resource: overrides.resource }; + return this.configurationService.updateValue(key, value, overrides, configurationTarget); + } + + private deriveConfigurationTarget(key: string, overrides: IConfigurationOverrides): ConfigurationTarget { + if (overrides.resource && this._workspaceContextService.getWorkbenchState() === WorkbenchState.WORKSPACE) { const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); if (configurationProperties[key] && (configurationProperties[key].scope === ConfigurationScope.RESOURCE || configurationProperties[key].scope === ConfigurationScope.RESOURCE_LANGUAGE)) { return ConfigurationTarget.WORKSPACE_FOLDER; diff --git a/src/vs/workbench/api/browser/mainThreadDebugService.ts b/src/vs/workbench/api/browser/mainThreadDebugService.ts index 673177f0f01..4e48c4bf864 100644 --- a/src/vs/workbench/api/browser/mainThreadDebugService.ts +++ b/src/vs/workbench/api/browser/mainThreadDebugService.ts @@ -154,7 +154,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return Promise.resolve(); } - public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise { + public $registerDebugConfigurationProvider(debugType: string, hasProvide: boolean, hasResolve: boolean, hasResolve2: boolean, hasProvideDebugAdapter: boolean, handle: number): Promise { const provider = { type: debugType @@ -169,6 +169,11 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb return this._proxy.$resolveDebugConfiguration(handle, folder, config, token); }; } + if (hasResolve2) { + provider.resolveDebugConfigurationWithSubstitutedVariables = (folder, config, token) => { + return this._proxy.$resolveDebugConfigurationWithSubstitutedVariables(handle, folder, config, token); + }; + } if (hasProvideDebugAdapter) { console.info('DebugConfigurationProvider.debugAdapterExecutable is deprecated and will be removed soon; please use DebugAdapterDescriptorFactory.createDebugAdapterDescriptor instead.'); provider.debugAdapterExecutable = (folder) => { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 05447ed8a4b..0ae9f987450 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -505,13 +505,17 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations.set(handle, callh.CallHierarchyProviderRegistry.register(selector, { prepareCallHierarchy: async (document, position, token) => { - const item = await this._proxy.$prepareCallHierarchy(handle, document.uri, position, token); - if (!item) { + const items = await this._proxy.$prepareCallHierarchy(handle, document.uri, position, token); + if (!items) { return undefined; } return { - dispose: () => this._proxy.$releaseCallHierarchy(handle, item._sessionId), - root: MainThreadLanguageFeatures._reviveCallHierarchyItemDto(item) + dispose: () => { + for (const item of items) { + this._proxy.$releaseCallHierarchy(handle, item._sessionId); + } + }, + roots: items.map(MainThreadLanguageFeatures._reviveCallHierarchyItemDto) }; }, diff --git a/src/vs/workbench/api/browser/mainThreadTask.ts b/src/vs/workbench/api/browser/mainThreadTask.ts index 5e9120d8360..79ae97c0082 100644 --- a/src/vs/workbench/api/browser/mainThreadTask.ts +++ b/src/vs/workbench/api/browser/mainThreadTask.ts @@ -514,15 +514,19 @@ export class MainThreadTask implements MainThreadTaskShape { if (TaskHandleDTO.is(value)) { const workspaceFolder = this._workspaceContextServer.getWorkspaceFolder(URI.revive(value.workspaceFolder)); if (workspaceFolder) { - this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task) => { - this._taskService.run(task).then(undefined, reason => { - // eat the error, it has already been surfaced to the user and we don't care about it here - }); - const result: TaskExecutionDTO = { - id: value.id, - task: TaskDTO.from(task) - }; - resolve(result); + this._taskService.getTask(workspaceFolder, value.id, true).then((task: Task | undefined) => { + if (!task) { + reject(new Error('Task not found')); + } else { + this._taskService.run(task).then(undefined, reason => { + // eat the error, it has already been surfaced to the user and we don't care about it here + }); + const result: TaskExecutionDTO = { + id: value.id, + task: TaskDTO.from(task) + }; + resolve(result); + } }, (_error) => { reject(new Error('Task not found')); }); diff --git a/src/vs/workbench/api/browser/mainThreadTheming.ts b/src/vs/workbench/api/browser/mainThreadTheming.ts new file mode 100644 index 00000000000..3caaa560e30 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadTheming.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { MainContext, IExtHostContext, ExtHostThemingShape, ExtHostContext, MainThreadThemingShape } from '../common/extHost.protocol'; +import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; +import { IDisposable } from 'vs/base/common/lifecycle'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; + +@extHostNamedCustomer(MainContext.MainThreadTheming) +export class MainThreadTheming implements MainThreadThemingShape { + + private readonly _themeService: IThemeService; + private readonly _proxy: ExtHostThemingShape; + private readonly _themeChangeListener: IDisposable; + + constructor( + extHostContext: IExtHostContext, + @IThemeService themeService: IThemeService + ) { + this._themeService = themeService; + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTheming); + + this._themeChangeListener = this._themeService.onThemeChange(e => { + this._proxy.$onColorThemeChange(this._themeService.getTheme().type); + }); + } + + dispose(): void { + this._themeChangeListener.dispose(); + } +} diff --git a/src/vs/workbench/api/browser/mainThreadTunnelService.ts b/src/vs/workbench/api/browser/mainThreadTunnelService.ts index 482fe0f8808..2a1b63a6f32 100644 --- a/src/vs/workbench/api/browser/mainThreadTunnelService.ts +++ b/src/vs/workbench/api/browser/mainThreadTunnelService.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; -import { TunnelOptions, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; +import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; -import { ITunnelProvider, ITunnelService } from 'vs/platform/remote/common/tunnel'; +import { ITunnelProvider, ITunnelService, TunnelOptions } from 'vs/platform/remote/common/tunnel'; @extHostNamedCustomer(MainContext.MainThreadTunnelService) export class MainThreadTunnelService implements MainThreadTunnelServiceShape { @@ -22,15 +22,15 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { } async $openTunnel(tunnelOptions: TunnelOptions): Promise { - const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remote.port, tunnelOptions.localPort, tunnelOptions.name); + const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localPort, tunnelOptions.label); if (tunnel) { return TunnelDto.fromServiceTunnel(tunnel); } return undefined; } - async $closeTunnel(remotePort: number): Promise { - return this.remoteExplorerService.close(remotePort); + async $closeTunnel(remote: { host: string, port: number }): Promise { + return this.remoteExplorerService.close(remote); } async $registerCandidateFinder(): Promise { @@ -44,11 +44,11 @@ export class MainThreadTunnelService implements MainThreadTunnelServiceShape { if (forward) { return forward.then(tunnel => { return { - tunnelRemotePort: tunnel.remote.port, - tunnelRemoteHost: tunnel.remote.host, + tunnelRemotePort: tunnel.remoteAddress.port, + tunnelRemoteHost: tunnel.remoteAddress.host, localAddress: tunnel.localAddress, dispose: () => { - this._proxy.$closeTunnel({ host: tunnel.remote.host, port: tunnel.remote.port }); + this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }); } }; }); diff --git a/src/vs/workbench/api/browser/mainThreadWebview.ts b/src/vs/workbench/api/browser/mainThreadWebview.ts index c98600d897b..d0fe331c5f5 100644 --- a/src/vs/workbench/api/browser/mainThreadWebview.ts +++ b/src/vs/workbench/api/browser/mainThreadWebview.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { onUnexpectedError } from 'vs/base/common/errors'; -import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { isWeb } from 'vs/base/common/platform'; import { startsWith } from 'vs/base/common/strings'; @@ -20,7 +20,7 @@ import { editorGroupToViewColumn, EditorViewColumn, viewColumnToEditorGroup } fr import { IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; -import { ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { ICustomEditorModel, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { WebviewExtensionDescription } from 'vs/workbench/contrib/webview/browser/webview'; import { WebviewInput } from 'vs/workbench/contrib/webview/browser/webviewEditorInput'; import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOptions } from 'vs/workbench/contrib/webview/browser/webviewWorkbenchService'; @@ -95,6 +95,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma private readonly _webviewInputs = new WebviewInputStore(); private readonly _revivers = new Map(); private readonly _editorProviders = new Map(); + private readonly _customEditorModels = new Map(); constructor( context: extHostProtocol.IExtHostContext, @@ -272,10 +273,9 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma webviewInput.webview.extension = extension; const resource = webviewInput.getResource(); - const model = await this.loadOrCreateModel(webviewInput, resource, viewType, capabilities); + const model = await this.retainCustomEditorModel(webviewInput, resource, viewType, capabilities); webviewInput.onDisposeWebview(() => { - // TODO: This should be reference counted - this._customEditorService.models.disposeModel(model); + this.releaseCustomEditorModel(model); }); try { @@ -306,36 +306,53 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma this._editorProviders.delete(viewType); } - private async loadOrCreateModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) { - const existingModel = this._customEditorService.models.get(webviewInput.getResource(), webviewInput.viewType); - if (existingModel) { - return existingModel; + private async retainCustomEditorModel(webviewInput: WebviewInput, resource: URI, viewType: string, capabilities: readonly extHostProtocol.WebviewEditorCapabilities[]) { + const model = await this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); + + const existingEntry = this._customEditorModels.get(model); + if (existingEntry) { + ++existingEntry.referenceCount; + // no need to hook up listeners again + return model; } - const newModel = await this._customEditorService.models.loadOrCreate(webviewInput.getResource(), webviewInput.viewType); + this._customEditorModels.set(model, { referenceCount: 1 }); const capabilitiesSet = new Set(capabilities); if (capabilitiesSet.has(extHostProtocol.WebviewEditorCapabilities.Editable)) { - newModel.onUndo(edits => { + model.onUndo(edits => { this._proxy.$undoEdits(resource, viewType, edits.map(x => x.data)); }); - newModel.onApplyEdit(edits => { - const editsToApply = edits.filter(x => x.source !== newModel).map(x => x.data); + model.onApplyEdit(edits => { + const editsToApply = edits.filter(x => x.source !== model).map(x => x.data); if (editsToApply.length) { this._proxy.$applyEdits(resource, viewType, editsToApply); } }); - newModel.onWillSave(e => { + model.onWillSave(e => { e.waitUntil(this._proxy.$onSave(resource.toJSON(), viewType)); }); - newModel.onWillSaveAs(e => { + model.onWillSaveAs(e => { e.waitUntil(this._proxy.$onSaveAs(e.resource.toJSON(), viewType, e.targetResource.toJSON())); }); } - return newModel; + return model; + } + + private async releaseCustomEditorModel(model: ICustomEditorModel) { + const entry = this._customEditorModels.get(model); + if (!entry) { + return; + } + + --entry.referenceCount; + if (entry.referenceCount <= 0) { + this._customEditorService.models.disposeModel(model); + this._customEditorModels.delete(model); + } } public $onEdit(resource: UriComponents, viewType: string, editData: any): void { diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 98a6468b92c..e721bb24267 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -8,7 +8,7 @@ import { forEach } from 'vs/base/common/collections'; import { IJSONSchema } from 'vs/base/common/jsonSchema'; import * as resources from 'vs/base/common/resources'; import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; -import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor } from 'vs/workbench/common/views'; +import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation } from 'vs/workbench/common/views'; import { CustomTreeViewPane, CustomTreeView } from 'vs/workbench/browser/parts/views/customView'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { coalesce, } from 'vs/base/common/arrays'; @@ -22,14 +22,13 @@ import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug'; import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/common/remote.contribution'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; -import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet'; +import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IStorageService } from 'vs/platform/storage/common/storage'; -import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -313,7 +312,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution { if (!viewContainer) { - viewContainer = this.viewContainersRegistry.registerViewContainer(id, true, extensionId); class CustomViewPaneContainer extends ViewPaneContainer { constructor( @@ -327,38 +325,17 @@ class ViewsExtensionHandler implements IWorkbenchContribution { @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, ) { - super(id, `${id}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + super(id, `${id}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); } } - // Register a viewlet - class CustomViewlet extends Viewlet { - constructor( - @IConfigurationService configurationService: IConfigurationService, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @ITelemetryService telemetryService: ITelemetryService, - @IWorkspaceContextService contextService: IWorkspaceContextService, - @IStorageService storageService: IStorageService, - @IEditorService editorService: IEditorService, - @IInstantiationService instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService contextMenuService: IContextMenuService, - @IExtensionService extensionService: IExtensionService - ) { - super(id, instantiationService.createInstance(CustomViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } - } - - const viewletDescriptor = ViewletDescriptor.create( - CustomViewlet, + viewContainer = this.viewContainersRegistry.registerViewContainer({ id, - title, - undefined, - order, - icon - ); - - Registry.as(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor); + name: title, extensionId, + ctorDescriptor: { ctor: CustomViewPaneContainer }, + hideIfEmpty: true, + icon, + }, ViewContainerLocation.Sidebar); // Register Action to Open Viewlet class OpenCustomViewletAction extends ShowViewletAction { @@ -432,8 +409,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution { const order = ExtensionIdentifier.equals(extension.description.identifier, container.extensionId) ? index + 1 - : container.orderDelegate - ? container.orderDelegate.getOrder(item.group) + : container.viewOrderDelegate + ? container.viewOrderDelegate.getOrder(item.group) : undefined; const viewDescriptor = { diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 1f8513f02bf..ebd5a6b3d01 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -39,17 +39,16 @@ const configurationEntrySchema: IJSONSchema = { }, scope: { type: 'string', - enum: ['application', 'machine', 'window', 'resource', 'resource-language', 'machine-overridable'], + enum: ['application', 'machine', 'window', 'resource', 'machine-overridable'], default: 'window', enumDescriptions: [ nls.localize('scope.application.description', "Configuration that can be configured only in the user settings."), nls.localize('scope.machine.description', "Configuration that can be configured only in the user settings when the extension is running locally, or only in the remote settings when the extension is running remotely."), nls.localize('scope.window.description', "Configuration that can be configured in the user, remote or workspace settings."), nls.localize('scope.resource.description', "Configuration that can be configured in the user, remote, workspace or folder settings."), - nls.localize('scope.resource-language.description', "Resource configuration that can be configured also in language specific settings."), nls.localize('scope.machine-overridable.description', "Machine configuration that can be configured also in workspace or folder settings.") ], - description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `application`, `machine`, `window`, `resource`, `resource-language` and `machine-overridable`.") + description: nls.localize('scope.description', "Scope in which the configuration is applicable. Available scopes are `application`, `machine`, `window`, `resource`, and `machine-overridable`.") }, enumDescriptions: { type: 'array', @@ -58,12 +57,12 @@ const configurationEntrySchema: IJSONSchema = { }, description: nls.localize('scope.enumDescriptions', 'Descriptions for enum values') }, - markdownEnumDescription: { + markdownEnumDescriptions: { type: 'array', items: { type: 'string', }, - description: nls.localize('scope.markdownEnumDescription', 'Descriptions for enum values in the markdown format.') + description: nls.localize('scope.markdownEnumDescriptions', 'Descriptions for enum values in the markdown format.') }, markdownDescription: { type: 'string', diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 815b1c913af..87558e6d0ec 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -67,6 +67,7 @@ import { ILogService } from 'vs/platform/log/common/log'; import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService'; +import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming'; import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService'; export interface IExtensionApiFactory { @@ -125,7 +126,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostComment = rpcProtocol.set(ExtHostContext.ExtHostComments, new ExtHostComments(rpcProtocol, extHostCommands, extHostDocuments)); const extHostWindow = rpcProtocol.set(ExtHostContext.ExtHostWindow, new ExtHostWindow(rpcProtocol)); const extHostProgress = rpcProtocol.set(ExtHostContext.ExtHostProgress, new ExtHostProgress(rpcProtocol.getProxy(MainContext.MainThreadProgress))); - const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHosLabelService, new ExtHostLabelService(rpcProtocol)); + const extHostLabelService = rpcProtocol.set(ExtHostContext.ExtHostLabelService, new ExtHostLabelService(rpcProtocol)); + const extHostTheming = rpcProtocol.set(ExtHostContext.ExtHostTheming, new ExtHostTheming(rpcProtocol)); // Check that no named customers are missing const expected: ProxyIdentifier[] = values(ExtHostContext); @@ -548,6 +550,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createInputBox(): vscode.InputBox { return extHostQuickOpen.createInputBox(extension.identifier); + }, + get activeColorTheme(): vscode.ColorTheme { + checkProposedApiEnabled(extension); + return extHostTheming.activeColorTheme; + }, + onDidChangeActiveColorTheme(listener, thisArg?, disposables?) { + checkProposedApiEnabled(extension); + return extHostTheming.onDidChangeActiveColorTheme(listener, thisArg, disposables); } }; @@ -664,9 +674,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidChangeConfiguration: (listener: (_: any) => any, thisArgs?: any, disposables?: extHostTypes.Disposable[]) => { return configProvider.onDidChangeConfiguration(listener, thisArgs, disposables); }, - getConfiguration(section?: string, resource?: vscode.Uri): vscode.WorkspaceConfiguration { - resource = arguments.length === 1 ? undefined : resource; - return configProvider.getConfiguration(section, resource, extension.identifier); + getConfiguration(section?: string, scope?: vscode.ConfigurationScope | null): vscode.WorkspaceConfiguration { + scope = arguments.length === 1 ? undefined : scope; + return configProvider.getConfiguration(section, scope, extension); }, registerTextDocumentContentProvider(scheme: string, provider: vscode.TextDocumentContentProvider) { return extHostDocumentContentProviders.registerTextDocumentContentProvider(scheme, provider); @@ -714,9 +724,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onWillRenameFiles: (listener: (e: vscode.FileWillRenameEvent) => any, thisArg?: any, disposables?: vscode.Disposable[]) => { return extHostFileSystemEvent.getOnWillRenameFileEvent(extension)(listener, thisArg, disposables); }, - makeTunnel: (forward: vscode.TunnelOptions) => { + openTunnel: (forward: vscode.TunnelOptions) => { checkProposedApiEnabled(extension); - return extHostTunnelService.makeTunnel(forward); + return extHostTunnelService.openTunnel(forward); } }; @@ -935,7 +945,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I DebugConsoleMode: extHostTypes.DebugConsoleMode, Decoration: extHostTypes.Decoration, WebviewContentState: extHostTypes.WebviewContentState, - UIKind: UIKind + UIKind: UIKind, + ColorThemeKind: extHostTypes.ColorThemeKind }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3cfcbb40737..076f847e2f6 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -22,7 +22,7 @@ import { IModelChangedEvent } from 'vs/editor/common/model/mirrorTextModel'; import * as modes from 'vs/editor/common/modes'; import { CharacterPair, CommentRule, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; import { ICommandHandlerDescription } from 'vs/platform/commands/common/commands'; -import { ConfigurationTarget, IConfigurationData, IConfigurationChange } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationData, IConfigurationChange, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import * as files from 'vs/platform/files/common/files'; @@ -47,7 +47,8 @@ import { createExtHostContextProxyIdentifier as createExtId, createMainContextPr import * as search from 'vs/workbench/services/search/common/search'; import { SaveReason } from 'vs/workbench/common/editor'; import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator'; -import { TunnelOptions, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; +import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; +import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -147,8 +148,8 @@ export interface MainThreadCommentsShape extends IDisposable { } export interface MainThreadConfigurationShape extends IDisposable { - $updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, resource: UriComponents | undefined): Promise; - $removeConfigurationOption(target: ConfigurationTarget | null, key: string, resource: UriComponents | undefined): Promise; + $updateConfigurationOption(target: ConfigurationTarget | null, key: string, value: any, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise; + $removeConfigurationOption(target: ConfigurationTarget | null, key: string, overrides: IConfigurationOverrides | undefined, scopeToLanguage: boolean | undefined): Promise; } export interface MainThreadDiagnosticsShape extends IDisposable { @@ -749,7 +750,7 @@ export interface MainThreadDebugServiceShape extends IDisposable { $acceptDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $acceptDAError(handle: number, name: string, message: string, stack: string | undefined): void; $acceptDAExit(handle: number, code: number | undefined, signal: string | undefined): void; - $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasProvideDaMethod: boolean, handle: number): Promise; + $registerDebugConfigurationProvider(type: string, hasProvideMethod: boolean, hasResolveMethod: boolean, hasResolve2Method: boolean, hasProvideDaMethod: boolean, handle: number): Promise; $registerDebugAdapterDescriptorFactory(type: string, handle: number): Promise; $unregisterDebugConfigurationProvider(handle: number): void; $unregisterDebugAdapterDescriptorFactory(handle: number): void; @@ -774,7 +775,7 @@ export interface MainThreadWindowShape extends IDisposable { export interface MainThreadTunnelServiceShape extends IDisposable { $openTunnel(tunnelOptions: TunnelOptions): Promise; - $closeTunnel(remotePort: number): Promise; + $closeTunnel(remote: { host: string, port: number }): Promise; $registerCandidateFinder(): Promise; $setTunnelProvider(): Promise; } @@ -1188,7 +1189,7 @@ export interface ExtHostLanguageFeaturesShape { $provideColorPresentations(handle: number, resource: UriComponents, colorInfo: IRawColorInfo, token: CancellationToken): Promise; $provideFoldingRanges(handle: number, resource: UriComponents, context: modes.FoldingContext, token: CancellationToken): Promise; $provideSelectionRanges(handle: number, resource: UriComponents, positions: IPosition[], token: CancellationToken): Promise; - $prepareCallHierarchy(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $prepareCallHierarchy(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideCallHierarchyIncomingCalls(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $provideCallHierarchyOutgoingCalls(handle: number, sessionId: string, itemId: string, token: CancellationToken): Promise; $releaseCallHierarchy(handle: number, sessionId: string): void; @@ -1336,6 +1337,7 @@ export interface ExtHostDebugServiceShape { $stopDASession(handle: number): Promise; $sendDAMessage(handle: number, message: DebugProtocol.ProtocolMessage): void; $resolveDebugConfiguration(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; + $resolveDebugConfigurationWithSubstitutedVariables(handle: number, folder: UriComponents | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; $provideDebugConfigurations(handle: number, folder: UriComponents | undefined, token: CancellationToken): Promise; $legacyDebugAdapterExecutable(handle: number, folderUri: UriComponents | undefined): Promise; // TODO@AW legacy $provideDebugAdapter(handle: number, session: IDebugSessionDto): Promise; @@ -1393,9 +1395,15 @@ export interface ExtHostStorageShape { $acceptValue(shared: boolean, key: string, value: object | undefined): void; } +export interface ExtHostThemingShape { + $onColorThemeChange(themeType: string): void; +} + +export interface MainThreadThemingShape extends IDisposable { +} export interface ExtHostTunnelServiceShape { - $findCandidatePorts(): Promise<{ port: number, detail: string }[]>; + $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]>; $forwardPort(tunnelOptions: TunnelOptions): Promise | undefined; $closeTunnel(remote: { host: string, port: number }): Promise; } @@ -1441,6 +1449,7 @@ export const MainContext = { MainThreadTask: createMainId('MainThreadTask'), MainThreadWindow: createMainId('MainThreadWindow'), MainThreadLabelService: createMainId('MainThreadLabelService'), + MainThreadTheming: createMainId('MainThreadTheming'), MainThreadTunnelService: createMainId('MainThreadTunnelService') }; @@ -1475,6 +1484,7 @@ export const ExtHostContext = { ExtHostStorage: createMainId('ExtHostStorage'), ExtHostUrls: createExtId('ExtHostUrls'), ExtHostOutputService: createMainId('ExtHostOutputService'), - ExtHosLabelService: createMainId('ExtHostLabelService'), + ExtHostLabelService: createMainId('ExtHostLabelService'), + ExtHostTheming: createMainId('ExtHostTheming'), ExtHostTunnelService: createMainId('ExtHostTunnelService') }; diff --git a/src/vs/workbench/api/common/extHostConfiguration.ts b/src/vs/workbench/api/common/extHostConfiguration.ts index 7fe37f3fc97..40bc2a63312 100644 --- a/src/vs/workbench/api/common/extHostConfiguration.ts +++ b/src/vs/workbench/api/common/extHostConfiguration.ts @@ -4,22 +4,23 @@ *--------------------------------------------------------------------------------------------*/ import { mixin, deepClone } from 'vs/base/common/objects'; -import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import * as vscode from 'vscode'; import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace'; import { ExtHostConfigurationShape, MainThreadConfigurationShape, IConfigurationInitData, MainContext } from './extHost.protocol'; import { ConfigurationTarget as ExtHostConfigurationTarget } from './extHostTypes'; -import { ConfigurationTarget, IConfigurationChange, IConfigurationData } from 'vs/platform/configuration/common/configuration'; +import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration'; import { Configuration, ConfigurationChangeEvent } from 'vs/platform/configuration/common/configurationModels'; import { ConfigurationScope, OVERRIDE_PROPERTY_PATTERN } from 'vs/platform/configuration/common/configurationRegistry'; import { isObject } from 'vs/base/common/types'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { Barrier } from 'vs/base/common/async'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; import { ILogService } from 'vs/platform/log/common/log'; import { Workspace } from 'vs/platform/workspace/common/workspace'; +import { URI } from 'vs/base/common/uri'; +import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; function lookUp(tree: any, key: string) { if (key) { @@ -34,12 +35,57 @@ function lookUp(tree: any, key: string) { type ConfigurationInspect = { key: string; + defaultValue?: T; globalValue?: T; - workspaceValue?: T; - workspaceFolderValue?: T; + workspaceValue?: T, + workspaceFolderValue?: T, + + defaultLanguageValue?: T; + userLanguageValue?: T; + workspaceLanguageValue?: T; + workspaceFolderLanguageValue?: T; }; +function isTextDocument(thing: any): thing is vscode.TextDocument { + return thing + && thing.uri instanceof URI + && (!thing.languageId || typeof thing.languageId === 'string'); +} + +function isWorkspaceFolder(thing: any): thing is vscode.WorkspaceFolder { + return thing + && thing.uri instanceof URI + && (!thing.name || typeof thing.name === 'string') + && (!thing.index || typeof thing.index === 'number'); +} + +function isUri(thing: any): thing is vscode.Uri { + return thing instanceof URI; +} + +function isResourceLanguage(thing: any): thing is { resource: URI, languageId: string } { + return thing + && thing.resource instanceof URI + && (!thing.languageId || typeof thing.languageId === 'string'); +} + +function scopeToOverrides(scope: vscode.ConfigurationScope | undefined | null): IConfigurationOverrides | undefined { + if (isUri(scope)) { + return { resource: scope }; + } + if (isWorkspaceFolder(scope)) { + return { resource: scope.uri }; + } + if (isTextDocument(scope)) { + return { resource: scope.uri, overrideIdentifier: scope.languageId }; + } + if (isResourceLanguage(scope)) { + return scope; + } + return undefined; +} + export class ExtHostConfiguration implements ExtHostConfigurationShape { readonly _serviceBrand: undefined; @@ -104,13 +150,17 @@ export class ExtHostConfigProvider { this._onDidChangeConfiguration.fire(this._toConfigurationChangeEvent(change, previous)); } - getConfiguration(section?: string, resource?: URI, extensionId?: ExtensionIdentifier): vscode.WorkspaceConfiguration { + getConfiguration(section?: string, scope?: vscode.ConfigurationScope | null, extensionDescription?: IExtensionDescription): vscode.WorkspaceConfiguration { + const overrides = scopeToOverrides(scope) || {}; + if (overrides.overrideIdentifier && extensionDescription) { + checkProposedApiEnabled(extensionDescription); + } const config = this._toReadonlyValue(section - ? lookUp(this._configuration.getValue(undefined, { resource }, this._extHostWorkspace.workspace), section) - : this._configuration.getValue(undefined, { resource }, this._extHostWorkspace.workspace)); + ? lookUp(this._configuration.getValue(undefined, overrides, this._extHostWorkspace.workspace), section) + : this._configuration.getValue(undefined, overrides, this._extHostWorkspace.workspace)); if (section) { - this._validateConfigurationAccess(section, resource, extensionId); + this._validateConfigurationAccess(section, overrides, extensionDescription?.identifier); } function parseConfigurationTarget(arg: boolean | ExtHostConfigurationTarget): ConfigurationTarget | null { @@ -133,7 +183,7 @@ export class ExtHostConfigProvider { return typeof lookUp(config, key) !== 'undefined'; }, get: (key: string, defaultValue?: T) => { - this._validateConfigurationAccess(section ? `${section}.${key}` : key, resource, extensionId); + this._validateConfigurationAccess(section ? `${section}.${key}` : key, overrides, extensionDescription?.identifier); let result = lookUp(config, key); if (typeof result === 'undefined') { result = defaultValue; @@ -189,25 +239,31 @@ export class ExtHostConfigProvider { } return result; }, - update: (key: string, value: any, arg: ExtHostConfigurationTarget | boolean) => { + update: (key: string, value: any, extHostConfigurationTarget: ExtHostConfigurationTarget | boolean, scopeToLanguage?: boolean) => { key = section ? `${section}.${key}` : key; - const target = parseConfigurationTarget(arg); + const target = parseConfigurationTarget(extHostConfigurationTarget); if (value !== undefined) { - return this._proxy.$updateConfigurationOption(target, key, value, resource); + return this._proxy.$updateConfigurationOption(target, key, value, overrides, scopeToLanguage); } else { - return this._proxy.$removeConfigurationOption(target, key, resource); + return this._proxy.$removeConfigurationOption(target, key, overrides, scopeToLanguage); } }, inspect: (key: string): ConfigurationInspect | undefined => { key = section ? `${section}.${key}` : key; - const config = deepClone(this._configuration.inspect(key, { resource }, this._extHostWorkspace.workspace)); + const config = deepClone(this._configuration.inspect(key, overrides, this._extHostWorkspace.workspace)); if (config) { return { key, + defaultValue: config.defaultValue, globalValue: config.userValue, workspaceValue: config.workspaceValue, - workspaceFolderValue: config.workspaceFolderValue + workspaceFolderValue: config.workspaceFolderValue, + + defaultLanguageValue: config.default?.override, + userLanguageValue: config.user?.override, + workspaceLanguageValue: config.workspace?.override, + workspaceFolderLanguageValue: config.workspaceFolder?.override, }; } return undefined; @@ -237,17 +293,17 @@ export class ExtHostConfigProvider { return readonlyProxy(result); } - private _validateConfigurationAccess(key: string, resource: URI | undefined, extensionId?: ExtensionIdentifier): void { + private _validateConfigurationAccess(key: string, overrides?: IConfigurationOverrides, extensionId?: ExtensionIdentifier): void { const scope = OVERRIDE_PROPERTY_PATTERN.test(key) ? ConfigurationScope.RESOURCE : this._configurationScopes.get(key); const extensionIdText = extensionId ? `[${extensionId.value}] ` : ''; if (ConfigurationScope.RESOURCE === scope) { - if (resource === undefined) { + if (overrides?.resource) { this._logService.warn(`${extensionIdText}Accessing a resource scoped configuration without providing a resource is not expected. To get the effective value for '${key}', provide the URI of a resource or 'null' for any resource.`); } return; } if (ConfigurationScope.WINDOW === scope) { - if (resource) { + if (overrides?.resource) { this._logService.warn(`${extensionIdText}Accessing a window scoped configuration for a resource is not expected. To associate '${key}' to a resource, define its scope to 'resource' in configuration contributions in 'package.json'.`); } return; @@ -257,7 +313,7 @@ export class ExtHostConfigProvider { private _toConfigurationChangeEvent(change: IConfigurationChange, previous: { data: IConfigurationData, workspace: Workspace | undefined }): vscode.ConfigurationChangeEvent { const event = new ConfigurationChangeEvent(change, previous, this._configuration, this._extHostWorkspace.workspace); return Object.freeze({ - affectsConfiguration: (section: string, resource?: URI) => event.affectsConfiguration(section, resource ? { resource } : undefined) + affectsConfiguration: (section: string, scope?: vscode.ConfigurationScope) => event.affectsConfiguration(section, scopeToOverrides(scope)) }); } diff --git a/src/vs/workbench/api/common/extHostDebugService.ts b/src/vs/workbench/api/common/extHostDebugService.ts index c1352774e0d..446a36120d4 100644 --- a/src/vs/workbench/api/common/extHostDebugService.ts +++ b/src/vs/workbench/api/common/extHostDebugService.ts @@ -315,6 +315,7 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb this._debugServiceProxy.$registerDebugConfigurationProvider(type, !!provider.provideDebugConfigurations, !!provider.resolveDebugConfiguration, + !!provider.resolveDebugConfigurationWithSubstitutedVariables, !!provider.debugAdapterExecutable, // TODO@AW: deprecated handle); @@ -628,6 +629,20 @@ export class ExtHostDebugServiceBase implements IExtHostDebugService, ExtHostDeb }); } + public $resolveDebugConfigurationWithSubstitutedVariables(configProviderHandle: number, folderUri: UriComponents | undefined, debugConfiguration: vscode.DebugConfiguration, token: CancellationToken): Promise { + return asPromise(async () => { + const provider = this.getConfigProviderByHandle(configProviderHandle); + if (!provider) { + throw new Error('no DebugConfigurationProvider found'); + } + if (!provider.resolveDebugConfigurationWithSubstitutedVariables) { + throw new Error('DebugConfigurationProvider has no method resolveDebugConfigurationWithSubstitutedVariables'); + } + const folder = await this.getFolder(folderUri); + return provider.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration, token); + }); + } + // TODO@AW deprecated and legacy public $legacyDebugAdapterExecutable(configProviderHandle: number, folderUri: UriComponents | undefined): Promise { return asPromise(async () => { diff --git a/src/vs/workbench/api/common/extHostExtensionService.ts b/src/vs/workbench/api/common/extHostExtensionService.ts index caa8fee5d81..944546d61d9 100644 --- a/src/vs/workbench/api/common/extHostExtensionService.ts +++ b/src/vs/workbench/api/common/extHostExtensionService.ts @@ -662,7 +662,7 @@ export abstract class AbstractExtHostExtensionService implements ExtHostExtensio value: { authority, options, - tunnelInformation: { detectedTunnels: result.detectedTunnels } + tunnelInformation: { environmentTunnels: result.environmentTunnels } } }; } catch (err) { diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index a9482c1616c..ba0f638fefd 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1200,18 +1200,23 @@ class CallHierarchyAdapter { private readonly _provider: vscode.CallHierarchyProvider ) { } - async prepareSession(uri: URI, position: IPosition, token: CancellationToken): Promise { + async prepareSession(uri: URI, position: IPosition, token: CancellationToken): Promise { const doc = this._documents.getDocument(uri); const pos = typeConvert.Position.to(position); - const item = await this._provider.prepareCallHierarchy(doc, pos, token); - if (!item) { + const items = await this._provider.prepareCallHierarchy(doc, pos, token); + if (!items) { return undefined; } - const sessionId = this._idPool.nextId(); + const sessionId = this._idPool.nextId(); this._cache.set(sessionId, new Map()); - return this._cacheAndConvertItem(sessionId, item); + + if (Array.isArray(items)) { + return items.map(item => this._cacheAndConvertItem(sessionId, item)); + } else { + return [this._cacheAndConvertItem(sessionId, items)]; + } } async provideCallsTo(sessionId: string, itemId: string, token: CancellationToken): Promise { @@ -1727,7 +1732,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._createDisposable(handle); } - $prepareCallHierarchy(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { + $prepareCallHierarchy(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { return this._withAdapter(handle, CallHierarchyAdapter, adapter => Promise.resolve(adapter.prepareSession(URI.revive(resource), position, token)), undefined); } diff --git a/src/vs/workbench/api/common/extHostTheming.ts b/src/vs/workbench/api/common/extHostTheming.ts new file mode 100644 index 00000000000..04c3d517fdc --- /dev/null +++ b/src/vs/workbench/api/common/extHostTheming.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ColorTheme, ColorThemeKind } from './extHostTypes'; +import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService'; +import { ExtHostThemingShape } from 'vs/workbench/api/common/extHost.protocol'; +import { Emitter, Event } from 'vs/base/common/event'; + +export class ExtHostTheming implements ExtHostThemingShape { + + readonly _serviceBrand: undefined; + + private _actual: ColorTheme; + private _onDidChangeActiveColorTheme: Emitter; + + constructor( + @IExtHostRpcService _extHostRpc: IExtHostRpcService + ) { + this._actual = new ColorTheme(ColorThemeKind.Dark); + this._onDidChangeActiveColorTheme = new Emitter(); + } + + public get activeColorTheme(): ColorTheme { + return this._actual; + } + + $onColorThemeChange(type: string): void { + let kind = type === 'light' ? ColorThemeKind.Light : type === 'dark' ? ColorThemeKind.Dark : ColorThemeKind.HighContrast; + this._actual = new ColorTheme(kind); + this._onDidChangeActiveColorTheme.fire(this._actual); + } + + public get onDidChangeActiveColorTheme(): Event { + return this._onDidChangeActiveColorTheme.event; + } +} diff --git a/src/vs/workbench/api/common/extHostTunnelService.ts b/src/vs/workbench/api/common/extHostTunnelService.ts index 40c37a6c00c..f3e81eab7bd 100644 --- a/src/vs/workbench/api/common/extHostTunnelService.ts +++ b/src/vs/workbench/api/common/extHostTunnelService.ts @@ -6,27 +6,20 @@ import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import * as vscode from 'vscode'; -import { RemoteTunnel } from 'vs/platform/remote/common/tunnel'; +import { RemoteTunnel, TunnelOptions } from 'vs/platform/remote/common/tunnel'; import { IDisposable } from 'vs/base/common/lifecycle'; -export interface TunnelOptions { - remote: { port: number, host: string }; - localPort?: number; - name?: string; - closeable?: boolean; -} - export interface TunnelDto { - remote: { port: number, host: string }; + remoteAddress: { port: number, host: string }; localAddress: string; } export namespace TunnelDto { export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto { - return { remote: tunnel.remote, localAddress: tunnel.localAddress }; + return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress }; } export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto { - return { remote: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; + return { remoteAddress: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress }; } } @@ -37,7 +30,7 @@ export interface Tunnel extends vscode.Disposable { export interface IExtHostTunnelService extends ExtHostTunnelServiceShape { readonly _serviceBrand: undefined; - makeTunnel(forward: TunnelOptions): Promise; + openTunnel(forward: TunnelOptions): Promise; setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise; } @@ -45,10 +38,10 @@ export const IExtHostTunnelService = createDecorator('IEx export class ExtHostTunnelService implements IExtHostTunnelService { _serviceBrand: undefined; - async makeTunnel(forward: TunnelOptions): Promise { + async openTunnel(forward: TunnelOptions): Promise { return undefined; } - async $findCandidatePorts(): Promise<{ port: number; detail: string; }[]> { + async $findCandidatePorts(): Promise<{ host: string, port: number; detail: string; }[]> { return []; } async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise { return { dispose: () => { } }; } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 51c2eff6d41..19819497a1e 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -2508,3 +2508,20 @@ export enum WebviewContentState { Unchanged = 2, Dirty = 3, } + + +//#region Theming + +@es5ClassCompat +export class ColorTheme implements vscode.ColorTheme { + constructor(public readonly kind: ColorThemeKind) { + } +} + +export enum ColorThemeKind { + Light = 1, + Dark = 2, + HighContrast = 3 +} + +//#endregion Theming diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index df971f54fad..4f893d318d4 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -51,6 +51,7 @@ namespace schema { case 'comments/commentThread/context': return MenuId.CommentThreadActions; case 'comments/comment/title': return MenuId.CommentTitle; case 'comments/comment/context': return MenuId.CommentActions; + case 'extension/context': return MenuId.ExtensionContext; } return undefined; @@ -209,6 +210,11 @@ namespace schema { type: 'array', items: menuItem }, + 'extension/context': { + description: localize('menus.extensionContext', "The extension context menu"), + type: 'array', + items: menuItem + }, } }; @@ -351,11 +357,8 @@ commandsExtensionPoint.setHandler(extensions => { let absoluteIcon: { dark: URI; light?: URI; } | ThemeIcon | undefined; if (icon) { if (typeof icon === 'string') { - if (extension.description.enableProposedApi) { - absoluteIcon = ThemeIcon.fromString(icon) || { dark: resources.joinPath(extension.description.extensionLocation, icon) }; - } else { - absoluteIcon = { dark: resources.joinPath(extension.description.extensionLocation, icon) }; - } + absoluteIcon = ThemeIcon.fromString(icon) || { dark: resources.joinPath(extension.description.extensionLocation, icon) }; + } else { absoluteIcon = { dark: resources.joinPath(extension.description.extensionLocation, icon.dark), diff --git a/src/vs/workbench/api/node/extHostTunnelService.ts b/src/vs/workbench/api/node/extHostTunnelService.ts index 9dbde0ddabb..794d8863dc9 100644 --- a/src/vs/workbench/api/node/extHostTunnelService.ts +++ b/src/vs/workbench/api/node/extHostTunnelService.ts @@ -13,16 +13,17 @@ import { exec } from 'child_process'; import * as resources from 'vs/base/common/resources'; import * as fs from 'fs'; import { isLinux } from 'vs/base/common/platform'; -import { IExtHostTunnelService, TunnelOptions, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; +import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService'; import { asPromise } from 'vs/base/common/async'; import { Event, Emitter } from 'vs/base/common/event'; +import { TunnelOptions } from 'vs/platform/remote/common/tunnel'; class ExtensionTunnel implements vscode.Tunnel { private _onDispose: Emitter = new Emitter(); - onDispose: Event = this._onDispose.event; + onDidDispose: Event = this._onDispose.event; constructor( - public readonly remote: { port: number; host: string; }, + public readonly remoteAddress: { port: number; host: string; }, public readonly localAddress: string, private readonly _dispose: () => void) { } @@ -48,11 +49,11 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe this.registerCandidateFinder(); } } - async makeTunnel(forward: TunnelOptions): Promise { + async openTunnel(forward: TunnelOptions): Promise { const tunnel = await this._proxy.$openTunnel(forward); if (tunnel) { - const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remote, tunnel.localAddress, () => { - return this._proxy.$closeTunnel(tunnel.remote.port); + const disposableTunnel: vscode.Tunnel = new ExtensionTunnel(tunnel.remoteAddress, tunnel.localAddress, () => { + return this._proxy.$closeTunnel(tunnel.remoteAddress); }); this._register(disposableTunnel); return disposableTunnel; @@ -65,8 +66,8 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe } async setForwardPortProvider(provider: vscode.RemoteAuthorityResolver | undefined): Promise { - if (provider && provider.forwardPort) { - this._forwardPortProvider = provider.forwardPort; + if (provider && provider.tunnelFactory) { + this._forwardPortProvider = provider.tunnelFactory; await this._proxy.$setTunnelProvider(); } else { this._forwardPortProvider = undefined; @@ -91,11 +92,11 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const providedPort = this._forwardPortProvider!(tunnelOptions); if (providedPort !== undefined) { return asPromise(() => providedPort).then(tunnel => { - if (!this._extensionTunnels.has(tunnelOptions.remote.host)) { - this._extensionTunnels.set(tunnelOptions.remote.host, new Map()); + if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) { + this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map()); } - this._extensionTunnels.get(tunnelOptions.remote.host)!.set(tunnelOptions.remote.port, tunnel); - this._register(tunnel.onDispose(() => this._proxy.$closeTunnel(tunnel.remote.port))); + this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, tunnel); + this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress))); return Promise.resolve(TunnelDto.fromApiTunnel(tunnel)); }); } @@ -104,12 +105,12 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe } - async $findCandidatePorts(): Promise<{ port: number, detail: string }[]> { + async $findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> { if (!isLinux) { return []; } - const ports: { port: number, detail: string }[] = []; + const ports: { host: string, port: number, detail: string }[] = []; const tcp: string = fs.readFileSync('/proc/net/tcp', 'utf8'); const tcp6: string = fs.readFileSync('/proc/net/tcp6', 'utf8'); const procSockets: string = await (new Promise(resolve => { @@ -150,7 +151,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => { const command = processMap[socketMap[socket].pid].cmd; if (!command.match('.*\.vscode\-server\-[a-zA-Z]+\/bin.*') && (command.indexOf('out/vs/server/main.js') === -1)) { - ports.push({ port, detail: processMap[socketMap[socket].pid].cmd }); + ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd }); } }); @@ -177,7 +178,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe const address = row.local_address.split(':'); return { socket: parseInt(row.inode, 10), - ip: address[0], + ip: this.parseIpAddress(address[0]), port: parseInt(address[1], 16) }; }).map(port => [port.port, port]) @@ -185,6 +186,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe ]; } + private parseIpAddress(hex: string): string { + let result = ''; + for (let i = hex.length - 2; (i >= 0); i -= 2) { + result += parseInt(hex.substr(i, 2), 16); + if (i !== 0) { + result += '.'; + } + } + return result; + } + private loadConnectionTable(stdout: string): Record[] { const lines = stdout.trim().split('\n'); const names = lines.shift()!.trim().split(/\s+/) diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index 2726a5c7d68..6f1eebf4841 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -16,7 +16,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/ import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { SideBarVisibleContext } from 'vs/workbench/common/viewlet'; -import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; +import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform'; import { PanelPositionContext } from 'vs/workbench/common/panel'; @@ -151,7 +151,7 @@ export class WorkbenchContextKeysHandler extends Disposable { // Panel Position this.panelPositionContext = PanelPositionContext.bindTo(this.contextKeyService); - this.panelPositionContext.set(this.layoutService.getPanelPosition() === Position.RIGHT ? 'right' : 'bottom'); + this.panelPositionContext.set(positionToString(this.layoutService.getPanelPosition())); this.registerListeners(); } diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index d4884d5c2a7..10d6cf85ef7 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -383,7 +383,7 @@ export function fillResourceDataTransfers(accessor: ServicesAccessor, resources: const model = textFileService.models.get(file.resource); if (model) { encoding = model.getEncoding(); - mode = model.textEditorModel?.getModeId(); + mode = model.getMode(); } } diff --git a/src/vs/workbench/browser/editor.ts b/src/vs/workbench/browser/editor.ts index e03a20900a5..af3e39d295e 100644 --- a/src/vs/workbench/browser/editor.ts +++ b/src/vs/workbench/browser/editor.ts @@ -9,6 +9,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IConstructorSignature0, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { find } from 'vs/base/common/arrays'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export interface IEditorDescriptor { instantiate(instantiationService: IInstantiationService): BaseEditor; @@ -30,7 +31,7 @@ export interface IEditorRegistry { * @param inputDescriptors A set of constructor functions that return an instance of EditorInput for which the * registered editor should be used for. */ - registerEditor(descriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): void; + registerEditor(descriptor: IEditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable; /** * Returns the editor descriptor for the given input or `undefined` if none. @@ -54,7 +55,7 @@ export interface IEditorRegistry { */ export class EditorDescriptor implements IEditorDescriptor { - public static create( + static create( ctor: { new(...services: Services): BaseEditor }, id: string, name: string @@ -87,14 +88,22 @@ export class EditorDescriptor implements IEditorDescriptor { class EditorRegistry implements IEditorRegistry { - private editors: EditorDescriptor[] = []; + private readonly editors: EditorDescriptor[] = []; private readonly mapEditorToInputs = new Map[]>(); - registerEditor(descriptor: EditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): void { - // Register (Support multiple Editors per Input) + registerEditor(descriptor: EditorDescriptor, inputDescriptors: readonly SyncDescriptor[]): IDisposable { this.mapEditorToInputs.set(descriptor, inputDescriptors); this.editors.push(descriptor); + + return toDisposable(() => { + this.mapEditorToInputs.delete(descriptor); + + const index = this.editors.indexOf(descriptor); + if (index !== -1) { + this.editors.splice(index, 1); + } + }); } getEditor(input: EditorInput): EditorDescriptor | undefined { @@ -156,10 +165,6 @@ class EditorRegistry implements IEditorRegistry { return this.editors.slice(0); } - setEditors(editorsToSet: EditorDescriptor[]): void { - this.editors = editorsToSet; - } - getEditorInputs(): SyncDescriptor[] { const inputClasses: SyncDescriptor[] = []; for (const editor of this.editors) { diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index 02f948b1d7c..623dfa72b75 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -14,7 +14,7 @@ import { pathsToEditors } from 'vs/workbench/common/editor'; import { SidebarPart } from 'vs/workbench/browser/parts/sidebar/sidebarPart'; import { PanelPart } from 'vs/workbench/browser/parts/panel/panelPart'; import { PanelRegistry, Extensions as PanelExtensions } from 'vs/workbench/browser/panel'; -import { Position, Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { Position, Parts, IWorkbenchLayoutService, positionFromString, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; @@ -58,6 +58,7 @@ enum Storage { PANEL_HIDDEN = 'workbench.panel.hidden', PANEL_POSITION = 'workbench.panel.location', PANEL_SIZE = 'workbench.panel.size', + PANEL_DIMENSION = 'workbench.panel.dimension', PANEL_LAST_NON_MAXIMIZED_WIDTH = 'workbench.panel.lastNonMaximizedWidth', PANEL_LAST_NON_MAXIMIZED_HEIGHT = 'workbench.panel.lastNonMaximizedHeight', @@ -178,8 +179,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi position: Position.BOTTOM, lastNonMaximizedWidth: 300, lastNonMaximizedHeight: 300, - panelToRestore: undefined as string | undefined, - restored: false + panelToRestore: undefined as string | undefined }, statusBar: { @@ -571,10 +571,9 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi private updatePanelPosition() { const defaultPanelPosition = this.configurationService.getValue(Settings.PANEL_POSITION); - const panelPosition = this.storageService.get(Storage.PANEL_POSITION, StorageScope.WORKSPACE, undefined); + const panelPosition = this.storageService.get(Storage.PANEL_POSITION, StorageScope.WORKSPACE, defaultPanelPosition); - this.state.panel.restored = panelPosition !== undefined; - this.state.panel.position = ((panelPosition || defaultPanelPosition) === 'right') ? Position.RIGHT : Position.BOTTOM; + this.state.panel.position = positionFromString(panelPosition || defaultPanelPosition); } registerPart(part: Part): void { @@ -667,15 +666,16 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi } getMaximumEditorDimensions(): Dimension { + const isColumn = this.state.panel.position === Position.RIGHT || this.state.panel.position === Position.LEFT; const takenWidth = (this.isVisible(Parts.ACTIVITYBAR_PART) ? this.activityBarPartView.minimumWidth : 0) + (this.isVisible(Parts.SIDEBAR_PART) ? this.sideBarPartView.minimumWidth : 0) + - (this.isVisible(Parts.PANEL_PART) && this.state.panel.position === Position.RIGHT ? this.panelPartView.minimumWidth : 0); + (this.isVisible(Parts.PANEL_PART) && isColumn ? this.panelPartView.minimumWidth : 0); const takenHeight = (this.isVisible(Parts.TITLEBAR_PART) ? this.titleBarPartView.minimumHeight : 0) + (this.isVisible(Parts.STATUSBAR_PART) ? this.statusBarPartView.minimumHeight : 0) + - (this.isVisible(Parts.PANEL_PART) && this.state.panel.position === Position.BOTTOM ? this.panelPartView.minimumHeight : 0); + (this.isVisible(Parts.PANEL_PART) && !isColumn ? this.panelPartView.minimumHeight : 0); const availableWidth = this.dimension.width - takenWidth; const availableHeight = this.dimension.height - takenHeight; @@ -899,6 +899,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi : (this.state.panel.position === Position.BOTTOM ? grid.getViewSize(this.panelPartView).height : grid.getViewSize(this.panelPartView).width); this.storageService.store(Storage.PANEL_SIZE, panelSize, StorageScope.GLOBAL); + this.storageService.store(Storage.PANEL_DIMENSION, positionToString(this.state.panel.position), StorageScope.GLOBAL); const gridSize = grid.getViewSize(); this.storageService.store(Storage.GRID_WIDTH, gridSize.width, StorageScope.GLOBAL); @@ -1202,26 +1203,18 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi return this.state.panel.position; } - setPanelPosition(position: Position.BOTTOM | Position.RIGHT): void { + setPanelPosition(position: Position): void { if (this.state.panel.hidden) { this.setPanelHidden(false); } const panelPart = this.getPart(Parts.PANEL_PART); - const newPositionValue = (position === Position.BOTTOM) ? 'bottom' : 'right'; - const oldPositionValue = (this.state.panel.position === Position.BOTTOM) ? 'bottom' : 'right'; + const oldPositionValue = positionToString(this.state.panel.position); + const newPositionValue = positionToString(position); this.state.panel.position = position; - function positionToString(position: Position): string { - switch (position) { - case Position.LEFT: return 'left'; - case Position.RIGHT: return 'right'; - case Position.BOTTOM: return 'bottom'; - } - } - // Save panel position - this.storageService.store(Storage.PANEL_POSITION, positionToString(this.state.panel.position), StorageScope.WORKSPACE); + this.storageService.store(Storage.PANEL_POSITION, newPositionValue, StorageScope.WORKSPACE); // Adjust CSS const panelContainer = assertIsDefined(panelPart.getContainer()); @@ -1250,14 +1243,16 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi if (position === Position.BOTTOM) { this.workbenchGrid.moveView(this.panelPartView, this.state.editor.hidden ? size.height : this.state.panel.lastNonMaximizedHeight, this.editorPartView, Direction.Down); - } else { + } else if (position === Position.RIGHT) { this.workbenchGrid.moveView(this.panelPartView, this.state.editor.hidden ? size.width : this.state.panel.lastNonMaximizedWidth, this.editorPartView, Direction.Right); + } else { + this.workbenchGrid.moveView(this.panelPartView, this.state.editor.hidden ? size.width : this.state.panel.lastNonMaximizedWidth, this.editorPartView, Direction.Left); } // Reset sidebar to original size before shifting the panel this.workbenchGrid.resizeView(this.sideBarPartView, sideBarSize); - this._onPanelPositionChange.fire(positionToString(this.state.panel.position)); + this._onPanelPositionChange.fire(newPositionValue); } isWindowMaximized() { @@ -1275,13 +1270,26 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi this._onMaximizeChange.fire(maximized); } + private arrangeEditorNodes(editorNode: ISerializedNode, panelNode: ISerializedNode, editorSectionWidth: number): ISerializedNode[] { + switch (this.state.panel.position) { + case Position.BOTTOM: + return [{ type: 'branch', data: [editorNode, panelNode], size: editorSectionWidth }]; + case Position.RIGHT: + return [editorNode, panelNode]; + case Position.LEFT: + return [panelNode, editorNode]; + } + } + private createGridDescriptor(): ISerializedGrid { const workbenchDimensions = this.getClientArea(); const width = this.storageService.getNumber(Storage.GRID_WIDTH, StorageScope.GLOBAL, workbenchDimensions.width); const height = this.storageService.getNumber(Storage.GRID_HEIGHT, StorageScope.GLOBAL, workbenchDimensions.height); // At some point, we will not fall back to old keys from legacy layout, but for now, let's migrate the keys const sideBarSize = this.storageService.getNumber(Storage.SIDEBAR_SIZE, StorageScope.GLOBAL, this.storageService.getNumber('workbench.sidebar.width', StorageScope.GLOBAL, Math.min(workbenchDimensions.width / 4, 300))); - const panelSize = this.state.panel.restored ? this.storageService.getNumber(Storage.PANEL_SIZE, StorageScope.GLOBAL, this.storageService.getNumber(this.state.panel.position === Position.BOTTOM ? 'workbench.panel.height' : 'workbench.panel.width', StorageScope.GLOBAL, workbenchDimensions.height / 3)) : workbenchDimensions.height / 3; + const panelDimension = positionFromString(this.storageService.get(Storage.PANEL_DIMENSION, StorageScope.GLOBAL, 'bottom')); + const fallbackPanelSize = this.state.panel.position === Position.BOTTOM ? workbenchDimensions.height / 3 : workbenchDimensions.width / 4; + const panelSize = panelDimension === this.state.panel.position ? this.storageService.getNumber(Storage.PANEL_SIZE, StorageScope.GLOBAL, this.storageService.getNumber(this.state.panel.position === Position.BOTTOM ? 'workbench.panel.height' : 'workbench.panel.width', StorageScope.GLOBAL, fallbackPanelSize)) : fallbackPanelSize; const titleBarHeight = this.titleBarPartView.minimumHeight; const statusBarHeight = this.statusBarPartView.minimumHeight; @@ -1319,9 +1327,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi visible: !this.state.panel.hidden }; - const editorSectionNode: ISerializedNode[] = this.state.panel.position === Position.BOTTOM - ? [{ type: 'branch', data: [editorNode, panelNode], size: editorSectionWidth }] - : [editorNode, panelNode]; + const editorSectionNode = this.arrangeEditorNodes(editorNode, panelNode, editorSectionWidth); const middleSection: ISerializedNode[] = this.state.sideBar.position === Position.LEFT ? [activityBarNode, sideBarNode, ...editorSectionNode] diff --git a/src/vs/workbench/browser/panel.ts b/src/vs/workbench/browser/panel.ts index 666029c78ae..1f83af0e2db 100644 --- a/src/vs/workbench/browser/panel.ts +++ b/src/vs/workbench/browser/panel.ts @@ -12,9 +12,12 @@ import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/bro import { IConstructorSignature0, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { isAncestor } from 'vs/base/browser/dom'; import { assertIsDefined } from 'vs/base/common/types'; +import { PaneComposite } from 'vs/workbench/browser/panecomposite'; export abstract class Panel extends Composite implements IPanel { } +export abstract class PaneCompositePanel extends PaneComposite implements IPanel { } + /** * A panel descriptor is a leightweight descriptor of a panel in the workbench. */ diff --git a/src/vs/workbench/browser/parts/editor/editor.ts b/src/vs/workbench/browser/parts/editor/editor.ts index 39bf1a94e44..edeb87233fa 100644 --- a/src/vs/workbench/browser/parts/editor/editor.ts +++ b/src/vs/workbench/browser/parts/editor/editor.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditor, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, EditorOptions, TextEditorOptions, IEditorInput, IEditorIdentifier, IEditorCloseEvent, IEditor, IEditorPartOptions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor'; import { EditorGroup } from 'vs/workbench/common/editor/editorGroup'; import { IEditorGroup, GroupDirection, IAddGroupOptions, IMergeGroupOptions, GroupsOrder, GroupsArrangement } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -63,11 +63,6 @@ export function getEditorPartOptions(config: IWorkbenchEditorConfiguration): IEd return options; } -export interface IEditorPartOptionsChangeEvent { - oldPartOptions: IEditorPartOptions; - newPartOptions: IEditorPartOptions; -} - export interface IEditorOpeningEvent extends IEditorIdentifier { options?: IEditorOptions; @@ -156,4 +151,9 @@ export interface EditorServiceImpl extends IEditorService { * Emitted when an editor failed to open. */ readonly onDidOpenEditorFail: Event; + + /** + * Emitted when the list of most recently active editors change. + */ + readonly onDidMostRecentlyActiveEditorsChange: Event; } diff --git a/src/vs/workbench/browser/parts/editor/editorActions.ts b/src/vs/workbench/browser/parts/editor/editorActions.ts index f0123947c0b..28cfc1b0ace 100644 --- a/src/vs/workbench/browser/parts/editor/editorActions.ts +++ b/src/vs/workbench/browser/parts/editor/editorActions.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { mixin } from 'vs/base/common/objects'; -import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason } from 'vs/workbench/common/editor'; +import { IEditorInput, EditorInput, IEditorIdentifier, IEditorCommandsContext, CloseDirection, SaveReason, EditorsOrder } from 'vs/workbench/common/editor'; import { QuickOpenEntryGroup } from 'vs/base/parts/quickopen/browser/quickOpenModel'; import { EditorQuickOpenEntry, EditorQuickOpenEntryGroup, IEditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; @@ -16,7 +16,7 @@ import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { CLOSE_EDITOR_COMMAND_ID, NAVIGATE_ALL_EDITORS_BY_APPEARANCE_PREFIX, MOVE_ACTIVE_EDITOR_COMMAND_ID, NAVIGATE_IN_ACTIVE_GROUP_BY_MOST_RECENTLY_USED_PREFIX, ActiveEditorMoveArguments, SPLIT_EDITOR_LEFT, SPLIT_EDITOR_RIGHT, SPLIT_EDITOR_UP, SPLIT_EDITOR_DOWN, splitEditor, LAYOUT_EDITOR_GROUPS_COMMAND_ID, mergeAllGroups, NAVIGATE_ALL_EDITORS_BY_MOST_RECENTLY_USED_PREFIX } from 'vs/workbench/browser/parts/editor/editorCommands'; -import { IEditorGroupsService, IEditorGroup, GroupsArrangement, EditorsOrder, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsArrangement, GroupLocation, GroupDirection, preferredSideBySideGroupDirection, IFindGroupScope, GroupOrientation, EditorGroupLayout, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { DisposableStore } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index 51be3d78e6d..2e06af3dc2e 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/editorgroupview'; import { EditorGroup, IEditorOpenOptions, EditorCloseEvent, ISerializedEditorGroup, isSerializedEditorGroup } from 'vs/workbench/common/editor/editorGroup'; -import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor, SaveReason, SaveContext } from 'vs/workbench/common/editor'; +import { EditorInput, EditorOptions, GroupIdentifier, SideBySideEditorInput, CloseDirection, IEditorCloseEvent, EditorGroupActiveEditorDirtyContext, IEditor, EditorGroupEditorsCountContext, toResource, SideBySideEditor, SaveReason, SaveContext, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; import { Event, Emitter, Relay } from 'vs/base/common/event'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { addClass, addClasses, Dimension, trackFocus, toggleClass, removeClass, addDisposableListener, EventType, EventHelper, findParentWithClass, clearNode, isAncestor } from 'vs/base/browser/dom'; @@ -17,7 +17,7 @@ import { attachProgressBarStyler } from 'vs/platform/theme/common/styler'; import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; import { editorBackground, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { Themable, EDITOR_GROUP_HEADER_TABS_BORDER, EDITOR_GROUP_HEADER_TABS_BACKGROUND, EDITOR_GROUP_HEADER_NO_TABS_BACKGROUND, EDITOR_GROUP_EMPTY_BACKGROUND, EDITOR_GROUP_FOCUSED_EMPTY_BORDER } from 'vs/workbench/common/theme'; -import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, EditorsOrder, GroupsOrder, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IMoveEditorOptions, ICopyEditorOptions, ICloseEditorsFilter, IGroupChangeEvent, GroupChangeKind, GroupsOrder, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; import { TabsTitleControl } from 'vs/workbench/browser/parts/editor/tabsTitleControl'; import { EditorControl } from 'vs/workbench/browser/parts/editor/editorControl'; import { IEditorProgressService } from 'vs/platform/progress/common/progress'; @@ -31,7 +31,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { RunOnceWorker } from 'vs/base/common/async'; import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch'; import { TitleControl } from 'vs/workbench/browser/parts/editor/titleControl'; -import { IEditorGroupsAccessor, IEditorGroupView, IEditorPartOptionsChangeEvent, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, getActiveTextEditorOptions, IEditorOpeningEvent } from 'vs/workbench/browser/parts/editor/editor'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; @@ -723,7 +723,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } get editors(): EditorInput[] { - return this._group.getEditors(); + return this._group.getEditors(EditorsOrder.SEQUENTIAL); } get count(): number { @@ -750,12 +750,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { return this._group.isActive(editor); } - getEditors(order?: EditorsOrder): EditorInput[] { - if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) { - return this._group.getEditors(true); - } - - return this.editors; + getEditors(order: EditorsOrder): EditorInput[] { + return this._group.getEditors(order); } getEditorByIndex(index: number): EditorInput | undefined { @@ -825,6 +821,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { private doOpenEditor(editor: EditorInput, options?: EditorOptions): Promise { + // Guard against invalid inputs. Disposed inputs + // should never open because they emit no events + // e.g. to indicate dirty changes. + if (editor.isDisposed()) { + return Promise.resolve(undefined); + } + // Determine options const openEditorOptions: IEditorOpenOptions = { index: options ? options.index : undefined, @@ -1019,7 +1022,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { // Use the first editor as active editor const { editor, options } = editors.shift()!; - let firstOpenedEditor = await this.openEditor(editor, options); + await this.openEditor(editor, options); // Open the other ones inactive const startingIndex = this.getIndexOfEditor(editor) + 1; @@ -1029,13 +1032,13 @@ export class EditorGroupView extends Themable implements IEditorGroupView { adjustedEditorOptions.pinned = true; adjustedEditorOptions.index = startingIndex + index; - const openedEditor = await this.openEditor(editor, adjustedEditorOptions); - if (!firstOpenedEditor) { - firstOpenedEditor = openedEditor; // only take if the first editor opening failed - } + await this.openEditor(editor, adjustedEditorOptions); })); - return firstOpenedEditor; + // Opening many editors at once can put any editor to be + // the active one depending on options. As such, we simply + // return the active control after this operation. + return this.editorControl.activeControl; } //#endregion @@ -1371,7 +1374,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { const filter = editors; const hasDirection = typeof filter.direction === 'number'; - let editorsToClose = this._group.getEditors(!hasDirection /* in MRU order only if direction is not specified */); + let editorsToClose = this._group.getEditors(hasDirection ? EditorsOrder.SEQUENTIAL : EditorsOrder.MOST_RECENTLY_ACTIVE); // in MRU order only if direction is not specified // Filter: saved only if (filter.savedOnly) { @@ -1432,7 +1435,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { } // Check for dirty and veto - const editors = this._group.getEditors(true); + const editors = this._group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); const veto = await this.handleDirtyClosing(editors.slice(0)); if (veto) { return; @@ -1496,10 +1499,10 @@ export class EditorGroupView extends Themable implements IEditorGroupView { }); // Handle inactive first - inactiveReplacements.forEach(({ editor, replacement, options }) => { + inactiveReplacements.forEach(async ({ editor, replacement, options }) => { // Open inactive editor - this.doOpenEditor(replacement, options); + await this.doOpenEditor(replacement, options); // Close replaced inactive editor unless they match if (!editor.matches(replacement)) { @@ -1526,8 +1529,6 @@ export class EditorGroupView extends Themable implements IEditorGroupView { //#endregion - //#endregion - //#region Themable protected updateStyles(): void { diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index f08c37e4ae1..3c75a60d829 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -12,11 +12,11 @@ import { contrastBorder, editorBackground } from 'vs/platform/theme/common/color import { GroupDirection, IAddGroupOptions, GroupsArrangement, GroupOrientation, IMergeGroupOptions, MergeGroupMode, ICopyEditorOptions, GroupsOrder, GroupChangeKind, GroupLocation, IFindGroupScope, EditorGroupLayout, GroupLayoutArgument, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IView, orthogonal, LayoutPriority, IViewSize, Direction, SerializableGrid, Sizing, ISerializedGrid, Orientation, GridBranchNode, isGridBranchNode, GridNode, createSerializedGrid, Grid } from 'vs/base/browser/ui/grid/grid'; -import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorPartOptions, IEditorPartOptionsChangeEvent } from 'vs/workbench/common/editor'; import { values } from 'vs/base/common/map'; import { EDITOR_GROUP_BORDER, EDITOR_PANE_BACKGROUND } from 'vs/workbench/common/theme'; import { distinct, coalesce } from 'vs/base/common/arrays'; -import { IEditorGroupsAccessor, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartOptionsChangeEvent, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor'; +import { IEditorGroupsAccessor, IEditorGroupView, getEditorPartOptions, impactsEditorPartOptions, IEditorPartCreationOptions } from 'vs/workbench/browser/parts/editor/editor'; import { EditorGroupView } from 'vs/workbench/browser/parts/editor/editorGroupView'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -110,9 +110,12 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro private readonly _onDidMoveGroup = this._register(new Emitter()); readonly onDidMoveGroup = this._onDidMoveGroup.event; - private onDidSetGridWidget = this._register(new Emitter<{ width: number; height: number; } | undefined>()); - private _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; } | undefined>()); - get onDidSizeConstraintsChange(): Event<{ width: number; height: number; } | undefined> { return Event.any(this.onDidSetGridWidget.event, this._onDidSizeConstraintsChange.event); } + private readonly onDidSetGridWidget = this._register(new Emitter<{ width: number; height: number; } | undefined>()); + private readonly _onDidSizeConstraintsChange = this._register(new Relay<{ width: number; height: number; } | undefined>()); + readonly onDidSizeConstraintsChange = Event.any(this.onDidSetGridWidget.event, this._onDidSizeConstraintsChange.event); + + private readonly _onDidEditorPartOptionsChange = this._register(new Emitter()); + readonly onDidEditorPartOptionsChange = this._onDidEditorPartOptionsChange.event; //#endregion @@ -155,13 +158,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this.registerListeners(); } - //#region IEditorGroupsAccessor - - private enforcedPartOptions: IEditorPartOptions[] = []; - - private readonly _onDidEditorPartOptionsChange: Emitter = this._register(new Emitter()); - readonly onDidEditorPartOptionsChange: Event = this._onDidEditorPartOptionsChange.event; - private registerListeners(): void { this._register(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(e))); } @@ -185,6 +181,10 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro this._onDidEditorPartOptionsChange.fire({ oldPartOptions, newPartOptions }); } + //#region IEditorGroupsService + + private enforcedPartOptions: IEditorPartOptions[] = []; + get partOptions(): IEditorPartOptions { return this._partOptions; } @@ -199,10 +199,6 @@ export class EditorPart extends Part implements IEditorGroupsService, IEditorGro }); } - //#endregion - - //#region IEditorGroupsService - private _contentDimension!: Dimension; get contentDimension(): Dimension { return this._contentDimension; } diff --git a/src/vs/workbench/browser/parts/editor/editorPicker.ts b/src/vs/workbench/browser/parts/editor/editorPicker.ts index 022e5258ed0..65fa73bcb34 100644 --- a/src/vs/workbench/browser/parts/editor/editorPicker.ts +++ b/src/vs/workbench/browser/parts/editor/editorPicker.ts @@ -13,12 +13,11 @@ import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { IModelService } from 'vs/editor/common/services/modelService'; import { QuickOpenHandler } from 'vs/workbench/browser/quickopen'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { toResource, SideBySideEditor, IEditorInput } from 'vs/workbench/common/editor'; +import { toResource, SideBySideEditor, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; import { compareItemsByScore, scoreItem, ScorerCache, prepareQuery } from 'vs/base/parts/quickopen/common/quickOpenScorer'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { IHistoryService } from 'vs/workbench/services/history/common/history'; export class EditorPickerEntry extends QuickOpenEntryGroup { @@ -209,14 +208,13 @@ export abstract class BaseAllEditorsPicker extends BaseEditorPicker { constructor( @IInstantiationService instantiationService: IInstantiationService, @IEditorService editorService: IEditorService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IHistoryService protected historyService: IHistoryService + @IEditorGroupsService editorGroupService: IEditorGroupsService ) { super(instantiationService, editorService, editorGroupService); } protected count(): number { - return this.historyService.getMostRecentlyUsedOpenEditors().length; + return this.editorService.count; } getEmptyLabel(searchString: string): string { @@ -262,7 +260,7 @@ export class AllEditorsByMostRecentlyUsedPicker extends BaseAllEditorsPicker { protected getEditorEntries(): EditorPickerEntry[] { const entries: EditorPickerEntry[] = []; - for (const { editor, groupId } of this.historyService.getMostRecentlyUsedOpenEditors()) { + for (const { editor, groupId } of this.editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) { entries.push(this.instantiationService.createInstance(EditorPickerEntry, editor, this.editorGroupService.getGroup(groupId)!)); } diff --git a/src/vs/workbench/browser/parts/editor/editorsObserver.ts b/src/vs/workbench/browser/parts/editor/editorsObserver.ts new file mode 100644 index 00000000000..0545c9710d1 --- /dev/null +++ b/src/vs/workbench/browser/parts/editor/editorsObserver.ts @@ -0,0 +1,400 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IEditorInput, IEditorInputFactoryRegistry, IEditorIdentifier, GroupIdentifier, Extensions, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; +import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { Event, Emitter } from 'vs/base/common/event'; +import { IEditorGroupsService, IEditorGroup, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { coalesce } from 'vs/base/common/arrays'; +import { LinkedMap, Touch } from 'vs/base/common/map'; +import { equals } from 'vs/base/common/objects'; + +interface ISerializedEditorsList { + entries: ISerializedEditorIdentifier[]; +} + +interface ISerializedEditorIdentifier { + groupId: GroupIdentifier; + index: number; +} + +/** + * A observer of opened editors across all editor groups by most recently used. + * Rules: + * - the last editor in the list is the one most recently activated + * - the first editor in the list is the one that was activated the longest time ago + * - an editor that opens inactive will be placed behind the currently active editor + * + * The observer may start to close editors based on the workbench.editor.limit setting. + */ +export class EditorsObserver extends Disposable { + + private static readonly STORAGE_KEY = 'editors.mru'; + + private readonly keyMap = new Map>(); + private readonly mostRecentEditorsMap = new LinkedMap(); + + private readonly _onDidChange = this._register(new Emitter()); + readonly onDidChange = this._onDidChange.event; + + get count(): number { + return this.mostRecentEditorsMap.size; + } + + get editors(): IEditorIdentifier[] { + return this.mostRecentEditorsMap.values(); + } + + constructor( + @IEditorGroupsService private editorGroupsService: IEditorGroupsService, + @IStorageService private readonly storageService: IStorageService + ) { + super(); + + this.registerListeners(); + } + + private registerListeners(): void { + this._register(this.storageService.onWillSaveState(() => this.saveState())); + this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group))); + this._register(this.editorGroupsService.onDidEditorPartOptionsChange(e => this.onDidEditorPartOptionsChange(e))); + + this.editorGroupsService.whenRestored.then(() => this.loadState()); + } + + private onGroupAdded(group: IEditorGroup): void { + + // Make sure to add any already existing editor + // of the new group into our list in LRU order + const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + for (let i = groupEditorsMru.length - 1; i >= 0; i--) { + this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */); + } + + // Make sure that active editor is put as first if group is active + if (this.editorGroupsService.activeGroup === group && group.activeEditor) { + this.addMostRecentEditor(group, group.activeEditor, true /* is active */); + } + + // Group Listeners + this.registerGroupListeners(group); + } + + private registerGroupListeners(group: IEditorGroup): void { + const groupDisposables = new DisposableStore(); + groupDisposables.add(group.onDidGroupChange(e => { + switch (e.kind) { + + // Group gets active: put active editor as most recent + case GroupChangeKind.GROUP_ACTIVE: { + if (this.editorGroupsService.activeGroup === group && group.activeEditor) { + this.addMostRecentEditor(group, group.activeEditor, true /* is active */); + } + + break; + } + + // Editor gets active: put active editor as most recent + // if group is active, otherwise second most recent + case GroupChangeKind.EDITOR_ACTIVE: { + if (e.editor) { + this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group); + } + + break; + } + + // Editor opens: put it as second most recent + // + // Also check for maximum allowed number of editors and + // start to close oldest ones if needed. + case GroupChangeKind.EDITOR_OPEN: { + if (e.editor) { + this.addMostRecentEditor(group, e.editor, false /* is not active */); + this.ensureOpenedEditorsLimit({ groupId: group.id, editor: e.editor }, group.id); + } + + break; + } + + // Editor closes: remove from recently opened + case GroupChangeKind.EDITOR_CLOSE: { + if (e.editor) { + this.removeMostRecentEditor(group, e.editor); + } + + break; + } + } + })); + + // Make sure to cleanup on dispose + Event.once(group.onWillDispose)(() => dispose(groupDisposables)); + } + + private onDidEditorPartOptionsChange(event: IEditorPartOptionsChangeEvent): void { + if (!equals(event.newPartOptions.limit, event.oldPartOptions.limit)) { + const activeGroup = this.editorGroupsService.activeGroup; + let exclude: IEditorIdentifier | undefined = undefined; + if (activeGroup.activeEditor) { + exclude = { editor: activeGroup.activeEditor, groupId: activeGroup.id }; + } + + this.ensureOpenedEditorsLimit(exclude); + } + } + + private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void { + const key = this.ensureKey(group, editor); + const mostRecentEditor = this.mostRecentEditorsMap.first; + + // Active or first entry: add to end of map + if (isActive || !mostRecentEditor) { + this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined); + } + + // Otherwise: insert before most recent + else { + // we have most recent editors. as such we + // put this newly opened editor right before + // the current most recent one because it cannot + // be the most recently active one unless + // it becomes active. but it is still more + // active then any other editor in the list. + this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */); + this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */); + } + + // Event + this._onDidChange.fire(); + } + + private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void { + const key = this.findKey(group, editor); + if (key) { + + // Remove from most recent editors + this.mostRecentEditorsMap.delete(key); + + // Remove from key map + const map = this.keyMap.get(group.id); + if (map && map.delete(key.editor) && map.size === 0) { + this.keyMap.delete(group.id); + } + + // Event + this._onDidChange.fire(); + } + } + + private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined { + const groupMap = this.keyMap.get(group.id); + if (!groupMap) { + return undefined; + } + + return groupMap.get(editor); + } + + private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier { + let groupMap = this.keyMap.get(group.id); + if (!groupMap) { + groupMap = new Map(); + + this.keyMap.set(group.id, groupMap); + } + + let key = groupMap.get(editor); + if (!key) { + key = { groupId: group.id, editor }; + groupMap.set(editor, key); + } + + return key; + } + + private async ensureOpenedEditorsLimit(exclude: IEditorIdentifier | undefined, groupId?: GroupIdentifier): Promise { + if ( + !this.editorGroupsService.partOptions.limit?.enabled || + typeof this.editorGroupsService.partOptions.limit.value !== 'number' || + this.editorGroupsService.partOptions.limit.value <= 0 + ) { + return; // return early if not enabled or invalid + } + + const limit = this.editorGroupsService.partOptions.limit.value; + + // In editor group + if (this.editorGroupsService.partOptions.limit?.perEditorGroup) { + + // For specific editor groups + if (typeof groupId === 'number') { + const group = this.editorGroupsService.getGroup(groupId); + if (group) { + await this.doEnsureOpenedEditorsLimit(limit, group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).map(editor => ({ editor, groupId })), exclude); + } + } + + // For all editor groups + else { + for (const group of this.editorGroupsService.groups) { + await this.ensureOpenedEditorsLimit(exclude, group.id); + } + } + } + + // Across all editor groups + else { + await this.doEnsureOpenedEditorsLimit(limit, this.mostRecentEditorsMap.values(), exclude); + } + } + + private async doEnsureOpenedEditorsLimit(limit: number, mostRecentEditors: IEditorIdentifier[], exclude?: IEditorIdentifier): Promise { + if (limit >= mostRecentEditors.length) { + return; // only if opened editors exceed setting and is valid and enabled + } + + // Extract least recently used editors that can be closed + const leastRecentlyClosableEditors = mostRecentEditors.reverse().filter(({ editor, groupId }) => { + if (editor.isDirty()) { + return false; // not dirty editors + } + + if (exclude && editor === exclude.editor && groupId === exclude.groupId) { + return false; // never the editor that should be excluded + } + + return true; + }); + + // Close editors until we reached the limit again + let editorsToCloseCount = mostRecentEditors.length - limit; + const mapGroupToEditorsToClose = new Map(); + for (const { groupId, editor } of leastRecentlyClosableEditors) { + let editorsInGroupToClose = mapGroupToEditorsToClose.get(groupId); + if (!editorsInGroupToClose) { + editorsInGroupToClose = []; + mapGroupToEditorsToClose.set(groupId, editorsInGroupToClose); + } + + editorsInGroupToClose.push(editor); + editorsToCloseCount--; + + if (editorsToCloseCount === 0) { + break; // limit reached + } + } + + for (const [groupId, editors] of mapGroupToEditorsToClose) { + const group = this.editorGroupsService.getGroup(groupId); + if (group) { + await group.closeEditors(editors, { preserveFocus: true }); + } + } + } + + private saveState(): void { + if (this.mostRecentEditorsMap.isEmpty()) { + this.storageService.remove(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE); + } else { + this.storageService.store(EditorsObserver.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE); + } + } + + private serialize(): ISerializedEditorsList { + const registry = Registry.as(Extensions.EditorInputFactories); + + const entries = this.mostRecentEditorsMap.values(); + const mapGroupToSerializableEditorsOfGroup = new Map(); + + return { + entries: coalesce(entries.map(({ editor, groupId }) => { + + // Find group for entry + const group = this.editorGroupsService.getGroup(groupId); + if (!group) { + return undefined; + } + + // Find serializable editors of group + let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group); + if (!serializableEditorsOfGroup) { + serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => { + const factory = registry.getEditorInputFactory(editor.getTypeId()); + + return factory?.canSerialize(editor); + }); + mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup); + } + + // Only store the index of the editor of that group + // which can be undefined if the editor is not serializable + const index = serializableEditorsOfGroup.indexOf(editor); + if (index === -1) { + return undefined; + } + + return { groupId, index }; + })) + }; + } + + private loadState(): void { + const serialized = this.storageService.get(EditorsObserver.STORAGE_KEY, StorageScope.WORKSPACE); + + // Previous state: + if (serialized) { + + // Load editors map from persisted state + this.deserialize(JSON.parse(serialized)); + } + + // No previous state: best we can do is add each editor + // from oldest to most recently used editor group + else { + const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); + for (let i = groups.length - 1; i >= 0; i--) { + const group = groups[i]; + const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); + for (let i = groupEditorsMru.length - 1; i >= 0; i--) { + this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */); + } + } + } + + // Ensure we listen on group changes for those that exist on startup + for (const group of this.editorGroupsService.groups) { + this.registerGroupListeners(group); + } + } + + private deserialize(serialized: ISerializedEditorsList): void { + const mapValues: [IEditorIdentifier, IEditorIdentifier][] = []; + + for (const { groupId, index } of serialized.entries) { + + // Find group for entry + const group = this.editorGroupsService.getGroup(groupId); + if (!group) { + continue; + } + + // Find editor for entry + const editor = group.getEditorByIndex(index); + if (!editor) { + continue; + } + + // Make sure key is registered as well + const editorIdentifier = this.ensureKey(group, editor); + mapValues.push([editorIdentifier, editorIdentifier]); + } + + // Fill map with deserialized values + this.mostRecentEditorsMap.fromJSON(mapValues); + } +} diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index d3f45474cda..1996880cf6b 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -23,8 +23,6 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -const TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; - export interface IEditorConfiguration { editor: object; diffEditor: object; @@ -35,6 +33,9 @@ export interface IEditorConfiguration { * be subclassed and not instantiated. */ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { + + static readonly TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState'; + private editorControl: IEditor | undefined; private editorContainer: HTMLElement | undefined; private hasPendingConfigurationChange: boolean | undefined; @@ -53,7 +54,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor { ) { super(id, telemetryService, themeService, storageService); - this.editorMemento = this.getEditorMemento(editorGroupService, TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); + this.editorMemento = this.getEditorMemento(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100); this._register(this.configurationService.onDidChangeConfiguration(e => { const resource = this.getResource(); diff --git a/src/vs/workbench/browser/parts/panel/media/panelpart.css b/src/vs/workbench/browser/parts/panel/media/panelpart.css index 4909d7e9738..abca35a41a7 100644 --- a/src/vs/workbench/browser/parts/panel/media/panelpart.css +++ b/src/vs/workbench/browser/parts/panel/media/panelpart.css @@ -37,6 +37,15 @@ border-left-width: 0; /* no border when editor area is hiden */ } +.monaco-workbench .part.panel.left { + border-right-width: 1px; + border-right-style: solid; +} + +.monaco-workbench.noeditorarea .part.panel.left { + border-right-width: 0; /* no border when editor area is hiden */ +} + .monaco-workbench .part.panel > .title > .title-actions .monaco-action-bar .action-item .action-label { outline-offset: -2px; } @@ -121,3 +130,10 @@ .monaco-workbench .part.panel.right .title-actions .codicon-chevron-down { transform: rotate(-90deg); } + +/* Rotate icons when panel is on left */ +.monaco-workbench .part.panel.left .title-actions .codicon-split-horizontal, +.monaco-workbench .part.panel.left .title-actions .codicon-chevron-up, +.monaco-workbench .part.panel.left .title-actions .codicon-chevron-down { + transform: rotate(90deg); +} diff --git a/src/vs/workbench/browser/parts/panel/panelActions.ts b/src/vs/workbench/browser/parts/panel/panelActions.ts index 7c0ba38cc48..1293a2149e8 100644 --- a/src/vs/workbench/browser/parts/panel/panelActions.ts +++ b/src/vs/workbench/browser/parts/panel/panelActions.ts @@ -12,11 +12,12 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/actions'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService'; -import { ActivityAction } from 'vs/workbench/browser/parts/compositeBarActions'; +import { IWorkbenchLayoutService, Parts, Position, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; +import { ActivityAction, ToggleCompositePinnedAction, ICompositeBar } from 'vs/workbench/browser/parts/compositeBarActions'; import { IActivity } from 'vs/workbench/common/activity'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ActivePanelContext, PanelPositionContext } from 'vs/workbench/common/panel'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; export class ClosePanelAction extends Action { @@ -88,42 +89,6 @@ class FocusPanelAction extends Action { } } -export class TogglePanelPositionAction extends Action { - - static readonly ID = 'workbench.action.togglePanelPosition'; - static readonly LABEL = nls.localize('toggledPanelPosition', "Toggle Panel Position"); - - static readonly MOVE_TO_RIGHT_LABEL = nls.localize('moveToRight', "Move Panel Right"); - static readonly MOVE_TO_BOTTOM_LABEL = nls.localize('moveToBottom', "Move Panel to Bottom"); - - private readonly toDispose = this._register(new DisposableStore()); - - constructor( - id: string, - label: string, - @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService, - @IEditorGroupsService editorGroupsService: IEditorGroupsService - ) { - super(id, label, layoutService.getPanelPosition() === Position.RIGHT ? 'move-panel-to-bottom' : 'move-panel-to-right'); - - const setClassAndLabel = () => { - const positionRight = this.layoutService.getPanelPosition() === Position.RIGHT; - this.class = positionRight ? 'move-panel-to-bottom' : 'move-panel-to-right'; - this.label = positionRight ? TogglePanelPositionAction.MOVE_TO_BOTTOM_LABEL : TogglePanelPositionAction.MOVE_TO_RIGHT_LABEL; - }; - - this.toDispose.add(editorGroupsService.onDidLayout(() => setClassAndLabel())); - - setClassAndLabel(); - } - - run(): Promise { - const position = this.layoutService.getPanelPosition(); - - this.layoutService.setPanelPosition(position === Position.BOTTOM ? Position.RIGHT : Position.BOTTOM); - return Promise.resolve(); - } -} export class ToggleMaximizedPanelAction extends Action { @@ -160,6 +125,54 @@ export class ToggleMaximizedPanelAction extends Action { } } +const PositionPanelActionId = { + LEFT: 'workbench.action.positionPanelLeft', + RIGHT: 'workbench.action.positionPanelRight', + BOTTOM: 'workbench.action.positionPanelBottom', +}; + +interface PanelActionConfig { + id: string; + when: ContextKeyExpr; + alias: string; + label: string; + value: T; +} + +function createPositionPanelActionConfig(id: string, alias: string, label: string, position: Position): PanelActionConfig { + return { + id, + alias, + label, + value: position, + when: PanelPositionContext.notEqualsTo(positionToString(position)) + }; +} + +export const PositionPanelActionConfigs: PanelActionConfig[] = [ + createPositionPanelActionConfig(PositionPanelActionId.LEFT, 'View: Panel Position Left', nls.localize('positionPanelLeft', 'Move Panel Left'), Position.LEFT), + createPositionPanelActionConfig(PositionPanelActionId.RIGHT, 'View: Panel Position Right', nls.localize('positionPanelRight', 'Move Panel Right'), Position.RIGHT), + createPositionPanelActionConfig(PositionPanelActionId.BOTTOM, 'View: Panel Position Bottom', nls.localize('positionPanelBottom', 'Move Panel To Bottom'), Position.BOTTOM), +]; + +const positionByActionId = new Map(PositionPanelActionConfigs.map(config => [config.id, config.value])); + +export class SetPanelPositionAction extends Action { + constructor( + id: string, + label: string, + @IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService + ) { + super(id, label); + } + + run(): Promise { + const position = positionByActionId.get(this.id); + this.layoutService.setPanelPosition(position === undefined ? Position.BOTTOM : position); + return Promise.resolve(); + } +} + export class PanelActivityAction extends ActivityAction { constructor( @@ -174,8 +187,34 @@ export class PanelActivityAction extends ActivityAction { this.activate(); return Promise.resolve(); } + + setActivity(activity: IActivity): void { + this.activity = activity; + } } +export class PlaceHolderPanelActivityAction extends PanelActivityAction { + + constructor( + id: string, + @IPanelService panelService: IPanelService + ) { + super({ id, name: id }, panelService); + } +} + +export class PlaceHolderToggleCompositePinnedAction extends ToggleCompositePinnedAction { + + constructor(id: string, compositeBar: ICompositeBar) { + super({ id, name: id, cssClass: undefined }, compositeBar); + } + + setActivity(activity: IActivity): void { + this.label = activity.name; + } +} + + export class SwitchPanelViewAction extends Action { constructor( @@ -247,7 +286,6 @@ actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(TogglePanelAc actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(FocusPanelAction, FocusPanelAction.ID, FocusPanelAction.LABEL), 'View: Focus into Panel', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, ToggleMaximizedPanelAction.LABEL), 'View: Toggle Maximized Panel', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ClosePanelAction, ClosePanelAction.ID, ClosePanelAction.LABEL), 'View: Close Panel', nls.localize('view', "View")); -actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL), 'View: Toggle Panel Position', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ToggleMaximizedPanelAction, ToggleMaximizedPanelAction.ID, undefined), 'View: Toggle Panel Position', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(PreviousPanelViewAction, PreviousPanelViewAction.ID, PreviousPanelViewAction.LABEL), 'View: Previous Panel View', nls.localize('view', "View")); actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(NextPanelViewAction, NextPanelViewAction.ID, NextPanelViewAction.LABEL), 'View: Next Panel View', nls.localize('view', "View")); @@ -262,22 +300,21 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { order: 5 }); -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_workbench_layout_move', - command: { - id: TogglePanelPositionAction.ID, - title: TogglePanelPositionAction.MOVE_TO_RIGHT_LABEL - }, - when: PanelPositionContext.isEqualTo('bottom'), - order: 5 -}); +function registerPositionPanelActionById(config: PanelActionConfig) { + const { id, label, alias, when } = config; + // register the workbench action + actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(SetPanelPositionAction, id, label), alias, nls.localize('view', "View"), when); + // register as a menu item + MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { + group: '3_workbench_layout_move', + command: { + id, + title: label + }, + when, + order: 5 + }); +} -MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, { - group: '3_workbench_layout_move', - command: { - id: TogglePanelPositionAction.ID, - title: TogglePanelPositionAction.MOVE_TO_BOTTOM_LABEL - }, - when: PanelPositionContext.isEqualTo('right'), - order: 5 -}); +// register each position panel action +PositionPanelActionConfigs.forEach(registerPositionPanelActionById); diff --git a/src/vs/workbench/browser/parts/panel/panelPart.ts b/src/vs/workbench/browser/parts/panel/panelPart.ts index be69e4c61e5..354466a8da0 100644 --- a/src/vs/workbench/browser/parts/panel/panelPart.ts +++ b/src/vs/workbench/browser/parts/panel/panelPart.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!./media/panelpart'; -import { IAction } from 'vs/base/common/actions'; +import { IAction, Action } from 'vs/base/common/actions'; import { Event } from 'vs/base/common/event'; import { Registry } from 'vs/platform/registry/common/platform'; import { ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar'; @@ -18,7 +18,7 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ClosePanelAction, TogglePanelPositionAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction } from 'vs/workbench/browser/parts/panel/panelActions'; +import { ClosePanelAction, PanelActivityAction, ToggleMaximizedPanelAction, TogglePanelAction, PlaceHolderPanelActivityAction, PlaceHolderToggleCompositePinnedAction, PositionPanelActionConfigs, SetPanelPositionAction } from 'vs/workbench/browser/parts/panel/panelActions'; import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { PANEL_BACKGROUND, PANEL_BORDER, PANEL_ACTIVE_TITLE_FOREGROUND, PANEL_INACTIVE_TITLE_FOREGROUND, PANEL_ACTIVE_TITLE_BORDER, PANEL_DRAG_AND_DROP_BACKGROUND, PANEL_INPUT_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, focusBorder, contrastBorder, editorBackground, badgeBackground, badgeForeground } from 'vs/platform/theme/common/colorRegistry'; @@ -28,17 +28,19 @@ import { IBadge } from 'vs/workbench/services/activity/common/activity'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { Dimension, trackFocus } from 'vs/base/browser/dom'; import { localize } from 'vs/nls'; -import { IDisposable } from 'vs/base/common/lifecycle'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; +import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types'; -import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, IViewDescriptorCollection } from 'vs/workbench/common/views'; interface ICachedPanel { id: string; pinned: boolean; order?: number; visible: boolean; + views?: { when?: string }[]; } export class PanelPart extends CompositePart implements IPanelService { @@ -52,7 +54,7 @@ export class PanelPart extends CompositePart implements IPanelService { //#region IView - readonly minimumWidth: number = 300; + readonly minimumWidth: number = 420; readonly maximumWidth: number = Number.POSITIVE_INFINITY; readonly minimumHeight: number = 77; readonly maximumHeight: number = Number.POSITIVE_INFINITY; @@ -80,9 +82,13 @@ export class PanelPart extends CompositePart implements IPanelService { private compositeBar: CompositeBar; private compositeActions: Map = new Map(); + private readonly panelDisposables: Map = new Map(); + private blockOpeningPanel = false; private _contentDimension: Dimension | undefined; + private panelRegistry: PanelRegistry; + constructor( @INotificationService notificationService: INotificationService, @IStorageService storageService: IStorageService, @@ -92,8 +98,9 @@ export class PanelPart extends CompositePart implements IPanelService { @IKeybindingService keybindingService: IKeybindingService, @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService, - @IContextKeyService contextKeyService: IContextKeyService, - @ILifecycleService private readonly lifecycleService: ILifecycleService + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IExtensionService private readonly extensionService: IExtensionService, + @IViewsService private readonly viewsService: IViewsService, ) { super( notificationService, @@ -114,6 +121,8 @@ export class PanelPart extends CompositePart implements IPanelService { { hasTitle: true } ); + this.panelRegistry = Registry.as(PanelExtensions.Panels); + this.compositeBar = this._register(this.instantiationService.createInstance(CompositeBar, this.getCachedPanels(), { icon: false, orientation: ActionsOrientation.HORIZONTAL, @@ -122,10 +131,13 @@ export class PanelPart extends CompositePart implements IPanelService { getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction, getOnCompositeClickAction: (compositeId: string) => this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), getContextMenuActions: () => [ - this.instantiationService.createInstance(TogglePanelPositionAction, TogglePanelPositionAction.ID, TogglePanelPositionAction.LABEL), + ...PositionPanelActionConfigs + // show the contextual menu item if it is not in that position + .filter(({ when }) => contextKeyService.contextMatchesRules(when)) + .map(({ id, label }) => this.instantiationService.createInstance(SetPanelPositionAction, id, label)), this.instantiationService.createInstance(TogglePanelAction, TogglePanelAction.ID, localize('hidePanel', "Hide Panel")) - ], - getDefaultCompositeId: () => Registry.as(PanelExtensions.Panels).getDefaultPanelId(), + ] as Action[], + getDefaultCompositeId: () => this.panelRegistry.getDefaultPanelId(), hidePart: () => this.layoutService.setPanelHidden(true), compositeSize: 0, overflowActionSize: 44, @@ -141,47 +153,156 @@ export class PanelPart extends CompositePart implements IPanelService { }) })); - for (const panel of this.getPanels()) { - this.compositeBar.addComposite(panel); - } - this.activePanelContextKey = ActivePanelContext.bindTo(contextKeyService); this.panelFocusContextKey = PanelFocusContext.bindTo(contextKeyService); this.registerListeners(); + this.onDidRegisterPanels([...this.getPanels()]); + } + + private onDidRegisterPanels(panels: PanelDescriptor[]): void { + for (const panel of panels) { + const cachedPanel = this.getCachedPanels().filter(({ id }) => id === panel.id)[0]; + const activePanel = this.getActivePanel(); + const isActive = activePanel?.getId() === panel.id; + + if (isActive || !this.shouldBeHidden(panel.id, cachedPanel)) { + this.compositeBar.addComposite(panel); + + // Pin it by default if it is new + if (!cachedPanel) { + this.compositeBar.pin(panel.id); + } + + if (isActive) { + this.compositeBar.activateComposite(panel.id); + } + } + } + + for (const panel of panels) { + this.enableCompositeActions(panel); + const viewContainer = this.getViewContainer(panel.id); + if (viewContainer?.hideIfEmpty) { + const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); + if (viewDescriptors) { + this.onDidChangeActiveViews(panel, viewDescriptors); + this.panelDisposables.set(panel.id, viewDescriptors.onDidChangeActiveViews(() => this.onDidChangeActiveViews(panel, viewDescriptors))); + } + } + } + } + + private onDidDeregisterPanel(panelId: string): void { + const disposable = this.panelDisposables.get(panelId); + if (disposable) { + disposable.dispose(); + } + + this.panelDisposables.delete(panelId); + this.hideComposite(panelId); + } + + private enableCompositeActions(panel: PanelDescriptor): void { + const { activityAction, pinnedAction } = this.getCompositeActions(panel.id); + if (activityAction instanceof PlaceHolderPanelActivityAction) { + activityAction.setActivity(panel); + } + + if (pinnedAction instanceof PlaceHolderToggleCompositePinnedAction) { + pinnedAction.setActivity(panel); + } + } + + private onDidChangeActiveViews(panel: PanelDescriptor, viewDescriptors: IViewDescriptorCollection): void { + if (viewDescriptors.activeViewDescriptors.length) { + this.compositeBar.addComposite(panel); + } else { + this.hideComposite(panel.id); + } + } + + private shouldBeHidden(panelId: string, cachedPanel?: ICachedPanel): boolean { + const viewContainer = this.getViewContainer(panelId); + if (!viewContainer || !viewContainer.hideIfEmpty) { + return false; + } + + return cachedPanel?.views && cachedPanel.views.length + ? cachedPanel.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when))) + : false; } private registerListeners(): void { + // Panel registration + this._register(this.registry.onDidRegister(panel => this.onDidRegisterPanels([panel]))); + this._register(this.registry.onDidDeregister(panel => this.onDidDeregisterPanel(panel.id))); - // Panel open/close + // Activate on panel open this._register(this.onDidPanelOpen(({ panel }) => this.onPanelOpen(panel))); + + // Deactivate on panel close this._register(this.onDidPanelClose(this.onPanelClose, this)); - // Panel register/deregister - this._register(this.registry.onDidRegister(panelDescriptor => this.compositeBar.addComposite(panelDescriptor))); - this._register(this.registry.onDidDeregister(panelDescriptor => { - this.compositeBar.hideComposite(panelDescriptor.id); - this.removeComposite(panelDescriptor.id); + // Extension registration + let disposables = this._register(new DisposableStore()); + this._register(this.extensionService.onDidRegisterExtensions(() => { + disposables.clear(); + this.onDidRegisterExtensions(); + this.compositeBar.onDidChange(() => this.saveCachedPanels(), this, disposables); + this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e), this, disposables); })); + } - // Activate panel action on opening of a panel - this._register(this.onDidPanelOpen(({ panel }) => { - this.compositeBar.activateComposite(panel.getId()); - this.layoutCompositeBar(); // Need to relayout composite bar since different panels have different action bar width - })); + private onDidRegisterExtensions(): void { + this.removeNotExistingComposites(); - // Deactivate panel action on close - this._register(this.onDidPanelClose(panel => this.compositeBar.deactivateComposite(panel.getId()))); + this.saveCachedPanels(); + } - // State - this.lifecycleService.when(LifecyclePhase.Eventually).then(() => { - this._register(this.compositeBar.onDidChange(() => this.saveCachedPanels())); - this._register(this.storageService.onDidChangeStorage(e => this.onDidStorageChange(e))); - }); + private removeNotExistingComposites(): void { + const panels = this.getPanels(); + for (const { id } of this.getCachedPanels()) { // should this value match viewlet (load on ctor) + if (panels.every(panel => panel.id !== id)) { + this.hideComposite(id); + } + } + } + + private hideComposite(compositeId: string): void { + this.compositeBar.hideComposite(compositeId); + + const compositeActions = this.compositeActions.get(compositeId); + if (compositeActions) { + compositeActions.activityAction.dispose(); + compositeActions.pinnedAction.dispose(); + this.compositeActions.delete(compositeId); + } } private onPanelOpen(panel: IPanel): void { this.activePanelContextKey.set(panel.getId()); + + const foundPanel = this.panelRegistry.getPanel(panel.getId()); + if (foundPanel) { + this.compositeBar.addComposite(foundPanel); + } + + // Activate composite when opened + this.compositeBar.activateComposite(panel.getId()); + + const panelDescriptor = this.panelRegistry.getPanel(panel.getId()); + if (panelDescriptor) { + const viewContainer = this.getViewContainer(panelDescriptor.id); + if (viewContainer?.hideIfEmpty) { + const viewDescriptors = this.viewsService.getViewDescriptors(viewContainer); + if (viewDescriptors?.activeViewDescriptors.length === 0) { + this.hideComposite(panelDescriptor.id); // Update the composite bar by hiding + } + } + } + + this.layoutCompositeBar(); // Need to relayout composite bar since different panels have different action bar width } private onPanelClose(panel: IPanel): void { @@ -190,6 +311,8 @@ export class PanelPart extends CompositePart implements IPanelService { if (this.activePanelContextKey.get() === id) { this.activePanelContextKey.reset(); } + + this.compositeBar.deactivateComposite(panel.getId()); } create(parent: HTMLElement): void { @@ -207,7 +330,9 @@ export class PanelPart extends CompositePart implements IPanelService { const container = assertIsDefined(this.getContainer()); container.style.backgroundColor = this.getColor(PANEL_BACKGROUND) || ''; - container.style.borderLeftColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder) || ''; + const borderColor = this.getColor(PANEL_BORDER) || this.getColor(contrastBorder) || ''; + container.style.borderLeftColor = borderColor; + container.style.borderRightColor = borderColor; const title = this.getTitleArea(); if (title) { @@ -238,11 +363,11 @@ export class PanelPart extends CompositePart implements IPanelService { } getPanel(panelId: string): IPanelIdentifier | undefined { - return Registry.as(PanelExtensions.Panels).getPanel(panelId); + return this.panelRegistry.getPanel(panelId); } getPanels(): readonly PanelDescriptor[] { - return Registry.as(PanelExtensions.Panels).getPanels() + return this.panelRegistry.getPanels() .sort((v1, v2) => typeof v1.order === 'number' && typeof v2.order === 'number' ? v1.order - v2.order : NaN); } @@ -326,10 +451,18 @@ export class PanelPart extends CompositePart implements IPanelService { private getCompositeActions(compositeId: string): { activityAction: PanelActivityAction, pinnedAction: ToggleCompositePinnedAction; } { let compositeActions = this.compositeActions.get(compositeId); if (!compositeActions) { - compositeActions = { - activityAction: this.instantiationService.createInstance(PanelActivityAction, assertIsDefined(this.getPanel(compositeId))), - pinnedAction: new ToggleCompositePinnedAction(this.getPanel(compositeId), this.compositeBar) - }; + const panel = this.getPanel(compositeId); + if (panel) { + compositeActions = { + activityAction: new PanelActivityAction(assertIsDefined(this.getPanel(compositeId)), this), + pinnedAction: new ToggleCompositePinnedAction(this.getPanel(compositeId), this.compositeBar) + }; + } else { + compositeActions = { + activityAction: new PlaceHolderPanelActivityAction(compositeId, this), + pinnedAction: new PlaceHolderToggleCompositePinnedAction(compositeId, this.compositeBar) + }; + } this.compositeActions.set(compositeId, compositeActions); } @@ -443,6 +576,11 @@ export class PanelPart extends CompositePart implements IPanelService { this.storageService.store(PanelPart.PINNED_PANELS, value, StorageScope.GLOBAL); } + private getViewContainer(panelId: string): ViewContainer | undefined { + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + return viewContainerRegistry.get(panelId); + } + toJSON(): object { return { type: Parts.PANEL_PART diff --git a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css index 3102b114c52..a0c9f9e0eb8 100644 --- a/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css +++ b/src/vs/workbench/browser/parts/statusbar/media/statusbarpart.css @@ -44,6 +44,7 @@ line-height: 22px; height: 100%; vertical-align: top; + max-width: 40vw; } .monaco-workbench .part.statusbar > .items-container > .statusbar-item.has-beak { @@ -94,7 +95,9 @@ height: 100%; padding: 0 5px 0 5px; white-space: pre; /* gives some degree of styling */ - align-items: center + align-items: center; + text-overflow: ellipsis; + overflow: hidden; } .monaco-workbench .part.statusbar > .items-container > .statusbar-item > a:hover { diff --git a/src/vs/workbench/browser/parts/views/customView.ts b/src/vs/workbench/browser/parts/views/customView.ts index d8379e3ae26..157494928b5 100644 --- a/src/vs/workbench/browser/parts/views/customView.ts +++ b/src/vs/workbench/browser/parts/views/customView.ts @@ -495,14 +495,12 @@ export class CustomTreeView extends Disposable implements ITreeView { private showMessage(message: string): void { DOM.removeClass(this.messageElement, 'hide'); - if (this._messageValue !== message) { - this.resetMessageElement(); - this._messageValue = message; - if (!isFalsyOrWhitespace(this._message)) { - this.messageElement.textContent = this._messageValue; - } - this.layout(this._height, this._width); + this.resetMessageElement(); + this._messageValue = message; + if (!isFalsyOrWhitespace(this._message)) { + this.messageElement.textContent = this._messageValue; } + this.layout(this._height, this._width); } private hideMessage(): void { diff --git a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts index 0dd8c50cffb..3400b61747e 100644 --- a/src/vs/workbench/browser/parts/views/viewPaneContainer.ts +++ b/src/vs/workbench/browser/parts/views/viewPaneContainer.ts @@ -25,7 +25,7 @@ import { PaneView, IPaneViewOptions, IPaneOptions, Pane, DefaultPaneDndControlle import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor } from 'vs/workbench/common/views'; +import { Extensions as ViewContainerExtensions, IView, FocusedViewContext, IViewContainersRegistry, IViewDescriptor, ViewContainer } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { assertIsDefined } from 'vs/base/common/types'; @@ -36,7 +36,6 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IViewPaneContainer } from 'vs/workbench/common/viewPaneContainer'; import { Component } from 'vs/workbench/common/component'; -import { Extensions, ViewletRegistry } from 'vs/workbench/browser/viewlet'; export interface IPaneColors extends IColorMapping { dropBackground?: ColorIdentifier; @@ -233,7 +232,8 @@ export abstract class ViewPane extends Pane implements IView { } export interface IViewPaneContainerOptions extends IPaneViewOptions { - showHeaderInTitleWhenSingleView: boolean; + mergeViewWithContainerWhenSingleView: boolean; + donotShowContainerTitleWhenMergedWithContainer?: boolean; } interface IViewPaneItem { @@ -243,6 +243,7 @@ interface IViewPaneItem { export class ViewPaneContainer extends Component implements IViewPaneContainer { + readonly viewContainer: ViewContainer; private lastFocusedPane: ViewPane | undefined; private paneItems: IViewPaneItem[] = []; private paneview?: PaneView; @@ -307,6 +308,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { this.options.dnd = new DefaultPaneDndController(); } + this.viewContainer = container; this.visibleViewsStorageId = `${id}.numberOfVisibleViews`; this.visibleViewsCountFromCache = this.storageService.getNumber(this.visibleViewsStorageId, StorageScope.WORKSPACE, undefined); this._register(toDisposable(() => this.viewDisposables = dispose(this.viewDisposables))); @@ -344,14 +346,15 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } getTitle(): string { - let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; - - if (this.isSingleView()) { + if (this.isViewMergedWithContainer()) { const paneItemTitle = this.paneItems[0].pane.title; - title = paneItemTitle ? `${title}: ${paneItemTitle}` : title; + if (this.options.donotShowContainerTitleWhenMergedWithContainer || this.viewContainer.name === paneItemTitle) { + return this.paneItems[0].pane.title; + } + return paneItemTitle ? `${this.viewContainer.name}: ${paneItemTitle}` : this.viewContainer.name; } - return title; + return this.viewContainer.name; } private showContextMenu(event: StandardMouseEvent): void { @@ -400,7 +403,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } getActions(): IAction[] { - if (this.isSingleView()) { + if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getActions(); } @@ -408,7 +411,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } getSecondaryActions(): IAction[] { - if (this.isSingleView()) { + if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getSecondaryActions(); } @@ -416,7 +419,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } getActionViewItem(action: IAction): IActionViewItem | undefined { - if (this.isSingleView()) { + if (this.isViewMergedWithContainer()) { return this.paneItems[0].pane.getActionViewItem(action); } @@ -457,14 +460,14 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } addPanes(panes: { pane: ViewPane, size: number, index?: number; }[]): void { - const wasSingleView = this.isSingleView(); + const wasMerged = this.isViewMergedWithContainer(); for (const { pane: pane, size, index } of panes) { this.addPane(pane, size, index); } this.updateViewHeaders(); - if (this.isSingleView() !== wasSingleView) { + if (this.isViewMergedWithContainer() !== wasMerged) { this.updateTitleArea(); } } @@ -641,7 +644,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { private addPane(pane: ViewPane, size: number, index = this.paneItems.length - 1): void { const onDidFocus = pane.onDidFocus(() => this.lastFocusedPane = pane); const onDidChangeTitleArea = pane.onDidChangeTitleArea(() => { - if (this.isSingleView()) { + if (this.isViewMergedWithContainer()) { this.updateTitleArea(); } }); @@ -666,12 +669,12 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } removePanes(panes: ViewPane[]): void { - const wasSingleView = this.isSingleView(); + const wasMerged = this.isViewMergedWithContainer(); panes.forEach(pane => this.removePane(pane)); this.updateViewHeaders(); - if (wasSingleView !== this.isSingleView()) { + if (wasMerged !== this.isViewMergedWithContainer()) { this.updateTitleArea(); } } @@ -724,8 +727,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { return assertIsDefined(this.paneview).getPaneSize(pane); } - protected updateViewHeaders(): void { - if (this.isSingleView()) { + private updateViewHeaders(): void { + if (this.isViewMergedWithContainer()) { this.paneItems[0].pane.setExpanded(true); this.paneItems[0].pane.headerVisible = false; } else { @@ -733,8 +736,8 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer { } } - protected isSingleView(): boolean { - if (!(this.options.showHeaderInTitleWhenSingleView && this.paneItems.length === 1)) { + private isViewMergedWithContainer(): boolean { + if (!(this.options.mergeViewWithContainerWhenSingleView && this.paneItems.length === 1)) { return false; } if (!this.areExtensionsReady) { diff --git a/src/vs/workbench/browser/parts/views/views.ts b/src/vs/workbench/browser/parts/views/views.ts index 2b35bf2b70c..2c5cf72cdfb 100644 --- a/src/vs/workbench/browser/parts/views/views.ts +++ b/src/vs/workbench/browser/parts/views/views.ts @@ -655,8 +655,8 @@ export class ViewsService extends Disposable implements IViewsService { this.viewDisposable.forEach(disposable => disposable.dispose()); this.viewDisposable.clear(); })); - this._register(viewContainersRegistry.onDidRegister(viewContainer => this.onDidRegisterViewContainer(viewContainer))); - this._register(viewContainersRegistry.onDidDeregister(viewContainer => this.onDidDeregisterViewContainer(viewContainer))); + this._register(viewContainersRegistry.onDidRegister(({ viewContainer }) => this.onDidRegisterViewContainer(viewContainer))); + this._register(viewContainersRegistry.onDidDeregister(({ viewContainer }) => this.onDidDeregisterViewContainer(viewContainer))); this._register(toDisposable(() => { this.viewDescriptorCollections.forEach(({ disposable }) => disposable.dispose()); this.viewDescriptorCollections.clear(); diff --git a/src/vs/workbench/browser/parts/views/viewsViewlet.ts b/src/vs/workbench/browser/parts/views/viewsViewlet.ts index fa22f5d0b97..078939500ef 100644 --- a/src/vs/workbench/browser/parts/views/viewsViewlet.ts +++ b/src/vs/workbench/browser/parts/views/viewsViewlet.ts @@ -46,7 +46,7 @@ export abstract class FilterViewPaneContainer extends ViewPaneContainer { @IWorkspaceContextService contextService: IWorkspaceContextService ) { - super(viewletId, `${viewletId}.state`, { showHeaderInTitleWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + super(viewletId, `${viewletId}.state`, { mergeViewWithContainerWhenSingleView: false }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); this._register(onDidChangeFilterValue(newFilterValue => { this.filterValue = newFilterValue; this.onFilterChanged(newFilterValue); diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 854db17c376..c14125bea17 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -8,6 +8,21 @@ import * as nls from 'vs/nls'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { isMacintosh, isWindows, isLinux, isWeb, isNative } from 'vs/base/common/platform'; import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; +import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor, PaneCompositePanel } from 'vs/workbench/browser/panel'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IStorageService } from 'vs/platform/storage/common/storage'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation } from 'vs/workbench/common/views'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; +import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Viewlet, ViewletDescriptor, ViewletRegistry, Extensions as ViewletExtensions } from 'vs/workbench/browser/viewlet'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { isString } from 'vs/base/common/types'; +import { URI } from 'vs/base/common/uri'; // Configuration (function registerConfiguration(): void { @@ -131,6 +146,22 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'default': true, 'description': nls.localize('centeredLayoutAutoResize', "Controls if the centered layout should automatically resize to maximum width when more than one group is open. Once only one group is open it will resize back to the original centered width.") }, + 'workbench.editor.limit.enabled': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('limitEditorsEnablement', "Controls if the number of opened editors should be limited or not. When enabled, less recently used editors that are not dirty will close to make space for newly opening editors.") + }, + 'workbench.editor.limit.value': { + 'type': 'number', + 'default': 10, + 'exclusiveMinimum': 0, + 'description': nls.localize('limitEditorsMaximum', "Controls the maximum number of opened editors. Use the `workbench.editor.limit.perEditorGroup` setting to control this limit per editor group or across all groups.") + }, + 'workbench.editor.limit.perEditorGroup': { + 'type': 'boolean', + 'default': false, + 'description': nls.localize('perEditorGroup', "Controls if the limit of maximum opened editors should apply per editor group or across all editor groups.") + }, 'workbench.commandPalette.history': { 'type': 'number', 'description': nls.localize('commandHistory', "Controls the number of recently used commands to keep in history for the command palette. Set to 0 to disable command history."), @@ -174,7 +205,7 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio }, 'workbench.panel.defaultLocation': { 'type': 'string', - 'enum': ['bottom', 'right'], + 'enum': ['left', 'bottom', 'right'], 'default': 'bottom', 'description': nls.localize('panelDefaultLocation', "Controls the default location of the panel (terminal, debug console, output, problems). It can either show at the bottom or on the right of the workbench.") }, @@ -207,24 +238,6 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio ], 'included': isMacintosh }, - 'workbench.settings.enableNaturalLanguageSearch': { - 'type': 'boolean', - 'description': nls.localize('enableNaturalLanguageSettingsSearch', "Controls whether to enable the natural language search mode for settings. The natural language search is provided by a Microsoft online service."), - 'default': true, - 'scope': ConfigurationScope.WINDOW, - 'tags': ['usesOnlineServices'] - }, - 'workbench.settings.settingsSearchTocBehavior': { - 'type': 'string', - 'enum': ['hide', 'filter'], - 'enumDescriptions': [ - nls.localize('settingsSearchTocBehavior.hide', "Hide the Table of Contents while searching."), - nls.localize('settingsSearchTocBehavior.filter', "Filter the Table of Contents to just categories that have matching settings. Clicking a category will filter the results to that category."), - ], - 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the settings editor Table of Contents while searching."), - 'default': 'filter', - 'scope': ConfigurationScope.WINDOW - }, 'workbench.settings.editor': { 'type': 'string', 'enum': ['ui', 'json'], @@ -235,12 +248,6 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio 'description': nls.localize('settings.editor.desc', "Determines which settings editor to use by default."), 'default': 'ui', 'scope': ConfigurationScope.WINDOW - }, - 'workbench.enableExperiments': { - 'type': 'boolean', - 'description': nls.localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."), - 'default': true, - 'tags': ['usesOnlineServices'] } } }); @@ -381,3 +388,74 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio } }); })(); + +// Viewlets & Panels +(function registerViewletsAndPanels(): void { + const registerPanel = (viewContainer: ViewContainer): void => { + class PaneContainerPanel extends PaneCompositePanel { + constructor( + @ITelemetryService telemetryService: ITelemetryService, + @IStorageService storageService: IStorageService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService, + @IWorkspaceContextService contextService: IWorkspaceContextService + ) { + super(viewContainer.id, (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.arguments || [])), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + } + } + Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( + PaneContainerPanel, + viewContainer.id, + viewContainer.name, + isString(viewContainer.icon) ? viewContainer.icon : undefined, + viewContainer.order, + viewContainer.focusCommand?.id, + )); + }; + + const registerViewlet = (viewContainer: ViewContainer): void => { + class PaneContainerViewlet extends Viewlet { + constructor( + @IConfigurationService configurationService: IConfigurationService, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService contextService: IWorkspaceContextService, + @IStorageService storageService: IStorageService, + @IEditorService editorService: IEditorService, + @IInstantiationService instantiationService: IInstantiationService, + @IThemeService themeService: IThemeService, + @IContextMenuService contextMenuService: IContextMenuService, + @IExtensionService extensionService: IExtensionService + ) { + super(viewContainer.id, (instantiationService as any).createInstance(viewContainer.ctorDescriptor!.ctor, ...(viewContainer.ctorDescriptor!.arguments || [])), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); + } + } + const viewletDescriptor = ViewletDescriptor.create( + PaneContainerViewlet, + viewContainer.id, + viewContainer.name, + isString(viewContainer.icon) ? viewContainer.icon : undefined, + viewContainer.order, + viewContainer.icon instanceof URI ? viewContainer.icon : undefined + ); + + Registry.as(ViewletExtensions.Viewlets).registerViewlet(viewletDescriptor); + }; + + const viewContainerRegistry = Registry.as(ViewContainerExtensions.ViewContainersRegistry); + viewContainerRegistry.getViewContainers(ViewContainerLocation.Panel).forEach(viewContainer => registerPanel(viewContainer)); + viewContainerRegistry.onDidRegister(({ viewContainer, viewContainerLocation }) => { + switch (viewContainerLocation) { + case ViewContainerLocation.Panel: + registerPanel(viewContainer); + return; + case ViewContainerLocation.Sidebar: + if (viewContainer.ctorDescriptor) { + registerViewlet(viewContainer); + } + return; + } + }); +})(); diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 5f1cbf50e79..7430a4265d1 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -18,7 +18,7 @@ import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } fr import { IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; import { IActionBarRegistry, Extensions as ActionBarExtensions } from 'vs/workbench/browser/actions'; import { getSingletonServiceDescriptors } from 'vs/platform/instantiation/common/extensions'; -import { Position, Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; +import { Position, Parts, IWorkbenchLayoutService, positionToString } from 'vs/workbench/services/layout/browser/layoutService'; import { IStorageService, WillSaveStateReason, StorageScope } from 'vs/platform/storage/common/storage'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; @@ -44,6 +44,7 @@ import { WorkbenchContextKeysHandler } from 'vs/workbench/browser/contextkeys'; import { coalesce } from 'vs/base/common/arrays'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { Layout } from 'vs/workbench/browser/layout'; +import { IHostService } from 'vs/workbench/services/host/browser/host'; export class Workbench extends Layout { @@ -140,6 +141,7 @@ export class Workbench extends Layout { const lifecycleService = accessor.get(ILifecycleService); const storageService = accessor.get(IStorageService); const configurationService = accessor.get(IConfigurationService); + const hostService = accessor.get(IHostService); // Layout this.initLayout(accessor); @@ -151,7 +153,7 @@ export class Workbench extends Layout { this._register(instantiationService.createInstance(WorkbenchContextKeysHandler)); // Register Listeners - this.registerListeners(lifecycleService, storageService, configurationService); + this.registerListeners(lifecycleService, storageService, configurationService, hostService); // Render Workbench this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService); @@ -224,7 +226,8 @@ export class Workbench extends Layout { private registerListeners( lifecycleService: ILifecycleService, storageService: IStorageService, - configurationService: IConfigurationService + configurationService: IConfigurationService, + hostService: IHostService ): void { // Configuration changes @@ -248,6 +251,13 @@ export class Workbench extends Layout { this._onShutdown.fire(); this.dispose(); })); + + // In some environments we do not get enough time to persist state on shutdown. + // In other cases, VSCode might crash, so we periodically save state to reduce + // the chance of loosing any state. + // The window loosing focus is a good indication that the user has stopped working + // in that window so we pick that at a time to collect state. + this._register(hostService.onDidChangeFocus(focus => { if (!focus) { storageService.flush(); } })); } private fontAliasing: 'default' | 'antialiased' | 'none' | 'auto' | undefined; @@ -339,7 +349,7 @@ export class Workbench extends Layout { { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] }, { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } }, - { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', this.state.panel.position === Position.BOTTOM ? 'bottom' : 'right'] }, + { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', positionToString(this.state.panel.position)] }, { id: Parts.STATUSBAR_PART, role: 'contentinfo', classes: ['statusbar'] } ].forEach(({ id, role, classes, options }) => { const partContainer = this.createPart(id, role, classes); diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index b15a2202780..7f8dc048690 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -7,7 +7,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { assign } from 'vs/base/common/objects'; import { isUndefinedOrNull, withNullAsUndefined, assertIsDefined } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; +import { IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IEditor as ICodeEditor, IEditorViewState, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon'; import { IEditorModel, IEditorOptions, ITextEditorOptions, IBaseResourceInput, IResourceInput, EditorActivation, EditorOpenContext } from 'vs/platform/editor/common/editor'; import { IInstantiationService, IConstructorSignature0, ServicesAccessor, BrandedService } from 'vs/platform/instantiation/common/instantiation'; @@ -181,7 +181,7 @@ export interface IEditorInputFactoryRegistry { * @param editorInputId the identifier of the editor input * @param factory the editor input factory for serialization/deserialization */ - registerEditorInputFactory(editorInputId: string, ctor: { new(...Services: Services): IEditorInputFactory }): void; + registerEditorInputFactory(editorInputId: string, ctor: { new(...Services: Services): IEditorInputFactory }): IDisposable; /** * Returns the editor input factory for the given editor input. @@ -618,12 +618,12 @@ export const enum EncodingMode { export interface IEncodingSupport { /** - * Gets the encoding of the input if known. + * Gets the encoding of the type if known. */ getEncoding(): string | undefined; /** - * Sets the encoding for the input for saving. + * Sets the encoding for the type for saving. */ setEncoding(encoding: string, mode: EncodingMode): void; } @@ -631,7 +631,7 @@ export interface IEncodingSupport { export interface IModeSupport { /** - * Sets the language mode of the input. + * Sets the language mode of the type. */ setMode(mode: string): void; } @@ -1171,12 +1171,22 @@ interface IEditorPartConfiguration { labelFormat?: 'default' | 'short' | 'medium' | 'long'; restoreViewState?: boolean; splitSizing?: 'split' | 'distribute'; + limit?: { + enabled?: boolean; + value?: number; + perEditorGroup?: boolean; + }; } export interface IEditorPartOptions extends IEditorPartConfiguration { iconTheme?: string; } +export interface IEditorPartOptionsChangeEvent { + oldPartOptions: IEditorPartOptions; + newPartOptions: IEditorPartOptions; +} + export enum SideBySideEditor { MASTER = 1, DETAILS = 2 @@ -1232,6 +1242,7 @@ export interface IEditorMemento { class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { private instantiationService: IInstantiationService | undefined; private fileInputFactory: IFileInputFactory | undefined; + private readonly editorInputFactoryConstructors: Map> = new Map(); private readonly editorInputFactoryInstances: Map = new Map(); @@ -1258,12 +1269,18 @@ class EditorInputFactoryRegistry implements IEditorInputFactoryRegistry { return assertIsDefined(this.fileInputFactory); } - registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): void { + registerEditorInputFactory(editorInputId: string, ctor: IConstructorSignature0): IDisposable { if (!this.instantiationService) { this.editorInputFactoryConstructors.set(editorInputId, ctor); } else { this.createEditorInputFactory(editorInputId, ctor, this.instantiationService); + } + + return toDisposable(() => { + this.editorInputFactoryConstructors.delete(editorInputId); + this.editorInputFactoryInstances.delete(editorInputId); + }); } getEditorInputFactory(editorInputId: string): IEditorInputFactory | undefined { @@ -1310,3 +1327,16 @@ export async function pathsToEditors(paths: IPathData[] | undefined, fileService return coalesce(editors); } + +export const enum EditorsOrder { + + /** + * Editors sorted by most recent activity (most recent active first) + */ + MOST_RECENTLY_ACTIVE, + + /** + * Editors sorted by sequential order + */ + SEQUENTIAL +} diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 6822bd68eda..0473aee2cbc 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event, Emitter } from 'vs/base/common/event'; -import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, CloseDirection, SideBySideEditorInput, IEditorInput } from 'vs/workbench/common/editor'; +import { Extensions, IEditorInputFactoryRegistry, EditorInput, IEditorIdentifier, IEditorCloseEvent, GroupIdentifier, CloseDirection, SideBySideEditorInput, IEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -130,8 +130,8 @@ export class EditorGroup extends Disposable { return this.editors.length; } - getEditors(mru?: boolean): EditorInput[] { - return mru ? this.mru.slice(0) : this.editors.slice(0); + getEditors(order: EditorsOrder): EditorInput[] { + return order === EditorsOrder.MOST_RECENTLY_ACTIVE ? this.mru.slice(0) : this.editors.slice(0); } getEditorByIndex(index: number): EditorInput | undefined { diff --git a/src/vs/workbench/common/views.ts b/src/vs/workbench/common/views.ts index 119e583c046..15ee4827de8 100644 --- a/src/vs/workbench/common/views.ts +++ b/src/vs/workbench/common/views.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Command } from 'vs/editor/common/modes'; -import { UriComponents } from 'vs/base/common/uri'; +import { UriComponents, URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ContextKeyExpr, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; @@ -12,11 +12,13 @@ import { IViewlet } from 'vs/workbench/common/viewlet'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { values, keys } from 'vs/base/common/map'; +import { values, keys, getOrSet } from 'vs/base/common/map'; import { Registry } from 'vs/platform/registry/common/platform'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IAction } from 'vs/base/common/actions'; import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { flatten } from 'vs/base/common/arrays'; +import { IViewPaneContainer } from 'vs/workbench/common/viewPaneContainer'; export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test'; export const FocusedViewContext = new RawContextKey('focusedView', ''); @@ -26,16 +28,43 @@ export namespace Extensions { export const ViewsRegistry = 'workbench.registry.view'; } +export enum ViewContainerLocation { + Sidebar, + Panel +} + +export interface IViewContainerDescriptor { + + readonly id: string; + + readonly name: string; + + readonly ctorDescriptor: { ctor: new (...args: any[]) => IViewPaneContainer, arguments?: any[] }; + + readonly icon?: string | URI; + + readonly order?: number; + + readonly focusCommand?: { id: string, keybindings?: IKeybindings }; + + readonly viewOrderDelegate?: ViewOrderDelegate; + + readonly hideIfEmpty?: boolean; + + readonly extensionId?: ExtensionIdentifier; + +} + export interface IViewContainersRegistry { /** * An event that is triggerred when a view container is registered. */ - readonly onDidRegister: Event; + readonly onDidRegister: Event<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>; /** * An event that is triggerred when a view container is deregistered. */ - readonly onDidDeregister: Event; + readonly onDidDeregister: Event<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>; /** * All registered view containers @@ -43,14 +72,15 @@ export interface IViewContainersRegistry { readonly all: ViewContainer[]; /** - * Registers a view container with given id - * No op if a view container is already registered with the given id. + * Registers a view container to given location. + * No op if a view container is already registered. * - * @param id of the view container. + * @param viewContainerDescriptor descriptor of view container + * @param location location of the view container * * @returns the registered ViewContainer. */ - registerViewContainer(id: string, hideIfEmpty?: boolean, extensionId?: ExtensionIdentifier, viewOrderDelegate?: ViewOrderDelegate): ViewContainer; + registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, location: ViewContainerLocation): ViewContainer; /** * Deregisters the given view container @@ -64,56 +94,67 @@ export interface IViewContainersRegistry { * @returns the view container with given id. */ get(id: string): ViewContainer | undefined; + + /** + * Returns all view containers in the given location + */ + getViewContainers(location: ViewContainerLocation): ViewContainer[]; } interface ViewOrderDelegate { getOrder(group?: string): number | undefined; } -export class ViewContainer { - protected constructor(readonly id: string, readonly hideIfEmpty: boolean, readonly extensionId?: ExtensionIdentifier, readonly orderDelegate?: ViewOrderDelegate) { } -} +export interface ViewContainer extends IViewContainerDescriptor { } class ViewContainersRegistryImpl extends Disposable implements IViewContainersRegistry { - private readonly _onDidRegister = this._register(new Emitter()); - readonly onDidRegister: Event = this._onDidRegister.event; + private readonly _onDidRegister = this._register(new Emitter<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>()); + readonly onDidRegister: Event<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }> = this._onDidRegister.event; - private readonly _onDidDeregister = this._register(new Emitter()); - readonly onDidDeregister: Event = this._onDidDeregister.event; + private readonly _onDidDeregister = this._register(new Emitter<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }>()); + readonly onDidDeregister: Event<{ viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation }> = this._onDidDeregister.event; - private viewContainers: Map = new Map(); + private viewContainers: Map = new Map(); get all(): ViewContainer[] { - return values(this.viewContainers); + return flatten(values(this.viewContainers)); } - registerViewContainer(id: string, hideIfEmpty?: boolean, extensionId?: ExtensionIdentifier, viewOrderDelegate?: ViewOrderDelegate): ViewContainer { - const existing = this.viewContainers.get(id); + registerViewContainer(viewContainerDescriptor: IViewContainerDescriptor, viewContainerLocation: ViewContainerLocation): ViewContainer { + const existing = this.get(viewContainerDescriptor.id); if (existing) { return existing; } - const viewContainer = new class extends ViewContainer { - constructor() { - super(id, !!hideIfEmpty, extensionId, viewOrderDelegate); - } - }; - this.viewContainers.set(id, viewContainer); - this._onDidRegister.fire(viewContainer); + const viewContainer: ViewContainer = { ...viewContainerDescriptor }; + const viewContainers = getOrSet(this.viewContainers, viewContainerLocation, []); + viewContainers.push(viewContainer); + this._onDidRegister.fire({ viewContainer, viewContainerLocation }); return viewContainer; } deregisterViewContainer(viewContainer: ViewContainer): void { - const existing = this.viewContainers.get(viewContainer.id); - if (existing) { - this.viewContainers.delete(viewContainer.id); - this._onDidDeregister.fire(viewContainer); + for (const viewContainerLocation of keys(this.viewContainers)) { + const viewContainers = this.viewContainers.get(viewContainerLocation)!; + const index = viewContainers?.indexOf(viewContainer); + if (index !== -1) { + viewContainers?.splice(index, 1); + if (viewContainers.length === 0) { + this.viewContainers.delete(viewContainerLocation); + } + this._onDidDeregister.fire({ viewContainer, viewContainerLocation }); + return; + } } } get(id: string): ViewContainer | undefined { - return this.viewContainers.get(id); + return this.all.filter(viewContainer => viewContainer.id === id)[0]; + } + + getViewContainers(location: ViewContainerLocation): ViewContainer[] { + return [...(this.viewContainers.get(location) || [])]; } } diff --git a/src/vs/workbench/contrib/backup/common/backupModelTracker.ts b/src/vs/workbench/contrib/backup/common/backupModelTracker.ts index bbc393aff32..584291fb088 100644 --- a/src/vs/workbench/contrib/backup/common/backupModelTracker.ts +++ b/src/vs/workbench/contrib/backup/common/backupModelTracker.ts @@ -37,7 +37,7 @@ export class BackupModelTracker extends Disposable implements IWorkbenchContribu this._register(this.textFileService.models.onModelDisposed(e => this.discardBackup(e))); // Listen for untitled model changes - this._register(this.untitledTextEditorService.onDidCreate(e => this.onUntitledModelChanged(e))); + this._register(this.untitledTextEditorService.onDidCreate(e => this.onUntitledModelCreated(e))); this._register(this.untitledTextEditorService.onDidChangeContent(e => this.onUntitledModelChanged(e))); this._register(this.untitledTextEditorService.onDidDisposeModel(e => this.discardBackup(e))); @@ -65,6 +65,12 @@ export class BackupModelTracker extends Disposable implements IWorkbenchContribu } } + private onUntitledModelCreated(resource: Uri): void { + if (this.untitledTextEditorService.isDirty(resource)) { + this.untitledTextEditorService.loadOrCreate({ resource }).then(model => model.backup()); + } + } + private onUntitledModelChanged(resource: Uri): void { if (this.untitledTextEditorService.isDirty(resource)) { this.untitledTextEditorService.loadOrCreate({ resource }).then(model => model.backup()); diff --git a/src/vs/workbench/contrib/backup/common/backupRestorer.ts b/src/vs/workbench/contrib/backup/common/backupRestorer.ts index cfe126edd4b..a205249e0dc 100644 --- a/src/vs/workbench/contrib/backup/common/backupRestorer.ts +++ b/src/vs/workbench/contrib/backup/common/backupRestorer.ts @@ -31,7 +31,7 @@ export class BackupRestorer implements IWorkbenchContribution { this.lifecycleService.when(LifecyclePhase.Restored).then(() => this.doRestoreBackups()); } - private async doRestoreBackups(): Promise { + protected async doRestoreBackups(): Promise { // Find all files and untitled with backups const backups = await this.backupFileService.getWorkspaceFileBackups(); diff --git a/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts new file mode 100644 index 00000000000..cc5d10a70a4 --- /dev/null +++ b/src/vs/workbench/contrib/backup/test/electron-browser/backupRestorer.test.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as platform from 'vs/base/common/platform'; +import * as os from 'os'; +import * as path from 'vs/base/common/path'; +import * as pfs from 'vs/base/node/pfs'; +import { URI } from 'vs/base/common/uri'; +import { createTextBufferFactory } from 'vs/editor/common/model/textModel'; +import { getRandomTestPath } from 'vs/base/test/node/testUtils'; +import { DefaultEndOfLine } from 'vs/editor/common/model'; +import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { hashPath } from 'vs/workbench/services/backup/node/backupFileService'; +import { BackupModelTracker } from 'vs/workbench/contrib/backup/common/backupModelTracker'; +import { TestTextFileService, workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; +import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager'; +import { BackupRestorer } from 'vs/workbench/contrib/backup/common/backupRestorer'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorInput } from 'vs/workbench/common/editor'; +import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; +import { TextFileEditor } from 'vs/workbench/contrib/files/browser/editors/textFileEditor'; +import { IBackupFileService } from 'vs/workbench/services/backup/common/backup'; +import { NodeTestBackupFileService } from 'vs/workbench/services/backup/test/electron-browser/backupFileService.test'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { Schemas } from 'vs/base/common/network'; +import { isEqual } from 'vs/base/common/resources'; + +const userdataDir = getRandomTestPath(os.tmpdir(), 'vsctests', 'backuprestorer'); +const backupHome = path.join(userdataDir, 'Backups'); +const workspacesJsonPath = path.join(backupHome, 'workspaces.json'); + +const workspaceResource = URI.file(platform.isWindows ? 'c:\\workspace' : '/workspace'); +const workspaceBackupPath = path.join(backupHome, hashPath(workspaceResource)); +const fooFile = URI.file(platform.isWindows ? 'c:\\Foo' : '/Foo'); +const barFile = URI.file(platform.isWindows ? 'c:\\Bar' : '/Bar'); +const untitledFile1 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-1' }); +const untitledFile2 = URI.from({ scheme: Schemas.untitled, path: 'Untitled-2' }); + +class TestBackupRestorer extends BackupRestorer { + async doRestoreBackups(): Promise { + return super.doRestoreBackups(); + } +} + +class ServiceAccessor { + constructor( + @ITextFileService public textFileService: TestTextFileService, + @IUntitledTextEditorService public untitledTextEditorService: IUntitledTextEditorService + ) { + } +} + +suite('BackupModelRestorer', () => { + let accessor: ServiceAccessor; + + let disposables: IDisposable[] = []; + + setup(async () => { + disposables.push(Registry.as(EditorExtensions.Editors).registerEditor( + EditorDescriptor.create( + TextFileEditor, + TextFileEditor.ID, + 'Text File Editor' + ), + [new SyncDescriptor(FileEditorInput)] + )); + + // Delete any existing backups completely and then re-create it. + await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + await pfs.mkdirp(backupHome); + + return pfs.writeFile(workspacesJsonPath, ''); + }); + + teardown(async () => { + dispose(disposables); + disposables = []; + + (accessor.textFileService.models).clear(); + (accessor.textFileService.models).dispose(); + accessor.untitledTextEditorService.revertAll(); + + return pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); + }); + + test('Restore backups', async function () { + this.timeout(20000); + + const backupFileService = new NodeTestBackupFileService(workspaceBackupPath); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IBackupFileService, backupFileService); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + instantiationService.stub(IEditorGroupsService, part); + + const editorService: EditorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); + + accessor = instantiationService.createInstance(ServiceAccessor); + + const tracker = instantiationService.createInstance(BackupModelTracker); + const restorer = instantiationService.createInstance(TestBackupRestorer); + + // Backup 2 normal files and 2 untitled file + await backupFileService.backupResource(untitledFile1, createTextBufferFactory('untitled-1').create(DefaultEndOfLine.LF).createSnapshot(false)); + await backupFileService.backupResource(untitledFile2, createTextBufferFactory('untitled-2').create(DefaultEndOfLine.LF).createSnapshot(false)); + await backupFileService.backupResource(fooFile, createTextBufferFactory('fooFile').create(DefaultEndOfLine.LF).createSnapshot(false)); + await backupFileService.backupResource(barFile, createTextBufferFactory('barFile').create(DefaultEndOfLine.LF).createSnapshot(false)); + + // Verify backups restored and opened as dirty + await restorer.doRestoreBackups(); + assert.equal(editorService.count, 4); + assert.ok(editorService.editors.every(editor => editor.isDirty())); + + let counter = 0; + for (const editor of editorService.editors) { + const resource = editor.getResource(); + if (isEqual(resource, untitledFile1)) { + const model = await accessor.untitledTextEditorService.createOrGet(resource).resolve(); + assert.equal(model.textEditorModel.getValue(), 'untitled-1'); + counter++; + } else if (isEqual(resource, untitledFile2)) { + const model = await accessor.untitledTextEditorService.createOrGet(resource).resolve(); + assert.equal(model.textEditorModel.getValue(), 'untitled-2'); + counter++; + } else if (isEqual(resource, fooFile)) { + const model = await accessor.textFileService.models.get(resource!)?.load(); + assert.equal(model?.textEditorModel?.getValue(), 'fooFile'); + counter++; + } else { + const model = await accessor.textFileService.models.get(resource!)?.load(); + assert.equal(model?.textEditorModel?.getValue(), 'barFile'); + counter++; + } + } + + assert.equal(counter, 4); + + part.dispose(); + tracker.dispose(); + }); +}); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.ts index ab12627fb50..c82d0a7739d 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.ts @@ -45,7 +45,7 @@ export interface OutgoingCall { } export interface CallHierarchySession { - root: CallHierarchyItem; + roots: CallHierarchyItem[]; dispose(): void; } @@ -92,15 +92,19 @@ export class CallHierarchyModel { if (!session) { return undefined; } - return new CallHierarchyModel(session.root._sessionId, provider, session.root, new RefCountedDisposabled(session)); + return new CallHierarchyModel(session.roots.reduce((p, c) => p + c._sessionId, ''), provider, session.roots, new RefCountedDisposabled(session)); } + readonly root: CallHierarchyItem; + private constructor( readonly id: string, readonly provider: CallHierarchyProvider, - readonly root: CallHierarchyItem, + readonly roots: CallHierarchyItem[], readonly ref: RefCountedDisposabled, - ) { } + ) { + this.root = roots[0]; + } dispose(): void { this.ref.release(); @@ -110,7 +114,7 @@ export class CallHierarchyModel { const that = this; return new class extends CallHierarchyModel { constructor() { - super(that.id, that.provider, item, that.ref.acquire()); + super(that.id, that.provider, [item], that.ref.acquire()); } }; } diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts index c412f7b992a..2e9cec145d3 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchyTree.ts @@ -43,7 +43,7 @@ export class DataSource implements IAsyncDataSource { async getChildren(element: CallHierarchyModel | Call): Promise { if (element instanceof CallHierarchyModel) { - return [new Call(element.root, undefined, element, undefined)]; + return element.roots.map(root => new Call(root, undefined, element, undefined)); } const { model, item } = element; diff --git a/src/vs/workbench/contrib/codeActions/common/configuration.ts b/src/vs/workbench/contrib/codeActions/common/configuration.ts index d8878cd0181..bdbcc19eb6a 100644 --- a/src/vs/workbench/contrib/codeActions/common/configuration.ts +++ b/src/vs/workbench/contrib/codeActions/common/configuration.ts @@ -34,7 +34,7 @@ const codeActionsOnSaveSchema: IConfigurationPropertySchema = { }, default: {}, description: nls.localize('codeActionsOnSave', "Code action kinds to be run on save."), - scope: ConfigurationScope.RESOURCE + scope: ConfigurationScope.RESOURCE_LANGUAGE, }; export const editorConfiguration = Object.freeze({ @@ -45,7 +45,7 @@ export const editorConfiguration = Object.freeze({ type: 'number', default: 750, description: nls.localize('codeActionsOnSaveTimeout', "Timeout in milliseconds after which the code actions that are run on save are cancelled."), - scope: ConfigurationScope.RESOURCE + scope: ConfigurationScope.RESOURCE_LANGUAGE, }, } }); diff --git a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts index ac40c376e27..b13b0f1aa57 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/simpleEditorOptions.ts @@ -34,7 +34,8 @@ export function getSimpleEditorOptions(): IEditorOptions { acceptSuggestionOnEnter: 'smart', minimap: { enabled: false - } + }, + renderIndentGuides: false }; } diff --git a/src/vs/workbench/contrib/customEditor/browser/commands.ts b/src/vs/workbench/contrib/customEditor/browser/commands.ts index f2b3fc113f2..1022aa8e3f8 100644 --- a/src/vs/workbench/contrib/customEditor/browser/commands.ts +++ b/src/vs/workbench/contrib/customEditor/browser/commands.ts @@ -12,17 +12,17 @@ import * as nls from 'vs/nls'; import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; -import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IListService } from 'vs/platform/list/browser/listService'; -import { IEditorCommandsContext, IEditorInput } from 'vs/workbench/common/editor'; +import { IEditorCommandsContext } from 'vs/workbench/common/editor'; +import { defaultEditorId } from 'vs/workbench/contrib/customEditor/browser/customEditors'; import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; +import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; -import { defaultEditorId } from 'vs/workbench/contrib/customEditor/browser/customEditors'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; +import { CustomFileEditorInput } from 'vs/workbench/contrib/customEditor/browser/customEditorInput'; const viewCategory = nls.localize('viewCategory', "View"); @@ -194,42 +194,32 @@ MenuRegistry.appendMenuItem(MenuId.CommandPalette, { const activeGroup = activeControl.group; const activeEditor = activeControl.input; - - if (!activeEditor) { - return; - } - const targetResource = activeEditor.getResource(); + if (!targetResource) { return; } const customEditorService = accessor.get(ICustomEditorService); - const activeCustomEditor = customEditorService.activeCustomEditor; let toggleView = defaultEditorId; - if (!activeCustomEditor) { - const viewIDs = customEditorService.getContributedCustomEditors(targetResource); - if (viewIDs && viewIDs.length) { - toggleView = viewIDs[0].id; - } - else { + if (!(activeEditor instanceof CustomFileEditorInput)) { + const bestAvailableEditor = customEditorService.getContributedCustomEditors(targetResource).bestAvailableEditor; + if (bestAvailableEditor) { + toggleView = bestAvailableEditor.id; + } else { return; } } - let replInput: IEditorInput; - if (toggleView === defaultEditorId) { - const instantiationService = accessor.get(IInstantiationService); - replInput = instantiationService.createInstance(FileEditorInput, targetResource, undefined, undefined); - } - else { - replInput = customEditorService.createInput(targetResource, toggleView, activeGroup); - } + const newEditorInput = customEditorService.createInput(targetResource, toggleView, activeGroup); editorService.replaceEditors([{ editor: activeEditor, - replacement: replInput, + replacement: newEditorInput, + options: { + ignoreOverrides: true, + } }], activeGroup); } }).register(); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 316fffe4b4f..483d0490e38 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -143,7 +143,9 @@ export class CustomFileEditorInput extends LazilyResolvedWebviewEditorInput { public async resolve(): Promise { this._model = await this.customEditorService.models.loadOrCreate(this.getResource(), this.viewType); this._register(this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); - this._onDidChangeDirty.fire(); + if (this.isDirty()) { + this._onDidChangeDirty.fire(); + } return await super.resolve(); } diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts index b2c75386617..52d8f474f43 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditors.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditors.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { coalesce, distinct, find, mergeSort } from 'vs/base/common/arrays'; +import { coalesce } from 'vs/base/common/arrays'; import { Lazy } from 'vs/base/common/lazy'; import { Disposable } from 'vs/base/common/lifecycle'; import { basename, isEqual } from 'vs/base/common/resources'; @@ -21,7 +21,7 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { EditorInput, EditorOptions, IEditor, IEditorInput } from 'vs/workbench/common/editor'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { webviewEditorsExtensionPoint } from 'vs/workbench/contrib/customEditor/browser/extensionPoint'; -import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; +import { CONTEXT_FOCUSED_CUSTOM_EDITOR_IS_EDITABLE, CONTEXT_HAS_CUSTOM_EDITORS, CustomEditorInfo, CustomEditorInfoCollection, CustomEditorPriority, CustomEditorSelector, ICustomEditor, ICustomEditorService } from 'vs/workbench/contrib/customEditor/common/customEditor'; import { CustomEditorModelManager } from 'vs/workbench/contrib/customEditor/common/customEditorModelManager'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { IWebviewService, webviewHasOwnEditFunctionsContext } from 'vs/workbench/contrib/webview/browser/webview'; @@ -130,15 +130,16 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ return this._editorInfoStore.get(viewType); } - public getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[] { - return this._editorInfoStore.getContributedEditors(resource); + public getContributedCustomEditors(resource: URI): CustomEditorInfoCollection { + return new CustomEditorInfoCollection(this._editorInfoStore.getContributedEditors(resource)); } - public getUserConfiguredCustomEditors(resource: URI): readonly CustomEditorInfo[] { + public getUserConfiguredCustomEditors(resource: URI): CustomEditorInfoCollection { const rawAssociations = this.configurationService.getValue(customEditorsAssociationsKey) || []; - return coalesce(rawAssociations - .filter(association => CustomEditorInfo.selectorMatches(association, resource)) - .map(association => this._editorInfoStore.get(association.viewType))); + return new CustomEditorInfoCollection( + coalesce(rawAssociations + .filter(association => CustomEditorInfo.selectorMatches(association, resource)) + .map(association => this._editorInfoStore.get(association.viewType)))); } public async promptOpenWith( @@ -146,21 +147,21 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ options?: ITextEditorOptions, group?: IEditorGroup, ): Promise { - const customEditors = distinct([ + const customEditors = new CustomEditorInfoCollection([ defaultEditorInfo, - ...this.getUserConfiguredCustomEditors(resource), - ...this.getContributedCustomEditors(resource), - ], editor => editor.id); + ...this.getUserConfiguredCustomEditors(resource).allEditors, + ...this.getContributedCustomEditors(resource).allEditors, + ]); let currentlyOpenedEditorType: undefined | string; for (const editor of group ? group.editors : []) { - if (editor.getResource() && isEqual(editor.getResource()!, resource)) { + if (editor.getResource() && isEqual(editor.getResource(), resource)) { currentlyOpenedEditorType = editor instanceof CustomFileEditorInput ? editor.viewType : defaultEditorId; break; } } - const items = customEditors.map((editorDescriptor): IQuickPickItem => ({ + const items = customEditors.allEditors.map((editorDescriptor): IQuickPickItem => ({ label: editorDescriptor.displayName, id: editorDescriptor.id, description: editorDescriptor.id === currentlyOpenedEditorType @@ -201,7 +202,11 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ viewType: string, group: IEditorGroup | undefined, options?: { readonly customClasses: string; }, - ): CustomFileEditorInput { + ): EditorInput { + if (viewType === defaultEditorId) { + return this.instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); + } + const id = generateUuid(); const webview = new Lazy(() => { return this.webviewService.createWebviewEditorOverlay(id, { customClasses: options?.customClasses }, {}); @@ -220,7 +225,7 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ group?: IEditorGroup ): Promise { if (group) { - const existingEditors = group.editors.filter(editor => editor.getResource() && isEqual(editor.getResource()!, resource)); + const existingEditors = group.editors.filter(editor => editor.getResource() && isEqual(editor.getResource(), resource)); if (existingEditors.length) { const existing = existingEditors[0]; if (!input.matches(existing)) { @@ -250,8 +255,8 @@ export class CustomEditorService extends Disposable implements ICustomEditorServ } const possibleEditors = [ - ...this.getContributedCustomEditors(resource), - ...this.getUserConfiguredCustomEditors(resource), + ...this.getContributedCustomEditors(resource).allEditors, + ...this.getUserConfiguredCustomEditors(resource).allEditors, ]; this._hasCustomEditor.set(possibleEditors.length > 0); this._focusedCustomEditorIsEditable.set(activeControl?.input instanceof CustomFileEditorInput); @@ -302,7 +307,7 @@ export class CustomEditorContribution implements IWorkbenchContribution { const userConfiguredEditors = this.customEditorService.getUserConfiguredCustomEditors(resource); if (userConfiguredEditors.length) { return { - override: this.customEditorService.openWith(resource, userConfiguredEditors[0].id, options, group), + override: this.customEditorService.openWith(resource, userConfiguredEditors.allEditors[0].id, options, group), }; } @@ -311,15 +316,7 @@ export class CustomEditorContribution implements IWorkbenchContribution { return; } - // Find the single default editor to use (if any) by looking at the editor's priority and the - // other contributed editors. - const defaultEditor = find(contributedEditors, editor => { - if (editor.priority !== CustomEditorPriority.default && editor.priority !== CustomEditorPriority.builtin) { - return false; - } - return contributedEditors.every(otherEditor => - otherEditor === editor || isLowerPriority(otherEditor, editor)); - }); + const defaultEditor = contributedEditors.defaultEditor; if (defaultEditor) { return { override: this.customEditorService.openWith(resource, defaultEditor.id, options, group), @@ -327,7 +324,7 @@ export class CustomEditorContribution implements IWorkbenchContribution { } // If we have all optional editors, then open VS Code's standard editor - if (contributedEditors.every(editor => editor.priority === CustomEditorPriority.option)) { + if (contributedEditors.allEditors.every(editor => editor.priority === CustomEditorPriority.option)) { return; } @@ -367,20 +364,17 @@ export class CustomEditorContribution implements IWorkbenchContribution { } // Prefer default editors in the diff editor case but ultimatly always take the first editor - const editors = mergeSort( - distinct([ - ...this.customEditorService.getUserConfiguredCustomEditors(resource), - ...this.customEditorService.getContributedCustomEditors(resource).filter(x => x.priority !== CustomEditorPriority.option), - ], editor => editor.id), - (a, b) => { - return priorityToRank(a.priority) - priorityToRank(b.priority); - }); + const allEditors = new CustomEditorInfoCollection([ + ...this.customEditorService.getUserConfiguredCustomEditors(resource).allEditors, + ...this.customEditorService.getContributedCustomEditors(resource).allEditors.filter(x => x.priority !== CustomEditorPriority.option), + ]); - if (!editors.length) { + const bestAvailableEditor = allEditors.bestAvailableEditor; + if (!bestAvailableEditor) { return undefined; } - return this.customEditorService.createInput(resource, editors[0].id, group, { customClasses }); + return this.customEditorService.createInput(resource, bestAvailableEditor.id, group, { customClasses }); }; const modifiedOverride = getCustomEditorOverrideForSubInput(editor.modifiedInput, 'modified'); @@ -399,18 +393,6 @@ export class CustomEditorContribution implements IWorkbenchContribution { } } -function isLowerPriority(otherEditor: CustomEditorInfo, editor: CustomEditorInfo): unknown { - return priorityToRank(otherEditor.priority) < priorityToRank(editor.priority); -} - -function priorityToRank(priority: CustomEditorPriority): number { - switch (priority) { - case CustomEditorPriority.default: return 3; - case CustomEditorPriority.builtin: return 2; - case CustomEditorPriority.option: return 1; - } -} - registerThemingParticipant((theme, collector) => { const shadow = theme.getColor(colorRegistry.scrollbarShadow); if (shadow) { diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index 8f37a7b6747..79fa6b1874d 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { distinct, find, mergeSort } from 'vs/base/common/arrays'; import { Event } from 'vs/base/common/event'; import * as glob from 'vs/base/common/glob'; import { basename } from 'vs/base/common/resources'; @@ -32,8 +33,8 @@ export interface ICustomEditorService { readonly activeCustomEditor: ICustomEditor | undefined; getCustomEditor(viewType: string): CustomEditorInfo | undefined; - getContributedCustomEditors(resource: URI): readonly CustomEditorInfo[]; - getUserConfiguredCustomEditors(resource: URI): readonly CustomEditorInfo[]; + getContributedCustomEditors(resource: URI): CustomEditorInfoCollection; + getUserConfiguredCustomEditors(resource: URI): CustomEditorInfoCollection; createInput(resource: URI, viewType: string, group: IEditorGroup | undefined, options?: { readonly customClasses: string }): EditorInput; @@ -122,3 +123,60 @@ export class CustomEditorInfo { return false; } } + +export class CustomEditorInfoCollection { + + public readonly allEditors: readonly CustomEditorInfo[]; + + constructor( + editors: readonly CustomEditorInfo[], + ) { + this.allEditors = distinct(editors, editor => editor.id); + } + + public get length(): number { return this.allEditors.length; } + + /** + * Find the single default editor to use (if any) by looking at the editor's priority and the + * other contributed editors. + */ + public get defaultEditor(): CustomEditorInfo | undefined { + return find(this.allEditors, editor => { + switch (editor.priority) { + case CustomEditorPriority.default: + case CustomEditorPriority.builtin: + // A default editor must have higher priority than all other contributed editors. + return this.allEditors.every(otherEditor => + otherEditor === editor || isLowerPriority(otherEditor, editor)); + + default: + return false; + } + }); + } + + /** + * Find the best available editor to use. + * + * Unlike the `defaultEditor`, a bestAvailableEditor can exist even if there are other editors with + * the same priority. + */ + public get bestAvailableEditor(): CustomEditorInfo | undefined { + const editors = mergeSort(Array.from(this.allEditors), (a, b) => { + return priorityToRank(a.priority) - priorityToRank(b.priority); + }); + return editors[0]; + } +} + +function isLowerPriority(otherEditor: CustomEditorInfo, editor: CustomEditorInfo): unknown { + return priorityToRank(otherEditor.priority) < priorityToRank(editor.priority); +} + +function priorityToRank(priority: CustomEditorPriority): number { + switch (priority) { + case CustomEditorPriority.default: return 3; + case CustomEditorPriority.builtin: return 2; + case CustomEditorPriority.option: return 1; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/callStackView.ts b/src/vs/workbench/contrib/debug/browser/callStackView.ts index 17f8cbd7da1..990db3c9a84 100644 --- a/src/vs/workbench/contrib/debug/browser/callStackView.ts +++ b/src/vs/workbench/contrib/debug/browser/callStackView.ts @@ -93,7 +93,7 @@ export class CallStackView extends ViewPane { if (thread && thread.stoppedDetails) { this.pauseMessageLabel.textContent = thread.stoppedDetails.description || nls.localize('debugStopped', "Paused on {0}", thread.stoppedDetails.reason || ''); this.pauseMessageLabel.title = thread.stoppedDetails.text || ''; - dom.toggleClass(this.pauseMessageLabel, 'exception', thread.stoppedDetails.reason === 'exception'); + this.pauseMessageLabel.toggleAttribute('exception', thread.stoppedDetails.reason === 'exception'); this.pauseMessage.hidden = false; if (this.toolbar) { this.toolbar.setActions([])(); @@ -511,11 +511,11 @@ class StackFramesRenderer implements ITreeRenderer, index: number, data: IStackFrameTemplateData): void { const stackFrame = element.element; - dom.toggleClass(data.stackFrame, 'disabled', !stackFrame.source || !stackFrame.source.available || isDeemphasized(stackFrame)); - dom.toggleClass(data.stackFrame, 'label', stackFrame.presentationHint === 'label'); - dom.toggleClass(data.stackFrame, 'subtle', stackFrame.presentationHint === 'subtle'); - const hasActions = stackFrame.thread.session.capabilities.supportsRestartFrame && stackFrame.presentationHint !== 'label' && stackFrame.presentationHint !== 'subtle'; - dom.toggleClass(data.stackFrame, 'has-actions', hasActions); + data.stackFrame.toggleAttribute('disabled', !stackFrame.source || !stackFrame.source.available || isDeemphasized(stackFrame)); + data.stackFrame.toggleAttribute('label', stackFrame.presentationHint === 'label'); + data.stackFrame.toggleAttribute('subtle', stackFrame.presentationHint === 'subtle'); + const hasActions = !!stackFrame.thread.session.capabilities.supportsRestartFrame && stackFrame.presentationHint !== 'label' && stackFrame.presentationHint !== 'subtle'; + data.stackFrame.toggleAttribute('has-actions', hasActions); data.file.title = stackFrame.source.inMemory ? stackFrame.source.uri.path : this.labelService.getUriLabel(stackFrame.source.uri); if (stackFrame.source.raw.origin) { diff --git a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts index fb50a8cdb98..fa2b03fb4e1 100644 --- a/src/vs/workbench/contrib/debug/browser/debug.contribution.ts +++ b/src/vs/workbench/contrib/debug/browser/debug.contribution.ts @@ -13,14 +13,14 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionRegistryExtensions } from 'vs/workbench/common/actions'; -import { ShowViewletAction, Extensions as ViewletExtensions, ViewletRegistry, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; +import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { TogglePanelAction, Extensions as PanelExtensions, PanelRegistry, PanelDescriptor } from 'vs/workbench/browser/panel'; import { BreakpointsView } from 'vs/workbench/contrib/debug/browser/breakpointsView'; import { CallStackView } from 'vs/workbench/contrib/debug/browser/callStackView'; import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IDebugService, VIEWLET_ID, REPL_ID, CONTEXT_IN_DEBUG_MODE, INTERNAL_CONSOLE_OPTIONS_SCHEMA, - CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, VIEW_CONTAINER, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, + CONTEXT_DEBUG_STATE, VARIABLES_VIEW_ID, CALLSTACK_VIEW_ID, WATCH_VIEW_ID, BREAKPOINTS_VIEW_ID, LOADED_SCRIPTS_VIEW_ID, CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_FOCUSED_SESSION_IS_ATTACH, CONTEXT_STEP_BACK_SUPPORTED, CONTEXT_CALLSTACK_ITEM_TYPE, CONTEXT_RESTART_FRAME_SUPPORTED, CONTEXT_JUMP_TO_CURSOR_SUPPORTED, CONTEXT_DEBUG_UX, } from 'vs/workbench/contrib/debug/common/debug'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; @@ -31,7 +31,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { registerCommands, ADD_CONFIGURATION_ID, TOGGLE_INLINE_BREAKPOINT_ID, COPY_STACK_TRACE_ID, REVERSE_CONTINUE_ID, STEP_BACK_ID, RESTART_SESSION_ID, TERMINATE_THREAD_ID, STEP_OVER_ID, STEP_INTO_ID, STEP_OUT_ID, PAUSE_ID, DISCONNECT_ID, STOP_ID, RESTART_FRAME_ID, CONTINUE_ID, FOCUS_REPL_ID, JUMP_TO_CURSOR_ID, RESTART_LABEL, STEP_INTO_LABEL, STEP_OVER_LABEL, STEP_OUT_LABEL, PAUSE_LABEL, DISCONNECT_LABEL, STOP_LABEL, CONTINUE_LABEL } from 'vs/workbench/contrib/debug/browser/debugCommands'; import { IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { StatusBarColorProvider } from 'vs/workbench/contrib/debug/browser/statusbarColorProvider'; -import { IViewsRegistry, Extensions as ViewExtensions } from 'vs/workbench/common/views'; +import { IViewsRegistry, Extensions as ViewExtensions, IViewContainersRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { isMacintosh } from 'vs/base/common/platform'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { URI } from 'vs/base/common/uri'; @@ -46,9 +46,9 @@ import { WatchExpressionsView } from 'vs/workbench/contrib/debug/browser/watchEx import { VariablesView } from 'vs/workbench/contrib/debug/browser/variablesView'; import { ClearReplAction, Repl } from 'vs/workbench/contrib/debug/browser/repl'; import { DebugContentProvider } from 'vs/workbench/contrib/debug/common/debugContentProvider'; -import { DebugViewlet } from 'vs/workbench/contrib/debug/browser/debugViewlet'; import { StartView } from 'vs/workbench/contrib/debug/browser/startView'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; +import { DebugViewPaneContainer } from 'vs/workbench/contrib/debug/browser/debugViewlet'; class OpenDebugViewletAction extends ShowViewletAction { public static readonly ID = VIEWLET_ID; @@ -79,14 +79,13 @@ class OpenDebugPanelAction extends TogglePanelAction { } } -// register viewlet -Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( - DebugViewlet, - VIEWLET_ID, - nls.localize('debugAndRun', "Debug and Run"), - 'codicon-debug-alt', - 3 -)); +const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: VIEWLET_ID, + name: nls.localize('runAndDebug', "Run and Debug"), + ctorDescriptor: { ctor: DebugViewPaneContainer }, + icon: 'codicon-debug-alt', + order: 3 +}, ViewContainerLocation.Sidebar); const openViewletKb: IKeybindings = { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_D @@ -107,12 +106,12 @@ Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor // Register default debug views const viewsRegistry = Registry.as(ViewExtensions.ViewsRegistry); -viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctorDescriptor: { ctor: VariablesView }, order: 10, weight: 40, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], VIEW_CONTAINER); -viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: { ctor: WatchExpressionsView }, order: 20, weight: 10, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], VIEW_CONTAINER); -viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: { ctor: CallStackView }, order: 30, weight: 30, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], VIEW_CONTAINER); -viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: { ctor: BreakpointsView }, order: 40, weight: 20, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], VIEW_CONTAINER); -viewsRegistry.registerViews([{ id: StartView.ID, name: StartView.LABEL, ctorDescriptor: { ctor: StartView }, order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], VIEW_CONTAINER); -viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: { ctor: LoadedScriptsView }, order: 35, weight: 5, canToggleVisibility: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], VIEW_CONTAINER); +viewsRegistry.registerViews([{ id: VARIABLES_VIEW_ID, name: nls.localize('variables', "Variables"), ctorDescriptor: { ctor: VariablesView }, order: 10, weight: 40, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusVariablesView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: WATCH_VIEW_ID, name: nls.localize('watch', "Watch"), ctorDescriptor: { ctor: WatchExpressionsView }, order: 20, weight: 10, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusWatchView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: CALLSTACK_VIEW_ID, name: nls.localize('callStack', "Call Stack"), ctorDescriptor: { ctor: CallStackView }, order: 30, weight: 30, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusCallStackView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: BREAKPOINTS_VIEW_ID, name: nls.localize('breakpoints', "Breakpoints"), ctorDescriptor: { ctor: BreakpointsView }, order: 40, weight: 20, canToggleVisibility: true, focusCommand: { id: 'workbench.debug.action.focusBreakpointsView' }, when: CONTEXT_DEBUG_UX.isEqualTo('default') }], viewContainer); +viewsRegistry.registerViews([{ id: StartView.ID, name: StartView.LABEL, ctorDescriptor: { ctor: StartView }, order: 10, weight: 40, canToggleVisibility: true, when: CONTEXT_DEBUG_UX.isEqualTo('simple') }], viewContainer); +viewsRegistry.registerViews([{ id: LOADED_SCRIPTS_VIEW_ID, name: nls.localize('loadedScripts', "Loaded Scripts"), ctorDescriptor: { ctor: LoadedScriptsView }, order: 35, weight: 5, canToggleVisibility: true, collapsed: true, when: ContextKeyExpr.and(CONTEXT_LOADED_SCRIPTS_SUPPORTED, CONTEXT_DEBUG_UX.isEqualTo('default')) }], viewContainer); registerCommands(); @@ -213,6 +212,11 @@ configurationRegistry.registerConfiguration({ default: 'onFirstSessionStart' }, 'debug.internalConsoleOptions': INTERNAL_CONSOLE_OPTIONS_SCHEMA, + 'debug.console.closeOnEnd': { + type: 'boolean', + description: nls.localize('debug.console.closeOnEnd', "Controls if the debug console should be automatically closed when the debug session ends."), + default: false + }, 'debug.openDebug': { enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart', 'openOnDebugBreak'], default: 'openOnSessionStart', diff --git a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts index bca1d167e25..767b547b64f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActionViewItems.ts @@ -157,25 +157,33 @@ export class StartDebugActionViewItem implements IActionViewItem { this.selected = 0; this.options = []; const manager = this.debugService.getConfigurationManager(); - const launches = manager.getLaunches(); const inWorkspace = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; - launches.forEach(launch => - launch.getConfigurationNames().forEach(name => { - if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) { - this.selected = this.options.length; + let lastGroup: string | undefined; + const disabledIdxs: number[] = []; + manager.getAllConfigurations().forEach(({ launch, name, presentation }) => { + if (name === manager.selectedConfiguration.name && launch === manager.selectedConfiguration.launch) { + this.selected = this.options.length; + } + if (lastGroup !== presentation?.group) { + lastGroup = presentation?.group; + if (this.options.length) { + this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: undefined }); + disabledIdxs.push(this.options.length - 1); } - const label = inWorkspace ? `${name} (${launch.name})` : name; - this.options.push({ label, handler: () => { manager.selectConfiguration(launch, name); return true; } }); - })); + } + + const label = inWorkspace ? `${name} (${launch.name})` : name; + this.options.push({ label, handler: () => { manager.selectConfiguration(launch, name); return true; } }); + }); if (this.options.length === 0) { this.options.push({ label: nls.localize('noConfigurations', "No Configurations"), handler: () => false }); } else { this.options.push({ label: StartDebugActionViewItem.SEPARATOR, handler: undefined }); + disabledIdxs.push(this.options.length - 1); } - const disabledIdx = this.options.length - 1; - launches.filter(l => !l.hidden).forEach(l => { + manager.getLaunches().filter(l => !l.hidden).forEach(l => { const label = inWorkspace ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); this.options.push({ label, handler: () => { @@ -185,7 +193,7 @@ export class StartDebugActionViewItem implements IActionViewItem { }); }); - this.selectBox.setOptions(this.options.map((data, index) => { text: data.label, isDisabled: (index === disabledIdx ? true : undefined) }), this.selected); + this.selectBox.setOptions(this.options.map((data, index) => { text: data.label, isDisabled: disabledIdxs.indexOf(index) !== -1 }), this.selected); } } diff --git a/src/vs/workbench/contrib/debug/browser/debugActions.ts b/src/vs/workbench/contrib/debug/browser/debugActions.ts index 6a01b97f2cf..3883e76be0a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugActions.ts +++ b/src/vs/workbench/contrib/debug/browser/debugActions.ts @@ -7,12 +7,13 @@ import * as nls from 'vs/nls'; import { Action } from 'vs/base/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, State, IEnablement, IBreakpoint, IDebugSession, ILaunch } from 'vs/workbench/contrib/debug/common/debug'; import { Variable, Breakpoint, FunctionBreakpoint } from 'vs/workbench/contrib/debug/common/debugModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; export abstract class AbstractDebugAction extends Action { @@ -60,7 +61,8 @@ export class ConfigureAction extends AbstractDebugAction { @IDebugService debugService: IDebugService, @IKeybindingService keybindingService: IKeybindingService, @INotificationService private readonly notificationService: INotificationService, - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService + @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, + @IQuickInputService private readonly quickInputService: IQuickInputService ) { super(id, label, 'debug-action codicon codicon-gear', debugService, keybindingService); this._register(debugService.getConfigurationManager().onDidSelectConfiguration(() => this.updateClass())); @@ -77,8 +79,7 @@ export class ConfigureAction extends AbstractDebugAction { private updateClass(): void { const configurationManager = this.debugService.getConfigurationManager(); - const configurationCount = configurationManager.getLaunches().map(l => l.getConfigurationNames().length).reduce((sum, current) => sum + current); - this.class = configurationCount > 0 ? 'debug-action codicon codicon-gear' : 'debug-action codicon codicon-gear notification'; + this.class = configurationManager.selectedConfiguration.name ? 'debug-action codicon codicon-gear' : 'debug-action codicon codicon-gear notification'; } async run(event?: any): Promise { @@ -87,10 +88,26 @@ export class ConfigureAction extends AbstractDebugAction { return; } - const sideBySide = !!(event && (event.ctrlKey || event.metaKey)); const configurationManager = this.debugService.getConfigurationManager(); - if (configurationManager.selectedConfiguration.launch) { - return configurationManager.selectedConfiguration.launch.openConfigFile(sideBySide, false); + let launch: ILaunch | undefined; + if (configurationManager.selectedConfiguration.name) { + launch = configurationManager.selectedConfiguration.launch; + } else { + const launches = configurationManager.getLaunches().filter(l => !!l.workspace); + if (launches.length === 1) { + launch = launches[0]; + } else { + const picks = launches.map(l => ({ label: l.name, launch: l })); + const picked = await this.quickInputService.pick<{ label: string, launch: ILaunch }>(picks, { activeItem: picks[0], placeHolder: nls.localize('selectWorkspaceFolder', "Select a workspace folder to create a launch.json file in") }); + if (picked) { + launch = picked.launch; + } + } + } + + if (launch) { + const sideBySide = !!(event && (event.ctrlKey || event.metaKey)); + return launch.openConfigFile(sideBySide, false); } } } @@ -127,7 +144,7 @@ export class StartAction extends AbstractDebugAction { if (debugService.state === State.Initializing) { return false; } - if ((sessions.length > 0) && debugService.getConfigurationManager().getLaunches().every(l => l.getConfigurationNames().length === 0)) { + if ((sessions.length > 0) && !debugService.getConfigurationManager().selectedConfiguration.name) { // There is already a debug session running and we do not have any launch configuration selected return false; } diff --git a/src/vs/workbench/contrib/debug/browser/debugCommands.ts b/src/vs/workbench/contrib/debug/browser/debugCommands.ts index 473869c258c..7ea1d5ed7cf 100644 --- a/src/vs/workbench/contrib/debug/browser/debugCommands.ts +++ b/src/vs/workbench/contrib/debug/browser/debugCommands.ts @@ -303,9 +303,14 @@ export function registerCommands(): void { id: RESTART_FRAME_ID, handler: async (accessor: ServicesAccessor, _: string, context: CallStackContext | unknown) => { const debugService = accessor.get(IDebugService); + const notificationService = accessor.get(INotificationService); let frame = getFrame(debugService, context); if (frame) { - await frame.restart(); + try { + await frame.restart(); + } catch (e) { + notificationService.error(e); + } } } }); diff --git a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts index c653b018cb3..8e925287d4c 100644 --- a/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts +++ b/src/vs/workbench/contrib/debug/browser/debugConfigurationManager.ts @@ -21,7 +21,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState, IWorkspaceFoldersChangeEvent } from 'vs/platform/workspace/common/workspace'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugConfigurationProvider, ICompound, IDebugConfiguration, IConfig, IGlobalConfig, IConfigurationManager, ILaunch, IDebugAdapterDescriptorFactory, IDebugAdapter, IDebugSession, IAdapterDescriptor, CONTEXT_DEBUG_CONFIGURATION_TYPE, IDebugAdapterFactory, IConfigPresentation } from 'vs/workbench/contrib/debug/common/debug'; import { Debugger } from 'vs/workbench/contrib/debug/common/debugger'; import { IEditorService, ACTIVE_GROUP, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import { isCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -68,8 +68,8 @@ export class ConfigurationManager implements IConfigurationManager { @ICommandService private readonly commandService: ICommandService, @IStorageService private readonly storageService: IStorageService, @IExtensionService private readonly extensionService: IExtensionService, - @IContextKeyService contextKeyService: IContextKeyService, - @IHistoryService historyService: IHistoryService + @IHistoryService private readonly historyService: IHistoryService, + @IContextKeyService contextKeyService: IContextKeyService ) { this.configProviders = []; this.adapterDescriptorFactories = []; @@ -83,13 +83,7 @@ export class ConfigurationManager implements IConfigurationManager { if (previousSelectedLaunch && previousSelectedLaunch.getConfigurationNames().length) { this.selectConfiguration(previousSelectedLaunch, this.storageService.get(DEBUG_SELECTED_CONFIG_NAME_KEY, StorageScope.WORKSPACE)); } else if (this.launches.length > 0) { - const rootUri = historyService.getLastActiveWorkspaceRoot(); - let launch = this.getLaunch(rootUri); - if (!launch || launch.getConfigurationNames().length === 0) { - launch = first(this.launches, l => !!(l && l.getConfigurationNames().length), launch) || this.launches[0]; - } - - this.selectConfiguration(launch); + this.selectConfiguration(undefined); } } @@ -213,6 +207,22 @@ export class ConfigurationManager implements IConfigurationManager { return result; } + async resolveDebugConfigurationWithSubstitutedVariables(folderUri: uri | undefined, type: string | undefined, config: IConfig, token: CancellationToken): Promise { + // pipe the config through the promises sequentially. Append at the end the '*' types + const providers = this.configProviders.filter(p => p.type === type && p.resolveDebugConfigurationWithSubstitutedVariables) + .concat(this.configProviders.filter(p => p.type === '*' && p.resolveDebugConfigurationWithSubstitutedVariables)); + + let result: IConfig | null | undefined = config; + await sequence(providers.map(provider => async () => { + // If any provider returned undefined or null make sure to respect that and do not pass the result to more resolver + if (result) { + result = await provider.resolveDebugConfigurationWithSubstitutedVariables!(folderUri, result, token); + } + })); + + return result; + } + async provideDebugConfigurations(folderUri: uri | undefined, type: string, token: CancellationToken): Promise { await this.activateDebuggers('onDebugInitialConfigurations'); const results = await Promise.all(this.configProviders.filter(p => p.type === type && p.provideDebugConfigurations).map(p => p.provideDebugConfigurations!(folderUri, token))); @@ -220,6 +230,43 @@ export class ConfigurationManager implements IConfigurationManager { return results.reduce((first, second) => first.concat(second), []); } + getAllConfigurations(): { launch: ILaunch; name: string; presentation?: IConfigPresentation }[] { + const all: { launch: ILaunch, name: string, presentation?: IConfigPresentation }[] = []; + for (let l of this.launches) { + for (let name of l.getConfigurationNames()) { + const config = l.getConfiguration(name) || l.getCompound(name); + if (config && !config.presentation?.hidden) { + all.push({ launch: l, name, presentation: config.presentation }); + } + } + } + + return all.sort((first, second) => { + if (!first.presentation) { + return 1; + } + if (!second.presentation) { + return -1; + } + if (!first.presentation.group) { + return 1; + } + if (!second.presentation.group) { + return -1; + } + if (first.presentation.group !== second.presentation.group) { + return first.presentation.group.localeCompare(second.presentation.group); + } + if (typeof first.presentation.order !== 'number') { + return 1; + } + if (typeof second.presentation.order !== 'number') { + return -1; + } + return first.presentation.order - second.presentation.order; + }); + } + private registerListeners(): void { debuggersExtPoint.setHandler((extensions, delta) => { delta.added.forEach(added => { @@ -286,13 +333,13 @@ export class ConfigurationManager implements IConfigurationManager { this.toDispose.push(Event.any(this.contextService.onDidChangeWorkspaceFolders, this.contextService.onDidChangeWorkbenchState)(() => { this.initLaunches(); - const toSelect = this.selectedLaunch || (this.launches.length > 0 ? this.launches[0] : undefined); - this.selectConfiguration(toSelect); + this.selectConfiguration(undefined); this.setCompoundSchemaValues(); })); this.toDispose.push(this.configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('launch')) { - this.selectConfiguration(this.selectedLaunch); + // A change happen in the launch.json. If there is already a launch configuration selected, do not change the selection. + this.selectConfiguration(undefined); this.setCompoundSchemaValues(); } })); @@ -306,7 +353,7 @@ export class ConfigurationManager implements IConfigurationManager { this.launches.push(this.instantiationService.createInstance(UserLaunch)); if (this.selectedLaunch && this.launches.indexOf(this.selectedLaunch) === -1) { - this.setSelectedLaunch(undefined); + this.selectConfiguration(undefined); } } @@ -355,10 +402,23 @@ export class ConfigurationManager implements IConfigurationManager { } selectConfiguration(launch: ILaunch | undefined, name?: string): void { + if (typeof launch === 'undefined') { + const rootUri = this.historyService.getLastActiveWorkspaceRoot(); + launch = this.getLaunch(rootUri); + if (!launch || launch.getConfigurationNames().length === 0) { + launch = first(this.launches, l => !!(l && l.getConfigurationNames().length), launch) || this.launches[0]; + } + } + const previousLaunch = this.selectedLaunch; const previousName = this.selectedName; + this.selectedLaunch = launch; - this.setSelectedLaunch(launch); + if (this.selectedLaunch) { + this.storageService.store(DEBUG_SELECTED_ROOT, this.selectedLaunch.uri.toString(), StorageScope.WORKSPACE); + } else { + this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); + } const names = launch ? launch.getConfigurationNames() : []; if (name && names.indexOf(name) >= 0) { this.setSelectedLaunchName(name); @@ -467,16 +527,6 @@ export class ConfigurationManager implements IConfigurationManager { } } - private setSelectedLaunch(selectedLaunch: ILaunch | undefined): void { - this.selectedLaunch = selectedLaunch; - - if (this.selectedLaunch) { - this.storageService.store(DEBUG_SELECTED_ROOT, this.selectedLaunch.uri.toString(), StorageScope.WORKSPACE); - } else { - this.storageService.remove(DEBUG_SELECTED_ROOT, StorageScope.WORKSPACE); - } - } - dispose(): void { this.toDispose = dispose(this.toDispose); } diff --git a/src/vs/workbench/contrib/debug/browser/debugQuickOpen.ts b/src/vs/workbench/contrib/debug/browser/debugQuickOpen.ts index 73dda0546df..f9df27bfe5e 100644 --- a/src/vs/workbench/contrib/debug/browser/debugQuickOpen.ts +++ b/src/vs/workbench/contrib/debug/browser/debugQuickOpen.ts @@ -96,18 +96,24 @@ export class DebugQuickOpenHandler extends QuickOpenHandler { const configurations: QuickOpenEntry[] = []; const configManager = this.debugService.getConfigurationManager(); - const launches = configManager.getLaunches(); - for (let launch of launches) { - launch.getConfigurationNames().map(config => ({ config: config, highlights: matchesFuzzy(input, config, true) || undefined })) - .filter(({ highlights }) => !!highlights) - .forEach(({ config, highlights }) => { - if (launch === configManager.selectedConfiguration.launch && config === configManager.selectedConfiguration.name) { - this.autoFocusIndex = configurations.length; - } - configurations.push(new StartDebugEntry(this.debugService, this.contextService, this.notificationService, launch, config, highlights)); - }); + const allConfigurations = configManager.getAllConfigurations(); + let lastGroup: string | undefined; + for (let config of allConfigurations) { + const highlights = matchesFuzzy(input, config.name, true); + if (highlights) { + if (config.launch === configManager.selectedConfiguration.launch && config.name === configManager.selectedConfiguration.name) { + this.autoFocusIndex = configurations.length; + } + let entry: QuickOpenEntry = new StartDebugEntry(this.debugService, this.contextService, this.notificationService, config.launch, config.name, highlights); + if (lastGroup !== config.presentation?.group) { + entry = new QuickOpenEntryGroup(entry, undefined, true); + lastGroup = config.presentation?.group; + } + configurations.push(entry); + } } - launches.filter(l => !l.hidden).forEach((l, index) => { + + configManager.getLaunches().filter(l => !l.hidden).forEach((l, index) => { const label = this.contextService.getWorkbenchState() === WorkbenchState.WORKSPACE ? nls.localize("addConfigTo", "Add Config ({0})...", l.name) : nls.localize('addConfiguration', "Add Configuration..."); const entry = new AddConfigEntry(label, l, this.commandService, this.contextService, matchesFuzzy(input, label, true) || undefined); diff --git a/src/vs/workbench/contrib/debug/browser/debugService.ts b/src/vs/workbench/contrib/debug/browser/debugService.ts index 0ef868283da..215e33e08a5 100644 --- a/src/vs/workbench/contrib/debug/browser/debugService.ts +++ b/src/vs/workbench/contrib/debug/browser/debugService.ts @@ -290,7 +290,7 @@ export class DebugService implements IDebugService { "Compound must have \"configurations\" attribute set in order to start multiple configurations.")); } if (compound.preLaunchTask) { - const taskResult = await this.taskRunner.runTaskAndCheckErrors(launch?.workspace || this.contextService.getWorkspace(), compound.preLaunchTask, this.showError); + const taskResult = await this.taskRunner.runTaskAndCheckErrors(launch?.workspace || this.contextService.getWorkspace(), compound.preLaunchTask, (msg, actions) => this.showError(msg, actions)); if (taskResult === TaskRunResult.Failure) { this.endInitializingState(); return false; @@ -379,13 +379,22 @@ export class DebugService implements IDebugService { // a falsy config indicates an aborted launch if (configByProviders && configByProviders.type) { try { - const resolvedConfig = await this.substituteVariables(launch, configByProviders); + let resolvedConfig = await this.substituteVariables(launch, configByProviders); if (!resolvedConfig) { // User canceled resolving of interactive variables, silently return return false; } + const cfg = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, type, resolvedConfig, this.initCancellationToken.token); + if (!cfg) { + if (launch && type && cfg === null) { // show launch.json only for "config" being "null". + await launch.openConfigFile(false, true, type, this.initCancellationToken ? this.initCancellationToken.token : undefined); + } + return false; + } + resolvedConfig = cfg; + if (!this.configurationManager.getDebugger(resolvedConfig.type) || (configByProviders.request !== 'attach' && configByProviders.request !== 'launch')) { let message: string; if (configByProviders.request !== 'attach' && configByProviders.request !== 'launch') { @@ -401,8 +410,8 @@ export class DebugService implements IDebugService { return false; } - const workspace = launch ? launch.workspace : this.contextService.getWorkspace(); - const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask, this.showError); + const workspace = launch?.workspace || this.contextService.getWorkspace(); + const taskResult = await this.taskRunner.runTaskAndCheckErrors(workspace, resolvedConfig.preLaunchTask, (msg, actions) => this.showError(msg, actions)); if (taskResult === TaskRunResult.Success) { return this.doCreateSession(launch?.workspace, { resolved: resolvedConfig, unresolved: unresolvedConfig }, options); } @@ -563,8 +572,11 @@ export class DebugService implements IDebugService { // Data breakpoints that can not be persisted should be cleared when a session ends const dataBreakpoints = this.model.getDataBreakpoints().filter(dbp => !dbp.canPersist); dataBreakpoints.forEach(dbp => this.model.removeDataBreakpoints(dbp.getId())); - } + if (this.panelService.getLastActivePanelId() === REPL_ID && this.configurationService.getValue('debug').console.closeOnEnd) { + this.panelService.hideActivePanel(); + } + } })); } @@ -579,7 +591,7 @@ export class DebugService implements IDebugService { } await this.taskRunner.runTask(session.root, session.configuration.postDebugTask); - return this.taskRunner.runTaskAndCheckErrors(session.root, session.configuration.preLaunchTask, this.showError); + return this.taskRunner.runTaskAndCheckErrors(session.root, session.configuration.preLaunchTask, (msg, actions) => this.showError(msg, actions)); }; const extensionDebugSession = getExtensionHostDebugSession(session); @@ -636,6 +648,9 @@ export class DebugService implements IDebugService { const resolvedByProviders = await this.configurationManager.resolveConfigurationByProviders(launch.workspace ? launch.workspace.uri : undefined, unresolved.type, unresolved, this.initCancellationToken.token); if (resolvedByProviders) { resolved = await this.substituteVariables(launch, resolvedByProviders); + if (resolved) { + resolved = await this.configurationManager.resolveDebugConfigurationWithSubstitutedVariables(launch && launch.workspace ? launch.workspace.uri : undefined, unresolved.type, resolved, this.initCancellationToken.token); + } } else { resolved = resolvedByProviders; } @@ -660,13 +675,13 @@ export class DebugService implements IDebugService { } stopSession(session: IDebugSession): Promise { - if (session) { return session.terminate(); } const sessions = this.model.getSessions(); if (sessions.length === 0) { + this.taskRunner.cancel(); this.endInitializingState(); } @@ -760,9 +775,11 @@ export class DebugService implements IDebugService { //---- watches - addWatchExpression(name: string): void { + addWatchExpression(name?: string): void { const we = this.model.addWatchExpression(name); - this.viewModel.setSelectedExpression(we); + if (!name) { + this.viewModel.setSelectedExpression(we); + } this.storeWatchExpressions(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugSession.ts b/src/vs/workbench/contrib/debug/browser/debugSession.ts index 2d7078c9096..aa9aea3369a 100644 --- a/src/vs/workbench/contrib/debug/browser/debugSession.ts +++ b/src/vs/workbench/contrib/debug/browser/debugSession.ts @@ -34,6 +34,7 @@ import { IOpenerService } from 'vs/platform/opener/common/opener'; import { variableSetEmitter } from 'vs/workbench/contrib/debug/browser/variablesView'; import { CancellationTokenSource, CancellationToken } from 'vs/base/common/cancellation'; import { distinct } from 'vs/base/common/arrays'; +import { INotificationService } from 'vs/platform/notification/common/notification'; export class DebugSession implements IDebugSession { @@ -74,7 +75,8 @@ export class DebugSession implements IDebugSession { @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IProductService private readonly productService: IProductService, @IExtensionHostDebugService private readonly extensionHostDebugService: IExtensionHostDebugService, - @IOpenerService private readonly openerService: IOpenerService + @IOpenerService private readonly openerService: IOpenerService, + @INotificationService private readonly notificationService: INotificationService ) { this.id = generateUuid(); this._options = options || {}; @@ -711,6 +713,7 @@ export class DebugSession implements IDebugSession { await this.raw.configurationDone(); } catch (e) { // Disconnect the debug session on configuration done error #10596 + this.notificationService.error(e); if (this.raw) { this.raw.disconnect(); } diff --git a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts index 19e63db2dac..f4e6bad2774 100644 --- a/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts +++ b/src/vs/workbench/contrib/debug/browser/debugTaskRunner.ts @@ -38,6 +38,8 @@ export const enum TaskRunResult { export class DebugTaskRunner { + private canceled = false; + constructor( @ITaskService private readonly taskService: ITaskService, @IMarkerService private readonly markerService: IMarkerService, @@ -46,9 +48,17 @@ export class DebugTaskRunner { @IDialogService private readonly dialogService: IDialogService, ) { } + cancel(): void { + this.canceled = true; + } + async runTaskAndCheckErrors(root: IWorkspaceFolder | IWorkspace | undefined, taskId: string | TaskIdentifier | undefined, onError: (msg: string, actions: IAction[]) => Promise): Promise { try { + this.canceled = false; const taskSummary = await this.runTask(root, taskId); + if (this.canceled) { + return TaskRunResult.Failure; + } const errorCount = taskId ? this.markerService.getStatistics().errors : 0; const successExitCode = taskSummary && taskSummary.exitCode === 0; diff --git a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts index 86fb545750b..5afeb4290cb 100644 --- a/src/vs/workbench/contrib/debug/browser/debugViewlet.ts +++ b/src/vs/workbench/contrib/debug/browser/debugViewlet.ts @@ -32,26 +32,8 @@ import { MenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryAc import { INotificationService } from 'vs/platform/notification/common/notification'; import { TogglePanelAction } from 'vs/workbench/browser/panel'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { Viewlet } from 'vs/workbench/browser/viewlet'; import { StartView } from 'vs/workbench/contrib/debug/browser/startView'; -// Register a lightweight viewlet responsible for making the container -export class DebugViewlet extends Viewlet { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService protected storageService: IStorageService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IExtensionService protected extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, - @IConfigurationService protected configurationService: IConfigurationService - ) { - super(VIEWLET_ID, instantiationService.createInstance(DebugViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } -} - export class DebugViewPaneContainer extends ViewPaneContainer { private startDebugActionViewItem: StartDebugActionViewItem | undefined; @@ -79,7 +61,7 @@ export class DebugViewPaneContainer extends ViewPaneContainer { @IContextKeyService private readonly contextKeyService: IContextKeyService, @INotificationService private readonly notificationService: INotificationService ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); this._register(this.debugService.onDidChangeState(state => this.onDebugServiceStateChange(state))); this._register(this.debugService.onDidNewSession(() => this.updateToolBar())); diff --git a/src/vs/workbench/contrib/debug/browser/repl.ts b/src/vs/workbench/contrib/debug/browser/repl.ts index a23b9f0f60c..e719772b78c 100644 --- a/src/vs/workbench/contrib/debug/browser/repl.ts +++ b/src/vs/workbench/contrib/debug/browser/repl.ts @@ -4,15 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import 'vs/css!vs/workbench/contrib/debug/browser/media/repl'; -import * as nls from 'vs/nls'; import { URI as uri } from 'vs/base/common/uri'; -import * as errors from 'vs/base/common/errors'; import { IAction, IActionViewItem, Action } from 'vs/base/common/actions'; import * as dom from 'vs/base/browser/dom'; import * as aria from 'vs/base/browser/ui/aria/aria'; import { CancellationToken } from 'vs/base/common/cancellation'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; -import severity from 'vs/base/common/severity'; import { SuggestController } from 'vs/editor/contrib/suggest/suggestController'; import { ITextModel } from 'vs/editor/common/model'; import { Position } from 'vs/editor/common/core/position'; @@ -25,12 +22,12 @@ import { IInstantiationService, createDecorator } from 'vs/platform/instantiatio import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Panel } from 'vs/workbench/browser/panel'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { memoize } from 'vs/base/common/decorators'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { dispose, IDisposable, Disposable } from 'vs/base/common/lifecycle'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; -import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IExpressionContainer, IExpression, IReplElementSource, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; +import { IDebugService, REPL_ID, DEBUG_SCHEME, CONTEXT_IN_DEBUG_REPL, IDebugSession, State, IReplElement, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { HistoryNavigator } from 'vs/base/common/history'; import { IHistoryNavigationWidget } from 'vs/base/browser/history'; import { createAndBindHistoryNavigationWidgetScopedContextKeyService } from 'vs/platform/browser/contextScopedHistoryWidget'; @@ -43,27 +40,21 @@ import { FocusSessionActionViewItem } from 'vs/workbench/contrib/debug/browser/d import { CompletionContext, CompletionList, CompletionProviderRegistry } from 'vs/editor/common/modes'; import { first } from 'vs/base/common/arrays'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; -import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; -import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; -import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; -import { ITreeRenderer, ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { ITreeNode, ITreeContextMenuEvent, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { renderExpressionValue, AbstractExpressionsRenderer, IExpressionTemplateData, renderVariable, IInputBoxOptions } from 'vs/workbench/contrib/debug/browser/baseDebugView'; -import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; -import { ILabelService } from 'vs/platform/label/common/label'; import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { removeAnsiEscapeCodes } from 'vs/base/common/strings'; import { WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ITextResourcePropertiesService } from 'vs/editor/common/services/textResourceConfigurationService'; import { RunOnceScheduler } from 'vs/base/common/async'; -import { FuzzyScore, createMatches } from 'vs/base/common/filters'; -import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { FuzzyScore } from 'vs/base/common/filters'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; +import { ReplDelegate, ReplVariablesRenderer, ReplSimpleElementsRenderer, ReplEvaluationInputsRenderer, ReplEvaluationResultsRenderer, ReplRawObjectsRenderer, ReplDataSource, ReplAccessibilityProvider } from 'vs/workbench/contrib/debug/browser/replViewer'; +import { localize } from 'vs/nls'; const $ = dom.$; @@ -89,8 +80,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati _serviceBrand: undefined; private static readonly REFRESH_DELAY = 100; // delay in ms to refresh the repl for new elements to show - private static readonly REPL_INPUT_INITIAL_HEIGHT = 19; - private static readonly REPL_INPUT_MAX_HEIGHT = 170; + private static readonly REPL_INPUT_LINE_HEIGHT = 19; private history: HistoryNavigator; private tree!: WorkbenchAsyncDataTree; @@ -99,13 +89,14 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati private replInput!: CodeEditorWidget; private replInputContainer!: HTMLElement; private dimension!: dom.Dimension; - private replInputHeight: number; + private replInputLineCount = 1; private model!: ITextModel; private historyNavigationEnablement!: IContextKey; private scopedInstantiationService!: IInstantiationService; private replElementsChangeListener: IDisposable | undefined; private styleElement: HTMLStyleElement | undefined; private completionItemProvider: IDisposable | undefined; + private modelChangeListener: IDisposable = Disposable.None; constructor( @IDebugService private readonly debugService: IDebugService, @@ -119,11 +110,11 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati @IContextMenuService private readonly contextMenuService: IContextMenuService, @IConfigurationService private readonly configurationService: IConfigurationService, @ITextResourcePropertiesService private readonly textResourcePropertiesService: ITextResourcePropertiesService, - @IClipboardService private readonly clipboardService: IClipboardService + @IClipboardService private readonly clipboardService: IClipboardService, + @IEditorService private readonly editorService: IEditorService ) { super(REPL_ID, telemetryService, themeService, storageService); - this.replInputHeight = Repl.REPL_INPUT_INITIAL_HEIGHT; this.history = new HistoryNavigator(JSON.parse(this.storageService.get(HISTORY_STORAGE_KEY, StorageScope.WORKSPACE, '[]')), 50); codeEditorService.registerDecorationType(DECORATION_KEY, {}); this.registerListeners(); @@ -147,7 +138,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati if (model) { const word = model.getWordAtPosition(position); const overwriteBefore = word ? word.word.length : 0; - const text = model.getLineContent(position.lineNumber); + const text = model.getValue(); const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame; const frameId = focusedStackFrame ? focusedStackFrame.frameId : undefined; const suggestions = await session.completions(frameId, text, position, overwriteBefore, token); @@ -181,6 +172,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati dispose(this.model); } else { this.model = this.modelService.createModel('', null, uri.parse(`${DEBUG_SCHEME}:replinput`), true); + this.setMode(); this.replInput.setModel(this.model); this.updateInputDecoration(); this.refreshReplElements(true); @@ -191,6 +183,9 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.onDidFontChange(); } })); + this._register(this.editorService.onDidActiveEditorChange(() => { + this.setMode(); + })); } get isReadonly(): boolean { @@ -215,6 +210,21 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.tree.domFocus(); } + private setMode(): void { + if (!this.isVisible()) { + return; + } + + const activeEditor = this.editorService.activeTextEditorWidget; + if (isCodeEditor(activeEditor)) { + this.modelChangeListener.dispose(); + this.modelChangeListener = activeEditor.onDidChangeModelLanguage(() => this.setMode()); + if (activeEditor.hasModel()) { + this.model.setMode(activeEditor.getModel().getLanguageIdentifier()); + } + } + } + private onDidFontChange(): void { if (this.styleElement) { const debugConsole = this.configurationService.getValue('debug').console; @@ -303,8 +313,8 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati revealLastElement(this.tree); this.history.add(this.replInput.getValue()); this.replInput.setValue(''); - const shouldRelayout = this.replInputHeight > Repl.REPL_INPUT_INITIAL_HEIGHT; - this.replInputHeight = Repl.REPL_INPUT_INITIAL_HEIGHT; + const shouldRelayout = this.replInputLineCount > 1; + this.replInputLineCount = 1; if (shouldRelayout) { // Trigger a layout to shrink a potential multi line input this.layout(this.dimension); @@ -330,18 +340,19 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati layout(dimension: dom.Dimension): void { this.dimension = dimension; + const replInputHeight = Repl.REPL_INPUT_LINE_HEIGHT * this.replInputLineCount; if (this.tree) { const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - const treeHeight = dimension.height - this.replInputHeight; + const treeHeight = dimension.height - replInputHeight; this.tree.getHTMLElement().style.height = `${treeHeight}px`; this.tree.layout(treeHeight, dimension.width); if (lastElementVisible) { revealLastElement(this.tree); } } - this.replInputContainer.style.height = `${this.replInputHeight}px`; + this.replInputContainer.style.height = `${replInputHeight}px`; - this.replInput.layout({ width: dimension.width - 20, height: this.replInputHeight }); + this.replInput.layout({ width: dimension.width - 20, height: replInputHeight }); } focus(): void { @@ -381,17 +392,17 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati @memoize private get refreshScheduler(): RunOnceScheduler { - return new RunOnceScheduler(() => { + return new RunOnceScheduler(async () => { if (!this.tree.getInput()) { return; } + const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - this.tree.updateChildren().then(() => { - if (lastElementVisible) { - // Only scroll if we were scrolled all the way down before tree refreshed #10486 - revealLastElement(this.tree); - } - }, errors.onUnexpectedError); + await this.tree.updateChildren(); + if (lastElementVisible) { + // Only scroll if we were scrolled all the way down before tree refreshed #10486 + revealLastElement(this.tree); + } }, Repl.REFRESH_DELAY); } @@ -422,7 +433,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati // https://github.com/microsoft/TypeScript/issues/32526 new ReplDataSource() as IAsyncDataSource, { - ariaLabel: nls.localize('replAriaLabel', "Read Eval Print Loop Panel"), + ariaLabel: localize('replAriaLabel', "Read Eval Print Loop Panel"), accessibilityProvider: new ReplAccessibilityProvider(), identityProvider: { getId: (element: IReplElement) => element.getId() }, mouseSupport: false, @@ -462,20 +473,18 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati [IContextKeyService, scopedContextKeyService], [IPrivateReplService, this])); const options = getSimpleEditorOptions(); options.readOnly = true; - options.ariaLabel = nls.localize('debugConsole', "Debug Console"); + options.ariaLabel = localize('debugConsole', "Debug Console"); this.replInput = this.scopedInstantiationService.createInstance(CodeEditorWidget, this.replInputContainer, options, getSimpleCodeEditorWidgetOptions()); - this._register(this.replInput.onDidScrollChange(e => { - if (!e.scrollHeightChanged) { - return; - } - this.replInputHeight = Math.max(Repl.REPL_INPUT_INITIAL_HEIGHT, Math.min(Repl.REPL_INPUT_MAX_HEIGHT, e.scrollHeight, this.dimension.height)); - this.layout(this.dimension); - })); this._register(this.replInput.onDidChangeModelContent(() => { const model = this.replInput.getModel(); this.historyNavigationEnablement.set(!!model && model.getValue() === ''); + const lineCount = model ? Math.min(10, model.getLineCount()) : 1; + if (lineCount !== this.replInputLineCount) { + this.replInputLineCount = lineCount; + this.layout(this.dimension); + } })); // We add the input decoration only when the focus is in the input #61126 this._register(this.replInput.onDidFocusEditorText(() => this.updateInputDecoration())); @@ -487,18 +496,18 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati private onContextMenu(e: ITreeContextMenuEvent): void { const actions: IAction[] = []; - actions.push(new Action('debug.replCopy', nls.localize('copy', "Copy"), undefined, true, async () => { + actions.push(new Action('debug.replCopy', localize('copy', "Copy"), undefined, true, async () => { const nativeSelection = window.getSelection(); if (nativeSelection) { await this.clipboardService.writeText(nativeSelection.toString()); } return Promise.resolve(); })); - actions.push(new Action('workbench.debug.action.copyAll', nls.localize('copyAll', "Copy All"), undefined, true, async () => { + actions.push(new Action('workbench.debug.action.copyAll', localize('copyAll', "Copy All"), undefined, true, async () => { await this.clipboardService.writeText(this.getVisibleContent()); return Promise.resolve(); })); - actions.push(new Action('debug.collapseRepl', nls.localize('collapse', "Collapse All"), undefined, true, () => { + actions.push(new Action('debug.collapseRepl', localize('collapse', "Collapse All"), undefined, true, () => { this.tree.collapseAll(); this.replInput.focus(); return Promise.resolve(); @@ -543,7 +552,7 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati }, renderOptions: { after: { - contentText: nls.localize('startDebugFirst', "Please start a debug session to evaluate expressions"), + contentText: localize('startDebugFirst', "Please start a debug session to evaluate expressions"), color: transparentForeground ? transparentForeground.toString() : undefined } } @@ -570,338 +579,11 @@ export class Repl extends Panel implements IPrivateReplService, IHistoryNavigati this.replElementsChangeListener.dispose(); } this.refreshScheduler.dispose(); + this.modelChangeListener.dispose(); super.dispose(); } } -// Repl tree - -interface IReplEvaluationInputTemplateData { - label: HighlightedLabel; -} - -interface IReplEvaluationResultTemplateData { - value: HTMLElement; - annotation: HTMLElement; -} - -interface ISimpleReplElementTemplateData { - container: HTMLElement; - value: HTMLElement; - source: HTMLElement; - getReplElementSource(): IReplElementSource | undefined; - toDispose: IDisposable[]; -} - -interface IRawObjectReplTemplateData { - container: HTMLElement; - expression: HTMLElement; - name: HTMLElement; - value: HTMLElement; - annotation: HTMLElement; - label: HighlightedLabel; -} - -class ReplEvaluationInputsRenderer implements ITreeRenderer { - static readonly ID = 'replEvaluationInput'; - - get templateId(): string { - return ReplEvaluationInputsRenderer.ID; - } - - renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { - dom.append(container, $('span.arrow.codicon.codicon-chevron-right')); - const input = dom.append(container, $('.expression')); - const label = new HighlightedLabel(input, false); - return { label }; - } - - renderElement(element: ITreeNode, index: number, templateData: IReplEvaluationInputTemplateData): void { - const evaluation = element.element; - templateData.label.set(evaluation.value, createMatches(element.filterData)); - } - - disposeTemplate(templateData: IReplEvaluationInputTemplateData): void { - // noop - } -} - -class ReplEvaluationResultsRenderer implements ITreeRenderer { - static readonly ID = 'replEvaluationResult'; - - get templateId(): string { - return ReplEvaluationResultsRenderer.ID; - } - - constructor(private readonly linkDetector: LinkDetector) { } - - renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData { - dom.append(container, $('span.arrow.codicon.codicon-chevron-left')); - const output = dom.append(container, $('.evaluation-result.expression')); - const value = dom.append(output, $('span.value')); - const annotation = dom.append(output, $('span')); - - return { value, annotation }; - } - - renderElement(element: ITreeNode, index: number, templateData: IReplEvaluationResultTemplateData): void { - const expression = element.element; - renderExpressionValue(expression, templateData.value, { - preserveWhitespace: !expression.hasChildren, - showHover: false, - colorize: true, - linkDetector: this.linkDetector - }); - if (expression.hasChildren) { - templateData.annotation.className = 'annotation codicon codicon-info'; - templateData.annotation.title = nls.localize('stateCapture', "Object state is captured from first evaluation"); - } - } - - disposeTemplate(templateData: IReplEvaluationResultTemplateData): void { - // noop - } -} - -class ReplSimpleElementsRenderer implements ITreeRenderer { - static readonly ID = 'simpleReplElement'; - - constructor( - private readonly linkDetector: LinkDetector, - @IEditorService private readonly editorService: IEditorService, - @ILabelService private readonly labelService: ILabelService, - @IThemeService private readonly themeService: IThemeService - ) { } - - get templateId(): string { - return ReplSimpleElementsRenderer.ID; - } - - renderTemplate(container: HTMLElement): ISimpleReplElementTemplateData { - const data: ISimpleReplElementTemplateData = Object.create(null); - dom.addClass(container, 'output'); - const expression = dom.append(container, $('.output.expression.value-and-source')); - - data.container = container; - data.value = dom.append(expression, $('span.value')); - data.source = dom.append(expression, $('.source')); - data.toDispose = []; - data.toDispose.push(dom.addDisposableListener(data.source, 'click', e => { - e.preventDefault(); - e.stopPropagation(); - const source = data.getReplElementSource(); - if (source) { - source.source.openInEditor(this.editorService, { - startLineNumber: source.lineNumber, - startColumn: source.column, - endLineNumber: source.lineNumber, - endColumn: source.column - }); - } - })); - - return data; - } - - renderElement({ element }: ITreeNode, index: number, templateData: ISimpleReplElementTemplateData): void { - // value - dom.clearNode(templateData.value); - // Reset classes to clear ansi decorations since templates are reused - templateData.value.className = 'value'; - const result = handleANSIOutput(element.value, this.linkDetector, this.themeService, element.session); - templateData.value.appendChild(result); - - dom.addClass(templateData.value, (element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info'); - templateData.source.textContent = element.sourceData ? `${element.sourceData.source.name}:${element.sourceData.lineNumber}` : ''; - templateData.source.title = element.sourceData ? this.labelService.getUriLabel(element.sourceData.source.uri) : ''; - templateData.getReplElementSource = () => element.sourceData; - } - - disposeTemplate(templateData: ISimpleReplElementTemplateData): void { - dispose(templateData.toDispose); - } -} - -export class ReplVariablesRenderer extends AbstractExpressionsRenderer { - - static readonly ID = 'replVariable'; - - get templateId(): string { - return ReplVariablesRenderer.ID; - } - - constructor( - private readonly linkDetector: LinkDetector, - @IDebugService debugService: IDebugService, - @IContextViewService contextViewService: IContextViewService, - @IThemeService themeService: IThemeService, - ) { - super(debugService, contextViewService, themeService); - } - - protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { - renderVariable(expression as Variable, data, true, highlights, this.linkDetector); - } - - protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined { - return undefined; - } -} - -class ReplRawObjectsRenderer implements ITreeRenderer { - static readonly ID = 'rawObject'; - - constructor(private readonly linkDetector: LinkDetector) { } - - get templateId(): string { - return ReplRawObjectsRenderer.ID; - } - - renderTemplate(container: HTMLElement): IRawObjectReplTemplateData { - dom.addClass(container, 'output'); - - const expression = dom.append(container, $('.output.expression')); - const name = dom.append(expression, $('span.name')); - const label = new HighlightedLabel(name, false); - const value = dom.append(expression, $('span.value')); - const annotation = dom.append(expression, $('span')); - - return { container, expression, name, label, value, annotation }; - } - - renderElement(node: ITreeNode, index: number, templateData: IRawObjectReplTemplateData): void { - // key - const element = node.element; - templateData.label.set(element.name ? `${element.name}:` : '', createMatches(node.filterData)); - if (element.name) { - templateData.name.textContent = `${element.name}:`; - } else { - templateData.name.textContent = ''; - } - - // value - renderExpressionValue(element.value, templateData.value, { - preserveWhitespace: true, - showHover: false, - linkDetector: this.linkDetector - }); - - // annotation if any - if (element.annotation) { - templateData.annotation.className = 'annotation codicon codicon-info'; - templateData.annotation.title = element.annotation; - } else { - templateData.annotation.className = ''; - templateData.annotation.title = ''; - } - } - - disposeTemplate(templateData: IRawObjectReplTemplateData): void { - // noop - } -} - -class ReplDelegate extends CachedListVirtualDelegate { - - constructor(private configurationService: IConfigurationService) { - super(); - } - - getHeight(element: IReplElement): number { - const config = this.configurationService.getValue('debug'); - - if (!config.console.wordWrap) { - return this.estimateHeight(element, true); - } - - return super.getHeight(element); - } - - protected estimateHeight(element: IReplElement, ignoreValueLength = false): number { - const config = this.configurationService.getValue('debug'); - const rowHeight = Math.ceil(1.4 * config.console.fontSize); - const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length); - const hasValue = (e: any): e is { value: string } => typeof e.value === 'string'; - - // Calculate a rough overestimation for the height - // For every 30 characters increase the number of lines needed - if (hasValue(element)) { - let value = element.value; - let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30)); - - return valueRows * rowHeight; - } - - return rowHeight; - } - - getTemplateId(element: IReplElement): string { - if (element instanceof Variable && element.name) { - return ReplVariablesRenderer.ID; - } - if (element instanceof ReplEvaluationResult) { - return ReplEvaluationResultsRenderer.ID; - } - if (element instanceof ReplEvaluationInput) { - return ReplEvaluationInputsRenderer.ID; - } - if (element instanceof SimpleReplElement || (element instanceof Variable && !element.name)) { - // Variable with no name is a top level variable which should be rendered like a repl element #17404 - return ReplSimpleElementsRenderer.ID; - } - - return ReplRawObjectsRenderer.ID; - } - - hasDynamicHeight(element: IReplElement): boolean { - // Empty elements should not have dynamic height since they will be invisible - return element.toString().length > 0; - } -} - -function isDebugSession(obj: any): obj is IDebugSession { - return typeof obj.getReplElements === 'function'; -} - -class ReplDataSource implements IAsyncDataSource { - - hasChildren(element: IReplElement | IDebugSession): boolean { - if (isDebugSession(element)) { - return true; - } - - return !!(element).hasChildren; - } - - getChildren(element: IReplElement | IDebugSession): Promise { - if (isDebugSession(element)) { - return Promise.resolve(element.getReplElements()); - } - if (element instanceof RawObjectReplElement) { - return element.getChildren(); - } - - return (element).getChildren(); - } -} - -class ReplAccessibilityProvider implements IAccessibilityProvider { - getAriaLabel(element: IReplElement): string { - if (element instanceof Variable) { - return nls.localize('replVariableAriaLabel', "Variable {0} has value {1}, read eval print loop, debug", element.name, element.value); - } - if (element instanceof SimpleReplElement || element instanceof ReplEvaluationInput || element instanceof ReplEvaluationResult) { - return nls.localize('replValueOutputAriaLabel', "{0}, read eval print loop, debug", element.value); - } - if (element instanceof RawObjectReplElement) { - return nls.localize('replRawObjectAriaLabel', "Repl variable {0} has value {1}, read eval print loop, debug", element.name, element.value); - } - - return ''; - } -} - - // Repl actions and commands class AcceptReplInputAction extends EditorAction { @@ -909,7 +591,7 @@ class AcceptReplInputAction extends EditorAction { constructor() { super({ id: 'repl.action.acceptInput', - label: nls.localize({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "REPL Accept Input"), + label: localize({ key: 'actions.repl.acceptInput', comment: ['Apply input from the debug console input box'] }, "REPL Accept Input"), alias: 'REPL Accept Input', precondition: CONTEXT_IN_DEBUG_REPL, kbOpts: { @@ -931,7 +613,7 @@ class FilterReplAction extends EditorAction { constructor() { super({ id: 'repl.action.filter', - label: nls.localize('repl.action.filter', "REPL Focus Content to Filter"), + label: localize('repl.action.filter', "REPL Focus Content to Filter"), alias: 'REPL Filter', precondition: CONTEXT_IN_DEBUG_REPL, kbOpts: { @@ -953,7 +635,7 @@ class ReplCopyAllAction extends EditorAction { constructor() { super({ id: 'repl.action.copyAll', - label: nls.localize('actions.repl.copyAll', "Debug: Console Copy All"), + label: localize('actions.repl.copyAll', "Debug: Console Copy All"), alias: 'Debug Console Copy All', precondition: CONTEXT_IN_DEBUG_REPL, }); @@ -986,7 +668,7 @@ class SelectReplActionViewItem extends FocusSessionActionViewItem { class SelectReplAction extends Action { static readonly ID = 'workbench.action.debug.selectRepl'; - static readonly LABEL = nls.localize('selectRepl', "Select Debug Console"); + static readonly LABEL = localize('selectRepl', "Select Debug Console"); constructor(id: string, label: string, @IDebugService private readonly debugService: IDebugService, @@ -1009,7 +691,7 @@ class SelectReplAction extends Action { export class ClearReplAction extends Action { static readonly ID = 'workbench.debug.panel.action.clearReplAction'; - static readonly LABEL = nls.localize('clearRepl', "Clear Console"); + static readonly LABEL = localize('clearRepl', "Clear Console"); constructor(id: string, label: string, @IPanelService private readonly panelService: IPanelService @@ -1020,6 +702,6 @@ export class ClearReplAction extends Action { async run(): Promise { const repl = this.panelService.openPanel(REPL_ID); await repl.clearRepl(); - aria.status(nls.localize('debugConsoleCleared', "Debug console was cleared")); + aria.status(localize('debugConsoleCleared', "Debug console was cleared")); } } diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts new file mode 100644 index 00000000000..11e4c1ddf55 --- /dev/null +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -0,0 +1,352 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import severity from 'vs/base/common/severity'; +import * as dom from 'vs/base/browser/dom'; +import { IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget'; +import { Variable } from 'vs/workbench/contrib/debug/common/debugModel'; +import { SimpleReplElement, RawObjectReplElement, ReplEvaluationInput, ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel'; +import { CachedListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { renderExpressionValue, AbstractExpressionsRenderer, IExpressionTemplateData, renderVariable, IInputBoxOptions } from 'vs/workbench/contrib/debug/browser/baseDebugView'; +import { handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling'; +import { ILabelService } from 'vs/platform/label/common/label'; +import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { FuzzyScore, createMatches } from 'vs/base/common/filters'; +import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; +import { IReplElementSource, IDebugService, IExpression, IReplElement, IDebugConfiguration, IDebugSession, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { localize } from 'vs/nls'; + +const $ = dom.$; + +interface IReplEvaluationInputTemplateData { + label: HighlightedLabel; +} + +interface IReplEvaluationResultTemplateData { + value: HTMLElement; + annotation: HTMLElement; +} + +interface ISimpleReplElementTemplateData { + container: HTMLElement; + value: HTMLElement; + source: HTMLElement; + getReplElementSource(): IReplElementSource | undefined; + toDispose: IDisposable[]; +} + +interface IRawObjectReplTemplateData { + container: HTMLElement; + expression: HTMLElement; + name: HTMLElement; + value: HTMLElement; + annotation: HTMLElement; + label: HighlightedLabel; +} + +export class ReplEvaluationInputsRenderer implements ITreeRenderer { + static readonly ID = 'replEvaluationInput'; + + get templateId(): string { + return ReplEvaluationInputsRenderer.ID; + } + + renderTemplate(container: HTMLElement): IReplEvaluationInputTemplateData { + dom.append(container, $('span.arrow.codicon.codicon-chevron-right')); + const input = dom.append(container, $('.expression')); + const label = new HighlightedLabel(input, false); + return { label }; + } + + renderElement(element: ITreeNode, index: number, templateData: IReplEvaluationInputTemplateData): void { + const evaluation = element.element; + templateData.label.set(evaluation.value, createMatches(element.filterData)); + } + + disposeTemplate(templateData: IReplEvaluationInputTemplateData): void { + // noop + } +} + +export class ReplEvaluationResultsRenderer implements ITreeRenderer { + static readonly ID = 'replEvaluationResult'; + + get templateId(): string { + return ReplEvaluationResultsRenderer.ID; + } + + constructor(private readonly linkDetector: LinkDetector) { } + + renderTemplate(container: HTMLElement): IReplEvaluationResultTemplateData { + dom.append(container, $('span.arrow.codicon.codicon-chevron-left')); + const output = dom.append(container, $('.evaluation-result.expression')); + const value = dom.append(output, $('span.value')); + const annotation = dom.append(output, $('span')); + + return { value, annotation }; + } + + renderElement(element: ITreeNode, index: number, templateData: IReplEvaluationResultTemplateData): void { + const expression = element.element; + renderExpressionValue(expression, templateData.value, { + preserveWhitespace: !expression.hasChildren, + showHover: false, + colorize: true, + linkDetector: this.linkDetector + }); + if (expression.hasChildren) { + templateData.annotation.className = 'annotation codicon codicon-info'; + templateData.annotation.title = localize('stateCapture', "Object state is captured from first evaluation"); + } + } + + disposeTemplate(templateData: IReplEvaluationResultTemplateData): void { + // noop + } +} + +export class ReplSimpleElementsRenderer implements ITreeRenderer { + static readonly ID = 'simpleReplElement'; + + constructor( + private readonly linkDetector: LinkDetector, + @IEditorService private readonly editorService: IEditorService, + @ILabelService private readonly labelService: ILabelService, + @IThemeService private readonly themeService: IThemeService + ) { } + + get templateId(): string { + return ReplSimpleElementsRenderer.ID; + } + + renderTemplate(container: HTMLElement): ISimpleReplElementTemplateData { + const data: ISimpleReplElementTemplateData = Object.create(null); + dom.addClass(container, 'output'); + const expression = dom.append(container, $('.output.expression.value-and-source')); + + data.container = container; + data.value = dom.append(expression, $('span.value')); + data.source = dom.append(expression, $('.source')); + data.toDispose = []; + data.toDispose.push(dom.addDisposableListener(data.source, 'click', e => { + e.preventDefault(); + e.stopPropagation(); + const source = data.getReplElementSource(); + if (source) { + source.source.openInEditor(this.editorService, { + startLineNumber: source.lineNumber, + startColumn: source.column, + endLineNumber: source.lineNumber, + endColumn: source.column + }); + } + })); + + return data; + } + + renderElement({ element }: ITreeNode, index: number, templateData: ISimpleReplElementTemplateData): void { + // value + dom.clearNode(templateData.value); + // Reset classes to clear ansi decorations since templates are reused + templateData.value.className = 'value'; + const result = handleANSIOutput(element.value, this.linkDetector, this.themeService, element.session); + templateData.value.appendChild(result); + + dom.addClass(templateData.value, (element.severity === severity.Warning) ? 'warn' : (element.severity === severity.Error) ? 'error' : (element.severity === severity.Ignore) ? 'ignore' : 'info'); + templateData.source.textContent = element.sourceData ? `${element.sourceData.source.name}:${element.sourceData.lineNumber}` : ''; + templateData.source.title = element.sourceData ? this.labelService.getUriLabel(element.sourceData.source.uri) : ''; + templateData.getReplElementSource = () => element.sourceData; + } + + disposeTemplate(templateData: ISimpleReplElementTemplateData): void { + dispose(templateData.toDispose); + } +} + +export class ReplVariablesRenderer extends AbstractExpressionsRenderer { + + static readonly ID = 'replVariable'; + + get templateId(): string { + return ReplVariablesRenderer.ID; + } + + constructor( + private readonly linkDetector: LinkDetector, + @IDebugService debugService: IDebugService, + @IContextViewService contextViewService: IContextViewService, + @IThemeService themeService: IThemeService, + ) { + super(debugService, contextViewService, themeService); + } + + protected renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void { + renderVariable(expression as Variable, data, true, highlights, this.linkDetector); + } + + protected getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined { + return undefined; + } +} + +export class ReplRawObjectsRenderer implements ITreeRenderer { + static readonly ID = 'rawObject'; + + constructor(private readonly linkDetector: LinkDetector) { } + + get templateId(): string { + return ReplRawObjectsRenderer.ID; + } + + renderTemplate(container: HTMLElement): IRawObjectReplTemplateData { + dom.addClass(container, 'output'); + + const expression = dom.append(container, $('.output.expression')); + const name = dom.append(expression, $('span.name')); + const label = new HighlightedLabel(name, false); + const value = dom.append(expression, $('span.value')); + const annotation = dom.append(expression, $('span')); + + return { container, expression, name, label, value, annotation }; + } + + renderElement(node: ITreeNode, index: number, templateData: IRawObjectReplTemplateData): void { + // key + const element = node.element; + templateData.label.set(element.name ? `${element.name}:` : '', createMatches(node.filterData)); + if (element.name) { + templateData.name.textContent = `${element.name}:`; + } else { + templateData.name.textContent = ''; + } + + // value + renderExpressionValue(element.value, templateData.value, { + preserveWhitespace: true, + showHover: false, + linkDetector: this.linkDetector + }); + + // annotation if any + if (element.annotation) { + templateData.annotation.className = 'annotation codicon codicon-info'; + templateData.annotation.title = element.annotation; + } else { + templateData.annotation.className = ''; + templateData.annotation.title = ''; + } + } + + disposeTemplate(templateData: IRawObjectReplTemplateData): void { + // noop + } +} + +export class ReplDelegate extends CachedListVirtualDelegate { + + constructor(private configurationService: IConfigurationService) { + super(); + } + + getHeight(element: IReplElement): number { + const config = this.configurationService.getValue('debug'); + + if (!config.console.wordWrap) { + return this.estimateHeight(element, true); + } + + return super.getHeight(element); + } + + protected estimateHeight(element: IReplElement, ignoreValueLength = false): number { + const config = this.configurationService.getValue('debug'); + const rowHeight = Math.ceil(1.4 * config.console.fontSize); + const countNumberOfLines = (str: string) => Math.max(1, (str && str.match(/\r\n|\n/g) || []).length); + const hasValue = (e: any): e is { value: string } => typeof e.value === 'string'; + + // Calculate a rough overestimation for the height + // For every 30 characters increase the number of lines needed + if (hasValue(element)) { + let value = element.value; + let valueRows = countNumberOfLines(value) + (ignoreValueLength ? 0 : Math.floor(value.length / 30)); + + return valueRows * rowHeight; + } + + return rowHeight; + } + + getTemplateId(element: IReplElement): string { + if (element instanceof Variable && element.name) { + return ReplVariablesRenderer.ID; + } + if (element instanceof ReplEvaluationResult) { + return ReplEvaluationResultsRenderer.ID; + } + if (element instanceof ReplEvaluationInput) { + return ReplEvaluationInputsRenderer.ID; + } + if (element instanceof SimpleReplElement || (element instanceof Variable && !element.name)) { + // Variable with no name is a top level variable which should be rendered like a repl element #17404 + return ReplSimpleElementsRenderer.ID; + } + + return ReplRawObjectsRenderer.ID; + } + + hasDynamicHeight(element: IReplElement): boolean { + // Empty elements should not have dynamic height since they will be invisible + return element.toString().length > 0; + } +} + +function isDebugSession(obj: any): obj is IDebugSession { + return typeof obj.getReplElements === 'function'; +} + +export class ReplDataSource implements IAsyncDataSource { + + hasChildren(element: IReplElement | IDebugSession): boolean { + if (isDebugSession(element)) { + return true; + } + + return !!(element).hasChildren; + } + + getChildren(element: IReplElement | IDebugSession): Promise { + if (isDebugSession(element)) { + return Promise.resolve(element.getReplElements()); + } + if (element instanceof RawObjectReplElement) { + return element.getChildren(); + } + + return (element).getChildren(); + } +} + +export class ReplAccessibilityProvider implements IAccessibilityProvider { + getAriaLabel(element: IReplElement): string { + if (element instanceof Variable) { + return localize('replVariableAriaLabel', "Variable {0} has value {1}, read eval print loop, debug", element.name, element.value); + } + if (element instanceof SimpleReplElement || element instanceof ReplEvaluationInput || element instanceof ReplEvaluationResult) { + return localize('replValueOutputAriaLabel', "{0}, read eval print loop, debug", element.value); + } + if (element instanceof RawObjectReplElement) { + return localize('replRawObjectAriaLabel', "Repl variable {0} has value {1}, read eval print loop, debug", element.name, element.value); + } + + return ''; + } +} diff --git a/src/vs/workbench/contrib/debug/browser/startView.ts b/src/vs/workbench/contrib/debug/browser/startView.ts index 840f81a3b2e..6e7aee1172d 100644 --- a/src/vs/workbench/contrib/debug/browser/startView.ts +++ b/src/vs/workbench/contrib/debug/browser/startView.ts @@ -14,7 +14,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { localize } from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { StartAction, RunAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; +import { StartAction, ConfigureAction } from 'vs/workbench/contrib/debug/browser/debugActions'; import { IDebugService } from 'vs/workbench/contrib/debug/common/debug'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; @@ -31,7 +31,6 @@ export class StartView extends ViewPane { static LABEL = localize('start', "Start"); private debugButton!: Button; - private runButton!: Button; private firstMessageContainer!: HTMLElement; private secondMessageContainer!: HTMLElement; private clickElement: HTMLElement | undefined; @@ -63,21 +62,13 @@ export class StartView extends ViewPane { const enabled = this.debuggerLabels.length > 0; this.debugButton.enabled = enabled; - this.runButton.enabled = enabled; const debugKeybinding = this.keybindingService.lookupKeybinding(StartAction.ID); - let debugLabel = this.debuggerLabels.length !== 1 ? localize('debug', "Debug") : localize('debugWith', "Debug with {0}", this.debuggerLabels[0]); + let debugLabel = this.debuggerLabels.length !== 1 ? localize('debug', "Run and Debug") : localize('debugWith', "Run and Debug {0}", this.debuggerLabels[0]); if (debugKeybinding) { debugLabel += ` (${debugKeybinding.getLabel()})`; } this.debugButton.label = debugLabel; - let runLabel = this.debuggerLabels.length !== 1 ? localize('run', "Run") : localize('runWith', "Run with {0}", this.debuggerLabels[0]); - const runKeybinding = this.keybindingService.lookupKeybinding(RunAction.ID); - if (runKeybinding) { - runLabel += ` (${runKeybinding.getLabel()})`; - } - this.runButton.label = runLabel; - const emptyWorkbench = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY; this.firstMessageContainer.innerHTML = ''; this.secondMessageContainer.innerHTML = ''; @@ -85,12 +76,12 @@ export class StartView extends ViewPane { this.secondMessageContainer.appendChild(secondMessageElement); const setSecondMessage = () => { - secondMessageElement.textContent = localize('specifyHowToRun', "To further configure Debug and Run"); + secondMessageElement.textContent = localize('specifyHowToRun', "To customize Run and Debug"); this.clickElement = this.createClickElement(localize('configure', " create a launch.json file."), () => this.commandService.executeCommand(ConfigureAction.ID)); this.secondMessageContainer.appendChild(this.clickElement); }; const setSecondMessageWithFolder = () => { - secondMessageElement.textContent = localize('noLaunchConfiguration', "To further configure Debug and Run, "); + secondMessageElement.textContent = localize('noLaunchConfiguration', "To customize Run and Debug, "); this.clickElement = this.createClickElement(localize('openFolder', " open a folder"), () => this.dialogService.pickFolderAndOpen({ forceNewWindow: false })); this.secondMessageContainer.appendChild(this.clickElement); @@ -152,14 +143,7 @@ export class StartView extends ViewPane { })); attachButtonStyler(this.debugButton, this.themeService); - this.runButton = new Button(container); - this.runButton.label = localize('run', "Run"); - dom.addClass(container, 'debug-start-view'); - this._register(this.runButton.onDidClick(() => { - this.commandService.executeCommand(RunAction.ID); - })); - attachButtonStyler(this.runButton, this.themeService); this.secondMessageContainer = $('.section'); container.appendChild(this.secondMessageContainer); diff --git a/src/vs/workbench/contrib/debug/common/debug.ts b/src/vs/workbench/contrib/debug/common/debug.ts index 10118761779..bbfe0526e1a 100644 --- a/src/vs/workbench/contrib/debug/common/debug.ts +++ b/src/vs/workbench/contrib/debug/common/debug.ts @@ -24,11 +24,8 @@ import { TaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks'; import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { Extensions as ViewContainerExtensions, IViewContainersRegistry, ViewContainer } from 'vs/workbench/common/views'; -import { Registry } from 'vs/platform/registry/common/platform'; export const VIEWLET_ID = 'workbench.view.debug'; -export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); export const VARIABLES_VIEW_ID = 'workbench.debug.variablesView'; export const WATCH_VIEW_ID = 'workbench.debug.watchExpressionsView'; @@ -466,6 +463,7 @@ export interface IDebugConfiguration { fontFamily: string; lineHeight: number; wordWrap: boolean; + closeOnEnd: boolean; }; focusWindowOnBreak: boolean; onTaskErrors: 'debugAnyway' | 'showErrors' | 'prompt'; @@ -487,13 +485,19 @@ export interface IEnvConfig { noDebug?: boolean; } +export interface IConfigPresentation { + hidden?: boolean; + group?: string; + order?: number; +} + export interface IConfig extends IEnvConfig { // fundamental attributes type: string; request: string; name: string; - + presentation?: IConfigPresentation; // platform specifics windows?: IEnvConfig; osx?: IEnvConfig; @@ -510,6 +514,7 @@ export interface ICompound { name: string; preLaunchTask?: string | TaskIdentifier; configurations: (string | { name: string, folder: string })[]; + presentation?: IConfigPresentation; } export interface IDebugAdapter extends IDisposable { @@ -594,6 +599,7 @@ export interface IDebuggerContribution extends IPlatformSpecificAdapterContribut export interface IDebugConfigurationProvider { readonly type: string; resolveDebugConfiguration?(folderUri: uri | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; + resolveDebugConfigurationWithSubstitutedVariables?(folderUri: uri | undefined, debugConfiguration: IConfig, token: CancellationToken): Promise; provideDebugConfigurations?(folderUri: uri | undefined, token: CancellationToken): Promise; debugAdapterExecutable?(folderUri: uri | undefined): Promise; // TODO@AW legacy } @@ -631,6 +637,8 @@ export interface IConfigurationManager { getLaunch(workspaceUri: uri | undefined): ILaunch | undefined; + getAllConfigurations(): { launch: ILaunch, name: string, presentation?: IConfigPresentation }[]; + /** * Allows to register on change of selected debug configuration. */ diff --git a/src/vs/workbench/contrib/debug/common/debugModel.ts b/src/vs/workbench/contrib/debug/common/debugModel.ts index a7530a41686..1fafbec02bf 100644 --- a/src/vs/workbench/contrib/debug/common/debugModel.ts +++ b/src/vs/workbench/contrib/debug/common/debugModel.ts @@ -118,9 +118,12 @@ export class ExpressionContainer implements IExpressionContainer { try { const response = await this.session!.variables(this.reference || 0, this.threadId, filter, start, count); return response && response.body && response.body.variables - ? distinct(response.body.variables.filter(v => !!v && isString(v.name)), (v: DebugProtocol.Variable) => v.name).map((v: DebugProtocol.Variable) => - new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type)) - : []; + ? distinct(response.body.variables.filter(v => !!v), v => v.name).map(v => { + if (isString(v.value) && isString(v.name) && typeof v.variablesReference === 'number') { + return new Variable(this.session, this.threadId, this, v.variablesReference, v.name, v.evaluateName, v.value, v.namedVariables, v.indexedVariables, v.presentationHint, v.type); + } + return new Variable(this.session, this.threadId, this, 0, '', undefined, nls.localize('invalidVariableAttributes', "Invalid variable attributes"), 0, 0, { kind: 'virtual' }, undefined, false); + }) : []; } catch (e) { return [new Variable(this.session, this.threadId, this, 0, '', undefined, e.message, 0, 0, { kind: 'virtual' }, undefined, false)]; } @@ -157,10 +160,6 @@ export class ExpressionContainer implements IExpressionContainer { this.session = session; try { const response = await session.evaluate(expression, stackFrame ? stackFrame.frameId : undefined, context); - if (response && response.success === false) { - this.value = response.message || ''; - return false; - } if (response && response.body) { this.value = response.body.result || ''; @@ -1183,8 +1182,8 @@ export class DebugModel implements IDebugModel { return this.watchExpressions; } - addWatchExpression(name: string): IExpression { - const we = new Expression(name); + addWatchExpression(name?: string): IExpression { + const we = new Expression(name || ''); this.watchExpressions.push(we); this._onDidChangeWatchExpressions.fire(we); diff --git a/src/vs/workbench/contrib/debug/common/debugSchemas.ts b/src/vs/workbench/contrib/debug/common/debugSchemas.ts index a2a4b16a6bf..20d546e3a31 100644 --- a/src/vs/workbench/contrib/debug/common/debugSchemas.ts +++ b/src/vs/workbench/contrib/debug/common/debugSchemas.ts @@ -132,6 +132,33 @@ export const breakpointsExtPoint = extensionsRegistry.ExtensionsRegistry.registe }); // debug general schema + +export const presentationSchema: IJSONSchema = { + type: 'object', + description: nls.localize('presentation', "Presentation options on how to show this configuration in the debug configuration dropdown and the command palette."), + properties: { + hidden: { + type: 'boolean', + default: false, + description: nls.localize('presentation.hidden', "Controls if this configuration should be shown in the configuration dropdown and the command palette.") + }, + group: { + type: 'string', + default: '', + description: nls.localize('presentation.group', "Group that this configuration belongs to. Used for grouping and sorting in the configuration dropdown and the command palette.") + }, + order: { + type: 'number', + default: 1, + description: nls.localize('presentation.order', "Order of this configuration within a group. Used for grouping and sorting in the configuration dropdown and the command palette.") + } + }, + default: { + hidden: false, + group: '', + order: 1 + } +}; const defaultCompound: ICompound = { name: 'Compound', configurations: [] }; export const launchSchema: IJSONSchema = { id: launchSchemaId, @@ -167,6 +194,7 @@ export const launchSchema: IJSONSchema = { type: 'string', description: nls.localize('app.launch.json.compound.name', "Name of compound. Appears in the launch configuration drop down menu.") }, + presentation: presentationSchema, configurations: { type: 'array', default: [], diff --git a/src/vs/workbench/contrib/debug/common/debugger.ts b/src/vs/workbench/contrib/debug/common/debugger.ts index 0e1045b3822..770bd7ef846 100644 --- a/src/vs/workbench/contrib/debug/common/debugger.ts +++ b/src/vs/workbench/contrib/debug/common/debugger.ts @@ -22,6 +22,7 @@ import { URI } from 'vs/base/common/uri'; import { Schemas } from 'vs/base/common/network'; import { isDebuggerMainContribution } from 'vs/workbench/contrib/debug/common/debugUtils'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; +import { presentationSchema } from 'vs/workbench/contrib/debug/common/debugSchemas'; export class Debugger implements IDebugger { @@ -222,7 +223,7 @@ export class Debugger implements IDebugger { }; properties['name'] = { type: 'string', - description: nls.localize('debugName', "Name of configuration; appears in the launch configuration drop down menu."), + description: nls.localize('debugName', "Name of configuration; appears in the launch configuration dropdown menu."), default: 'Launch' }; properties['request'] = { @@ -250,6 +251,7 @@ export class Debugger implements IDebugger { defaultSnippets: [{ body: { task: '', type: '' } }], description: nls.localize('debugPostDebugTask', "Task to run after debug session ends.") }; + properties['presentation'] = presentationSchema; properties['internalConsoleOptions'] = INTERNAL_CONSOLE_OPTIONS_SCHEMA; // Clear out windows, linux and osx fields to not have cycles inside the properties object delete properties['windows']; diff --git a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts index bead472a090..97d165e501a 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugANSIHandling.test.ts @@ -30,7 +30,7 @@ suite('Debug - ANSI Handling', () => { */ setup(() => { model = new DebugModel([], [], [], [], [], { isDirty: (e: any) => false }); - session = new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); + session = new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, undefined, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); const instantiationService: TestInstantiationService = workbenchInstantiationService(); linkDetector = instantiationService.createInstance(LinkDetector); diff --git a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts index 97c4bc39c37..977a5a0d861 100644 --- a/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts +++ b/src/vs/workbench/contrib/debug/test/browser/debugModel.test.ts @@ -18,7 +18,7 @@ import { RawDebugSession } from 'vs/workbench/contrib/debug/browser/rawDebugSess import { timeout } from 'vs/base/common/async'; function createMockSession(model: DebugModel, name = 'mockSession', options?: IDebugSessionOptions): DebugSession { - return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService); + return new DebugSession({ resolved: { name, type: 'node', request: 'launch' }, unresolved: undefined }, undefined!, model, options, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, undefined!, NullOpenerService, undefined!); } suite('Debug - Model', () => { diff --git a/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts index 67b6159734f..cc24a8695e8 100644 --- a/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts +++ b/src/vs/workbench/contrib/experiments/browser/experiments.contribution.ts @@ -3,13 +3,31 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IExperimentService, ExperimentService } from 'vs/workbench/contrib/experiments/common/experimentService'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { ExperimentalPrompts } from 'vs/workbench/contrib/experiments/browser/experimentalPrompt'; +import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; +import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuration'; registerSingleton(IExperimentService, ExperimentService, true); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(ExperimentalPrompts, LifecyclePhase.Eventually); + +const registry = Registry.as(ConfigurationExtensions.Configuration); + +// Configuration +registry.registerConfiguration({ + ...workbenchConfigurationNodeBase, + 'properties': { + 'workbench.enableExperiments': { + 'type': 'boolean', + 'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."), + 'default': true, + 'tags': ['usesOnlineServices'] + } + } +}); diff --git a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts index a083e174f82..80f565cbd2d 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts @@ -6,7 +6,7 @@ import { localize } from 'vs/nls'; import { KeyMod, KeyChord, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; -import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { SyncActionDescriptor, MenuRegistry, MenuId, registerAction } from 'vs/platform/actions/common/actions'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { ExtensionsLabel, ExtensionsChannelId, PreferencesLabel, IExtensionManagementService, IExtensionGalleryService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/workbench/services/extensionManagement/common/extensionManagement'; @@ -22,9 +22,8 @@ import { EnableAllAction, EnableAllWorkspaceAction, DisableAllAction, DisableAllWorkspaceAction, CheckForUpdatesAction, ShowLanguageExtensionsAction, ShowAzureExtensionsAction, EnableAutoUpdateAction, DisableAutoUpdateAction, ConfigureRecommendedExtensionsCommandsContributor, InstallVSIXAction, ReinstallAction, InstallSpecificVersionOfExtensionAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { ExtensionsInput } from 'vs/workbench/contrib/extensions/common/extensionsInput'; -import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor } from 'vs/workbench/browser/viewlet'; import { ExtensionEditor } from 'vs/workbench/contrib/extensions/browser/extensionEditor'; -import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewlet } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; +import { StatusUpdater, MaliciousExtensionChecker, ExtensionsViewletViewsContribution, ExtensionsViewPaneContainer } from 'vs/workbench/contrib/extensions/browser/extensionsViewlet'; import { IQuickOpenRegistry, Extensions, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import * as jsonContributionRegistry from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -44,6 +43,10 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { ExtensionType } from 'vs/platform/extensions/common/extensions'; import { RemoteExtensionsInstaller } from 'vs/workbench/contrib/extensions/browser/remoteExtensionsInstaller'; import { ExtensionTipsService } from 'vs/workbench/contrib/extensions/browser/extensionTipsService'; +import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; +import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; +import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; // Singletons registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService); @@ -75,17 +78,15 @@ Registry.as(EditorExtensions.Editors).registerEditor( new SyncDescriptor(ExtensionsInput) ]); -// Viewlet -const viewletDescriptor = ViewletDescriptor.create( - ExtensionsViewlet, - VIEWLET_ID, - localize('extensions', "Extensions"), - 'codicon-extensions', - 4 -); +Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer( + { + id: VIEWLET_ID, + name: localize('extensions', "Extensions"), + ctorDescriptor: { ctor: ExtensionsViewPaneContainer }, + icon: 'codicon-extensions', + order: 4 + }, ViewContainerLocation.Sidebar); -Registry.as(ViewletExtensions.Viewlets) - .registerViewlet(viewletDescriptor); // Global actions const actionRegistry = Registry.as(WorkbenchActionExtensions.WorkbenchActions); @@ -338,6 +339,58 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { order: 3 }); +// Extension Context Menu + +registerAction({ + id: 'workbench.extensions.action.copyExtension', + title: { value: localize('workbench.extensions.action.copyExtension', "Copy"), original: 'Copy' }, + async handler(accessor, extensionId: string) { + const extensionWorkbenchService = accessor.get(IExtensionsWorkbenchService); + let extension = extensionWorkbenchService.local.filter(e => areSameExtensions(e.identifier, { id: extensionId }))[0] + || (await extensionWorkbenchService.queryGallery({ names: [extensionId], pageSize: 1 }, CancellationToken.None)).firstPage[0]; + if (extension) { + const name = localize('extensionInfoName', 'Name: {0}', extension.displayName); + const id = localize('extensionInfoId', 'Id: {0}', extensionId); + const description = localize('extensionInfoDescription', 'Description: {0}', extension.description); + const verision = localize('extensionInfoVersion', 'Version: {0}', extension.version); + const publisher = localize('extensionInfoPublisher', 'Publisher: {0}', extension.publisherDisplayName); + const link = extension.url ? localize('extensionInfoVSMarketplaceLink', 'VS Marketplace Link: {0}', `${extension.url}`) : null; + const clipboardStr = `${name}\n${id}\n${description}\n${verision}\n${publisher}${link ? '\n' + link : ''}`; + await accessor.get(IClipboardService).writeText(clipboardStr); + } + }, + menu: { + menuId: MenuId.ExtensionContext, + group: '1_copy' + }, +}); + +registerAction({ + id: 'workbench.extensions.action.copyExtensionId', + title: { value: localize('workbench.extensions.action.copyExtensionId', "Copy Extension Id"), original: 'Copy Extension Id' }, + async handler(accessor, id: string) { + await accessor.get(IClipboardService).writeText(id); + }, + menu: { + menuId: MenuId.ExtensionContext, + group: '1_copy' + }, +}); + +registerAction({ + id: 'workbench.extensions.action.configure', + title: { value: localize('workbench.extensions.action.configure', "Configure..."), original: 'Configure...' }, + async handler(accessor, id: string) { + await accessor.get(IPreferencesService).openSettings(false, `@ext:${id}`); + }, + menu: { + menuId: MenuId.ExtensionContext, + group: '2_configure', + when: ContextKeyExpr.and(ContextKeyExpr.equals('extensionStatus', 'installed'), ContextKeyExpr.has('extensionHasConfiguration')) + }, + +}); + const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); class ExtensionsContributions implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts index b999f540f0b..aec0c61777e 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsActions.ts @@ -39,7 +39,7 @@ import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { PagedModel } from 'vs/base/common/paging'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; +import { MenuRegistry, MenuId, IMenuService } from 'vs/platform/actions/common/actions'; import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IOpenerService } from 'vs/platform/opener/common/opener'; @@ -55,10 +55,8 @@ import { coalesce } from 'vs/base/common/arrays'; import { IWorkbenchThemeService, COLOR_THEME_SETTING, ICON_THEME_SETTING, IFileIconTheme, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { ILabelService } from 'vs/platform/label/common/label'; import { prefersExecuteOnUI, prefersExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil'; -import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IProductService } from 'vs/platform/product/common/productService'; -import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; @@ -662,6 +660,14 @@ export class DropDownMenuActionViewItem extends ExtensionActionViewItem { } } +export function getContextMenuActions(menuService: IMenuService, contextKeyService: IContextKeyService): ExtensionAction[][] { + const groups: ExtensionAction[][] = []; + const menu = menuService.createMenu(MenuId.ExtensionContext, contextKeyService); + menu.getActions({ shouldForwardArgs: true }).forEach(([, actions]) => groups.push(actions.map(action => new MenuItemExtensionAction(action)))); + menu.dispose(); + return groups; +} + export class ManageExtensionAction extends ExtensionDropDownAction { static readonly ID = 'extensions.manage'; @@ -671,7 +677,9 @@ export class ManageExtensionAction extends ExtensionDropDownAction { constructor( @IInstantiationService instantiationService: IInstantiationService, @IExtensionService private readonly extensionService: IExtensionService, - @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService + @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, + @IMenuService private readonly menuService: IMenuService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, ) { super(ManageExtensionAction.ID, '', '', true, true, instantiationService); @@ -708,13 +716,10 @@ export class ManageExtensionAction extends ExtensionDropDownAction { groups.push([this.instantiationService.createInstance(UninstallAction)]); groups.push([this.instantiationService.createInstance(InstallAnotherVersionAction)]); - if (this.extension) { - const extensionActions: ExtensionAction[] = [this.instantiationService.createInstance(ExtensionInfoAction)]; - if (this.extension.local && this.extension.local.manifest.contributes && this.extension.local.manifest.contributes.configuration) { - extensionActions.push(this.instantiationService.createInstance(ExtensionSettingsAction)); - } - groups.push(extensionActions); - } + const contextKeyService = this.contextKeyService.createScoped(); + contextKeyService.createKey('extensionStatus', 'installed'); + contextKeyService.createKey('extensionHasConfiguration', !!this.extension && !!this.extension.local && !!this.extension.local.manifest.contributes && !!this.extension.local.manifest.contributes.configuration); + getContextMenuActions(this.menuService, contextKeyService).forEach(actions => groups.push(actions)); groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = this.extension)); @@ -740,6 +745,21 @@ export class ManageExtensionAction extends ExtensionDropDownAction { } } +export class MenuItemExtensionAction extends ExtensionAction { + + constructor(private readonly action: IAction) { + super(action.id, action.label); + } + + update() { } + + async run(): Promise { + if (this.extension) { + return this.action.run(this.extension.identifier.id); + } + } +} + export class InstallAnotherVersionAction extends ExtensionAction { static readonly ID = 'workbench.extensions.action.install.anotherVersion'; @@ -792,65 +812,6 @@ export class InstallAnotherVersionAction extends ExtensionAction { } } -export class ExtensionInfoAction extends ExtensionAction { - - static readonly ID = 'extensions.extensionInfo'; - static readonly LABEL = localize('extensionInfoAction', "Copy Extension Information"); - - constructor( - @IClipboardService private readonly clipboardService: IClipboardService - ) { - super(ExtensionInfoAction.ID, ExtensionInfoAction.LABEL); - this.update(); - } - - update(): void { - this.enabled = !!this.extension; - } - - async run(): Promise { - if (!this.extension) { - return; - } - - const name = localize('extensionInfoName', 'Name: {0}', this.extension.displayName); - const id = localize('extensionInfoId', 'Id: {0}', this.extension.identifier.id); - const description = localize('extensionInfoDescription', 'Description: {0}', this.extension.description); - const verision = localize('extensionInfoVersion', 'Version: {0}', this.extension.version); - const publisher = localize('extensionInfoPublisher', 'Publisher: {0}', this.extension.publisherDisplayName); - const link = this.extension.url ? localize('extensionInfoVSMarketplaceLink', 'VS Marketplace Link: {0}', this.extension.url.toString()) : null; - - const clipboardStr = `${name}\n${id}\n${description}\n${verision}\n${publisher}${link ? '\n' + link : ''}`; - - return this.clipboardService.writeText(clipboardStr); - } -} - -export class ExtensionSettingsAction extends ExtensionAction { - - static readonly ID = 'extensions.extensionSettings'; - static readonly LABEL = localize('extensionSettingsAction', "Configure Extension Settings"); - - constructor( - @IPreferencesService private readonly preferencesService: IPreferencesService - ) { - super(ExtensionSettingsAction.ID, ExtensionSettingsAction.LABEL); - this.update(); - } - - update(): void { - this.enabled = !!this.extension; - } - - async run(): Promise { - if (!this.extension) { - return; - } - this.preferencesService.openSettings(false, `@ext:${this.extension.identifier.id}`); - return Promise.resolve(); - } -} - export class EnableForWorkspaceAction extends ExtensionAction { static readonly ID = 'extensions.enableForWorkspace'; diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts index 32944476223..eb05d9ef19a 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViewlet.ts @@ -18,7 +18,7 @@ import { append, $, addClass, toggleClass, Dimension } from 'vs/base/browser/dom import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, ShowRecommendationsOnlyOnDemandKey, CloseExtensionDetailsOnViewChangeKey, VIEW_CONTAINER } from '../common/extensions'; +import { IExtensionsWorkbenchService, IExtensionsViewPaneContainer, VIEWLET_ID, AutoUpdateConfigurationKey, ShowRecommendationsOnlyOnDemandKey, CloseExtensionDetailsOnViewChangeKey } from '../common/extensions'; import { ShowEnabledExtensionsAction, ShowInstalledExtensionsAction, ShowRecommendedExtensionsAction, ShowPopularExtensionsAction, ShowDisabledExtensionsAction, ShowOutdatedExtensionsAction, ClearExtensionsInputAction, ChangeSortAction, UpdateAllAction, CheckForUpdatesAction, DisableAllAction, EnableAllAction, @@ -35,7 +35,7 @@ import Severity from 'vs/base/common/severity'; import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; +import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewContainersRegistry } from 'vs/workbench/common/views'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IContextKeyService, ContextKeyExpr, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -57,7 +57,6 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { RemoteNameContext } from 'vs/workbench/browser/contextkeys'; import { ILabelService } from 'vs/platform/label/common/label'; import { MementoObject } from 'vs/workbench/common/memento'; -import { Viewlet } from 'vs/workbench/browser/viewlet'; const NonEmptyWorkspaceContext = new RawContextKey('nonEmptyWorkspace', false); const DefaultViewsContext = new RawContextKey('defaultExtensionViews', true); @@ -88,10 +87,13 @@ const viewIdNameMappings: { [id: string]: string } = { export class ExtensionsViewletViewsContribution implements IWorkbenchContribution { + private readonly container: ViewContainer; + constructor( @IExtensionManagementServerService private readonly extensionManagementServerService: IExtensionManagementServerService, @ILabelService private readonly labelService: ILabelService, ) { + this.container = Registry.as(Extensions.ViewContainersRegistry).get(VIEWLET_ID)!; this.registerViews(); } @@ -117,7 +119,7 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio viewDescriptors.push(...this.createExtensionsViewDescriptorsForServer(this.extensionManagementServerService.remoteExtensionManagementServer)); } - Registry.as(Extensions.ViewsRegistry).registerViews(viewDescriptors, VIEW_CONTAINER); + Registry.as(Extensions.ViewsRegistry).registerViews(viewDescriptors, this.container); } // View used for any kind of searching @@ -314,22 +316,6 @@ export class ExtensionsViewletViewsContribution implements IWorkbenchContributio } -export class ExtensionsViewlet extends Viewlet { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService protected storageService: IStorageService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IExtensionService protected extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, - @IConfigurationService protected configurationService: IConfigurationService - ) { - super(VIEWLET_ID, instantiationService.createInstance(ExtensionsViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } -} - export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IExtensionsViewPaneContainer { private readonly _onSearchChange: Emitter = this._register(new Emitter()); @@ -370,7 +356,7 @@ export class ExtensionsViewPaneContainer extends ViewPaneContainer implements IE @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); this.searchDelayer = new Delayer(500); this.nonEmptyWorkspaceContextKey = NonEmptyWorkspaceContext.bindTo(contextKeyService); diff --git a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts index adaa265da5c..6047594ffb5 100644 --- a/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts +++ b/src/vs/workbench/contrib/extensions/browser/extensionsViews.ts @@ -28,7 +28,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge'; import { Separator } from 'vs/base/browser/ui/actionbar/actionbar'; -import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; +import { InstallWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, ManageExtensionAction, InstallLocalExtensionsInRemoteAction, getContextMenuActions } from 'vs/workbench/contrib/extensions/browser/extensionsActions'; import { WorkbenchPagedList } from 'vs/platform/list/browser/listService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; @@ -48,6 +48,7 @@ import { IProductService } from 'vs/platform/product/common/productService'; import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { IMenuService } from 'vs/platform/actions/common/actions'; // Extensions that are automatically classified as Programming Language extensions, but should be Feature extensions const FORCE_FEATURE_EXTENSIONS = ['vscode.git', 'vscode.search-result']; @@ -106,7 +107,8 @@ export class ExtensionsListView extends ViewPane { @IWorkbenchThemeService private readonly workbenchThemeService: IWorkbenchThemeService, @IExtensionManagementServerService protected readonly extensionManagementServerService: IExtensionManagementServerService, @IProductService protected readonly productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IMenuService private readonly menuService: IMenuService, ) { super({ ...(options as IViewPaneOptions), ariaHeaderLabel: options.title, showActionsAlways: true }, keybindingService, contextMenuService, configurationService, contextKeyService); this.server = options.server; @@ -228,16 +230,27 @@ export class ExtensionsListView extends ViewPane { const fileIconThemes = await this.workbenchThemeService.getFileIconThemes(); const manageExtensionAction = this.instantiationService.createInstance(ManageExtensionAction); manageExtensionAction.extension = e.element; - const groups = manageExtensionAction.getActionGroups(runningExtensions, colorThemes, fileIconThemes); - let actions: IAction[] = []; - for (const menuActions of groups) { - actions = [...actions, ...menuActions, new Separator()]; - } if (manageExtensionAction.enabled) { + const groups = manageExtensionAction.getActionGroups(runningExtensions, colorThemes, fileIconThemes); + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } this.contextMenuService.showContextMenu({ getAnchor: () => e.anchor, getActions: () => actions.slice(0, actions.length - 1) }); + } else if (e.element) { + const groups = getContextMenuActions(this.menuService, this.contextKeyService.createScoped()); + groups.forEach(group => group.forEach(extensionAction => extensionAction.extension = e.element!)); + let actions: IAction[] = []; + for (const menuActions of groups) { + actions = [...actions, ...menuActions, new Separator()]; + } + this.contextMenuService.showContextMenu({ + getAnchor: () => e.anchor, + getActions: () => actions + }); } } } @@ -866,10 +879,11 @@ export class ServerExtensionsView extends ExtensionsListView { @IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService, @IExtensionManagementServerService extensionManagementServerService: IExtensionManagementServerService, @IProductService productService: IProductService, - @IContextKeyService contextKeyService: IContextKeyService + @IContextKeyService contextKeyService: IContextKeyService, + @IMenuService menuService: IMenuService, ) { options.server = server; - super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService); + super(options, notificationService, keybindingService, contextMenuService, instantiationService, themeService, extensionService, extensionsWorkbenchService, editorService, tipsService, telemetryService, configurationService, contextService, experimentService, workbenchThemeService, extensionManagementServerService, productService, contextKeyService, menuService); this._register(onDidChangeTitle(title => this.updateTitle(title))); } diff --git a/src/vs/workbench/contrib/extensions/common/extensions.ts b/src/vs/workbench/contrib/extensions/common/extensions.ts index 373ee7b0430..d54dd9ac3f2 100644 --- a/src/vs/workbench/contrib/extensions/common/extensions.ts +++ b/src/vs/workbench/contrib/extensions/common/extensions.ts @@ -14,11 +14,8 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions'; import { URI } from 'vs/base/common/uri'; import { IViewPaneContainer } from 'vs/workbench/common/viewPaneContainer'; -import { Extensions as ViewContainerExtensions, ViewContainer, IViewContainersRegistry } from 'vs/workbench/common/views'; -import { Registry } from 'vs/platform/registry/common/platform'; export const VIEWLET_ID = 'workbench.view.extensions'; -export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); export const EXTENSIONS_CONFIG = '.vscode/extensions.json'; diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts index 92699a26fa3..a1bf560bef7 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorTracker.ts @@ -5,11 +5,10 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { URI } from 'vs/base/common/uri'; -import * as resources from 'vs/base/common/resources'; import { IEditorViewState } from 'vs/editor/common/editorCommon'; import { toResource, SideBySideEditorInput, IWorkbenchEditorConfiguration, SideBySideEditor as SideBySideEditorChoice } from 'vs/workbench/common/editor'; import { ITextFileService, TextFileModelChangeEvent, ModelState } from 'vs/workbench/services/textfile/common/textfiles'; -import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files'; +import { FileOperationEvent, FileOperation, IFileService, FileChangeType, FileChangesEvent, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle'; import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -25,6 +24,7 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor import { timeout } from 'vs/base/common/async'; import { withNullAsUndefined } from 'vs/base/common/types'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { isEqualOrParent, joinPath } from 'vs/base/common/resources'; export class FileEditorTracker extends Disposable implements IWorkbenchContribution { @@ -42,7 +42,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, @IHostService private readonly hostService: IHostService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService + @ICodeEditorService private readonly codeEditorService: ICodeEditorService, ) { super(); @@ -101,22 +101,20 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Update Editor if file (or any parent of the input) got renamed or moved const resource = editor.getResource(); - if (resources.isEqualOrParent(resource, oldResource)) { + if (isEqualOrParent(resource, oldResource)) { let reopenFileResource: URI; if (oldResource.toString() === resource.toString()) { reopenFileResource = newResource; // file got moved } else { - const index = this.getIndexOfPath(resource.path, oldResource.path, resources.hasToIgnoreCase(resource)); - reopenFileResource = resources.joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved + const ignoreCase = !this.fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive); + const index = this.getIndexOfPath(resource.path, oldResource.path, ignoreCase); + reopenFileResource = joinPath(newResource, resource.path.substr(index + oldResource.path.length + 1)); // parent folder got moved } let encoding: string | undefined = undefined; - let mode: string | undefined = undefined; - const model = this.textFileService.models.get(resource); if (model) { encoding = model.getEncoding(); - mode = model.textEditorModel?.getModeId(); } this.editorService.replaceEditors([{ @@ -124,7 +122,6 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut replacement: { resource: reopenFileResource, encoding, - mode, options: { preserveFocus: true, pinned: group.isPinned(editor), @@ -199,7 +196,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut // Do NOT close any opened editor that matches the resource path (either equal or being parent) of the // resource we move to (movedTo). Otherwise we would close a resource that has been renamed to the same // path but different casing. - if (movedTo && resources.isEqualOrParent(resource, movedTo)) { + if (movedTo && isEqualOrParent(resource, movedTo)) { return; } @@ -207,7 +204,7 @@ export class FileEditorTracker extends Disposable implements IWorkbenchContribut if (arg1 instanceof FileChangesEvent) { matches = arg1.contains(resource, FileChangeType.DELETED); } else { - matches = resources.isEqualOrParent(resource, arg1); + matches = isEqualOrParent(resource, arg1); } if (!matches) { diff --git a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts index 601ed0673d3..94d591094b6 100644 --- a/src/vs/workbench/contrib/files/browser/explorerViewlet.ts +++ b/src/vs/workbench/contrib/files/browser/explorerViewlet.ts @@ -6,7 +6,7 @@ import 'vs/css!./media/explorerviewlet'; import { localize } from 'vs/nls'; import * as DOM from 'vs/base/browser/dom'; -import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext, VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, ExplorerViewletVisibleContext, IFilesConfiguration, OpenEditorsVisibleContext } from 'vs/workbench/contrib/files/common/files'; import { IViewletViewOptions } from 'vs/workbench/browser/parts/views/viewsViewlet'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { ExplorerView } from 'vs/workbench/contrib/files/browser/views/explorerView'; @@ -20,7 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; import { IThemeService } from 'vs/platform/theme/common/themeService'; -import { IViewsRegistry, IViewDescriptor, Extensions } from 'vs/workbench/common/views'; +import { IViewsRegistry, IViewDescriptor, Extensions, ViewContainer, IViewContainersRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { Disposable } from 'vs/base/common/lifecycle'; import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -33,8 +33,6 @@ import { ViewPane, ViewPaneContainer } from 'vs/workbench/browser/parts/views/vi import { KeyChord, KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { Registry } from 'vs/platform/registry/common/platform'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; -import { withUndefinedAsNull } from 'vs/base/common/types'; -import { Viewlet } from 'vs/workbench/browser/viewlet'; export class ExplorerViewletViewsContribution extends Disposable implements IWorkbenchContribution { @@ -147,22 +145,6 @@ export class ExplorerViewletViewsContribution extends Disposable implements IWor } } -export class ExplorerViewlet extends Viewlet { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService protected storageService: IStorageService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IExtensionService protected extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, - @IConfigurationService protected configurationService: IConfigurationService - ) { - super(VIEWLET_ID, instantiationService.createInstance(ExplorerViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } -} - export class ExplorerViewPaneContainer extends ViewPaneContainer { private static readonly EXPLORER_VIEWS_STATE = 'workbench.explorer.views.state'; @@ -183,7 +165,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { @IExtensionService extensionService: IExtensionService ) { - super(VIEWLET_ID, ExplorerViewPaneContainer.EXPLORER_VIEWS_STATE, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + super(VIEWLET_ID, ExplorerViewPaneContainer.EXPLORER_VIEWS_STATE, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); this.viewletVisibleContextKey = ExplorerViewletVisibleContext.bindTo(contextKeyService); @@ -202,8 +184,7 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { // without causing the animation in the opened editors view to kick in and change scroll position. // We try to be smart and only use the delay if we recognize that the user action is likely to cause // a new entry in the opened editors view. - const delegatingEditorService = this.instantiationService.createInstance(DelegatingEditorService); - delegatingEditorService.setEditorOpenHandler(async (delegate, group, editor, options): Promise => { + const delegatingEditorService = this.instantiationService.createInstance(DelegatingEditorService, async (delegate, group, editor, options): Promise => { let openEditorsView = this.getOpenEditorsView(); if (openEditorsView) { let delay = 0; @@ -219,19 +200,16 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { openEditorsView.setStructuralRefreshDelay(delay); } - let openedEditor: IEditor | undefined; try { - openedEditor = await delegate(group, editor, options); + return await delegate(group, editor, options); } catch (error) { - // ignore + return null; // ignore } finally { const openEditorsView = this.getOpenEditorsView(); if (openEditorsView) { openEditorsView.setStructuralRefreshDelay(0); } } - - return withUndefinedAsNull(openedEditor); }); const explorerInstantiator = this.instantiationService.createChild(new ServiceCollection([IEditorService, delegatingEditorService])); @@ -262,3 +240,14 @@ export class ExplorerViewPaneContainer extends ViewPaneContainer { } } } + +/** + * Explorer viewlet container. + */ +export const VIEW_CONTAINER: ViewContainer = Registry.as(Extensions.ViewContainersRegistry).registerViewContainer({ + id: VIEWLET_ID, + name: localize('explore', "Explorer"), + ctorDescriptor: { ctor: ExplorerViewPaneContainer }, + icon: 'codicon-files', + order: 0 +}, ViewContainerLocation.Sidebar); diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index d682f3b8d69..3cdf705bf44 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -33,14 +33,14 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands'; import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { Schemas } from 'vs/base/common/network'; -import { IDialogService, IConfirmationResult, getConfirmMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IConfirmationResult, getFileNamesMessage, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Constants } from 'vs/base/common/uint'; import { CLOSE_EDITORS_AND_GROUP_COMMAND_ID } from 'vs/workbench/browser/parts/editor/editorCommands'; import { coalesce } from 'vs/base/common/arrays'; import { ExplorerItem, NewExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; -import { onUnexpectedError, getErrorMessage } from 'vs/base/common/errors'; +import { getErrorMessage } from 'vs/base/common/errors'; import { asDomUri, triggerDownload } from 'vs/base/browser/dom'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; @@ -199,11 +199,17 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi // Confirm for moving to trash else if (useTrash) { - const message = getMoveToTrashMessage(distinctElements); + let { message, detail } = getMoveToTrashMessage(distinctElements); + detail += detail ? '\n' : ''; + if (isWindows) { + detail += nls.localize('undoBin', "You can restore from the Recycle Bin."); + } else { + detail += nls.localize('undoTrash', "You can restore from the Trash."); + } confirmDeletePromise = dialogService.confirm({ message, - detail: isWindows ? nls.localize('undoBin', "You can restore from the Recycle Bin.") : nls.localize('undoTrash', "You can restore from the Trash."), + detail, primaryButton, checkbox: { label: nls.localize('doNotAskAgain', "Do not ask me again") @@ -214,10 +220,12 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi // Confirm for deleting permanently else { - const message = getDeleteMessage(distinctElements); + let { message, detail } = getDeleteMessage(distinctElements); + detail += detail ? '\n' : ''; + detail += nls.localize('irreversible', "This action is irreversible!"); confirmDeletePromise = dialogService.confirm({ message, - detail: nls.localize('irreversible', "This action is irreversible!"), + detail, primaryButton, type: 'warning' }); @@ -280,44 +288,62 @@ async function deleteFiles(textFileService: ITextFileService, dialogService: IDi }); } -function getMoveToTrashMessage(distinctElements: ExplorerItem[]): string { +function getMoveToTrashMessage(distinctElements: ExplorerItem[]): { message: string, detail: string } { if (containsBothDirectoryAndFile(distinctElements)) { - return getConfirmMessage(nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmMoveTrashMessageFilesAndDirectories', "Are you sure you want to delete the following {0} files/directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements.length > 1) { if (distinctElements[0].isDirectory) { - return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmMoveTrashMessageMultipleDirectories', "Are you sure you want to delete the following {0} directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } - return getConfirmMessage(nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmMoveTrashMessageMultiple', "Are you sure you want to delete the following {0} files?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements[0].isDirectory) { - return nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name); + return { message: nls.localize('confirmMoveTrashMessageFolder', "Are you sure you want to delete '{0}' and its contents?", distinctElements[0].name), detail: '' }; } - return nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name); + return { message: nls.localize('confirmMoveTrashMessageFile', "Are you sure you want to delete '{0}'?", distinctElements[0].name), detail: '' }; } -function getDeleteMessage(distinctElements: ExplorerItem[]): string { +function getDeleteMessage(distinctElements: ExplorerItem[]): { message: string, detail: string } { if (containsBothDirectoryAndFile(distinctElements)) { - return getConfirmMessage(nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmDeleteMessageFilesAndDirectories', "Are you sure you want to permanently delete the following {0} files/directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements.length > 1) { if (distinctElements[0].isDirectory) { - return getConfirmMessage(nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmDeleteMessageMultipleDirectories', "Are you sure you want to permanently delete the following {0} directories and their contents?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } - return getConfirmMessage(nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), distinctElements.map(e => e.resource)); + return { + message: nls.localize('confirmDeleteMessageMultiple', "Are you sure you want to permanently delete the following {0} files?", distinctElements.length), + detail: getFileNamesMessage(distinctElements.map(e => e.resource)) + }; } if (distinctElements[0].isDirectory) { - return nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name); + return { message: nls.localize('confirmDeleteMessageFolder', "Are you sure you want to permanently delete '{0}' and its contents?", distinctElements[0].name), detail: '' }; } - return nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name); + return { message: nls.localize('confirmDeleteMessageFile', "Are you sure you want to permanently delete '{0}'?", distinctElements[0].name), detail: '' }; } function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean { @@ -328,12 +354,12 @@ function containsBothDirectoryAndFile(distinctElements: ExplorerItem[]): boolean } -export function findValidPasteFileTarget(targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }, incrementalNaming: 'simple' | 'smart'): URI { +export function findValidPasteFileTarget(explorerService: IExplorerService, targetFolder: ExplorerItem, fileToPaste: { resource: URI, isDirectory?: boolean, allowOverwrite: boolean }, incrementalNaming: 'simple' | 'smart'): URI { let name = resources.basenameOrAuthority(fileToPaste.resource); let candidate = resources.joinPath(targetFolder.resource, name); while (true && !fileToPaste.allowOverwrite) { - if (!targetFolder.root.find(candidate)) { + if (!explorerService.findClosest(candidate)) { break; } @@ -870,8 +896,9 @@ async function openExplorerAndCreate(accessor: ServicesAccessor, isFolder: boole throw new Error('Parent folder is readonly.'); } - const newStat = new NewExplorerItem(folder, isFolder); - await folder.fetchChildren(fileService, explorerService); + const newStat = new NewExplorerItem(fileService, folder, isFolder); + const sortOrder = explorerService.sortOrder; + await folder.fetchChildren(sortOrder); folder.addChild(newStat); @@ -915,6 +942,7 @@ CommandsRegistry.registerCommand({ export const renameHandler = (accessor: ServicesAccessor) => { const explorerService = accessor.get(IExplorerService); const textFileService = accessor.get(ITextFileService); + const notificationService = accessor.get(INotificationService); const stats = explorerService.getContext(false); const stat = stats.length > 0 ? stats[0] : undefined; @@ -924,12 +952,17 @@ export const renameHandler = (accessor: ServicesAccessor) => { explorerService.setEditable(stat, { validationMessage: value => validateFileName(stat, value), - onFinish: (value, success) => { + onFinish: async (value, success) => { if (success) { const parentResource = stat.parent!.resource; const targetResource = resources.joinPath(parentResource, value); if (stat.resource.toString() !== targetResource.toString()) { - textFileService.move(stat.resource, targetResource).then(() => refreshIfSeparator(value, explorerService), onUnexpectedError); + try { + await textFileService.move(stat.resource, targetResource); + refreshIfSeparator(value, explorerService); + } catch (e) { + notificationService.error(e); + } } } explorerService.setEditable(stat, null); @@ -1049,7 +1082,7 @@ export const pasteFileHandler = async (accessor: ServicesAccessor) => { } const incrementalNaming = configurationService.getValue().explorer.incrementalNaming; - const targetFile = findValidPasteFileTarget(target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming); + const targetFile = findValidPasteFileTarget(explorerService, target, { resource: fileToPaste, isDirectory: fileToPasteStat.isDirectory, allowOverwrite: pasteShouldMove }, incrementalNaming); // Move/Copy File if (pasteShouldMove) { diff --git a/src/vs/workbench/contrib/files/browser/fileCommands.ts b/src/vs/workbench/contrib/files/browser/fileCommands.ts index b837f81fa5d..bb781656124 100644 --- a/src/vs/workbench/contrib/files/browser/fileCommands.ts +++ b/src/vs/workbench/contrib/files/browser/fileCommands.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; -import { toResource, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason, SideBySideEditorInput } from 'vs/workbench/common/editor'; +import { toResource, IEditorCommandsContext, SideBySideEditor, IEditorIdentifier, SaveReason, SideBySideEditorInput, EditorsOrder } from 'vs/workbench/common/editor'; import { IWindowOpenable, IOpenWindowOptions, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -30,7 +30,7 @@ import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { IEditorService, SIDE_GROUP, ISaveEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; -import { IEditorGroupsService, GroupsOrder, EditorsOrder, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, GroupsOrder, IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ILabelService } from 'vs/platform/label/common/label'; import { basename, joinPath, isEqual } from 'vs/base/common/resources'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; diff --git a/src/vs/workbench/contrib/files/browser/files.contribution.ts b/src/vs/workbench/contrib/files/browser/files.contribution.ts index 615c14812d8..edb3d92e1c6 100644 --- a/src/vs/workbench/contrib/files/browser/files.contribution.ts +++ b/src/vs/workbench/contrib/files/browser/files.contribution.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { URI, UriComponents } from 'vs/base/common/uri'; -import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from 'vs/workbench/browser/viewlet'; +import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet'; import * as nls from 'vs/nls'; import { sep } from 'vs/base/common/path'; import { SyncActionDescriptor, MenuId, MenuRegistry } from 'vs/platform/actions/common/actions'; @@ -14,7 +14,7 @@ import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/wor import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { IEditorInputFactory, EditorInput, IFileEditorInput, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; import { AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files'; -import { VIEWLET_ID, SortOrderConfiguration, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { VIEWLET_ID, SortOrder, FILE_EDITOR_INPUT_ID, IExplorerService } from 'vs/workbench/contrib/files/common/files'; import { FileEditorTracker } from 'vs/workbench/contrib/files/browser/editors/fileEditorTracker'; import { TextFileSaveErrorHandler } from 'vs/workbench/contrib/files/browser/editors/textFileSaveErrorHandler'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; @@ -25,7 +25,7 @@ import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry' import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import * as platform from 'vs/base/common/platform'; -import { ExplorerViewletViewsContribution, ExplorerViewlet } from 'vs/workbench/contrib/files/browser/explorerViewlet'; +import { ExplorerViewletViewsContribution } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { IEditorRegistry, EditorDescriptor, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; @@ -73,15 +73,6 @@ class FileUriLabelContribution implements IWorkbenchContribution { } } -// Register Viewlet -Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( - ExplorerViewlet, - VIEWLET_ID, - nls.localize('explore', "Explorer"), - 'codicon-files', - 0 -)); - registerSingleton(IExplorerService, ExplorerService, true); Registry.as(ViewletExtensions.Viewlets).setDefaultViewletId(VIEWLET_ID); @@ -396,8 +387,8 @@ configurationRegistry.registerConfiguration({ }, 'explorer.sortOrder': { 'type': 'string', - 'enum': [SortOrderConfiguration.DEFAULT, SortOrderConfiguration.MIXED, SortOrderConfiguration.FILES_FIRST, SortOrderConfiguration.TYPE, SortOrderConfiguration.MODIFIED], - 'default': SortOrderConfiguration.DEFAULT, + 'enum': [SortOrder.Default, SortOrder.Mixed, SortOrder.FilesFirst, SortOrder.Type, SortOrder.Modified], + 'default': SortOrder.Default, 'enumDescriptions': [ nls.localize('sortOrder.default', 'Files and folders are sorted by their names, in alphabetical order. Folders are displayed before files.'), nls.localize('sortOrder.mixed', 'Files and folders are sorted by their names, in alphabetical order. Files are interwoven with folders.'), diff --git a/src/vs/workbench/contrib/files/browser/files.ts b/src/vs/workbench/contrib/files/browser/files.ts index c501bd0f486..1274f2b97d5 100644 --- a/src/vs/workbench/contrib/files/browser/files.ts +++ b/src/vs/workbench/contrib/files/browser/files.ts @@ -57,7 +57,7 @@ export function getMultiSelectedResources(resource: URI | object | undefined, li const list = listService.lastFocusedList; if (list?.getHTMLElement() === document.activeElement) { // Explorer - if (list instanceof AsyncDataTree) { + if (list instanceof AsyncDataTree && list.getFocus().every(item => item instanceof ExplorerItem)) { // Explorer const context = explorerService.getContext(true); if (context.length) { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index bd1e27a2150..52008b759ec 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -64,6 +64,20 @@ interface IExplorerViewStyles { listDropBackground?: Color; } +function hasExpandedRootChild(tree: WorkbenchCompressibleAsyncDataTree, treeInput: ExplorerItem[]): boolean { + for (const folder of treeInput) { + if (tree.hasNode(folder) && !tree.isCollapsed(folder)) { + for (const [, child] of folder.children.entries()) { + if (tree.hasNode(child) && !tree.isCollapsed(child)) { + return true; + } + } + } + } + + return false; +} + export class ExplorerView extends ViewPane { static readonly ID: string = 'workbench.explorer.fileView'; static readonly TREE_VIEW_STATE_STORAGE_KEY: string = 'workbench.explorer.treeViewState'; @@ -83,7 +97,6 @@ export class ExplorerView extends ViewPane { private compressedFocusContext: IContextKey; private compressedFocusFirstContext: IContextKey; private compressedFocusLastContext: IContextKey; - private compressedNavigationController: ICompressedNavigationController | undefined; // Refresh is needed on the initial explorer open private shouldRefresh = true; @@ -140,7 +153,7 @@ export class ExplorerView extends ViewPane { return this.name; } - set title(value: string) { + set title(_: string) { // noop } @@ -286,18 +299,17 @@ export class ExplorerView extends ViewPane { getContext(respectMultiSelection: boolean): ExplorerItem[] { let focusedStat: ExplorerItem | undefined; - if (this.compressedNavigationController) { - focusedStat = this.compressedNavigationController.current; - } else { - const focus = this.tree.getFocus(); - focusedStat = focus.length ? focus[0] : undefined; - } + const focus = this.tree.getFocus(); + focusedStat = focus.length ? focus[0] : undefined; + + const compressedNavigationController = focusedStat && this.renderer.getCompressedNavigationController(focusedStat); + focusedStat = compressedNavigationController ? compressedNavigationController.current : focusedStat; const selectedStats: ExplorerItem[] = []; for (const stat of this.tree.getSelection()) { const controller = this.renderer.getCompressedNavigationController(stat); - if (controller && focusedStat && controller === this.compressedNavigationController) { + if (controller && focusedStat && controller === compressedNavigationController) { if (stat === focusedStat) { selectedStats.push(stat); } @@ -438,7 +450,7 @@ export class ExplorerView extends ViewPane { } })); - // save view state on shutdown + // save view state this._register(this.storageService.onWillSaveState(() => { this.storageService.store(ExplorerView.TREE_VIEW_STATE_STORAGE_KEY, JSON.stringify(this.tree.getViewState()), StorageScope.WORKSPACE); })); @@ -532,16 +544,15 @@ export class ExplorerView extends ViewPane { this.resourceMoveableToTrash.reset(); } - this.compressedNavigationController = stat && this.renderer.getCompressedNavigationController(stat); + const compressedNavigationController = stat && this.renderer.getCompressedNavigationController(stat); - if (!this.compressedNavigationController) { + if (!compressedNavigationController) { this.compressedFocusContext.set(false); return; } this.compressedFocusContext.set(true); - // this.compressedNavigationController.last(); - this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + this.updateCompressedNavigationContextKeys(compressedNavigationController); } // General methods @@ -573,8 +584,6 @@ export class ExplorerView extends ViewPane { return DOM.getLargestChildWidth(parentNode, childNodes); } - // private didLoad = false; - private setTreeInput(): Promise { if (!this.isBodyVisible()) { this.shouldRefresh = true; @@ -687,43 +696,62 @@ export class ExplorerView extends ViewPane { } collapseAll(): void { + const treeInput = this.tree.getInput(); + if (Array.isArray(treeInput)) { + if (hasExpandedRootChild(this.tree, treeInput)) { + treeInput.forEach(folder => { + folder.children.forEach(child => this.tree.hasNode(child) && this.tree.collapse(child)); + }); + + return; + } + } + this.tree.collapseAll(); } previousCompressedStat(): void { - if (!this.compressedNavigationController) { + const focused = this.tree.getFocus(); + if (!focused.length) { return; } - this.compressedNavigationController.previous(); - this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + const compressedNavigationController = this.renderer.getCompressedNavigationController(focused[0])!; + compressedNavigationController.previous(); + this.updateCompressedNavigationContextKeys(compressedNavigationController); } nextCompressedStat(): void { - if (!this.compressedNavigationController) { + const focused = this.tree.getFocus(); + if (!focused.length) { return; } - this.compressedNavigationController.next(); - this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + const compressedNavigationController = this.renderer.getCompressedNavigationController(focused[0])!; + compressedNavigationController.next(); + this.updateCompressedNavigationContextKeys(compressedNavigationController); } firstCompressedStat(): void { - if (!this.compressedNavigationController) { + const focused = this.tree.getFocus(); + if (!focused.length) { return; } - this.compressedNavigationController.first(); - this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + const compressedNavigationController = this.renderer.getCompressedNavigationController(focused[0])!; + compressedNavigationController.first(); + this.updateCompressedNavigationContextKeys(compressedNavigationController); } lastCompressedStat(): void { - if (!this.compressedNavigationController) { + const focused = this.tree.getFocus(); + if (!focused.length) { return; } - this.compressedNavigationController.last(); - this.updateCompressedNavigationContextKeys(this.compressedNavigationController); + const compressedNavigationController = this.renderer.getCompressedNavigationController(focused[0])!; + compressedNavigationController.last(); + this.updateCompressedNavigationContextKeys(compressedNavigationController); } private updateCompressedNavigationContextKeys(controller: ICompressedNavigationController): void { diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 96ad2636400..c98ad1df6a6 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -9,7 +9,7 @@ import * as glob from 'vs/base/common/glob'; import { IListVirtualDelegate, ListDragOverEffect } from 'vs/base/browser/ui/list/list'; import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; -import { IFileService, FileKind, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { IFileService, FileKind, FileOperationError, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IDisposable, Disposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -20,7 +20,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IFilesConfiguration, IExplorerService } from 'vs/workbench/contrib/files/common/files'; -import { dirname, joinPath, isEqualOrParent, basename, hasToIgnoreCase, distinctParents } from 'vs/base/common/resources'; +import { dirname, joinPath, isEqualOrParent, basename, distinctParents } from 'vs/base/common/resources'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; import { localize } from 'vs/nls'; import { attachInputBoxStyler } from 'vs/platform/theme/common/styler'; @@ -36,7 +36,7 @@ import { IDragAndDropData, DataTransfers } from 'vs/base/browser/dnd'; import { Schemas } from 'vs/base/common/network'; import { DesktopDragAndDropData, ExternalElementsDragAndDropData, ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView'; import { isMacintosh, isWeb } from 'vs/base/common/platform'; -import { IDialogService, IConfirmation, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; +import { IDialogService, IConfirmation, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IHostService } from 'vs/workbench/services/host/browser/host'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing'; @@ -90,12 +90,13 @@ export class ExplorerDataSource implements IAsyncDataSource { + const sortOrder = this.explorerService.sortOrder; + const promise = element.fetchChildren(sortOrder).then(undefined, e => { if (element instanceof ExplorerItem && element.isRoot) { if (this.contextService.getWorkbenchState() === WorkbenchState.FOLDER) { // Single folder create a dummy explorer item to show error - const placeholder = new ExplorerItem(element.resource, undefined, false); + const placeholder = new ExplorerItem(element.resource, this.fileService, undefined, false); placeholder.isError = true; return [placeholder]; } else { @@ -160,11 +161,13 @@ export class CompressedNavigationController implements ICompressedNavigationCont private updateLabels(templateData: IFileTemplateData): void { this._labels = Array.from(templateData.container.querySelectorAll('.label-name')) as HTMLElement[]; - for (let i = 0; i < this.items.length; i++) { + for (let i = 0; i < this.labels.length; i++) { this.labels[i].setAttribute('aria-label', this.items[i].name); } - DOM.addClass(this.labels[this._index], 'active'); + if (this._index < this.labels.length) { + DOM.addClass(this.labels[this._index], 'active'); + } } previous(): void { @@ -919,17 +922,17 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Check for name collisions const targetNames = new Set(); + const caseSensitive = this.fileService.hasCapability(target.resource, FileSystemProviderCapabilities.PathCaseSensitive); if (targetStat.children) { - const ignoreCase = hasToIgnoreCase(target.resource); targetStat.children.forEach(child => { - targetNames.add(ignoreCase ? child.name.toLowerCase() : child.name); + targetNames.add(caseSensitive ? child.name : child.name.toLowerCase()); }); } // Run add in sequence const addPromisesFactory: ITask>[] = []; await Promise.all(resources.map(async resource => { - if (targetNames.has(!hasToIgnoreCase(resource) ? basename(resource) : basename(resource).toLowerCase())) { + if (targetNames.has(caseSensitive ? basename(resource) : basename(resource).toLowerCase())) { const confirmationResult = await this.dialogService.confirm(getFileOverwriteConfirm(basename(resource))); if (!confirmationResult.confirmed) { return; @@ -968,11 +971,15 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Handle confirm setting const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); if (confirmDragAndDrop) { + const message = items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") + : items.length > 1 ? localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name) + : items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name) + : localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name); + const detail = items.length > 1 && !items.every(s => s.isRoot) ? getFileNamesMessage(items.map(i => i.resource)) : undefined; + const confirmation = await this.dialogService.confirm({ - message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") - : items.length > 1 ? getConfirmMessage(localize('confirmMultiMove', "Are you sure you want to move the following {0} files into '{1}'?", items.length, target.name), items.map(s => s.resource)) - : items[0].isRoot ? localize('confirmRootMove', "Are you sure you want to change the order of root folder '{0}' in your workspace?", items[0].name) - : localize('confirmMove', "Are you sure you want to move '{0}' into '{1}'?", items[0].name, target.name), + message, + detail, checkbox: { label: localize('doNotAskAgain', "Do not ask me again") }, @@ -1031,7 +1038,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { // Reuse duplicate action if user copies if (isCopy) { const incrementalNaming = this.configurationService.getValue().explorer.incrementalNaming; - const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)); + const stat = await this.textFileService.copy(source.resource, findValidPasteFileTarget(this.explorerService, target, { resource: source.resource, isDirectory: source.isDirectory, allowOverwrite: false }, incrementalNaming)); if (!stat.isDirectory) { await this.editorService.openEditor({ resource: stat.resource, options: { pinned: true } }); } diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index dd17f19dfb6..fd01f455765 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -6,7 +6,6 @@ import { URI } from 'vs/base/common/uri'; import { isEqual } from 'vs/base/common/extpath'; import { posix } from 'vs/base/common/path'; -import * as resources from 'vs/base/common/resources'; import { ResourceMap } from 'vs/base/common/map'; import { IFileStat, IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { rtrim, startsWithIgnoreCase, startsWith, equalsIgnoreCase } from 'vs/base/common/strings'; @@ -15,7 +14,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { memoize } from 'vs/base/common/decorators'; import { Emitter, Event } from 'vs/base/common/event'; -import { IExplorerService } from 'vs/workbench/contrib/files/common/files'; +import { joinPath, isEqualOrParent, basenameOrAuthority } from 'vs/base/common/resources'; +import { SortOrder } from 'vs/workbench/contrib/files/common/files'; export class ExplorerModel implements IDisposable { @@ -23,9 +23,12 @@ export class ExplorerModel implements IDisposable { private _listener: IDisposable; private readonly _onDidChangeRoots = new Emitter(); - constructor(private readonly contextService: IWorkspaceContextService) { + constructor( + private readonly contextService: IWorkspaceContextService, + fileService: IFileService + ) { const setRoots = () => this._roots = this.contextService.getWorkspace().folders - .map(folder => new ExplorerItem(folder.uri, undefined, true, false, false, folder.name)); + .map(folder => new ExplorerItem(folder.uri, fileService, undefined, true, false, folder.name)); setRoots(); this._listener = this.contextService.onDidChangeWorkspaceFolders(() => { @@ -80,11 +83,11 @@ export class ExplorerItem { constructor( public resource: URI, + private readonly fileService: IFileService, private _parent: ExplorerItem | undefined, private _isDirectory?: boolean, private _isSymbolicLink?: boolean, - private _isReadonly?: boolean, - private _name: string = resources.basenameOrAuthority(resource), + private _name: string = basenameOrAuthority(resource), private _mtime?: number, ) { this._isDirectoryResolved = false; @@ -108,7 +111,7 @@ export class ExplorerItem { } get isReadonly(): boolean { - return !!this._isReadonly; + return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly); } get mtime(): number | undefined { @@ -154,8 +157,8 @@ export class ExplorerItem { return this === this.root; } - static create(service: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { - const stat = new ExplorerItem(raw.resource, parent, raw.isDirectory, raw.isSymbolicLink, service.hasCapability(raw.resource, FileSystemProviderCapabilities.Readonly), raw.name, raw.mtime); + static create(fileService: IFileService, raw: IFileStat, parent: ExplorerItem | undefined, resolveTo?: readonly URI[]): ExplorerItem { + const stat = new ExplorerItem(raw.resource, fileService, parent, raw.isDirectory, raw.isSymbolicLink, raw.name, raw.mtime); // Recursively add children if present if (stat.isDirectory) { @@ -164,13 +167,13 @@ export class ExplorerItem { // the folder is fully resolved if either it has a list of children or the client requested this by using the resolveTo // array of resource path to resolve. stat._isDirectoryResolved = !!raw.children || (!!resolveTo && resolveTo.some((r) => { - return resources.isEqualOrParent(r, stat.resource); + return isEqualOrParent(r, stat.resource); })); // Recurse into children if (raw.children) { for (let i = 0, len = raw.children.length; i < len; i++) { - const child = ExplorerItem.create(service, raw.children[i], stat, resolveTo); + const child = ExplorerItem.create(fileService, raw.children[i], stat, resolveTo); stat.addChild(child); } } @@ -204,7 +207,6 @@ export class ExplorerItem { local._mtime = disk.mtime; local._isDirectoryResolved = disk._isDirectoryResolved; local._isSymbolicLink = disk.isSymbolicLink; - local._isReadonly = disk.isReadonly; local.isError = disk.isError; // Merge Children if resolved @@ -255,14 +257,14 @@ export class ExplorerItem { return this.children.get(this.getPlatformAwareName(name)); } - async fetchChildren(fileService: IFileService, explorerService: IExplorerService): Promise { + async fetchChildren(sortOrder: SortOrder): Promise { if (!this._isDirectoryResolved) { // Resolve metadata only when the mtime is needed since this can be expensive // Mtime is only used when the sort order is 'modified' - const resolveMetadata = explorerService.sortOrder === 'modified'; + const resolveMetadata = sortOrder === SortOrder.Modified; try { - const stat = await fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata }); - const resolved = ExplorerItem.create(fileService, stat, this); + const stat = await this.fileService.resolve(this.resource, { resolveSingleChildDescendants: true, resolveMetadata }); + const resolved = ExplorerItem.create(this.fileService, stat, this); ExplorerItem.mergeLocalWithDisk(resolved, this); } catch (e) { this.isError = true; @@ -302,7 +304,7 @@ export class ExplorerItem { } private getPlatformAwareName(name: string): string { - return (!name || !resources.hasToIgnoreCase(this.resource)) ? name : name.toLowerCase(); + return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.PathCaseSensitive) ? name : name.toLowerCase(); } /** @@ -319,7 +321,7 @@ export class ExplorerItem { private updateResource(recursive: boolean): void { if (this._parent) { - this.resource = resources.joinPath(this._parent.resource, this.name); + this.resource = joinPath(this._parent.resource, this.name); } if (recursive) { @@ -352,16 +354,17 @@ export class ExplorerItem { find(resource: URI): ExplorerItem | null { // Return if path found // For performance reasons try to do the comparison as fast as possible + const ignoreCase = !this.fileService.hasCapability(resource, FileSystemProviderCapabilities.PathCaseSensitive); if (resource && this.resource.scheme === resource.scheme && equalsIgnoreCase(this.resource.authority, resource.authority) && - (resources.hasToIgnoreCase(resource) ? startsWithIgnoreCase(resource.path, this.resource.path) : startsWith(resource.path, this.resource.path))) { - return this.findByPath(rtrim(resource.path, posix.sep), this.resource.path.length); + (ignoreCase ? startsWithIgnoreCase(resource.path, this.resource.path) : startsWith(resource.path, this.resource.path))) { + return this.findByPath(rtrim(resource.path, posix.sep), this.resource.path.length, ignoreCase); } return null; //Unable to find } - private findByPath(path: string, index: number): ExplorerItem | null { - if (isEqual(rtrim(this.resource.path, posix.sep), path, resources.hasToIgnoreCase(this.resource))) { + private findByPath(path: string, index: number, ignoreCase: boolean): ExplorerItem | null { + if (isEqual(rtrim(this.resource.path, posix.sep), path, ignoreCase)) { return this; } @@ -383,7 +386,7 @@ export class ExplorerItem { if (child) { // We found a child with the given name, search inside it - return child.findByPath(path, indexOfNextSep); + return child.findByPath(path, indexOfNextSep, ignoreCase); } } @@ -392,7 +395,7 @@ export class ExplorerItem { } export class NewExplorerItem extends ExplorerItem { - constructor(parent: ExplorerItem, isDirectory: boolean) { - super(URI.file(''), parent, isDirectory); + constructor(fileService: IFileService, parent: ExplorerItem, isDirectory: boolean) { + super(URI.file(''), fileService, parent, isDirectory); } } diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index 23311249367..077aa3ddc26 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -6,7 +6,7 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { DisposableStore } from 'vs/base/common/lifecycle'; -import { IExplorerService, IFilesConfiguration, SortOrder, SortOrderConfiguration, IContextProvider } from 'vs/workbench/contrib/files/common/files'; +import { IExplorerService, IFilesConfiguration, SortOrder, IContextProvider } from 'vs/workbench/contrib/files/common/files'; import { ExplorerItem, ExplorerModel } from 'vs/workbench/contrib/files/common/explorerModel'; import { URI } from 'vs/base/common/uri'; import { FileOperationEvent, FileOperation, IFileStat, IFileService, FileChangesEvent, FILES_EXCLUDE_CONFIG, FileChangeType, IResolveFileOptions } from 'vs/platform/files/common/files'; @@ -41,8 +41,8 @@ export class ExplorerService implements IExplorerService { private editable: { stat: ExplorerItem, data: IEditableData } | undefined; private _sortOrder: SortOrder; private cutItems: ExplorerItem[] | undefined; - private fileSystemProviderSchemes = new Set(); private contextProvider: IContextProvider | undefined; + private model: ExplorerModel; constructor( @IFileService private fileService: IFileService, @@ -53,6 +53,25 @@ export class ExplorerService implements IExplorerService { @IEditorService private editorService: IEditorService, ) { this._sortOrder = this.configurationService.getValue('explorer.sortOrder'); + + this.model = new ExplorerModel(this.contextService, this.fileService); + this.disposables.add(this.model); + this.disposables.add(this.fileService.onAfterOperation(e => this.onFileOperation(e))); + this.disposables.add(this.fileService.onFileChanges(e => this.onFileChanges(e))); + this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); + this.disposables.add(Event.any<{ scheme: string }>(this.fileService.onDidChangeFileSystemProviderRegistrations, this.fileService.onDidChangeFileSystemProviderCapabilities)(e => { + let affected = false; + this.model.roots.forEach(r => { + if (r.resource.scheme === e.scheme) { + affected = true; + r.forgetChildren(); + } + }); + if (affected) { + this._onDidChangeItem.fire({ recursive: true }); + } + })); + this.disposables.add(this.model.onDidChangeRoots(() => this._onDidChangeRoots.fire())); } get roots(): ExplorerItem[] { @@ -107,26 +126,6 @@ export class ExplorerService implements IExplorerService { return fileEventsFilter; } - @memoize get model(): ExplorerModel { - const model = new ExplorerModel(this.contextService); - this.disposables.add(model); - this.disposables.add(this.fileService.onAfterOperation(e => this.onFileOperation(e))); - this.disposables.add(this.fileService.onFileChanges(e => this.onFileChanges(e))); - this.disposables.add(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); - this.disposables.add(this.fileService.onDidChangeFileSystemProviderRegistrations(e => { - if (e.added && this.fileSystemProviderSchemes.has(e.scheme)) { - // A file system provider got re-registered, we should update all file stats since they might change (got read-only) - this.model.roots.forEach(r => r.forgetChildren()); - this._onDidChangeItem.fire({ recursive: true }); - } else { - this.fileSystemProviderSchemes.add(e.scheme); - } - })); - this.disposables.add(model.onDidChangeRoots(() => this._onDidChangeRoots.fire())); - - return model; - } - // IExplorerService methods findClosest(resource: URI): ExplorerItem | null { @@ -174,7 +173,7 @@ export class ExplorerService implements IExplorerService { } // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === 'modified' }; + const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === SortOrder.Modified }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); if (workspaceFolder === null) { return Promise.resolve(undefined); @@ -348,7 +347,7 @@ export class ExplorerService implements IExplorerService { } // Handle updated files/folders if we sort by modified - if (this._sortOrder === SortOrderConfiguration.MODIFIED) { + if (this._sortOrder === SortOrder.Modified) { const updated = e.getUpdated(); // Check updated: Refresh if updated file/folder part of resolved root @@ -374,7 +373,7 @@ export class ExplorerService implements IExplorerService { private filterToViewRelevantEvents(e: FileChangesEvent): FileChangesEvent { return new FileChangesEvent(e.changes.filter(change => { - if (change.type === FileChangeType.UPDATED && this._sortOrder !== SortOrderConfiguration.MODIFIED) { + if (change.type === FileChangeType.UPDATED && this._sortOrder !== SortOrder.Modified) { return false; // we only are about updated if we sort by modified time } diff --git a/src/vs/workbench/contrib/files/common/files.ts b/src/vs/workbench/contrib/files/common/files.ts index dea52047854..c2bdc69cf7b 100644 --- a/src/vs/workbench/contrib/files/common/files.ts +++ b/src/vs/workbench/contrib/files/common/files.ts @@ -16,8 +16,7 @@ import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { InputFocusedContextKey } from 'vs/platform/contextkey/common/contextkeys'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainer, IEditableData } from 'vs/workbench/common/views'; +import { IEditableData } from 'vs/workbench/common/views'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; @@ -30,11 +29,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic */ export const VIEWLET_ID = 'workbench.view.explorer'; -/** - * Explorer viewlet container. - */ -export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); - export interface IExplorerService { _serviceBrand: undefined; readonly roots: ExplorerItem[]; @@ -132,15 +126,13 @@ export interface IFileResource { isDirectory?: boolean; } -export const SortOrderConfiguration = { - DEFAULT: 'default', - MIXED: 'mixed', - FILES_FIRST: 'filesFirst', - TYPE: 'type', - MODIFIED: 'modified' -}; - -export type SortOrder = 'default' | 'mixed' | 'filesFirst' | 'type' | 'modified'; +export const enum SortOrder { + Default = 'default', + Mixed = 'mixed', + FilesFirst = 'filesFirst', + Type = 'type', + Modified = 'modified' +} export class TextFileContentProvider extends Disposable implements ITextModelContentProvider { private readonly fileWatcherDisposable = this._register(new MutableDisposable()); diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index 932c54fa018..d1a86fceb9a 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -28,7 +28,6 @@ class ServiceAccessor { } suite('Files - FileEditorInput', () => { - let instantiationService: IInstantiationService; let accessor: ServiceAccessor; diff --git a/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts b/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts index b269b6eb84c..6966534a25b 100644 --- a/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts +++ b/src/vs/workbench/contrib/files/test/electron-browser/explorerModel.test.ts @@ -10,9 +10,11 @@ import { join } from 'vs/base/common/path'; import { validateFileName } from 'vs/workbench/contrib/files/browser/fileActions'; import { ExplorerItem } from 'vs/workbench/contrib/files/common/explorerModel'; import { toResource } from 'vs/base/test/common/utils'; +import { TestFileService } from 'vs/workbench/test/workbenchTestServices'; +const fileService = new TestFileService(); function createStat(this: any, path: string, name: string, isFolder: boolean, hasChildren: boolean, size: number, mtime: number): ExplorerItem { - return new ExplorerItem(toResource.call(this, path), undefined, isFolder, false, false, name, mtime); + return new ExplorerItem(toResource.call(this, path), fileService, undefined, isFolder, false, name, mtime); } suite('Files - View Model', function () { @@ -243,19 +245,19 @@ suite('Files - View Model', function () { }); test('Merge Local with Disk', function () { - const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, true, false, false, 'to', Date.now()); - const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), undefined, true, false, false, 'to', Date.now()); + const merge1 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, 'to', Date.now()); + const merge2 = new ExplorerItem(URI.file(join('C:\\', '/path/to')), fileService, undefined, true, false, 'to', Date.now()); // Merge Properties ExplorerItem.mergeLocalWithDisk(merge2, merge1); assert.strictEqual(merge1.mtime, merge2.mtime); // Merge Child when isDirectoryResolved=false is a no-op - merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now())); + merge2.addChild(new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, 'foo.html', Date.now())); ExplorerItem.mergeLocalWithDisk(merge2, merge1); // Merge Child with isDirectoryResolved=true - const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), undefined, true, false, false, 'foo.html', Date.now()); + const child = new ExplorerItem(URI.file(join('C:\\', '/path/to/foo.html')), fileService, undefined, true, false, 'foo.html', Date.now()); merge2.removeChild(child); merge2.addChild(child); (merge2)._isDirectoryResolved = true; diff --git a/src/vs/workbench/contrib/markers/browser/constants.ts b/src/vs/workbench/contrib/markers/browser/constants.ts index 8d4348b87a7..470a762569e 100644 --- a/src/vs/workbench/contrib/markers/browser/constants.ts +++ b/src/vs/workbench/contrib/markers/browser/constants.ts @@ -7,6 +7,8 @@ import { RawContextKey } from 'vs/platform/contextkey/common/contextkey'; export default { MARKERS_PANEL_ID: 'workbench.panel.markers', + MARKERS_PANEL_STORAGE_ID: 'workbench.panel.markers', + MARKERS_VIEW_ID: 'workbench.panel.markers.view', MARKER_COPY_ACTION_ID: 'problems.action.copy', MARKER_COPY_MESSAGE_ACTION_ID: 'problems.action.copyMessage', RELATED_INFORMATION_COPY_MESSAGE_ACTION_ID: 'problems.action.copyRelatedInformationMessage', diff --git a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts index 42fd074794f..1baf6e1c4f9 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.contribution.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.contribution.ts @@ -12,11 +12,11 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { localize } from 'vs/nls'; import { Marker, RelatedInformation } from 'vs/workbench/contrib/markers/browser/markersModel'; -import { MarkersPanel } from 'vs/workbench/contrib/markers/browser/markersPanel'; +import { MarkersView, getMarkersView } from 'vs/workbench/contrib/markers/browser/markersView'; import { MenuId, MenuRegistry, SyncActionDescriptor, registerAction } from 'vs/platform/actions/common/actions'; -import { PanelRegistry, Extensions as PanelExtensions, PanelDescriptor } from 'vs/workbench/browser/panel'; +import { TogglePanelAction } from 'vs/workbench/browser/panel'; import { Registry } from 'vs/platform/registry/common/platform'; -import { ToggleMarkersPanelAction, ShowProblemsPanelAction } from 'vs/workbench/contrib/markers/browser/markersPanelActions'; +import { ShowProblemsPanelAction } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions'; @@ -29,6 +29,9 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment, IStatusbarEntry } from 'vs/workbench/services/statusbar/common/statusbar'; import { IMarkerService, MarkerStatistics } from 'vs/platform/markers/common/markers'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation, IViewsRegistry } from 'vs/workbench/common/views'; +import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; registerSingleton(IMarkersWorkbenchService, MarkersWorkbenchService, false); @@ -41,8 +44,8 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ primary: KeyMod.WinCtrl | KeyCode.Enter }, handler: (accessor, args: any) => { - const markersPanel = (accessor.get(IPanelService).getActivePanel()); - markersPanel.openFileAtElement(markersPanel.getFocusElement(), false, true, true); + const markersView = getMarkersView(accessor.get(IPanelService))!; + markersView.openFileAtElement(markersView.getFocusElement(), false, true, true); } }); @@ -62,10 +65,10 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({ when: Constants.MarkerFocusContextKey, primary: KeyMod.CtrlCmd | KeyCode.US_DOT, handler: (accessor, args: any) => { - const markersPanel = (accessor.get(IPanelService).getActivePanel()); - const focusedElement = markersPanel.getFocusElement(); + const markersView = getMarkersView(accessor.get(IPanelService))!; + const focusedElement = markersView.getFocusElement(); if (focusedElement instanceof Marker) { - markersPanel.showQuickFixes(focusedElement); + markersView.showQuickFixes(focusedElement); } } }); @@ -90,16 +93,37 @@ Registry.as(Extensions.Configuration).registerConfigurat } }); +class ToggleMarkersPanelAction extends TogglePanelAction { -// markers panel -Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( - MarkersPanel, - Constants.MARKERS_PANEL_ID, - Messages.MARKERS_PANEL_TITLE_PROBLEMS, - 'markersPanel', - 10, - ToggleMarkersPanelAction.ID -)); + public static readonly ID = 'workbench.actions.view.problems'; + public static readonly LABEL = Messages.MARKERS_PANEL_TOGGLE_LABEL; + + constructor(id: string, label: string, + @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, + @IPanelService panelService: IPanelService + ) { + super(id, label, Constants.MARKERS_PANEL_ID, panelService, layoutService); + } +} + +// markers view container +const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ + id: Constants.MARKERS_PANEL_ID, + name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, + ctorDescriptor: { ctor: ViewPaneContainer, arguments: [Constants.MARKERS_PANEL_ID, Constants.MARKERS_PANEL_STORAGE_ID, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }] }, + focusCommand: { + id: ToggleMarkersPanelAction.ID, keybindings: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_M + } + } +}, ViewContainerLocation.Panel); + +Registry.as(ViewContainerExtensions.ViewsRegistry).registerViews([{ + id: Constants.MARKERS_VIEW_ID, + name: Messages.MARKERS_PANEL_TITLE_PROBLEMS, + canToggleVisibility: false, + ctorDescriptor: { ctor: MarkersView }, +}], VIEW_CONTAINER); // workbench const workbenchRegistry = Registry.as(WorkbenchExtensions.Workbench); @@ -183,10 +207,9 @@ registerAction({ registerAction({ id: Constants.MARKERS_PANEL_SHOW_MULTILINE_MESSAGE, handler(accessor) { - const panelService = accessor.get(IPanelService); - const panel = panelService.getActivePanel(); - if (panel instanceof MarkersPanel) { - panel.markersViewModel.multiline = true; + const markersView = getMarkersView(accessor.get(IPanelService)); + if (markersView) { + markersView.markersViewModel.multiline = true; } }, title: { value: localize('show multiline', "Show message in multiple lines"), original: 'Problems: Show message in multiple lines' }, @@ -199,10 +222,9 @@ registerAction({ registerAction({ id: Constants.MARKERS_PANEL_SHOW_SINGLELINE_MESSAGE, handler(accessor) { - const panelService = accessor.get(IPanelService); - const panel = panelService.getActivePanel(); - if (panel instanceof MarkersPanel) { - panel.markersViewModel.multiline = false; + const markersView = getMarkersView(accessor.get(IPanelService)); + if (markersView) { + markersView.markersViewModel.multiline = false; } }, title: { value: localize('show singleline', "Show message in single line"), original: 'Problems: Show message in single line' }, @@ -214,9 +236,9 @@ registerAction({ }); async function copyMarker(panelService: IPanelService, clipboardService: IClipboardService) { - const activePanel = panelService.getActivePanel(); - if (activePanel instanceof MarkersPanel) { - const element = (activePanel).getFocusElement(); + const markersView = getMarkersView(panelService); + if (markersView) { + const element = markersView.getFocusElement(); if (element instanceof Marker) { await clipboardService.writeText(`${element}`); } @@ -224,9 +246,9 @@ async function copyMarker(panelService: IPanelService, clipboardService: IClipbo } async function copyMessage(panelService: IPanelService, clipboardService: IClipboardService) { - const activePanel = panelService.getActivePanel(); - if (activePanel instanceof MarkersPanel) { - const element = (activePanel).getFocusElement(); + const markersView = getMarkersView(panelService); + if (markersView) { + const element = markersView.getFocusElement(); if (element instanceof Marker) { await clipboardService.writeText(element.marker.message); } @@ -234,9 +256,9 @@ async function copyMessage(panelService: IPanelService, clipboardService: IClipb } async function copyRelatedInformationMessage(panelService: IPanelService, clipboardService: IClipboardService) { - const activePanel = panelService.getActivePanel(); - if (activePanel instanceof MarkersPanel) { - const element = (activePanel).getFocusElement(); + const markersView = getMarkersView(panelService); + if (markersView) { + const element = markersView.getFocusElement(); if (element instanceof RelatedInformation) { await clipboardService.writeText(element.raw.message); } @@ -244,16 +266,16 @@ async function copyRelatedInformationMessage(panelService: IPanelService, clipbo } function focusProblemsView(panelService: IPanelService) { - const activePanel = panelService.getActivePanel(); - if (activePanel instanceof MarkersPanel) { - activePanel.focus(); + const markersView = getMarkersView(panelService); + if (markersView) { + markersView.focus(); } } -function focusProblemsFilter(panelService: IPanelService) { - const activePanel = panelService.getActivePanel(); - if (activePanel instanceof MarkersPanel) { - activePanel.focusFilter(); +function focusProblemsFilter(panelService: IPanelService): void { + const markersView = getMarkersView(panelService); + if (markersView) { + markersView.focusFilter(); } } diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index 19fac10efdc..4b63f2f07b4 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -17,7 +17,7 @@ import { attachBadgeStyler } from 'vs/platform/theme/common/styler'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IDisposable, dispose, Disposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; -import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersPanelActions'; +import { QuickFixAction, QuickFixActionViewItem } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { ILabelService } from 'vs/platform/label/common/label'; import { dirname, basename, isEqual } from 'vs/base/common/resources'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; diff --git a/src/vs/workbench/contrib/markers/browser/markersPanel.ts b/src/vs/workbench/contrib/markers/browser/markersView.ts similarity index 86% rename from src/vs/workbench/contrib/markers/browser/markersPanel.ts rename to src/vs/workbench/contrib/markers/browser/markersView.ts index 92455b67c18..20c6c153a44 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanel.ts +++ b/src/vs/workbench/contrib/markers/browser/markersView.ts @@ -9,12 +9,12 @@ import { URI } from 'vs/base/common/uri'; import * as dom from 'vs/base/browser/dom'; import { IAction, IActionViewItem, Action } from 'vs/base/common/actions'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { Panel } from 'vs/workbench/browser/panel'; +import { PaneCompositePanel } from 'vs/workbench/browser/panel'; import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/editor/common/editorService'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; import { Marker, ResourceMarkers, RelatedInformation, MarkerChangesEvent } from 'vs/workbench/contrib/markers/browser/markersModel'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { MarkersFilterActionViewItem, MarkersFilterAction, IMarkersFilterActionChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersPanelActions'; +import { MarkersFilterActionViewItem, MarkersFilterAction, IMarkersFilterActionChangeEvent, IMarkerFilterController } from 'vs/workbench/contrib/markers/browser/markersViewActions'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import { RangeHighlightDecorations } from 'vs/workbench/browser/parts/editor/rangeDecorations'; @@ -42,12 +42,22 @@ import { domEvent } from 'vs/base/browser/event'; import { ResourceLabels } from 'vs/workbench/browser/labels'; import { IMarker } from 'vs/platform/markers/common/markers'; import { withUndefinedAsNull } from 'vs/base/common/types'; -import { MementoObject } from 'vs/workbench/common/memento'; +import { MementoObject, Memento } from 'vs/workbench/common/memento'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility'; import { PANEL_BACKGROUND } from 'vs/workbench/common/theme'; import { KeyCode } from 'vs/base/common/keyCodes'; import { editorLightBulbForeground, editorLightBulbAutoFixForeground } from 'vs/platform/theme/common/colorRegistry'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; + +export function getMarkersView(panelService: IPanelService): MarkersView | undefined { + const activePanel = panelService.getActivePanel(); + if (activePanel instanceof PaneCompositePanel) { + return activePanel.getViewPaneContainer().getView(Constants.MARKERS_VIEW_ID); + } + return undefined; +} function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterator> { const markersIt = Iterator.fromArray(resourceMarkers.markers); @@ -61,7 +71,7 @@ function createResourceMarkersIterator(resourceMarkers: ResourceMarkers): Iterat } -export class MarkersPanel extends Panel implements IMarkerFilterController { +export class MarkersView extends ViewPane implements IMarkerFilterController { private lastSelectedRelativeTop: number = 0; private currentActiveResource: URI | null = null; @@ -69,11 +79,10 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private readonly rangeHighlightDecorations: RangeHighlightDecorations; private readonly filter: Filter; - private tree!: MarkersTree; - private filterActionBar!: ActionBar; - private messageBoxContainer!: HTMLElement; - private ariaLabelElement!: HTMLElement; - + private tree: MarkersTree | undefined; + private filterActionBar: ActionBar | undefined; + private messageBoxContainer: HTMLElement | undefined; + private ariaLabelElement: HTMLElement | undefined; private readonly collapseAllAction: IAction; private readonly filterAction: MarkersFilterAction; @@ -88,23 +97,25 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { readonly markersViewModel: MarkersViewModel; private isSmallLayout: boolean = false; + readonly onDidChangeVisibility = this.onDidChangeBodyVisibility; + constructor( + options: IViewPaneOptions, @IInstantiationService private readonly instantiationService: IInstantiationService, @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, + @IConfigurationService configurationService: IConfigurationService, + @ITelemetryService private readonly telemetryService: ITelemetryService, @IMarkersWorkbenchService private readonly markersWorkbenchService: IMarkersWorkbenchService, - @IStorageService storageService: IStorageService, @IContextKeyService contextKeyService: IContextKeyService, @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, + @IContextMenuService contextMenuService: IContextMenuService, @IMenuService private readonly menuService: IMenuService, - @IKeybindingService private readonly keybindingService: IKeybindingService, + @IKeybindingService keybindingService: IKeybindingService, + @IStorageService storageService: IStorageService, ) { - super(Constants.MARKERS_PANEL_ID, telemetryService, themeService, storageService); + super({ ...(options as IViewPaneOptions), id: Constants.MARKERS_VIEW_ID, ariaHeaderLabel: Messages.MARKERS_PANEL_TITLE_PROBLEMS }, keybindingService, contextMenuService, configurationService, contextKeyService); this.panelFoucusContextKey = Constants.MarkerPanelFocusContextKey.bindTo(contextKeyService); - this.panelState = this.getMemento(StorageScope.WORKSPACE); + this.panelState = new Memento(Constants.MARKERS_PANEL_STORAGE_ID, storageService).getMemento(StorageScope.WORKSPACE); this.markersViewModel = this._register(instantiationService.createInstance(MarkersViewModel, this.panelState['multiline'])); this._register(this.markersViewModel.onDidChange(marker => this.onDidChangeViewState(marker))); this.setCurrentActiveEditor(); @@ -125,9 +136,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { })); } - public create(parent: HTMLElement): void { - super.create(parent); - + public renderBody(parent: HTMLElement): void { dom.addClass(parent, 'markers-panel'); @@ -152,35 +161,41 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } })); - this.filterActionBar.push(this.filterAction); - this.render(); + this.filterActionBar!.push(this.filterAction); + this.renderContent(); } public getTitle(): string { return Messages.MARKERS_PANEL_TITLE_PROBLEMS; } - public layout(dimension: dom.Dimension): void { + public layoutBody(height: number, width: number): void { const wasSmallLayout = this.isSmallLayout; - this.isSmallLayout = dimension.width < 600; + this.isSmallLayout = width < 600 && height > 100; if (this.isSmallLayout !== wasSmallLayout) { - this.updateTitleArea(); - dom.toggleClass(this.filterActionBar.getContainer(), 'hide', !this.isSmallLayout); + this.updateActions(); + if (this.filterActionBar) { + dom.toggleClass(this.filterActionBar.getContainer(), 'hide', !this.isSmallLayout); + } } - const height = this.isSmallLayout ? dimension.height - 44 : dimension.height; - this.tree.layout(height, dimension.width); - this.messageBoxContainer.style.height = `${height}px`; - this.filterAction.layout(this.isSmallLayout ? dimension.width : dimension.width - 200); + const contentHeight = this.isSmallLayout ? height - 44 : height; + if (this.tree) { + this.tree.layout(contentHeight, width); + } + if (this.messageBoxContainer) { + this.messageBoxContainer.style.height = `${contentHeight}px`; + } + this.filterAction.layout(this.isSmallLayout ? width : width - 200); } public focus(): void { - if (this.tree.getHTMLElement() === document.activeElement) { + if (this.tree && this.tree.getHTMLElement() === document.activeElement) { return; } - if (this.isEmpty()) { + if (this.isEmpty() && this.messageBoxContainer) { this.messageBoxContainer.focus(); - } else { + } else if (this.tree) { this.tree.getHTMLElement().focus(); } } @@ -243,7 +258,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private refreshPanel(markerOrChange?: Marker | MarkerChangesEvent): void { - if (this.isVisible()) { + if (this.isVisible() && this.tree) { this.cachedFilterStats = undefined; if (markerOrChange) { @@ -277,6 +292,9 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private resetTree(): void { + if (!this.tree) { + return; + } let resourceMarkers: ResourceMarkers[] = []; if (this.filterAction.activeFile) { if (this.currentActiveResource) { @@ -294,11 +312,15 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private updateFilter() { this.cachedFilterStats = undefined; this.filter.options = new FilterOptions(this.filterAction.filterText, this.getFilesExcludeExpressions(), this.filterAction.showWarnings, this.filterAction.showErrors, this.filterAction.showInfos); - this.tree.refilter(); + if (this.tree) { + this.tree.refilter(); + } this._onDidFilter.fire(); const { total, filtered } = this.getFilterStats(); - this.tree.toggleVisibility(total === 0 || filtered === 0); + if (this.tree) { + this.tree.toggleVisibility(total === 0 || filtered === 0); + } this.renderMessage(); } @@ -354,7 +376,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { }; this.tree = this._register(this.instantiationService.createInstance(MarkersTree, - 'MarkersPanel', + 'MarkersView', dom.append(parent, dom.$('.tree-container.show-file-icons')), virtualDelegate, renderers, @@ -412,7 +434,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { })); this._register(Event.any(this.tree.onDidChangeSelection, this.tree.onDidChangeFocus)(() => { - const elements = [...this.tree.getSelection(), ...this.tree.getFocus()]; + const elements = [...this.tree!.getSelection(), ...this.tree!.getFocus()]; for (const element of elements) { if (element instanceof Marker) { const viewModel = this.markersViewModel.getViewModel(element); @@ -425,11 +447,13 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private collapseAll(): void { - this.tree.collapseAll(); - this.tree.setSelection([]); - this.tree.setFocus([]); - this.tree.getHTMLElement().focus(); - this.tree.focusFirst(); + if (this.tree) { + this.tree.collapseAll(); + this.tree.setSelection([]); + this.tree.setFocus([]); + this.tree.getHTMLElement().focus(); + this.tree.focusFirst(); + } } private createListeners(): void { @@ -440,7 +464,9 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this.onActiveEditorChanged(); } })); - this._register(this.tree.onDidChangeSelection(() => this.onSelected())); + if (this.tree) { + this._register(this.tree.onDidChangeSelection(() => this.onSelected())); + } this._register(this.filterAction.onDidChange((event: IMarkersFilterActionChangeEvent) => { this.reportFilteringUsed(); if (event.activeFile) { @@ -499,9 +525,11 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private onSelected(): void { - let selection = this.tree.getSelection(); - if (selection && selection.length > 0) { - this.lastSelectedRelativeTop = this.tree.getRelativeTop(selection[0]) || 0; + if (this.tree) { + let selection = this.tree.getSelection(); + if (selection && selection.length > 0) { + this.lastSelectedRelativeTop = this.tree!.getRelativeTop(selection[0]) || 0; + } } } @@ -510,14 +538,19 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { return total === 0 || filtered === 0; } - private render(): void { + private renderContent(): void { this.cachedFilterStats = undefined; this.resetTree(); - this.tree.toggleVisibility(this.isEmpty()); + if (this.tree) { + this.tree.toggleVisibility(this.isEmpty()); + } this.renderMessage(); } private renderMessage(): void { + if (!this.messageBoxContainer || !this.ariaLabelElement) { + return; + } dom.clearNode(this.messageBoxContainer); const { total, filtered } = this.getFilterStats(); @@ -567,19 +600,19 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { e.stopPropagation(); } }); - this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS); + this.ariaLabelElement!.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_FILTERS); } private renderNoProblemsMessageForActiveFile(container: HTMLElement) { const span = dom.append(container, dom.$('span')); span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT; - this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT); + this.ariaLabelElement!.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_ACTIVE_FILE_BUILT); } private renderNoProblemsMessage(container: HTMLElement) { const span = dom.append(container, dom.$('span')); span.textContent = Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT; - this.ariaLabelElement.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT); + this.ariaLabelElement!.setAttribute('aria-label', Messages.MARKERS_PANEL_NO_PROBLEMS_BUILT); } private clearFilters(): void { @@ -592,7 +625,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private autoReveal(focus: boolean = false): void { // No need to auto reveal if active file filter is on - if (this.filterAction.activeFile) { + if (this.filterAction.activeFile || !this.tree) { return; } let autoReveal = this.configurationService.getValue('problems.autoReveal'); @@ -625,11 +658,13 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private hasSelectedMarkerFor(resource: ResourceMarkers): boolean { - let selectedElement = this.tree.getSelection(); - if (selectedElement && selectedElement.length > 0) { - if (selectedElement[0] instanceof Marker) { - if (resource.resource.toString() === (selectedElement[0]).marker.resource.toString()) { - return true; + if (this.tree) { + let selectedElement = this.tree.getSelection(); + if (selectedElement && selectedElement.length > 0) { + if (selectedElement[0] instanceof Marker) { + if (resource.resource.toString() === (selectedElement[0]).marker.resource.toString()) { + return true; + } } } } @@ -638,13 +673,13 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { private updateRangeHighlights() { this.rangeHighlightDecorations.removeHighlightRange(); - if (this.tree.getHTMLElement() === document.activeElement) { + if (this.tree && this.tree.getHTMLElement() === document.activeElement) { this.highlightCurrentSelectedMarkerRange(); } } private highlightCurrentSelectedMarkerRange() { - const selections = this.tree.getSelection(); + const selections = this.tree ? this.tree.getSelection() : []; if (selections.length !== 1) { return; @@ -680,7 +715,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { }, onHide: (wasCancelled?: boolean) => { if (wasCancelled) { - this.tree.domFocus(); + this.tree!.domFocus(); } } }); @@ -700,7 +735,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } } - const menu = this.menuService.createMenu(MenuId.ProblemsPanelContext, this.tree.contextKeyService); + const menu = this.menuService.createMenu(MenuId.ProblemsPanelContext, this.tree!.contextKeyService); const groups = menu.getActions(); menu.dispose(); @@ -715,7 +750,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } public getFocusElement() { - return this.tree.getFocus()[0]; + return this.tree ? this.tree.getFocus()[0] : undefined; } public getActionViewItem(action: IAction): IActionViewItem | undefined { @@ -738,13 +773,15 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { } private computeFilterStats(): { total: number; filtered: number; } { - const root = this.tree.getNode(); let filtered = 0; + if (this.tree) { + const root = this.tree.getNode(); - for (const resourceMarkerNode of root.children) { - for (const markerNode of resourceMarkerNode.children) { - if (resourceMarkerNode.visible && markerNode.visible) { - filtered++; + for (const resourceMarkerNode of root.children) { + for (const markerNode of resourceMarkerNode.children) { + if (resourceMarkerNode.visible && markerNode.visible) { + filtered++; + } } } } @@ -776,7 +813,7 @@ export class MarkersPanel extends Panel implements IMarkerFilterController { this.telemetryService.publicLog('problems.filter', data); } - protected saveState(): void { + saveState(): void { this.panelState['filter'] = this.filterAction.filterText; this.panelState['filterHistory'] = this.filterAction.filterHistory; this.panelState['showErrors'] = this.filterAction.showErrors; diff --git a/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts similarity index 96% rename from src/vs/workbench/contrib/markers/browser/markersPanelActions.ts rename to src/vs/workbench/contrib/markers/browser/markersViewActions.ts index 39719499d5d..c21572f70bf 100644 --- a/src/vs/workbench/contrib/markers/browser/markersPanelActions.ts +++ b/src/vs/workbench/contrib/markers/browser/markersViewActions.ts @@ -10,10 +10,8 @@ import { HistoryInputBox } from 'vs/base/browser/ui/inputbox/inputBox'; import { KeyCode } from 'vs/base/common/keyCodes'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { TogglePanelAction } from 'vs/workbench/browser/panel'; import Messages from 'vs/workbench/contrib/markers/browser/messages'; import Constants from 'vs/workbench/contrib/markers/browser/constants'; -import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IThemeService, registerThemingParticipant, ICssStyleCollector, ITheme } from 'vs/platform/theme/common/themeService'; import { attachInputBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; @@ -30,19 +28,6 @@ import { FilterOptions } from 'vs/workbench/contrib/markers/browser/markersFilte import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdown'; import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview'; -export class ToggleMarkersPanelAction extends TogglePanelAction { - - public static readonly ID = 'workbench.actions.view.problems'; - public static readonly LABEL = Messages.MARKERS_PANEL_TOGGLE_LABEL; - - constructor(id: string, label: string, - @IWorkbenchLayoutService layoutService: IWorkbenchLayoutService, - @IPanelService panelService: IPanelService - ) { - super(id, label, Constants.MARKERS_PANEL_ID, panelService, layoutService); - } -} - export class ShowProblemsPanelAction extends Action { public static readonly ID = 'workbench.action.problems.focus'; @@ -304,6 +289,7 @@ export class MarkersFilterActionViewItem extends BaseActionViewItem { this.element.className = this.action.class || ''; this.createInput(this.element); this.createControls(this.element); + this.updateClass(); this.adjustInputBox(); } diff --git a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts index dc855f0f9d6..d3d712fe18a 100644 --- a/src/vs/workbench/contrib/outline/browser/outline.contribution.ts +++ b/src/vs/workbench/contrib/outline/browser/outline.contribution.ts @@ -6,10 +6,10 @@ import { localize } from 'vs/nls'; import { IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions } from 'vs/workbench/common/views'; import { OutlinePane } from './outlinePane'; -import { VIEW_CONTAINER } from 'vs/workbench/contrib/files/common/files'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { OutlineConfigKeys, OutlineViewId } from 'vs/editor/contrib/documentSymbols/outline'; +import { VIEW_CONTAINER } from 'vs/workbench/contrib/files/browser/explorerViewlet'; // import './outlineNavigation'; diff --git a/src/vs/workbench/contrib/output/browser/outputActions.ts b/src/vs/workbench/contrib/output/browser/outputActions.ts index 1d16cfea3b1..4686280ec9a 100644 --- a/src/vs/workbench/contrib/output/browser/outputActions.ts +++ b/src/vs/workbench/contrib/output/browser/outputActions.ts @@ -173,7 +173,7 @@ export class SwitchOutputActionViewItem extends SelectActionViewItem { selected = logChannelIndex !== -1 ? separatorIndex + 1 + logChannelIndex : 0; } } - this.setOptions(options.map((label, index) => { text: label, isDisabled: (index === separatorIndex ? true : undefined) }), Math.max(0, selected)); + this.setOptions(options.map((label, index) => { text: label, isDisabled: (index === separatorIndex ? true : false) }), Math.max(0, selected)); } } diff --git a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts index cde13f2ab32..fef8e28c6c8 100644 --- a/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts +++ b/src/vs/workbench/contrib/preferences/common/preferencesContribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as nls from 'vs/nls'; import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { endsWith } from 'vs/base/common/strings'; @@ -22,6 +23,7 @@ import { IEditorInput } from 'vs/workbench/common/editor'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { FOLDER_SETTINGS_PATH, IPreferencesService, USE_SPLIT_JSON_SETTING } from 'vs/workbench/services/preferences/common/preferences'; +import { Extensions, IConfigurationRegistry, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; const schemaRegistry = Registry.as(JSONContributionRegistry.Extensions.JSONContribution); @@ -147,3 +149,27 @@ export class PreferencesContribution implements IWorkbenchContribution { dispose(this.settingsListener); } } + +const registry = Registry.as(Extensions.Configuration); +registry.registerConfiguration({ + 'properties': { + 'workbench.settings.enableNaturalLanguageSearch': { + 'type': 'boolean', + 'description': nls.localize('enableNaturalLanguageSettingsSearch', "Controls whether to enable the natural language search mode for settings. The natural language search is provided by a Microsoft online service."), + 'default': true, + 'scope': ConfigurationScope.WINDOW, + 'tags': ['usesOnlineServices'] + }, + 'workbench.settings.settingsSearchTocBehavior': { + 'type': 'string', + 'enum': ['hide', 'filter'], + 'enumDescriptions': [ + nls.localize('settingsSearchTocBehavior.hide', "Hide the Table of Contents while searching."), + nls.localize('settingsSearchTocBehavior.filter', "Filter the Table of Contents to just categories that have matching settings. Clicking a category will filter the results to that category."), + ], + 'description': nls.localize('settingsSearchTocBehavior', "Controls the behavior of the settings editor Table of Contents while searching."), + 'default': 'filter', + 'scope': ConfigurationScope.WINDOW + }, + } +}); diff --git a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css index 0b34a1d1259..659c875492f 100644 --- a/src/vs/workbench/contrib/remote/browser/media/tunnelView.css +++ b/src/vs/workbench/contrib/remote/browser/media/tunnelView.css @@ -6,7 +6,3 @@ .customview-tree .tunnel-view-label { flex: 1; } - -.customview-tree .tunnel-view-label .action-label.codicon { - margin-top: 4px; -} diff --git a/src/vs/workbench/contrib/remote/browser/remote.ts b/src/vs/workbench/contrib/remote/browser/remote.ts index cbd0e4e7728..3edc8581464 100644 --- a/src/vs/workbench/contrib/remote/browser/remote.ts +++ b/src/vs/workbench/contrib/remote/browser/remote.ts @@ -5,6 +5,7 @@ import 'vs/css!./remoteViewlet'; import * as nls from 'vs/nls'; +import * as dom from 'vs/base/browser/dom'; import { URI } from 'vs/base/common/uri'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; @@ -16,15 +17,15 @@ import { IThemeService } from 'vs/platform/theme/common/themeService'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { FilterViewPaneContainer } from 'vs/workbench/browser/parts/views/viewsViewlet'; -import { VIEWLET_ID, VIEW_CONTAINER } from 'vs/workbench/contrib/remote/common/remote.contribution'; +import { VIEWLET_ID } from 'vs/workbench/contrib/remote/common/remote.contribution'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IViewDescriptor, IViewsRegistry, Extensions } from 'vs/workbench/common/views'; +import { IViewDescriptor, IViewsRegistry, Extensions, ViewContainerLocation, IViewContainersRegistry } from 'vs/workbench/common/views'; import { Registry } from 'vs/platform/registry/common/platform'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { IOpenerService } from 'vs/platform/opener/common/opener'; -import { IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput'; +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; import { ICommandService } from 'vs/platform/commands/common/commands'; -import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction, Viewlet } from 'vs/workbench/browser/viewlet'; +import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; @@ -41,17 +42,124 @@ import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { SwitchRemoteViewItem, SwitchRemoteAction } from 'vs/workbench/contrib/remote/browser/explorerViewItems'; import { Action, IActionViewItem, IAction } from 'vs/base/common/actions'; import { isStringArray } from 'vs/base/common/types'; -import { IRemoteExplorerService, HelpInformation } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { startsWith } from 'vs/base/common/strings'; import { TunnelPanelDescriptor, TunnelViewModel, forwardedPortsViewEnabled } from 'vs/workbench/contrib/remote/browser/tunnelView'; import { IAddedViewDescriptorRef } from 'vs/workbench/browser/parts/views/views'; -import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { ViewPane, IViewPaneOptions } from 'vs/workbench/browser/parts/views/viewPaneContainer'; +import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; +import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree'; +import { WorkbenchAsyncDataTree, TreeResourceNavigator2 } from 'vs/platform/list/browser/listService'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { Event } from 'vs/base/common/event'; +import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; + +export interface HelpInformation { + extensionDescription: IExtensionDescription; + getStarted?: string; + documentation?: string; + feedback?: string; + issues?: string; + remoteName?: string[] | string; +} + +const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint({ + extensionPoint: 'remoteHelp', + jsonSchema: { + description: nls.localize('RemoteHelpInformationExtPoint', 'Contributes help information for Remote'), + type: 'object', + properties: { + 'getStarted': { + description: nls.localize('RemoteHelpInformationExtPoint.getStarted', "The url to your project's Getting Started page"), + type: 'string' + }, + 'documentation': { + description: nls.localize('RemoteHelpInformationExtPoint.documentation', "The url to your project's documentation page"), + type: 'string' + }, + 'feedback': { + description: nls.localize('RemoteHelpInformationExtPoint.feedback', "The url to your project's feedback reporter"), + type: 'string' + }, + 'issues': { + description: nls.localize('RemoteHelpInformationExtPoint.issues', "The url to your project's issues list"), + type: 'string' + } + } + } +}); + +interface IViewModel { + helpInformation: HelpInformation[]; +} + +class HelpTreeVirtualDelegate implements IListVirtualDelegate { + getHeight(element: IHelpItem): number { + return 22; + } + + getTemplateId(element: IHelpItem): string { + return 'HelpItemTemplate'; + } +} + +interface IHelpItemTemplateData { + parent: HTMLElement; + icon: HTMLElement; +} + +class HelpTreeRenderer implements ITreeRenderer { + templateId: string = 'HelpItemTemplate'; + + renderTemplate(container: HTMLElement): IHelpItemTemplateData { + dom.addClass(container, 'remote-help-tree-node-item'); + const icon = dom.append(container, dom.$('.remote-help-tree-node-item-icon')); + const data = Object.create(null); + data.parent = container; + data.icon = icon; + return data; + } + + renderElement(element: ITreeNode, index: number, templateData: IHelpItemTemplateData, height: number | undefined): void { + const container = templateData.parent; + dom.append(container, templateData.icon); + dom.addClasses(templateData.icon, ...element.element.iconClasses); + const labelContainer = dom.append(container, dom.$('.help-item-label')); + labelContainer.innerText = element.element.label; + } + + disposeTemplate(templateData: IHelpItemTemplateData): void { + + } +} + +class HelpDataSource implements IAsyncDataSource { + hasChildren(element: any) { + return element instanceof HelpModel; + } + + getChildren(element: any) { + if (element instanceof HelpModel && element.items) { + return element.items; + } + + return []; + } +} + +interface IHelpItem { + key: string; + iconClasses: string[]; + label: string; + handleClick(): Promise; +} class HelpModel { items: IHelpItem[] | undefined; constructor( + viewModel: IViewModel, openerService: IOpenerService, quickInputService: IQuickInputService, commandService: ICommandService, @@ -59,12 +167,12 @@ class HelpModel { environmentService: IWorkbenchEnvironmentService ) { let helpItems: IHelpItem[] = []; - const getStarted = remoteExplorerService.helpInformation.filter(info => info.getStarted); + const getStarted = viewModel.helpInformation.filter(info => info.getStarted); if (getStarted.length) { helpItems.push(new HelpItem( - ['getStarted'], - nls.localize('remote.help.getStarted', "$(star) Get Started"), + 'star', + nls.localize('remote.help.getStarted', "Get Started"), getStarted.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.getStarted!, @@ -77,12 +185,12 @@ class HelpModel { )); } - const documentation = remoteExplorerService.helpInformation.filter(info => info.documentation); + const documentation = viewModel.helpInformation.filter(info => info.documentation); if (documentation.length) { helpItems.push(new HelpItem( - ['documentation'], - nls.localize('remote.help.documentation', "$(book) Read Documentation"), + 'book', + nls.localize('remote.help.documentation', "Read Documentation"), documentation.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.documentation!, @@ -95,12 +203,12 @@ class HelpModel { )); } - const feedback = remoteExplorerService.helpInformation.filter(info => info.feedback); + const feedback = viewModel.helpInformation.filter(info => info.feedback); if (feedback.length) { helpItems.push(new HelpItem( - ['feedback'], - nls.localize('remote.help.feedback', "$(twitter) Provide Feedback"), + 'twitter', + nls.localize('remote.help.feedback', "Provide Feedback"), feedback.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.feedback!, @@ -113,12 +221,12 @@ class HelpModel { )); } - const issues = remoteExplorerService.helpInformation.filter(info => info.issues); + const issues = viewModel.helpInformation.filter(info => info.issues); if (issues.length) { helpItems.push(new HelpItem( - ['issues'], - nls.localize('remote.help.issues', "$(issues) Review Issues"), + 'issues', + nls.localize('remote.help.issues', "Review Issues"), issues.map((info: HelpInformation) => ({ extensionDescription: info.extensionDescription, url: info.issues!, @@ -133,9 +241,9 @@ class HelpModel { if (helpItems.length) { helpItems.push(new IssueReporterItem( - ['issueReporter'], - nls.localize('remote.help.report', "$(comment) Report Issue"), - remoteExplorerService.helpInformation.map(info => ({ + 'comment', + nls.localize('remote.help.report', "Report Issue"), + viewModel.helpInformation.map(info => ({ extensionDescription: info.extensionDescription, remoteAuthority: (typeof info.remoteName === 'string') ? [info.remoteName] : info.remoteName })), @@ -152,21 +260,19 @@ class HelpModel { } } -interface IHelpItem extends IQuickPickItem { - label: string; - handleClick(): Promise; -} - abstract class HelpItemBase implements IHelpItem { + public iconClasses: string[] = []; constructor( - public iconClasses: string[], + public key: string, public label: string, public values: { extensionDescription: IExtensionDescription, url?: string, remoteAuthority: string[] | undefined }[], private quickInputService: IQuickInputService, private environmentService: IWorkbenchEnvironmentService, private remoteExplorerService: IRemoteExplorerService ) { - iconClasses.push('remote-help-tree-node-item-icon'); + this.iconClasses.push(`codicon-${key}`); + this.iconClasses.push('remote-help-tree-node-item-icon'); + this.iconClasses.push('codicon'); } async handleClick() { @@ -208,7 +314,7 @@ abstract class HelpItemBase implements IHelpItem { class HelpItem extends HelpItemBase { constructor( - iconClasses: string[], + key: string, label: string, values: { extensionDescription: IExtensionDescription; url: string, remoteAuthority: string[] | undefined }[], quickInputService: IQuickInputService, @@ -216,7 +322,7 @@ class HelpItem extends HelpItemBase { private openerService: IOpenerService, remoteExplorerService: IRemoteExplorerService ) { - super(iconClasses, label, values, quickInputService, environmentService, remoteExplorerService); + super(key, label, values, quickInputService, environmentService, remoteExplorerService); } protected async takeAction(extensionDescription: IExtensionDescription, url: string): Promise { @@ -226,7 +332,7 @@ class HelpItem extends HelpItemBase { class IssueReporterItem extends HelpItemBase { constructor( - iconClasses: string[], + key: string, label: string, values: { extensionDescription: IExtensionDescription; remoteAuthority: string[] | undefined }[], quickInputService: IQuickInputService, @@ -234,7 +340,7 @@ class IssueReporterItem extends HelpItemBase { private commandService: ICommandService, remoteExplorerService: IRemoteExplorerService ) { - super(iconClasses, label, values, quickInputService, environmentService, remoteExplorerService); + super(key, label, values, quickInputService, environmentService, remoteExplorerService); } protected async takeAction(extensionDescription: IExtensionDescription): Promise { @@ -242,53 +348,77 @@ class IssueReporterItem extends HelpItemBase { } } -class HelpAction extends Action { - static readonly ID = 'remote.explorer.help'; - static readonly LABEL = nls.localize('remote.explorer.help', "Help, Documentation, and Feedback"); - private helpModel: HelpModel; +class HelpPanel extends ViewPane { + static readonly ID = '~remote.helpPanel'; + static readonly TITLE = nls.localize('remote.help', "Help and feedback"); + private tree!: WorkbenchAsyncDataTree; - constructor(id: string, - label: string, - @IOpenerService private readonly openerService: IOpenerService, - @IQuickInputService private readonly quickInputService: IQuickInputService, - @ICommandService private readonly commandService: ICommandService, - @IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService, - @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService - ) { - super(id, label, 'codicon codicon-question'); - this.helpModel = new HelpModel(openerService, quickInputService, commandService, remoteExplorerService, environmentService); - } - - async run(event?: any): Promise { - if (!this.helpModel.items) { - this.helpModel = new HelpModel(this.openerService, this.quickInputService, this.commandService, this.remoteExplorerService, this.environmentService); - } - if (this.helpModel.items) { - const selection = await this.quickInputService.pick(this.helpModel.items, { placeHolder: nls.localize('remote.explorer.helpPlaceholder', "Help and Feedback") }); - if (selection) { - return selection.handleClick(); - } - } - } -} - -export class RemoteViewlet extends Viewlet { constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService protected storageService: IStorageService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, + protected viewModel: IViewModel, + options: IViewPaneOptions, + @IKeybindingService protected keybindingService: IKeybindingService, @IContextMenuService protected contextMenuService: IContextMenuService, - @IExtensionService protected extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, - @IConfigurationService protected configurationService: IConfigurationService + @IContextKeyService protected contextKeyService: IContextKeyService, + @IConfigurationService protected configurationService: IConfigurationService, + @IInstantiationService protected readonly instantiationService: IInstantiationService, + @IOpenerService protected openerService: IOpenerService, + @IQuickInputService protected quickInputService: IQuickInputService, + @ICommandService protected commandService: ICommandService, + @IRemoteExplorerService protected readonly remoteExplorerService: IRemoteExplorerService, + @IWorkbenchEnvironmentService protected readonly workbenchEnvironmentService: IWorkbenchEnvironmentService ) { - super(VIEWLET_ID, instantiationService.createInstance(RemoteViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); + super(options, keybindingService, contextMenuService, configurationService, contextKeyService); + } + + protected renderBody(container: HTMLElement): void { + dom.addClass(container, 'remote-help'); + const treeContainer = document.createElement('div'); + dom.addClass(treeContainer, 'remote-help-content'); + container.appendChild(treeContainer); + + this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, + 'RemoteHelp', + treeContainer, + new HelpTreeVirtualDelegate(), + [new HelpTreeRenderer()], + new HelpDataSource(), + { + keyboardSupport: true, + } + ); + + const model = new HelpModel(this.viewModel, this.openerService, this.quickInputService, this.commandService, this.remoteExplorerService, this.workbenchEnvironmentService); + + this.tree.setInput(model); + + const helpItemNavigator = this._register(new TreeResourceNavigator2(this.tree, { openOnFocus: false, openOnSelection: false })); + + this._register(Event.debounce(helpItemNavigator.onDidOpenResource, (last, event) => event, 75, true)(e => { + e.element.handleClick(); + })); + } + + protected layoutBody(height: number, width: number): void { + this.tree.layout(height, width); } } -export class RemoteViewPaneContainer extends FilterViewPaneContainer { +class HelpPanelDescriptor implements IViewDescriptor { + readonly id = HelpPanel.ID; + readonly name = HelpPanel.TITLE; + readonly ctorDescriptor: { ctor: any, arguments?: any[] }; + readonly canToggleVisibility = true; + readonly hideByDefault = false; + readonly workspace = true; + + constructor(viewModel: IViewModel) { + this.ctorDescriptor = { ctor: HelpPanel, arguments: [viewModel] }; + } +} + +export class RemoteViewPaneContainer extends FilterViewPaneContainer implements IViewModel { + private helpPanelDescriptor = new HelpPanelDescriptor(this); + helpInformation: HelpInformation[] = []; private actions: IAction[] | undefined; private tunnelPanelDescriptor: TunnelPanelDescriptor | undefined; @@ -307,6 +437,41 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer { @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(VIEWLET_ID, remoteExplorerService.onDidChangeTargetType, configurationService, layoutService, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService); + this.addConstantViewDescriptors([this.helpPanelDescriptor]); + remoteHelpExtPoint.setHandler((extensions) => { + let helpInformation: HelpInformation[] = []; + for (let extension of extensions) { + this._handleRemoteInfoExtensionPoint(extension, helpInformation); + } + + this.helpInformation = helpInformation; + + const viewsRegistry = Registry.as(Extensions.ViewsRegistry); + if (this.helpInformation.length) { + viewsRegistry.registerViews([this.helpPanelDescriptor], this.viewContainer); + } else { + viewsRegistry.deregisterViews([this.helpPanelDescriptor], this.viewContainer); + } + }); + } + + private _handleRemoteInfoExtensionPoint(extension: IExtensionPointUser, helpInformation: HelpInformation[]) { + if (!extension.description.enableProposedApi) { + return; + } + + if (!extension.value.documentation && !extension.value.feedback && !extension.value.getStarted && !extension.value.issues) { + return; + } + + helpInformation.push({ + extensionDescription: extension.description, + getStarted: extension.value.getStarted, + documentation: extension.value.documentation, + feedback: extension.value.feedback, + issues: extension.value.issues, + remoteName: extension.value.remoteName + }); } protected getFilterOn(viewDescriptor: IViewDescriptor): string | undefined { @@ -315,7 +480,7 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer { public getActionViewItem(action: Action): IActionViewItem | undefined { if (action.id === SwitchRemoteAction.ID) { - return this.instantiationService.createInstance(SwitchRemoteViewItem, action, SwitchRemoteViewItem.createOptionItems(Registry.as(Extensions.ViewsRegistry).getViews(VIEW_CONTAINER), this.contextKeyService)); + return this.instantiationService.createInstance(SwitchRemoteViewItem, action, SwitchRemoteViewItem.createOptionItems(Registry.as(Extensions.ViewsRegistry).getViews(this.viewContainer), this.contextKeyService)); } return super.getActionViewItem(action); @@ -324,8 +489,7 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer { public getActions(): IAction[] { if (!this.actions) { this.actions = [ - this.instantiationService.createInstance(SwitchRemoteAction, SwitchRemoteAction.ID, SwitchRemoteAction.LABEL), - this.instantiationService.createInstance(HelpAction, HelpAction.ID, HelpAction.LABEL) + this.instantiationService.createInstance(SwitchRemoteAction, SwitchRemoteAction.ID, SwitchRemoteAction.LABEL) ]; this.actions.forEach(a => { this._register(a); @@ -347,19 +511,41 @@ export class RemoteViewPaneContainer extends FilterViewPaneContainer { if (this.environmentService.configuration.remoteAuthority && !this.tunnelPanelDescriptor && viewEnabled) { this.tunnelPanelDescriptor = new TunnelPanelDescriptor(new TunnelViewModel(this.remoteExplorerService), this.environmentService); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([this.tunnelPanelDescriptor!], VIEW_CONTAINER); + viewsRegistry.registerViews([this.tunnelPanelDescriptor!], this.viewContainer); } return panels; } } -Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( - RemoteViewlet, - VIEWLET_ID, - nls.localize('remote.explorer', "Remote Explorer"), - 'codicon-remote-explorer', - 4 -)); +Registry.as(Extensions.ViewContainersRegistry).registerViewContainer( + { + id: VIEWLET_ID, + name: nls.localize('remote.explorer', "Remote Explorer"), + ctorDescriptor: { ctor: RemoteViewPaneContainer }, + hideIfEmpty: true, + viewOrderDelegate: { + getOrder: (group?: string) => { + if (!group) { + return; + } + + let matches = /^targets@(\d+)$/.exec(group); + if (matches) { + return -1000; + } + + matches = /^details(@(\d+))?$/.exec(group); + + if (matches) { + return -500; + } + + return; + } + }, + icon: 'codicon-remote-explorer', + order: 4 + }, ViewContainerLocation.Sidebar); class OpenRemoteViewletAction extends ShowViewletAction { diff --git a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css index c8f0d9f2964..c1d72e03738 100644 --- a/src/vs/workbench/contrib/remote/browser/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/remoteViewlet.css @@ -3,14 +3,32 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.remote-help-tree-node-item-icon { +.remote-help-content .monaco-list .monaco-list-row .remote-help-tree-node-item { + display: flex; + height: 22px; + line-height: 22px; + flex: 1; + text-overflow: ellipsis; + overflow: hidden; + flex-wrap: nowrap; +} + +.remote-help-content .monaco-list .monaco-list-row .remote-help-tree-node-item>.remote-help-tree-node-item-icon { background-size: 16px; background-position: left center; background-repeat: no-repeat; + padding-right: 6px; + padding-top: 3px; + width: 16px; + height: 22px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +.remote-help-content .monaco-list .monaco-list-row .monaco-tl-twistie { + width: 0px !important; +} + .remote-help-tree-node-item-icon .monaco-icon-label-description-container { padding-left: 22px; } diff --git a/src/vs/workbench/contrib/remote/browser/tunnelView.ts b/src/vs/workbench/contrib/remote/browser/tunnelView.ts index 1e881877649..f2006794e8f 100644 --- a/src/vs/workbench/contrib/remote/browser/tunnelView.ts +++ b/src/vs/workbench/contrib/remote/browser/tunnelView.ts @@ -20,13 +20,13 @@ import { Event, Emitter } from 'vs/base/common/event'; import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list'; import { ITreeRenderer, ITreeNode, IAsyncDataSource, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; -import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, toDisposable, MutableDisposable, dispose, DisposableStore } from 'vs/base/common/lifecycle'; import { ActionBar, ActionViewItem, IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { ActionRunner, IAction } from 'vs/base/common/actions'; import { IMenuService, MenuId, IMenu, MenuRegistry, MenuItemAction } from 'vs/platform/actions/common/actions'; import { createAndFillInContextMenuActions, createAndFillInActionBarActions, ContextAwareMenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem'; -import { IRemoteExplorerService, TunnelModel } from 'vs/workbench/services/remote/common/remoteExplorerService'; +import { IRemoteExplorerService, TunnelModel, MakeAddress, TunnelType, ITunnelItem } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { InputBox, MessageType } from 'vs/base/browser/ui/inputbox/inputBox'; @@ -55,6 +55,7 @@ export interface ITunnelViewModel { readonly forwarded: TunnelItem[]; readonly detected: TunnelItem[]; readonly candidates: Promise; + readonly input: ITunnelItem | ITunnelGroup | undefined; groups(): Promise; } @@ -62,6 +63,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { private _onForwardedPortsChanged: Emitter = new Emitter(); public onForwardedPortsChanged: Event = this._onForwardedPortsChanged.event; private model: TunnelModel; + private _input: ITunnelItem | ITunnelGroup | undefined; constructor( @IRemoteExplorerService remoteExplorerService: IRemoteExplorerService) { @@ -70,6 +72,7 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { this._register(this.model.onForwardPort(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onClosePort(() => this._onForwardedPortsChanged.fire())); this._register(this.model.onPortName(() => this._onForwardedPortsChanged.fire())); + this._register(this.model.onCandidatesChanged(() => this._onForwardedPortsChanged.fire())); } async groups(): Promise { @@ -96,22 +99,25 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { items: candidates }); } - groups.push({ - label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), - tunnelType: TunnelType.Add, - }); + if (!this._input) { + this._input = { + label: nls.localize('remote.tunnelsView.add', "Forward a Port..."), + tunnelType: TunnelType.Add, + }; + } + groups.push(this._input); return groups; } get forwarded(): TunnelItem[] { return Array.from(this.model.forwarded.values()).map(tunnel => { - return new TunnelItem(TunnelType.Forwarded, tunnel.remote, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description); + return new TunnelItem(TunnelType.Forwarded, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, tunnel.closeable, tunnel.name, tunnel.description); }); } get detected(): TunnelItem[] { return Array.from(this.model.detected.values()).map(tunnel => { - return new TunnelItem(TunnelType.Detected, tunnel.remote, tunnel.localAddress, false, tunnel.name, tunnel.description); + return new TunnelItem(TunnelType.Detected, tunnel.remoteHost, tunnel.remotePort, tunnel.localAddress, false, tunnel.name, tunnel.description); }); } @@ -119,14 +125,19 @@ export class TunnelViewModel extends Disposable implements ITunnelViewModel { return this.model.candidates.then(values => { const candidates: TunnelItem[] = []; values.forEach(value => { - if (!this.model.forwarded.has(value.port) && !this.model.detected.has(value.port)) { - candidates.push(new TunnelItem(TunnelType.Candidate, value.port, undefined, false, undefined, value.detail)); + const key = MakeAddress(value.host, value.port); + if (!this.model.forwarded.has(key) && !this.model.detected.has(key)) { + candidates.push(new TunnelItem(TunnelType.Candidate, value.host, value.port, undefined, false, undefined, value.detail)); } }); return candidates; }); } + get input(): ITunnelItem | ITunnelGroup | undefined { + return this._input; + } + dispose() { super.dispose(); } @@ -185,7 +196,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRendereritem).remote); + return !!((item).remotePort); } renderElement(element: ITreeNode, index: number, templateData: ITunnelTemplateData): void { @@ -196,7 +207,7 @@ class TunnelTreeRenderer extends Disposable implements ITreeRenderer; } -interface ITunnelItem { - tunnelType: TunnelType; - remote: number; - localAddress?: string; - name?: string; - closeable?: boolean; - readonly description?: string; - readonly label: string; -} - class TunnelItem implements ITunnelItem { constructor( public tunnelType: TunnelType, - public remote: number, + public remoteHost: string, + public remotePort: number, public localAddress?: string, public closeable?: boolean, public name?: string, @@ -359,9 +355,9 @@ class TunnelItem implements ITunnelItem { if (this.name) { return nls.localize('remote.tunnelsView.forwardedPortLabel0', "{0}", this.name); } else if (this.localAddress) { - return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0} to {1}", this.remote, this.localAddress); + return nls.localize('remote.tunnelsView.forwardedPortLabel2', "{0} to {1}", this.remotePort, this.localAddress); } else { - return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} not forwarded", this.remote); + return nls.localize('remote.tunnelsView.forwardedPortLabel3', "{0} not forwarded", this.remotePort); } } @@ -369,7 +365,7 @@ class TunnelItem implements ITunnelItem { if (this._description) { return this._description; } else if (this.name) { - return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remote, this.localAddress); + return nls.localize('remote.tunnelsView.forwardedPortDescription0', "{0} to {1}", this.remotePort, this.localAddress); } return undefined; } @@ -429,12 +425,11 @@ export class TunnelPanel extends ViewPane { } protected renderBody(container: HTMLElement): void { - dom.addClass(container, '.tree-explorer-viewlet-tree-view'); - const treeContainer = document.createElement('div'); - dom.addClass(treeContainer, 'customview-tree'); + const panelContainer = dom.append(container, dom.$('.tree-explorer-viewlet-tree-view')); + const treeContainer = dom.append(panelContainer, dom.$('.customview-tree')); dom.addClass(treeContainer, 'file-icon-themable-tree'); dom.addClass(treeContainer, 'show-file-icons'); - container.appendChild(treeContainer); + const renderer = new TunnelTreeRenderer(TunnelPanel.ID, this.menuService, this.contextKeyService, this.instantiationService, this.contextViewService, this.themeService, this.remoteExplorerService); this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree, 'RemoteTunnels', @@ -469,7 +464,7 @@ export class TunnelPanel extends ViewPane { this._register(Event.debounce(navigator.onDidOpenResource, (last, event) => event, 75, true)(e => { if (e.element && (e.element.tunnelType === TunnelType.Add)) { - this.commandService.executeCommand(ForwardPortAction.ID); + this.commandService.executeCommand(ForwardPortAction.INLINE_ID); } })); @@ -484,6 +479,7 @@ export class TunnelPanel extends ViewPane { if (isEditing) { dom.addClass(treeContainer, 'highlight'); + this.tree.reveal(e ? e : this.viewModel.input); } else { this.tree.domFocus(); } @@ -491,8 +487,7 @@ export class TunnelPanel extends ViewPane { } private get contributedContextMenu(): IMenu { - const contributedContextMenu = this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService); - this._register(contributedContextMenu); + const contributedContextMenu = this._register(this.menuService.createMenu(MenuId.TunnelContext, this.tree.contextKeyService)); return contributedContextMenu; } @@ -575,12 +570,12 @@ namespace LabelTunnelAction { return async (accessor, arg) => { if (arg instanceof TunnelItem) { const remoteExplorerService = accessor.get(IRemoteExplorerService); - remoteExplorerService.setEditable(arg.remote, { + remoteExplorerService.setEditable(arg, { onFinish: (value, success) => { if (success) { - remoteExplorerService.tunnelModel.name(arg.remote, value); + remoteExplorerService.tunnelModel.name(arg.remoteHost, arg.remotePort, value); } - remoteExplorerService.setEditable(arg.remote, null); + remoteExplorerService.setEditable(arg, null); }, validationMessage: () => null, placeholder: nls.localize('remote.tunnelsView.labelPlaceholder', "Port label"), @@ -593,36 +588,63 @@ namespace LabelTunnelAction { } namespace ForwardPortAction { - export const ID = 'remote.tunnel.forward'; + export const INLINE_ID = 'remote.tunnel.forwardInline'; + export const COMMANDPALETTE_ID = 'remote.tunnel.forwardCommandPalette'; export const LABEL = nls.localize('remote.tunnel.forward', "Forward a Port"); + const forwardPrompt = nls.localize('remote.tunnel.forwardPrompt', "Port number or address (eg. 3000 or 10.10.10.10:2000)."); - export function handler(): ICommandHandler { + function parseInput(value: string): { host: string, port: number } | undefined { + const matches = value.match(/^([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+\:|localhost:)?([0-9]+)$/); + if (!matches) { + return undefined; + } + return { host: matches[1]?.substring(0, matches[1].length - 1) || 'localhost', port: Number(matches[2]) }; + } + + function validateInput(value: string): string | null { + if (!parseInput(value)) { + return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid"); + } + return null; + } + + export function inlineHandler(): ICommandHandler { return async (accessor, arg) => { const remoteExplorerService = accessor.get(IRemoteExplorerService); if (arg instanceof TunnelItem) { - remoteExplorerService.tunnelModel.forward(arg.remote); + remoteExplorerService.forward({ host: arg.remoteHost, port: arg.remotePort }); } else { - const viewsService = accessor.get(IViewsService); - await viewsService.openView(TunnelPanel.ID, true); remoteExplorerService.setEditable(undefined, { onFinish: (value, success) => { - if (success) { - remoteExplorerService.tunnelModel.forward(Number(value)); + let parsed: { host: string, port: number } | undefined; + if (success && (parsed = parseInput(value))) { + remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); } remoteExplorerService.setEditable(undefined, null); }, - validationMessage: (value) => { - const asNumber = Number(value); - if ((value === '') || isNaN(asNumber) || (asNumber < 0) || (asNumber > 65535)) { - return nls.localize('remote.tunnelsView.portNumberValid', "Port number is invalid"); - } - return null; - }, - placeholder: nls.localize('remote.tunnelsView.forwardPortPlaceholder', "Port number") + validationMessage: validateInput, + placeholder: forwardPrompt }); } }; } + + export function commandPaletteHandler(): ICommandHandler { + return async (accessor, arg) => { + const remoteExplorerService = accessor.get(IRemoteExplorerService); + const viewsService = accessor.get(IViewsService); + const quickInputService = accessor.get(IQuickInputService); + await viewsService.openView(TunnelPanel.ID, true); + const value = await quickInputService.input({ + prompt: forwardPrompt, + validateInput: (value) => Promise.resolve(validateInput(value)) + }); + let parsed: { host: string, port: number } | undefined; + if (value && (parsed = parseInput(value))) { + remoteExplorerService.forward({ host: parsed.host, port: parsed.port }); + } + }; + } } namespace ClosePortAction { @@ -633,7 +655,7 @@ namespace ClosePortAction { return async (accessor, arg) => { if (arg instanceof TunnelItem) { const remoteExplorerService = accessor.get(IRemoteExplorerService); - await remoteExplorerService.tunnelModel.close(arg.remote); + await remoteExplorerService.close({ host: arg.remoteHost, port: arg.remotePort }); } }; } @@ -648,9 +670,10 @@ namespace OpenPortInBrowserAction { if (arg instanceof TunnelItem) { const model = accessor.get(IRemoteExplorerService).tunnelModel; const openerService = accessor.get(IOpenerService); - const tunnel = model.forwarded.has(arg.remote) ? model.forwarded.get(arg.remote) : model.detected.get(arg.remote); + const key = MakeAddress(arg.remoteHost, arg.remotePort); + const tunnel = model.forwarded.get(key) || model.detected.get(key); let address: string | undefined; - if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remote))) { + if (tunnel && tunnel.localAddress && (address = model.address(tunnel.remoteHost, tunnel.remotePort))) { return openerService.open(URI.parse('http://' + address)); } return Promise.resolve(); @@ -668,7 +691,7 @@ namespace CopyAddressAction { if (arg instanceof TunnelItem) { const model = accessor.get(IRemoteExplorerService).tunnelModel; const clipboard = accessor.get(IClipboardService); - const address = model.address(arg.remote); + const address = model.address(arg.remoteHost, arg.remotePort); if (address) { await clipboard.writeText(address.toString()); } @@ -677,30 +700,51 @@ namespace CopyAddressAction { } } +namespace RefreshTunnelViewAction { + export const ID = 'remote.tunnel.refresh'; + export const LABEL = nls.localize('remote.tunnel.refreshView', "Refresh"); + + export function handler(): ICommandHandler { + return (accessor, arg) => { + const remoteExplorerService = accessor.get(IRemoteExplorerService); + return remoteExplorerService.refresh(); + }; + } +} + CommandsRegistry.registerCommand(LabelTunnelAction.ID, LabelTunnelAction.handler()); -CommandsRegistry.registerCommand(ForwardPortAction.ID, ForwardPortAction.handler()); +CommandsRegistry.registerCommand(ForwardPortAction.INLINE_ID, ForwardPortAction.inlineHandler()); +CommandsRegistry.registerCommand(ForwardPortAction.COMMANDPALETTE_ID, ForwardPortAction.commandPaletteHandler()); CommandsRegistry.registerCommand(ClosePortAction.ID, ClosePortAction.handler()); CommandsRegistry.registerCommand(OpenPortInBrowserAction.ID, OpenPortInBrowserAction.handler()); CommandsRegistry.registerCommand(CopyAddressAction.ID, CopyAddressAction.handler()); +CommandsRegistry.registerCommand(RefreshTunnelViewAction.ID, RefreshTunnelViewAction.handler()); MenuRegistry.appendMenuItem(MenuId.CommandPalette, ({ command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.COMMANDPALETTE_ID, title: ForwardPortAction.LABEL }, when: forwardedPortsViewEnabled })); - - MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({ group: 'navigation', order: 0, command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.INLINE_ID, title: ForwardPortAction.LABEL, icon: { id: 'codicon/plus' } } })); +MenuRegistry.appendMenuItem(MenuId.TunnelTitle, ({ + group: 'navigation', + order: 1, + command: { + id: RefreshTunnelViewAction.ID, + title: RefreshTunnelViewAction.LABEL, + icon: { id: 'codicon/refresh' } + } +})); MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '0_manage', order: 0, @@ -732,7 +776,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelContext, ({ group: '0_manage', order: 1, command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.INLINE_ID, title: ForwardPortAction.LABEL, }, when: TunnelTypeContextKey.isEqualTo(TunnelType.Candidate) @@ -759,7 +803,7 @@ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ MenuRegistry.appendMenuItem(MenuId.TunnelInline, ({ order: 0, command: { - id: ForwardPortAction.ID, + id: ForwardPortAction.INLINE_ID, title: ForwardPortAction.LABEL, icon: { id: 'codicon/plus' } }, diff --git a/src/vs/workbench/contrib/remote/common/remote.contribution.ts b/src/vs/workbench/contrib/remote/common/remote.contribution.ts index 3135630c1b4..bd643628670 100644 --- a/src/vs/workbench/contrib/remote/common/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/common/remote.contribution.ts @@ -16,34 +16,8 @@ import { IOutputChannelRegistry, Extensions as OutputExt, } from 'vs/workbench/s import { localize } from 'vs/nls'; import { joinPath } from 'vs/base/common/resources'; import { Disposable } from 'vs/base/common/lifecycle'; -import { ViewContainer, IViewContainersRegistry, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; export const VIEWLET_ID = 'workbench.view.remote'; -export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer( - VIEWLET_ID, - true, - undefined, - { - getOrder: (group?: string) => { - if (!group) { - return; - } - - let matches = /^targets@(\d+)$/.exec(group); - if (matches) { - return -1000; - } - - matches = /^details(@(\d+))?$/.exec(group); - - if (matches) { - return -500; - } - - return; - } - } -); export class LabelContribution implements IWorkbenchContribution { constructor( diff --git a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts index 52105cb7f24..8de660c78fe 100644 --- a/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts +++ b/src/vs/workbench/contrib/remote/electron-browser/remote.contribution.ts @@ -401,6 +401,11 @@ Registry.as(ConfigurationExtensions.Configuration) type: 'boolean', markdownDescription: nls.localize('remote.downloadExtensionsLocally', "When enabled extensions are downloaded locally and installed on remote."), default: false + }, + 'remote.restoreForwardedPorts': { + type: 'boolean', + markdownDescription: nls.localize('remote.restoreForwardedPorts', "Restores the ports you forwarded in a workspace."), + default: false } } }); diff --git a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts index 0021ac28e7d..73e1959661d 100644 --- a/src/vs/workbench/contrib/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/contrib/scm/browser/scm.contribution.ts @@ -7,14 +7,13 @@ import { localize } from 'vs/nls'; import { Registry } from 'vs/platform/registry/common/platform'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions'; import { DirtyDiffWorkbenchController } from './dirtydiffDecorator'; -import { ViewletRegistry, Extensions as ViewletExtensions, ViewletDescriptor, ShowViewletAction } from 'vs/workbench/browser/viewlet'; +import { ShowViewletAction } from 'vs/workbench/browser/viewlet'; import { VIEWLET_ID, ISCMRepository, ISCMService } from 'vs/workbench/contrib/scm/common/scm'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } from 'vs/workbench/common/actions'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { SCMStatusController } from './activity'; -import { SCMViewlet } from 'vs/workbench/contrib/scm/browser/scmViewlet'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -24,6 +23,8 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { SCMService } from 'vs/workbench/contrib/scm/common/scmService'; +import { IViewContainersRegistry, ViewContainerLocation, Extensions as ViewContainerExtensions } from 'vs/workbench/common/views'; +import { SCMViewPaneContainer } from 'vs/workbench/contrib/scm/browser/scmViewlet'; class OpenSCMViewletAction extends ShowViewletAction { @@ -38,13 +39,13 @@ class OpenSCMViewletAction extends ShowViewletAction { Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(DirtyDiffWorkbenchController, LifecyclePhase.Restored); -Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( - SCMViewlet, - VIEWLET_ID, - localize('source control', "Source Control"), - 'codicon-source-control', - 2 -)); +Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ + id: VIEWLET_ID, + name: localize('source control', "Source Control"), + ctorDescriptor: { ctor: SCMViewPaneContainer }, + icon: 'codicon-source-control', + order: 2 +}, ViewContainerLocation.Sidebar); Registry.as(WorkbenchExtensions.Workbench) .registerWorkbenchContribution(SCMStatusController, LifecyclePhase.Restored); diff --git a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts index 145596f7591..adc0b970af7 100644 --- a/src/vs/workbench/contrib/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/contrib/scm/browser/scmViewlet.ts @@ -8,7 +8,7 @@ import { localize } from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { append, $, toggleClass, addClasses } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { VIEWLET_ID, ISCMService, ISCMRepository, VIEW_CONTAINER } from 'vs/workbench/contrib/scm/common/scm'; +import { VIEWLET_ID, ISCMService, ISCMRepository } from 'vs/workbench/contrib/scm/common/scm'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; @@ -31,7 +31,6 @@ import { nextTick } from 'vs/base/common/process'; import { RepositoryPane, RepositoryViewDescriptor } from 'vs/workbench/contrib/scm/browser/repositoryPane'; import { MainPaneDescriptor, MainPane } from 'vs/workbench/contrib/scm/browser/mainPane'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -import { Viewlet } from 'vs/workbench/browser/viewlet'; export interface ISpliceEvent { index: number; @@ -51,22 +50,6 @@ export interface IViewModel { readonly onDidChangeVisibility: Event; } -export class SCMViewlet extends Viewlet { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService protected storageService: IStorageService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IExtensionService protected extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, - @IConfigurationService protected configurationService: IConfigurationService - ) { - super(VIEWLET_ID, instantiationService.createInstance(SCMViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } -} - export class SCMViewPaneContainer extends ViewPaneContainer implements IViewModel { private static readonly STATE_KEY = 'workbench.scm.views.state'; @@ -116,7 +99,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode @IWorkspaceContextService protected contextService: IWorkspaceContextService, @IContextKeyService contextKeyService: IContextKeyService, ) { - super(VIEWLET_ID, SCMViewPaneContainer.STATE_KEY, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); + super(VIEWLET_ID, SCMViewPaneContainer.STATE_KEY, { mergeViewWithContainerWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); this.menus = instantiationService.createInstance(SCMMenus, undefined); this._register(this.menus.onDidChangeTitle(this.updateTitleArea, this)); @@ -124,7 +107,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode this.message = $('.empty-message', { tabIndex: 0 }, localize('no open repo', "No source control providers registered.")); const viewsRegistry = Registry.as(Extensions.ViewsRegistry); - viewsRegistry.registerViews([new MainPaneDescriptor(this)], VIEW_CONTAINER); + viewsRegistry.registerViews([new MainPaneDescriptor(this)], this.viewContainer); this._register(configurationService.onDidChangeConfiguration(e => { if (e.affectsConfiguration('scm.alwaysShowProviders') && configurationService.getValue('scm.alwaysShowProviders')) { @@ -153,7 +136,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode this._repositories.push(repository); const viewDescriptor = new RepositoryViewDescriptor(repository, false); - Registry.as(Extensions.ViewsRegistry).registerViews([viewDescriptor], VIEW_CONTAINER); + Registry.as(Extensions.ViewsRegistry).registerViews([viewDescriptor], this.viewContainer); this.viewDescriptors.push(viewDescriptor); this._onDidSplice.fire({ index, deleteCount: 0, elements: [repository] }); @@ -169,7 +152,7 @@ export class SCMViewPaneContainer extends ViewPaneContainer implements IViewMode return; } - Registry.as(Extensions.ViewsRegistry).deregisterViews([this.viewDescriptors[index]], VIEW_CONTAINER); + Registry.as(Extensions.ViewsRegistry).deregisterViews([this.viewDescriptors[index]], this.viewContainer); this._repositories.splice(index, 1); this.viewDescriptors.splice(index, 1); diff --git a/src/vs/workbench/contrib/scm/common/scm.ts b/src/vs/workbench/contrib/scm/common/scm.ts index 2a6b8d33430..cf63ad255d9 100644 --- a/src/vs/workbench/contrib/scm/common/scm.ts +++ b/src/vs/workbench/contrib/scm/common/scm.ts @@ -9,11 +9,8 @@ import { Event } from 'vs/base/common/event'; import { IDisposable } from 'vs/base/common/lifecycle'; import { Command } from 'vs/editor/common/modes'; import { ISequence } from 'vs/base/common/sequence'; -import { Extensions as ViewContainerExtensions, ViewContainer, IViewContainersRegistry } from 'vs/workbench/common/views'; -import { Registry } from 'vs/platform/registry/common/platform'; export const VIEWLET_ID = 'workbench.view.scm'; -export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID); export interface IBaselineResourceProvider { getBaselineResource(resource: URI): Promise; diff --git a/src/vs/workbench/contrib/search/browser/search.contribution.ts b/src/vs/workbench/contrib/search/browser/search.contribution.ts index 6986f568acd..6a83637408e 100644 --- a/src/vs/workbench/contrib/search/browser/search.contribution.ts +++ b/src/vs/workbench/contrib/search/browser/search.contribution.ts @@ -31,10 +31,9 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { Extensions as PanelExtensions, PanelDescriptor, PanelRegistry } from 'vs/workbench/browser/panel'; import { defaultQuickOpenContextKey } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { Extensions as QuickOpenExtensions, IQuickOpenRegistry, QuickOpenHandlerDescriptor } from 'vs/workbench/browser/quickopen'; -import { Extensions as ViewletExtensions, ViewletDescriptor, ViewletRegistry } from 'vs/workbench/browser/viewlet'; import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; -import { Extensions as ViewExtensions, IViewsRegistry } from 'vs/workbench/common/views'; +import { Extensions as ViewExtensions, IViewsRegistry, IViewContainersRegistry, ViewContainerLocation } from 'vs/workbench/common/views'; import { getMultiSelectedResources } from 'vs/workbench/contrib/files/browser/files'; import { ExplorerFolderContext, ExplorerRootContext, FilesExplorerFocusCondition, IExplorerService, VIEWLET_ID as VIEWLET_ID_FILES } from 'vs/workbench/contrib/files/common/files'; import { OpenAnythingHandler } from 'vs/workbench/contrib/search/browser/openAnythingHandler'; @@ -43,7 +42,6 @@ import { registerContributions as replaceContributions } from 'vs/workbench/cont import { clearHistoryCommand, ClearSearchResultsAction, CloseReplaceAction, CollapseDeepestExpandedLevelAction, copyAllCommand, copyMatchCommand, copyPathCommand, FocusNextInputAction, FocusNextSearchResultAction, FocusPreviousInputAction, FocusPreviousSearchResultAction, focusSearchListCommand, getSearchView, openSearchView, OpenSearchViewletAction, RefreshAction, RemoveAction, ReplaceAction, ReplaceAllAction, ReplaceAllInFolderAction, ReplaceInFilesAction, toggleCaseSensitiveCommand, toggleRegexCommand, toggleWholeWordCommand, FindInFilesCommand, ToggleSearchOnTypeAction, OpenResultsInEditorAction, RerunEditorSearchAction, RerunEditorSearchWithContextAction, ExpandAllAction } from 'vs/workbench/contrib/search/browser/searchActions'; import { SearchPanel } from 'vs/workbench/contrib/search/browser/searchPanel'; import { SearchView, SearchViewPosition } from 'vs/workbench/contrib/search/browser/searchView'; -import { SearchViewlet } from 'vs/workbench/contrib/search/browser/searchViewlet'; import { registerContributions as searchWidgetContributions } from 'vs/workbench/contrib/search/browser/searchWidget'; import * as Constants from 'vs/workbench/contrib/search/common/constants'; import { getWorkspaceSymbols } from 'vs/workbench/contrib/search/common/search'; @@ -51,12 +49,13 @@ import { ISearchHistoryService, SearchHistoryService } from 'vs/workbench/contri import { FileMatchOrMatch, ISearchWorkbenchService, RenderableMatch, SearchWorkbenchService, FileMatch, Match, FolderMatch } from 'vs/workbench/contrib/search/common/searchModel'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; -import { ISearchConfiguration, ISearchConfigurationProperties, PANEL_ID, VIEWLET_ID, VIEW_ID, VIEW_CONTAINER, SearchSortOrder } from 'vs/workbench/services/search/common/search'; +import { ISearchConfiguration, ISearchConfigurationProperties, PANEL_ID, VIEWLET_ID, VIEW_ID, SearchSortOrder } from 'vs/workbench/services/search/common/search'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { ExplorerViewPaneContainer } from 'vs/workbench/contrib/files/browser/explorerViewlet'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { assertType } from 'vs/base/common/types'; +import { SearchViewPaneContainer } from 'vs/workbench/contrib/search/browser/searchViewlet'; registerSingleton(ISearchWorkbenchService, SearchWorkbenchService, true); registerSingleton(ISearchHistoryService, SearchHistoryService, true); @@ -505,13 +504,14 @@ class ShowAllSymbolsAction extends Action { } } -Registry.as(ViewletExtensions.Viewlets).registerViewlet(ViewletDescriptor.create( - SearchViewlet, - VIEWLET_ID, - nls.localize('name', "Search"), - 'codicon-search', - 1 -)); +const viewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ + id: VIEWLET_ID, + name: nls.localize('name', "Search"), + ctorDescriptor: { ctor: SearchViewPaneContainer }, + hideIfEmpty: true, + icon: 'codicon-search', + order: 1 +}, ViewContainerLocation.Sidebar); Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( SearchPanel, @@ -532,7 +532,7 @@ class RegisterSearchViewContribution implements IWorkbenchContribution { const updateSearchViewLocation = (open: boolean) => { const config = configurationService.getValue(); if (config.search.location === 'panel') { - viewsRegistry.deregisterViews(viewsRegistry.getViews(VIEW_CONTAINER), VIEW_CONTAINER); + viewsRegistry.deregisterViews(viewsRegistry.getViews(viewContainer), viewContainer); Registry.as(PanelExtensions.Panels).registerPanel(PanelDescriptor.create( SearchPanel, PANEL_ID, @@ -545,7 +545,7 @@ class RegisterSearchViewContribution implements IWorkbenchContribution { } } else { Registry.as(PanelExtensions.Panels).deregisterPanel(PANEL_ID); - viewsRegistry.registerViews([{ id: VIEW_ID, name: nls.localize('search', "Search"), ctorDescriptor: { ctor: SearchView, arguments: [SearchViewPosition.SideBar] }, canToggleVisibility: false }], VIEW_CONTAINER); + viewsRegistry.registerViews([{ id: VIEW_ID, name: nls.localize('search', "Search"), ctorDescriptor: { ctor: SearchView, arguments: [SearchViewPosition.SideBar] }, canToggleVisibility: false }], viewContainer); if (open) { viewletService.openViewlet(VIEWLET_ID); } diff --git a/src/vs/workbench/contrib/search/browser/searchViewlet.ts b/src/vs/workbench/contrib/search/browser/searchViewlet.ts index a216091af0c..592a3153e67 100644 --- a/src/vs/workbench/contrib/search/browser/searchViewlet.ts +++ b/src/vs/workbench/contrib/search/browser/searchViewlet.ts @@ -14,27 +14,9 @@ import { IContextMenuService } from 'vs/platform/contextview/browser/contextView import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { VIEWLET_ID, VIEW_ID } from 'vs/workbench/services/search/common/search'; import { SearchView } from 'vs/workbench/contrib/search/browser/searchView'; -import { Registry } from 'vs/platform/registry/common/platform'; -import { ViewletRegistry, Extensions, Viewlet } from 'vs/workbench/browser/viewlet'; import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer'; -export class SearchViewlet extends Viewlet { - constructor( - @ITelemetryService telemetryService: ITelemetryService, - @IStorageService protected storageService: IStorageService, - @IInstantiationService protected instantiationService: IInstantiationService, - @IThemeService themeService: IThemeService, - @IContextMenuService protected contextMenuService: IContextMenuService, - @IExtensionService protected extensionService: IExtensionService, - @IWorkspaceContextService protected contextService: IWorkspaceContextService, - @IWorkbenchLayoutService protected layoutService: IWorkbenchLayoutService, - @IConfigurationService protected configurationService: IConfigurationService - ) { - super(VIEWLET_ID, instantiationService.createInstance(SearchViewPaneContainer), telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService, layoutService, configurationService); - } -} - export class SearchViewPaneContainer extends ViewPaneContainer { constructor( @@ -48,11 +30,7 @@ export class SearchViewPaneContainer extends ViewPaneContainer { @IContextMenuService contextMenuService: IContextMenuService, @IExtensionService extensionService: IExtensionService, ) { - super(VIEWLET_ID, `${VIEWLET_ID}.state`, { showHeaderInTitleWhenSingleView: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); - } - - getTitle(): string { - return Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; + super(VIEWLET_ID, `${VIEWLET_ID}.state`, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }, instantiationService, configurationService, layoutService, contextMenuService, telemetryService, extensionService, themeService, storageService, contextService); } getSearchView(): SearchView | undefined { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index b50a1b58d58..d78ae0bb9f1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -478,7 +478,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { fastScrollSensitivity: editorOptions.fastScrollSensitivity, scrollSensitivity: editorOptions.mouseWheelScrollSensitivity, rendererType: config.rendererType === 'auto' || config.rendererType === 'experimentalWebgl' ? 'canvas' : config.rendererType, - wordSeparator: ' ()[]{}\',:;"`' + wordSeparator: ' ()[]{}\',"`' }); this._xterm = xterm; this._xtermCore = (xterm as any)._core as XTermCore; diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index ab0760b511f..03133778a0b 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -61,7 +61,7 @@ export class IFrameWebview extends BaseWebview implements Web protected createElement(options: WebviewOptions) { const element = document.createElement('iframe'); element.className = `webview ${options.customClasses || ''}`; - element.sandbox.add('allow-scripts', 'allow-same-origin'); + element.sandbox.add('allow-scripts', 'allow-same-origin', 'allow-forms'); element.setAttribute('src', `${this.externalEndpoint}/index.html?id=${this.id}`); element.style.border = 'none'; element.style.width = '100%'; diff --git a/src/vs/workbench/contrib/webview/common/portMapping.ts b/src/vs/workbench/contrib/webview/common/portMapping.ts index 69c216631ba..fe5a6d21962 100644 --- a/src/vs/workbench/contrib/webview/common/portMapping.ts +++ b/src/vs/workbench/contrib/webview/common/portMapping.ts @@ -68,7 +68,7 @@ export class WebviewPortMappingManager extends Disposable { if (existing) { return existing; } - const tunnel = this.tunnelService.openTunnel(remotePort); + const tunnel = this.tunnelService.openTunnel(undefined, remotePort); if (tunnel) { this._tunnels.set(remotePort, tunnel); } diff --git a/src/vs/workbench/contrib/webview/common/resourceLoader.ts b/src/vs/workbench/contrib/webview/common/resourceLoader.ts index e34d1ec28bd..68d4c606728 100644 --- a/src/vs/workbench/contrib/webview/common/resourceLoader.ts +++ b/src/vs/workbench/contrib/webview/common/resourceLoader.ts @@ -104,5 +104,5 @@ function containsResource(root: URI, resource: URI): boolean { resourceFsPath = resourceFsPath.toLowerCase(); } - return startsWith(resource.fsPath, rootPath); + return startsWith(resourceFsPath, rootPath); } diff --git a/src/vs/workbench/electron-browser/desktop.contribution.ts b/src/vs/workbench/electron-browser/desktop.contribution.ts index 821219da530..6eaa0687e48 100644 --- a/src/vs/workbench/electron-browser/desktop.contribution.ts +++ b/src/vs/workbench/electron-browser/desktop.contribution.ts @@ -239,7 +239,7 @@ import product from 'vs/platform/product/common/product'; nls.localize('window.reopenFolders.one', "Reopen the last active window."), nls.localize('window.reopenFolders.none', "Never reopen a window. Always start with an empty one.") ], - 'default': 'one', + 'default': 'all', 'scope': ConfigurationScope.APPLICATION, 'description': nls.localize('restoreWindows', "Controls how windows are being reopened after a restart.") }, diff --git a/src/vs/workbench/electron-browser/window.ts b/src/vs/workbench/electron-browser/window.ts index 389d3a6c14d..02735f4a472 100644 --- a/src/vs/workbench/electron-browser/window.ts +++ b/src/vs/workbench/electron-browser/window.ts @@ -283,6 +283,7 @@ export class ElectronWindow extends Disposable { })); } + // Detect minimize / maximize this._register(Event.any( Event.map(Event.filter(this.electronService.onWindowMaximize, id => id === this.electronEnvironmentService.windowId), () => true), Event.map(Event.filter(this.electronService.onWindowUnmaximize, id => id === this.electronEnvironmentService.windowId), () => false) @@ -452,7 +453,7 @@ export class ElectronWindow extends Disposable { if (options?.allowTunneling) { const portMappingRequest = extractLocalHostUriMetaDataForPortMapping(uri); if (portMappingRequest) { - const tunnel = await this.tunnelService.openTunnel(portMappingRequest.port); + const tunnel = await this.tunnelService.openTunnel(undefined, portMappingRequest.port); if (tunnel) { return { resolved: uri.with({ authority: `127.0.0.1:${tunnel.tunnelLocalPort}` }), diff --git a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts index 02fae436721..f49293fd2ab 100644 --- a/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts +++ b/src/vs/workbench/services/backup/test/electron-browser/backupFileService.test.ts @@ -51,14 +51,13 @@ class TestBackupEnvironmentService extends NativeWorkbenchEnvironmentService { constructor(backupPath: string) { super({ ...parseArgs(process.argv, OPTIONS), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath, 0); } - } -class TestBackupFileService extends BackupFileService { +export class NodeTestBackupFileService extends BackupFileService { readonly fileService: IFileService; - constructor(workspace: URI, backupHome: string, workspacesJsonPath: string) { + constructor(workspaceBackupPath: string) { const environmentService = new TestBackupEnvironmentService(workspaceBackupPath); const fileService = new FileService(new NullLogService()); const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); @@ -76,10 +75,10 @@ class TestBackupFileService extends BackupFileService { } suite('BackupFileService', () => { - let service: TestBackupFileService; + let service: NodeTestBackupFileService; setup(async () => { - service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath); + service = new NodeTestBackupFileService(workspaceBackupPath); // Delete any existing backups completely and then re-create it. await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); @@ -141,7 +140,7 @@ suite('BackupFileService', () => { test('should return whether a backup resource exists', async () => { await pfs.mkdirp(path.dirname(fooBackupPath)); fs.writeFileSync(fooBackupPath, 'foo'); - service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath); + service = new NodeTestBackupFileService(workspaceBackupPath); const resource = await service.loadBackupResource(fooFile); assert.ok(resource); assert.equal(path.basename(resource!.fsPath), path.basename(fooBackupPath)); @@ -528,10 +527,10 @@ suite('BackupFileService', () => { suite('BackupFilesModel', () => { - let service: TestBackupFileService; + let service: NodeTestBackupFileService; setup(async () => { - service = new TestBackupFileService(workspaceResource, backupHome, workspacesJsonPath); + service = new NodeTestBackupFileService(workspaceBackupPath); // Delete any existing backups completely and then re-create it. await pfs.rimraf(backupHome, pfs.RimRafMode.MOVE); diff --git a/src/vs/workbench/services/configuration/browser/configuration.ts b/src/vs/workbench/services/configuration/browser/configuration.ts index d2450a267f7..455ea3af671 100644 --- a/src/vs/workbench/services/configuration/browser/configuration.ts +++ b/src/vs/workbench/services/configuration/browser/configuration.ts @@ -7,12 +7,12 @@ import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import { Event, Emitter } from 'vs/base/common/event'; import * as errors from 'vs/base/common/errors'; -import { Disposable, IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle'; -import { RunOnceScheduler } from 'vs/base/common/async'; +import { Disposable, IDisposable, dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; +import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async'; import { FileChangeType, FileChangesEvent, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files'; import { ConfigurationModel, ConfigurationModelParser } from 'vs/platform/configuration/common/configurationModels'; import { WorkspaceConfigurationModelParser, StandaloneConfigurationModelParser } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_SETTINGS_PATH, TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; +import { TASKS_CONFIGURATION_KEY, FOLDER_SETTINGS_NAME, LAUNCH_CONFIGURATION_KEY, IConfigurationCache, ConfigurationKey, REMOTE_MACHINE_SCOPES, FOLDER_SCOPES, WORKSPACE_SCOPES } from 'vs/workbench/services/configuration/common/configuration'; import { IStoredWorkspaceFolder, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { JSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditingService'; import { WorkbenchState, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; @@ -26,28 +26,61 @@ import { hash } from 'vs/base/common/hash'; export class UserConfiguration extends Disposable { - private readonly parser: ConfigurationModelParser; - private readonly reloadConfigurationScheduler: RunOnceScheduler; - protected readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); + private readonly _onDidInitializeCompleteConfiguration: Emitter = this._register(new Emitter()); + private readonly _onDidChangeConfiguration: Emitter = this._register(new Emitter()); readonly onDidChangeConfiguration: Event = this._onDidChangeConfiguration.event; + private readonly userConfiguration: MutableDisposable = this._register(new MutableDisposable()); + private readonly reloadConfigurationScheduler: RunOnceScheduler; + constructor( private readonly userSettingsResource: URI, private readonly scopes: ConfigurationScope[] | undefined, private readonly fileService: IFileService ) { super(); - - this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); + this.userConfiguration.value = new UserSettings(this.userSettingsResource, this.scopes, this.fileService); + this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule())); this.reloadConfigurationScheduler = this._register(new RunOnceScheduler(() => this.reload().then(configurationModel => this._onDidChangeConfiguration.fire(configurationModel)), 50)); - this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this.reloadConfigurationScheduler.schedule())); + + runWhenIdle(() => this._onDidInitializeCompleteConfiguration.fire(), 5000); + this._register(Event.once(this._onDidInitializeCompleteConfiguration.event)(() => this.reloadConfigurationScheduler.schedule())); } async initialize(): Promise { - return this.reload(); + return this.userConfiguration.value!.loadConfiguration(); } async reload(): Promise { + if (!(this.userConfiguration.value instanceof FileServiceBasedConfigurationWithNames)) { + this.userConfiguration.value = new FileServiceBasedConfigurationWithNames(resources.dirname(this.userSettingsResource), [FOLDER_SETTINGS_NAME, TASKS_CONFIGURATION_KEY], this.scopes, this.fileService); + this._register(this.userConfiguration.value.onDidChange(() => this.reloadConfigurationScheduler.schedule())); + } + return this.userConfiguration.value!.loadConfiguration(); + } + + reprocess(): ConfigurationModel { + return this.userConfiguration.value!.reprocess(); + } +} + +class UserSettings extends Disposable { + + private readonly parser: ConfigurationModelParser; + protected readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor( + private readonly userSettingsResource: URI, + private readonly scopes: ConfigurationScope[] | undefined, + private readonly fileService: IFileService + ) { + super(); + this.parser = new ConfigurationModelParser(this.userSettingsResource.toString(), this.scopes); + this._register(Event.filter(this.fileService.onFileChanges, e => e.contains(this.userSettingsResource))(() => this._onDidChange.fire())); + } + + async loadConfiguration(): Promise { try { const content = await this.fileService.readFile(this.userSettingsResource); this.parser.parseContent(content.value.toString() || '{}'); @@ -63,6 +96,127 @@ export class UserConfiguration extends Disposable { } } +class FileServiceBasedConfigurationWithNames extends Disposable { + + private _folderSettingsModelParser: ConfigurationModelParser; + private _standAloneConfigurations: ConfigurationModel[]; + private _cache: ConfigurationModel; + + protected readonly configurationResources: URI[]; + protected changeEventTriggerScheduler: RunOnceScheduler; + protected readonly _onDidChange: Emitter = this._register(new Emitter()); + readonly onDidChange: Event = this._onDidChange.event; + + constructor(protected readonly configurationFolder: URI, + private readonly configurationNames: string[], + private readonly scopes: ConfigurationScope[] | undefined, + private fileService: IFileService) { + super(); + this.configurationResources = this.configurationNames.map(name => resources.joinPath(this.configurationFolder, `${name}.json`)); + this._folderSettingsModelParser = new ConfigurationModelParser(this.configurationFolder.toString(), this.scopes); + this._standAloneConfigurations = []; + this._cache = new ConfigurationModel(); + + this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); + this._register(this.fileService.onFileChanges((e) => this.handleFileEvents(e))); + } + + async loadConfiguration(): Promise { + const configurationContents = await Promise.all(this.configurationResources.map(async resource => { + try { + const content = await this.fileService.readFile(resource); + return content.value.toString(); + } catch (error) { + const exists = await this.fileService.exists(resource); + if (exists) { + errors.onUnexpectedError(error); + } + } + return undefined; + })); + + // reset + this._standAloneConfigurations = []; + this._folderSettingsModelParser.parseContent(''); + + // parse + if (configurationContents[0]) { + this._folderSettingsModelParser.parseContent(configurationContents[0]); + } + for (let index = 1; index < configurationContents.length; index++) { + const contents = configurationContents[index]; + if (contents) { + const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.configurationResources[index].toString(), this.configurationNames[index]); + standAloneConfigurationModelParser.parseContent(contents); + this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); + } + } + + // Consolidate (support *.json files in the workspace settings folder) + this.consolidate(); + + return this._cache; + } + + reprocess(): ConfigurationModel { + const oldContents = this._folderSettingsModelParser.configurationModel.contents; + this._folderSettingsModelParser.parse(); + if (!equals(oldContents, this._folderSettingsModelParser.configurationModel.contents)) { + this.consolidate(); + } + return this._cache; + } + + private consolidate(): void { + this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations); + } + + protected async handleFileEvents(event: FileChangesEvent): Promise { + const events = event.changes; + let affectedByChanges = false; + + // Find changes that affect workspace configuration files + for (let i = 0, len = events.length; i < len; i++) { + const resource = events[i].resource; + const basename = resources.basename(resource); + const isJson = extname(basename) === '.json'; + const isConfigurationFolderDeleted = (events[i].type === FileChangeType.DELETED && resources.isEqual(resource, this.configurationFolder)); + + if (!isJson && !isConfigurationFolderDeleted) { + continue; // only JSON files or the actual settings folder + } + + const folderRelativePath = this.toFolderRelativePath(resource); + if (!folderRelativePath) { + continue; // event is not inside folder + } + + // Handle case where ".vscode" got deleted + if (isConfigurationFolderDeleted) { + affectedByChanges = true; + break; + } + + // only valid workspace config files + if (this.configurationResources.some(configurationResource => resources.isEqual(configurationResource, resource))) { + affectedByChanges = true; + break; + } + } + + if (affectedByChanges) { + this.changeEventTriggerScheduler.schedule(); + } + } + + private toFolderRelativePath(resource: URI): string | undefined { + if (resources.isEqualOrParent(resource, this.configurationFolder)) { + return resources.relativePath(this.configurationFolder, resource); + } + return undefined; + } +} + export class RemoteUserConfiguration extends Disposable { private readonly _cachedConfiguration: CachedRemoteUserConfiguration; @@ -546,125 +700,12 @@ export interface IFolderConfiguration extends IDisposable { reprocess(): ConfigurationModel; } -class FileServiceBasedFolderConfiguration extends Disposable implements IFolderConfiguration { +class FileServiceBasedFolderConfiguration extends FileServiceBasedConfigurationWithNames implements IFolderConfiguration { - private _folderSettingsModelParser: ConfigurationModelParser; - private _standAloneConfigurations: ConfigurationModel[]; - private _cache: ConfigurationModel; - - private readonly configurationNames: string[]; - protected readonly configurationResources: URI[]; - private changeEventTriggerScheduler: RunOnceScheduler; - protected readonly _onDidChange: Emitter = this._register(new Emitter()); - readonly onDidChange: Event = this._onDidChange.event; - - constructor(protected readonly configurationFolder: URI, workbenchState: WorkbenchState, private fileService: IFileService) { - super(); - - this.configurationNames = [FOLDER_SETTINGS_NAME /*First one should be settings */, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY]; - this.configurationResources = this.configurationNames.map(name => resources.joinPath(this.configurationFolder, `${name}.json`)); - this._folderSettingsModelParser = new ConfigurationModelParser(FOLDER_SETTINGS_PATH, WorkbenchState.WORKSPACE === workbenchState ? FOLDER_SCOPES : WORKSPACE_SCOPES); - this._standAloneConfigurations = []; - this._cache = new ConfigurationModel(); - - this.changeEventTriggerScheduler = this._register(new RunOnceScheduler(() => this._onDidChange.fire(), 50)); - this._register(fileService.onFileChanges(e => this.handleWorkspaceFileEvents(e))); + constructor(configurationFolder: URI, workbenchState: WorkbenchState, fileService: IFileService) { + super(configurationFolder, [FOLDER_SETTINGS_NAME /*First one should be settings */, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY], WorkbenchState.WORKSPACE === workbenchState ? FOLDER_SCOPES : WORKSPACE_SCOPES, fileService); } - async loadConfiguration(): Promise { - const configurationContents = await Promise.all(this.configurationResources.map(async resource => { - try { - const content = await this.fileService.readFile(resource); - return content.value.toString(); - } catch (error) { - const exists = await this.fileService.exists(resource); - if (exists) { - errors.onUnexpectedError(error); - } - } - return undefined; - })); - - // reset - this._standAloneConfigurations = []; - this._folderSettingsModelParser.parseContent(''); - - // parse - if (configurationContents[0]) { - this._folderSettingsModelParser.parseContent(configurationContents[0]); - } - for (let index = 1; index < configurationContents.length; index++) { - const contents = configurationContents[index]; - if (contents) { - const standAloneConfigurationModelParser = new StandaloneConfigurationModelParser(this.configurationResources[index].toString(), this.configurationNames[index]); - standAloneConfigurationModelParser.parseContent(contents); - this._standAloneConfigurations.push(standAloneConfigurationModelParser.configurationModel); - } - } - - // Consolidate (support *.json files in the workspace settings folder) - this.consolidate(); - - return this._cache; - } - - reprocess(): ConfigurationModel { - const oldContents = this._folderSettingsModelParser.configurationModel.contents; - this._folderSettingsModelParser.parse(); - if (!equals(oldContents, this._folderSettingsModelParser.configurationModel.contents)) { - this.consolidate(); - } - return this._cache; - } - - private consolidate(): void { - this._cache = this._folderSettingsModelParser.configurationModel.merge(...this._standAloneConfigurations); - } - - private handleWorkspaceFileEvents(event: FileChangesEvent): void { - const events = event.changes; - let affectedByChanges = false; - - // Find changes that affect workspace configuration files - for (let i = 0, len = events.length; i < len; i++) { - const resource = events[i].resource; - const basename = resources.basename(resource); - const isJson = extname(basename) === '.json'; - const isConfigurationFolderDeleted = (events[i].type === FileChangeType.DELETED && resources.isEqual(resource, this.configurationFolder)); - - if (!isJson && !isConfigurationFolderDeleted) { - continue; // only JSON files or the actual settings folder - } - - const folderRelativePath = this.toFolderRelativePath(resource); - if (!folderRelativePath) { - continue; // event is not inside folder - } - - // Handle case where ".vscode" got deleted - if (isConfigurationFolderDeleted) { - affectedByChanges = true; - break; - } - - // only valid workspace config files - if (this.configurationResources.some(configurationResource => resources.isEqual(configurationResource, resource))) { - affectedByChanges = true; - break; - } - } - - if (affectedByChanges) { - this.changeEventTriggerScheduler.schedule(); - } - } - - private toFolderRelativePath(resource: URI): string | undefined { - if (resources.isEqualOrParent(resource, this.configurationFolder)) { - return resources.relativePath(this.configurationFolder, resource); - } - return undefined; - } } class CachedFolderConfiguration extends Disposable implements IFolderConfiguration { diff --git a/src/vs/workbench/services/configuration/common/configuration.ts b/src/vs/workbench/services/configuration/common/configuration.ts index 069871e52db..7cec0a25421 100644 --- a/src/vs/workbench/services/configuration/common/configuration.ts +++ b/src/vs/workbench/services/configuration/common/configuration.ts @@ -28,6 +28,8 @@ export const LAUNCH_CONFIGURATION_KEY = 'launch'; export const WORKSPACE_STANDALONE_CONFIGURATIONS = Object.create(null); WORKSPACE_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${TASKS_CONFIGURATION_KEY}.json`; WORKSPACE_STANDALONE_CONFIGURATIONS[LAUNCH_CONFIGURATION_KEY] = `${FOLDER_CONFIG_FOLDER_NAME}/${LAUNCH_CONFIGURATION_KEY}.json`; +export const USER_STANDALONE_CONFIGURATIONS = Object.create(null); +USER_STANDALONE_CONFIGURATIONS[TASKS_CONFIGURATION_KEY] = `${TASKS_CONFIGURATION_KEY}.json`; export type ConfigurationKey = { type: 'user' | 'workspaces' | 'folder', key: string }; diff --git a/src/vs/workbench/services/configuration/common/configurationEditingService.ts b/src/vs/workbench/services/configuration/common/configurationEditingService.ts index 68bb46913a1..e313353286b 100644 --- a/src/vs/workbench/services/configuration/common/configurationEditingService.ts +++ b/src/vs/workbench/services/configuration/common/configurationEditingService.ts @@ -5,6 +5,7 @@ import * as nls from 'vs/nls'; import { URI } from 'vs/base/common/uri'; +import * as resources from 'vs/base/common/resources'; import * as json from 'vs/base/common/json'; import * as strings from 'vs/base/common/strings'; import { setProperty } from 'vs/base/common/jsonEdit'; @@ -19,7 +20,7 @@ import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/ import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IConfigurationService, IConfigurationOverrides, keyFromOverrideIdentifier } from 'vs/platform/configuration/common/configuration'; -import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_SETTINGS_PATH, WORKSPACE_STANDALONE_CONFIGURATIONS, TASKS_CONFIGURATION_KEY, LAUNCH_CONFIGURATION_KEY, USER_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; import { IFileService } from 'vs/platform/files/common/files'; import { ITextModelService, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService'; import { OVERRIDE_PROPERTY_PATTERN, IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; @@ -430,8 +431,8 @@ export class ConfigurationEditingService { } if (operation.workspaceStandAloneConfigurationKey) { - // Global tasks and launches are not supported - if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) { + // Global launches are not supported + if ((operation.workspaceStandAloneConfigurationKey !== TASKS_CONFIGURATION_KEY) && (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE)) { return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET, target, operation); } } @@ -460,7 +461,7 @@ export class ConfigurationEditingService { if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) { const configurationProperties = Registry.as(ConfigurationExtensions.Configuration).getConfigurationProperties(); - if (configurationProperties[operation.key].scope !== ConfigurationScope.RESOURCE) { + if (!(configurationProperties[operation.key].scope === ConfigurationScope.RESOURCE || configurationProperties[operation.key].scope === ConfigurationScope.RESOURCE_LANGUAGE)) { return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation); } } @@ -497,9 +498,10 @@ export class ConfigurationEditingService { // Check for standalone workspace configurations if (config.key) { - const standaloneConfigurationKeys = Object.keys(WORKSPACE_STANDALONE_CONFIGURATIONS); + const standaloneConfigurationMap = target === EditableConfigurationTarget.USER_LOCAL ? USER_STANDALONE_CONFIGURATIONS : WORKSPACE_STANDALONE_CONFIGURATIONS; + const standaloneConfigurationKeys = Object.keys(standaloneConfigurationMap); for (const key of standaloneConfigurationKeys) { - const resource = this.getConfigurationFileResource(target, config, WORKSPACE_STANDALONE_CONFIGURATIONS[key], overrides.resource); + const resource = this.getConfigurationFileResource(target, config, standaloneConfigurationMap[key], overrides.resource); // Check for prefix if (config.key === key) { @@ -536,7 +538,11 @@ export class ConfigurationEditingService { private getConfigurationFileResource(target: EditableConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null { if (target === EditableConfigurationTarget.USER_LOCAL) { - return this.environmentService.settingsResource; + if (relativePath) { + return resources.joinPath(resources.dirname(this.environmentService.settingsResource), relativePath); + } else { + return this.environmentService.settingsResource; + } } if (target === EditableConfigurationTarget.USER_REMOTE) { return this.remoteSettingsResource; diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts index d315118d91c..401d5a63768 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationEditingService.test.ts @@ -18,7 +18,7 @@ import * as uuid from 'vs/base/common/uuid'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry'; import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService'; import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService'; -import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH } from 'vs/workbench/services/configuration/common/configuration'; +import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH, USER_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; @@ -60,6 +60,7 @@ suite('ConfigurationEditingService', () => { let parentDir: string; let workspaceDir: string; let globalSettingsFile: string; + let globalTasksFile: string; let workspaceSettingsDir; suiteSetup(() => { @@ -94,6 +95,7 @@ suite('ConfigurationEditingService', () => { parentDir = path.join(os.tmpdir(), 'vsctests', id); workspaceDir = path.join(parentDir, 'workspaceconfig', id); globalSettingsFile = path.join(workspaceDir, 'settings.json'); + globalTasksFile = path.join(workspaceDir, 'tasks.json'); workspaceSettingsDir = path.join(workspaceDir, '.vscode'); return await mkdirp(workspaceSettingsDir, 493); @@ -149,12 +151,6 @@ suite('ConfigurationEditingService', () => { (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_UNKNOWN_KEY)); }); - test('errors cases - invalid target', () => { - return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'tasks.something', value: 'value' }) - .then(() => assert.fail('Should fail with ERROR_INVALID_TARGET'), - (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_USER_TARGET)); - }); - test('errors cases - no workspace', () => { return setUpServices(true) .then(() => testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'configurationEditing.service.testSetting', value: 'value' })) @@ -162,11 +158,19 @@ suite('ConfigurationEditingService', () => { (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_NO_WORKSPACE_OPENED)); }); - test('errors cases - invalid configuration', () => { - fs.writeFileSync(globalSettingsFile, ',,,,,,,,,,,,,,'); - return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key: 'configurationEditing.service.testSetting', value: 'value' }) + function errorCasesInvalidConfig(file: string, key: string) { + fs.writeFileSync(file, ',,,,,,,,,,,,,,'); + return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key, value: 'value' }) .then(() => assert.fail('Should fail with ERROR_INVALID_CONFIGURATION'), (error: ConfigurationEditingError) => assert.equal(error.code, ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION)); + } + + test('errors cases - invalid configuration', () => { + return errorCasesInvalidConfig(globalSettingsFile, 'configurationEditing.service.testSetting'); + }); + + test('errors cases - invalid global tasks configuration', () => { + return errorCasesInvalidConfig(globalTasksFile, 'tasks.configurationEditing.service.testSetting'); }); test('errors cases - dirty', () => { @@ -271,44 +275,89 @@ suite('ConfigurationEditingService', () => { }); }); - test('write workspace standalone setting - empty file', () => { - return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks.service.testSetting', value: 'value' }) + function writeStandaloneSettingEmptyFile(configTarget: EditableConfigurationTarget, pathMap: any) { + return testObject.writeConfiguration(configTarget, { key: 'tasks.service.testSetting', value: 'value' }) .then(() => { - const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); + const target = path.join(workspaceDir, pathMap['tasks']); const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); assert.equal(parsed['service.testSetting'], 'value'); }); + } + + test('write workspace standalone setting - empty file', () => { + return writeStandaloneSettingEmptyFile(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS); }); - test('write workspace standalone setting - existing file', () => { - const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['launch']); + test('write user standalone setting - empty file', () => { + return writeStandaloneSettingEmptyFile(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS); + }); + + function writeStandaloneSettingExitingFile(configTarget: EditableConfigurationTarget, pathMap: any) { + const target = path.join(workspaceDir, pathMap['tasks']); fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }'); - return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'launch.service.testSetting', value: 'value' }) + return testObject.writeConfiguration(configTarget, { key: 'tasks.service.testSetting', value: 'value' }) .then(() => { const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); assert.equal(parsed['service.testSetting'], 'value'); assert.equal(parsed['my.super.setting'], 'my.super.value'); }); + } + + test('write workspace standalone setting - existing file', () => { + return writeStandaloneSettingExitingFile(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS); }); + test('write user standalone setting - existing file', () => { + return writeStandaloneSettingExitingFile(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS); + }); + + function writeStandaloneSettingEmptyFileFullJson(configTarget: EditableConfigurationTarget, pathMap: any) { + return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) + .then(() => { + const target = path.join(workspaceDir, pathMap['tasks']); + const contents = fs.readFileSync(target).toString('utf8'); + const parsed = json.parse(contents); + + assert.equal(parsed['version'], '1.0.0'); + assert.equal(parsed['tasks'][0]['taskName'], 'myTask'); + }); + } + test('write workspace standalone setting - empty file - full JSON', () => { - return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) + return writeStandaloneSettingEmptyFileFullJson(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS); + }); + + test('write user standalone setting - empty file - full JSON', () => { + return writeStandaloneSettingEmptyFileFullJson(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS); + }); + + function writeStandaloneSettingExistingFileFullJson(configTarget: EditableConfigurationTarget, pathMap: any) { + const target = path.join(workspaceDir, pathMap['tasks']); + fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }'); + return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) .then(() => { - const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); assert.equal(parsed['version'], '1.0.0'); assert.equal(parsed['tasks'][0]['taskName'], 'myTask'); }); - }); + } test('write workspace standalone setting - existing file - full JSON', () => { - const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); - fs.writeFileSync(target, '{ "my.super.setting": "my.super.value" }'); - return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) + return writeStandaloneSettingExistingFileFullJson(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS); + }); + + test('write user standalone setting - existing file - full JSON', () => { + return writeStandaloneSettingExistingFileFullJson(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS); + }); + + function writeStandaloneSettingExistingFileWithJsonErrorFullJson(configTarget: EditableConfigurationTarget, pathMap: any) { + const target = path.join(workspaceDir, pathMap['tasks']); + fs.writeFileSync(target, '{ "my.super.setting": '); // invalid JSON + return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) .then(() => { const contents = fs.readFileSync(target).toString('utf8'); const parsed = json.parse(contents); @@ -316,23 +365,18 @@ suite('ConfigurationEditingService', () => { assert.equal(parsed['version'], '1.0.0'); assert.equal(parsed['tasks'][0]['taskName'], 'myTask'); }); - }); + } test('write workspace standalone setting - existing file with JSON errors - full JSON', () => { - const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); - fs.writeFileSync(target, '{ "my.super.setting": '); // invalid JSON - return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask' }] } }) - .then(() => { - const contents = fs.readFileSync(target).toString('utf8'); - const parsed = json.parse(contents); - - assert.equal(parsed['version'], '1.0.0'); - assert.equal(parsed['tasks'][0]['taskName'], 'myTask'); - }); + return writeStandaloneSettingExistingFileWithJsonErrorFullJson(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS); }); - test('write workspace standalone setting should replace complete file', () => { - const target = path.join(workspaceDir, WORKSPACE_STANDALONE_CONFIGURATIONS['tasks']); + test('write user standalone setting - existing file with JSON errors - full JSON', () => { + return writeStandaloneSettingExistingFileWithJsonErrorFullJson(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS); + }); + + function writeStandaloneSettingShouldReplace(configTarget: EditableConfigurationTarget, pathMap: any) { + const target = path.join(workspaceDir, pathMap['tasks']); fs.writeFileSync(target, `{ "version": "1.0.0", "tasks": [ @@ -344,11 +388,19 @@ suite('ConfigurationEditingService', () => { } ] }`); - return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] } }) + return testObject.writeConfiguration(configTarget, { key: 'tasks', value: { 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] } }) .then(() => { const actual = fs.readFileSync(target).toString('utf8'); const expected = JSON.stringify({ 'version': '1.0.0', tasks: [{ 'taskName': 'myTask1' }] }, null, '\t'); assert.equal(actual, expected); }); + } + + test('write workspace standalone setting should replace complete file', () => { + return writeStandaloneSettingShouldReplace(EditableConfigurationTarget.WORKSPACE, WORKSPACE_STANDALONE_CONFIGURATIONS); + }); + + test('write user standalone setting should replace complete file', () => { + return writeStandaloneSettingShouldReplace(EditableConfigurationTarget.USER_LOCAL, USER_STANDALONE_CONFIGURATIONS); }); }); diff --git a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts index e0d180d5470..bd9070dd0b5 100644 --- a/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts +++ b/src/vs/workbench/services/configuration/test/electron-browser/configurationService.test.ts @@ -47,6 +47,7 @@ import { FileUserDataProvider } from 'vs/workbench/services/userData/common/file import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing'; import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; +import { timeout } from 'vs/base/common/async'; class TestEnvironmentService extends NativeWorkbenchEnvironmentService { @@ -717,7 +718,7 @@ suite('WorkspaceService - Initialization', () => { suite('WorkspaceConfigurationService - Folder', () => { - let workspaceName = `testWorkspace${uuid.generateUuid()}`, parentResource: string, workspaceDir: string, testObject: IConfigurationService, globalSettingsFile: string; + let workspaceName = `testWorkspace${uuid.generateUuid()}`, parentResource: string, workspaceDir: string, testObject: IConfigurationService, globalSettingsFile: string, globalTasksFile: string, workspaceService: WorkspaceService; const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); suiteSetup(() => { @@ -744,6 +745,11 @@ suite('WorkspaceConfigurationService - Folder', () => { 'type': 'string', 'default': 'isSet', scope: ConfigurationScope.RESOURCE + }, + 'configurationService.folder.languageSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.RESOURCE_LANGUAGE } } }); @@ -756,6 +762,7 @@ suite('WorkspaceConfigurationService - Folder', () => { parentResource = parentDir; workspaceDir = folderDir; globalSettingsFile = path.join(parentDir, 'settings.json'); + globalTasksFile = path.join(parentDir, 'tasks.json'); const instantiationService = workbenchInstantiationService(); const environmentService = new TestEnvironmentService(URI.file(parentDir)); @@ -765,7 +772,7 @@ suite('WorkspaceConfigurationService - Folder', () => { const diskFileSystemProvider = new DiskFileSystemProvider(new NullLogService()); fileService.registerProvider(Schemas.file, diskFileSystemProvider); fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService)); - const workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); + workspaceService = new WorkspaceService({ configurationCache: new ConfigurationCache(environmentService) }, environmentService, fileService, remoteAgentService); instantiationService.stub(IWorkspaceContextService, workspaceService); instantiationService.stub(IConfigurationService, workspaceService); instantiationService.stub(IEnvironmentService, environmentService); @@ -792,7 +799,7 @@ suite('WorkspaceConfigurationService - Folder', () => { }); test('defaults', () => { - assert.deepEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet' } }); + assert.deepEqual(testObject.getValue('configurationService'), { 'folder': { 'applicationSetting': 'isSet', 'machineSetting': 'isSet', 'machineOverridableSetting': 'isSet', 'testSetting': 'isSet', 'languageSetting': 'isSet' } }); }); test('globals override defaults', () => { @@ -1026,6 +1033,16 @@ suite('WorkspaceConfigurationService - Folder', () => { .then(() => assert.equal(testObject.getValue('tasks.service.testSetting'), 'value')); }); + test('update resource configuration', () => { + return testObject.updateValue('configurationService.folder.testSetting', 'value', { resource: workspaceService.getWorkspace().folders[0].uri }, ConfigurationTarget.WORKSPACE_FOLDER) + .then(() => assert.equal(testObject.getValue('configurationService.folder.testSetting'), 'value')); + }); + + test('update resource language configuration', () => { + return testObject.updateValue('configurationService.folder.languageSetting', 'value', { resource: workspaceService.getWorkspace().folders[0].uri }, ConfigurationTarget.WORKSPACE_FOLDER) + .then(() => assert.equal(testObject.getValue('configurationService.folder.languageSetting'), 'value')); + }); + test('update application setting into workspace configuration in a workspace is not supported', () => { return testObject.updateValue('configurationService.folder.applicationSetting', 'workspaceValue', {}, ConfigurationTarget.WORKSPACE, true) .then(() => assert.fail('Should not be supported'), (e) => assert.equal(e.code, ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION)); @@ -1074,6 +1091,17 @@ suite('WorkspaceConfigurationService - Folder', () => { .then(() => assert.ok(target.called)); }); + test('no change event when there are no global tasks', async () => { + const target = sinon.spy(); + testObject.onDidChangeConfiguration(target); + await timeout(500); + assert.ok(target.notCalled); + }); + + test('change event when there are global tasks', () => { + fs.writeFileSync(globalTasksFile, '{ "version": "1.0.0", "tasks": [{ "taskName": "myTask" }'); + return new Promise((c) => testObject.onDidChangeConfiguration(() => c())); + }); }); suite('WorkspaceConfigurationService-Multiroot', () => { @@ -1109,6 +1137,11 @@ suite('WorkspaceConfigurationService-Multiroot', () => { 'type': 'string', 'default': 'isSet', scope: ConfigurationScope.RESOURCE + }, + 'configurationService.workspace.testLanguageSetting': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.RESOURCE_LANGUAGE } } }); @@ -1286,6 +1319,26 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); }); + test('resource language setting in folder is read after it is registered later', () => { + fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewResourceLanguageSetting2": "workspaceFolderValue" }'); + return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ key: 'settings', value: { 'configurationService.workspace.testNewResourceLanguageSetting2': 'workspaceValue' } }], true) + .then(() => testObject.reloadConfiguration()) + .then(() => { + configurationRegistry.registerConfiguration({ + 'id': '_test', + 'type': 'object', + 'properties': { + 'configurationService.workspace.testNewResourceLanguageSetting2': { + 'type': 'string', + 'default': 'isSet', + scope: ConfigurationScope.RESOURCE_LANGUAGE + } + } + }); + assert.equal(testObject.getValue('configurationService.workspace.testNewResourceLanguageSetting2', { resource: workspaceContextService.getWorkspace().folders[0].uri }), 'workspaceFolderValue'); + }); + }); + test('machine overridable setting in folder is read after it is registered later', () => { fs.writeFileSync(workspaceContextService.getWorkspace().folders[0].toResource('.vscode/settings.json').fsPath, '{ "configurationService.workspace.testNewMachineOverridableSetting2": "workspaceFolderValue" }'); return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ key: 'settings', value: { 'configurationService.workspace.testNewMachineOverridableSetting2': 'workspaceValue' } }], true) @@ -1430,7 +1483,7 @@ suite('WorkspaceConfigurationService-Multiroot', () => { }); }); - test('inspect tasks configuration', () => { + test('inspect tasks configuration', async () => { const expectedTasksConfiguration = { 'version': '2.0.0', 'tasks': [ @@ -1445,12 +1498,10 @@ suite('WorkspaceConfigurationService-Multiroot', () => { } ] }; - return jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ key: 'tasks', value: expectedTasksConfiguration }], true) - .then(() => testObject.reloadConfiguration()) - .then(() => { - const actual = testObject.inspect('tasks').workspaceValue; - assert.deepEqual(actual, expectedTasksConfiguration); - }); + await jsonEditingServce.write(workspaceContextService.getWorkspace().configuration!, [{ key: 'tasks', value: expectedTasksConfiguration }], true); + await testObject.reloadConfiguration(); + const actual = testObject.inspect('tasks').workspaceValue; + assert.deepEqual(actual, expectedTasksConfiguration); }); test('update user configuration', () => { @@ -1493,6 +1544,12 @@ suite('WorkspaceConfigurationService-Multiroot', () => { .then(() => assert.equal(testObject.getValue('configurationService.workspace.testResourceSetting', { resource: workspace.folders[0].uri }), 'workspaceFolderValue')); }); + test('update resource language configuration in workspace folder', () => { + const workspace = workspaceContextService.getWorkspace(); + return testObject.updateValue('configurationService.workspace.testLanguageSetting', 'workspaceFolderValue', { resource: workspace.folders[0].uri }, ConfigurationTarget.WORKSPACE_FOLDER) + .then(() => assert.equal(testObject.getValue('configurationService.workspace.testLanguageSetting', { resource: workspace.folders[0].uri }), 'workspaceFolderValue')); + }); + test('update workspace folder configuration should trigger change event before promise is resolve', () => { const workspace = workspaceContextService.getWorkspace(); const target = sinon.spy(); diff --git a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts index 9fdab42186d..60fe745cfc8 100644 --- a/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts +++ b/src/vs/workbench/services/dialogs/browser/abstractFileDialogService.ts @@ -5,7 +5,7 @@ import * as nls from 'vs/nls'; import { IWindowOpenable } from 'vs/platform/windows/common/windows'; -import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs'; +import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getFileNamesMessage } from 'vs/platform/dialogs/common/dialogs'; import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; @@ -90,10 +90,12 @@ export abstract class AbstractFileDialogService implements IFileDialogService { } let message: string; + let detail = nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them."); if (fileNamesOrResources.length === 1) { message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNamesOrResources[0] === 'string' ? fileNamesOrResources[0] : resources.basename(fileNamesOrResources[0])); } else { - message = getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length), fileNamesOrResources); + message = nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length); + detail = getFileNamesMessage(fileNamesOrResources) + '\n' + detail; } const buttons: string[] = [ @@ -104,7 +106,7 @@ export abstract class AbstractFileDialogService implements IFileDialogService { const { choice } = await this.dialogService.show(Severity.Warning, message, buttons, { cancelId: 2, - detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.") + detail }); switch (choice) { diff --git a/src/vs/workbench/services/editor/browser/editorService.ts b/src/vs/workbench/services/editor/browser/editorService.ts index d64e64a3564..90dbcef6583 100644 --- a/src/vs/workbench/services/editor/browser/editorService.ts +++ b/src/vs/workbench/services/editor/browser/editorService.ts @@ -5,7 +5,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, ITextEditorOptions, IEditorOptions, EditorActivation } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor, IRevertOptions, SaveReason, EditorsOrder } from 'vs/workbench/common/editor'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { Registry } from 'vs/platform/registry/common/platform'; import { ResourceMap } from 'vs/base/common/map'; @@ -17,7 +17,7 @@ import { URI } from 'vs/base/common/uri'; import { basename, isEqual } from 'vs/base/common/resources'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { localize } from 'vs/nls'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection, EditorsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions } from 'vs/workbench/services/editor/common/editorService'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle'; @@ -27,6 +27,7 @@ import { IEditorGroupView, IEditorOpeningEvent, EditorServiceImpl } from 'vs/wor import { ILabelService } from 'vs/platform/label/common/label'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { withNullAsUndefined } from 'vs/base/common/types'; +import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; type CachedEditorInput = ResourceEditorInput | IFileEditorInput; type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE; @@ -51,14 +52,19 @@ export class EditorService extends Disposable implements EditorServiceImpl { private readonly _onDidOpenEditorFail = this._register(new Emitter()); readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event; + private readonly _onDidMostRecentlyActiveEditorsChange = this._register(new Emitter()); + readonly onDidMostRecentlyActiveEditorsChange = this._onDidMostRecentlyActiveEditorsChange.event; + //#endregion private fileInputFactory: IFileInputFactory; - private openEditorHandlers: IOpenEditorOverrideHandler[] = []; + private readonly openEditorHandlers: IOpenEditorOverrideHandler[] = []; private lastActiveEditor: IEditorInput | undefined = undefined; private lastActiveGroupId: GroupIdentifier | undefined = undefined; + private readonly editorsObserver = this._register(this.instantiationService.createInstance(EditorsObserver)); + constructor( @IEditorGroupsService private readonly editorGroupService: IEditorGroupsService, @IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService, @@ -80,6 +86,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { this.editorGroupService.whenRestored.then(() => this.onEditorsRestored()); this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group)); this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView)); + this.editorsObserver.onDidChange(() => this._onDidMostRecentlyActiveEditorsChange.fire()); } private onEditorsRestored(): void { @@ -179,10 +186,23 @@ export class EditorService extends Disposable implements EditorServiceImpl { return undefined; } + get count(): number { + return this.editorsObserver.count; + } + get editors(): IEditorInput[] { - const editors: IEditorInput[] = []; - this.editorGroupService.groups.forEach(group => { - editors.push(...group.editors); + return this.getEditors(EditorsOrder.SEQUENTIAL).map(({ editor }) => editor); + } + + getEditors(order: EditorsOrder): ReadonlyArray { + if (order === EditorsOrder.MOST_RECENTLY_ACTIVE) { + return this.editorsObserver.editors; + } + + const editors: IEditorIdentifier[] = []; + + this.editorGroupService.getGroups(GroupsOrder.GRID_APPEARANCE).forEach(group => { + editors.push(...group.getEditors(EditorsOrder.SEQUENTIAL).map(editor => ({ editor, groupId: group.id }))); }); return editors; @@ -228,6 +248,17 @@ export class EditorService extends Disposable implements EditorServiceImpl { openEditor(editor: IResourceDiffInput, group?: OpenInEditorGroup): Promise; openEditor(editor: IResourceSideBySideInput, group?: OpenInEditorGroup): Promise; async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { + const result = this.doResolveEditorOpenRequest(editor, optionsOrGroup, group); + if (result) { + const [resolvedGroup, resolvedEditor, resolvedOptions] = result; + + return withNullAsUndefined(await resolvedGroup.openEditor(resolvedEditor, resolvedOptions)); + } + + return undefined; + } + + doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined { let resolvedGroup: IEditorGroup | undefined; let candidateGroup: OpenInEditorGroup | undefined; @@ -275,16 +306,12 @@ export class EditorService extends Disposable implements EditorServiceImpl { typedOptions.overwrite({ activation: EditorActivation.ACTIVATE }); } - return this.doOpenEditor(resolvedGroup, typedEditor, typedOptions); + return [resolvedGroup, typedEditor, typedOptions]; } return undefined; } - protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise { - return withNullAsUndefined(await group.openEditor(editor, options)); - } - private findTargetGroup(input: IEditorInput, options?: IEditorOptions, group?: OpenInEditorGroup): IEditorGroup { let targetGroup: IEditorGroup | undefined; @@ -756,7 +783,7 @@ export class EditorService extends Disposable implements EditorServiceImpl { export interface IEditorOpenHandler { ( - delegate: (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => Promise, + delegate: (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => Promise, group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions @@ -765,51 +792,90 @@ export interface IEditorOpenHandler { /** * The delegating workbench editor service can be used to override the behaviour of the openEditor() - * method by providing a IEditorOpenHandler. + * method by providing a IEditorOpenHandler. All calls are being delegated to the existing editor + * service otherwise. */ -export class DelegatingEditorService extends EditorService { - private editorOpenHandler: IEditorOpenHandler | undefined; +export class DelegatingEditorService implements IEditorService { + + _serviceBrand: undefined; constructor( - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService, - @IInstantiationService instantiationService: IInstantiationService, - @ILabelService labelService: ILabelService, - @IFileService fileService: IFileService, - @IConfigurationService configurationService: IConfigurationService - ) { - super( - editorGroupService, - untitledTextEditorService, - instantiationService, - labelService, - fileService, - configurationService - ); - } + private editorOpenHandler: IEditorOpenHandler, + @IEditorService private editorService: EditorService + ) { } - setEditorOpenHandler(handler: IEditorOpenHandler): void { - this.editorOpenHandler = handler; - } + openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise; + openEditor(editor: IResourceInput | IUntitledTextResourceInput, group?: OpenInEditorGroup): Promise; + openEditor(editor: IResourceDiffInput, group?: OpenInEditorGroup): Promise; + openEditor(editor: IResourceSideBySideInput, group?: OpenInEditorGroup): Promise; + async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise { + const result = this.editorService.doResolveEditorOpenRequest(editor, optionsOrGroup, group); + if (result) { + const [resolvedGroup, resolvedEditor, resolvedOptions] = result; - protected async doOpenEditor(group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions): Promise { - if (!this.editorOpenHandler) { - return super.doOpenEditor(group, editor, options); + // Pass on to editor open handler + const control = await this.editorOpenHandler( + (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => group.openEditor(editor, options), + resolvedGroup, + resolvedEditor, + resolvedOptions + ); + + if (control) { + return control; // the opening was handled, so return early + } + + return withNullAsUndefined(await resolvedGroup.openEditor(resolvedEditor, resolvedOptions)); } - const control = await this.editorOpenHandler( - (group: IEditorGroup, editor: IEditorInput, options?: IEditorOptions) => super.doOpenEditor(group, editor, options), - group, - editor, - options - ); - - if (control) { - return control; // the opening was handled, so return early - } - - return super.doOpenEditor(group, editor, options); + return undefined; } + + //#region Delegate to IEditorService + + get onDidActiveEditorChange(): Event { return this.editorService.onDidActiveEditorChange; } + get onDidVisibleEditorsChange(): Event { return this.editorService.onDidVisibleEditorsChange; } + + get activeEditor(): IEditorInput | undefined { return this.editorService.activeEditor; } + get activeControl(): IVisibleEditor | undefined { return this.editorService.activeControl; } + get activeTextEditorWidget(): ICodeEditor | IDiffEditor | undefined { return this.editorService.activeTextEditorWidget; } + get visibleEditors(): ReadonlyArray { return this.editorService.visibleEditors; } + get visibleControls(): ReadonlyArray { return this.editorService.visibleControls; } + get visibleTextEditorWidgets(): ReadonlyArray { return this.editorService.visibleTextEditorWidgets; } + get editors(): ReadonlyArray { return this.editorService.editors; } + get count(): number { return this.editorService.count; } + + getEditors(order: EditorsOrder): ReadonlyArray { return this.editorService.getEditors(order); } + + openEditors(editors: IEditorInputWithOptions[], group?: OpenInEditorGroup): Promise; + openEditors(editors: IResourceEditor[], group?: OpenInEditorGroup): Promise; + openEditors(editors: Array, group?: OpenInEditorGroup): Promise { + return this.editorService.openEditors(editors, group); + } + + replaceEditors(editors: IResourceEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; + replaceEditors(editors: IEditorReplacement[], group: IEditorGroup | GroupIdentifier): Promise; + replaceEditors(editors: Array, group: IEditorGroup | GroupIdentifier): Promise { + return this.editorService.replaceEditors(editors as IResourceEditorReplacement[] /* TS fail */, group); + } + + isOpen(editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): boolean { return this.editorService.isOpen(editor); } + + getOpened(editor: IResourceInput | IUntitledTextResourceInput): IEditorInput | undefined { return this.editorService.getOpened(editor); } + + overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable { return this.editorService.overrideOpenEditor(handler); } + + invokeWithinEditorContext(fn: (accessor: ServicesAccessor) => T): T { return this.editorService.invokeWithinEditorContext(fn); } + + createInput(input: IResourceEditor): IEditorInput { return this.editorService.createInput(input); } + + save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise { return this.editorService.save(editors, options); } + saveAll(options?: ISaveAllEditorsOptions): Promise { return this.editorService.saveAll(options); } + + revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise { return this.editorService.revert(editors, options); } + revertAll(options?: IRevertAllEditorsOptions): Promise { return this.editorService.revertAll(options); } + + //#endregion } registerSingleton(IEditorService, EditorService); diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 805ecedb425..ab64d9085c7 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -5,7 +5,7 @@ import { Event } from 'vs/base/common/event'; import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorOptions, ITextEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IVisibleEditor } from 'vs/workbench/services/editor/common/editorService'; @@ -134,19 +134,6 @@ export const enum GroupsOrder { GRID_APPEARANCE } -export const enum EditorsOrder { - - /** - * Editors sorted by most recent activity (most recent active first) - */ - MOST_RECENTLY_ACTIVE, - - /** - * Editors sorted by sequential order - */ - SEQUENTIAL -} - export interface IEditorGroupsService { _serviceBrand: undefined; @@ -224,11 +211,11 @@ export interface IEditorGroupsService { readonly willRestoreEditors: boolean; /** - * Get all groups that are currently visible in the editor area optionally - * sorted by being most recent active or grid order. Will sort by creation - * time by default (oldest group first). + * Get all groups that are currently visible in the editor area. + * + * @param order the order of the editors to use */ - getGroups(order?: GroupsOrder): ReadonlyArray; + getGroups(order: GroupsOrder): ReadonlyArray; /** * Allows to convert a group identifier to a group. @@ -344,6 +331,11 @@ export interface IEditorGroupsService { */ readonly partOptions: IEditorPartOptions; + /** + * An event that notifies when editor part options change. + */ + readonly onDidEditorPartOptionsChange: Event; + /** * Enforce editor part options temporarily. */ @@ -428,16 +420,16 @@ export interface IEditorGroup { readonly count: number; /** - * All opened editors in the group. There can only be one editor active. + * All opened editors in the group in sequential order of their appearance. */ readonly editors: ReadonlyArray; /** - * Get all editors that are currently opened in the group optionally - * sorted by being most recent active. Will sort by sequential appearance - * by default (from left to right). + * Get all editors that are currently opened in the group. + * + * @param order the order of the editors to use */ - getEditors(order?: EditorsOrder): ReadonlyArray; + getEditors(order: EditorsOrder): ReadonlyArray; /** * Returns the editor at a specific index of the group. diff --git a/src/vs/workbench/services/editor/common/editorService.ts b/src/vs/workbench/services/editor/common/editorService.ts index 6eb6cd569cc..ecc324078b4 100644 --- a/src/vs/workbench/services/editor/common/editorService.ts +++ b/src/vs/workbench/services/editor/common/editorService.ts @@ -5,7 +5,7 @@ import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder } from 'vs/workbench/common/editor'; import { Event } from 'vs/base/common/event'; import { IEditor as ICodeEditor, IDiffEditor } from 'vs/editor/common/editorCommon'; import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -123,11 +123,26 @@ export interface IEditorService { readonly visibleTextEditorWidgets: ReadonlyArray; /** - * All editors that are opened across all editor groups. This includes active as well as inactive - * editors in each editor group. + * All editors that are opened across all editor groups in sequential order + * of appearance. + * + * This includes active as well as inactive editors in each editor group. */ readonly editors: ReadonlyArray; + /** + * The total number of editors that are opened either inactive or active. + */ + readonly count: number; + + /** + * All editors that are opened across all editor groups with their group + * identifier. + * + * @param order the order of the editors to use + */ + getEditors(order: EditorsOrder): ReadonlyArray; + /** * Open an editor in an editor group. * diff --git a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts index b22215f7f2c..1d5cb41cc69 100644 --- a/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorGroupsService.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; -import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, EditorsOrder, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { EditorInput, IFileEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorOptions, CloseDirection, IEditorPartOptions } from 'vs/workbench/common/editor'; +import { EditorInput, IFileEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions, EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -19,10 +19,14 @@ import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtil import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; -export class TestEditorControl extends BaseEditor { +const TEST_EDITOR_ID = 'MyFileEditorForEditorGroupService'; +const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorGroupService'; - constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyFileEditorForEditorGroupService', NullTelemetryService, new TestThemeService(), new TestStorageService()); } +class TestEditorControl extends BaseEditor { + + constructor(@ITelemetryService telemetryService: ITelemetryService) { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { super.setInput(input, options, token); @@ -30,16 +34,16 @@ export class TestEditorControl extends BaseEditor { await input.resolve(); } - getId(): string { return 'MyFileEditorForEditorGroupService'; } + getId(): string { return TEST_EDITOR_ID; } layout(): void { } createEditor(): any { } } -export class TestEditorInput extends EditorInput implements IFileEditorInput { +class TestEditorInput extends EditorInput implements IFileEditorInput { constructor(private resource: URI) { super(); } - getTypeId() { return 'testEditorInputForEditorGroupService'; } + getTypeId() { return TEST_EDITOR_INPUT_ID; } resolve(): Promise { return Promise.resolve(null); } matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } setEncoding(encoding: string) { } @@ -53,8 +57,9 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput { suite('EditorGroupsService', () => { - function registerTestEditorInput(): void { + let disposables: IDisposable[] = []; + setup(() => { interface ISerializedTestEditorInput { resource: string; } @@ -81,11 +86,14 @@ suite('EditorGroupsService', () => { } } - (Registry.as(EditorExtensions.EditorInputFactories)).registerEditorInputFactory('testEditorInputForGroupsService', TestEditorInputFactory); - (Registry.as(Extensions.Editors)).registerEditor(EditorDescriptor.create(TestEditorControl, 'MyTestEditorForGroupsService', 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)]); - } + disposables.push((Registry.as(EditorExtensions.EditorInputFactories)).registerEditorInputFactory(TEST_EDITOR_INPUT_ID, TestEditorInputFactory)); + disposables.push((Registry.as(Extensions.Editors)).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)])); + }); - registerTestEditorInput(); + teardown(() => { + dispose(disposables); + disposables = []; + }); function createPart(): EditorPart { const instantiationService = workbenchInstantiationService(); @@ -460,7 +468,7 @@ suite('EditorGroupsService', () => { assert.equal(group.activeEditor, input); assert.ok(group.activeControl instanceof TestEditorControl); - assert.equal(group.editors.length, 2); + assert.equal(group.count, 2); const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input); diff --git a/src/vs/workbench/services/editor/test/browser/editorService.test.ts b/src/vs/workbench/services/editor/test/browser/editorService.test.ts index bfcc32372e1..b36462a5491 100644 --- a/src/vs/workbench/services/editor/test/browser/editorService.test.ts +++ b/src/vs/workbench/services/editor/test/browser/editorService.test.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor'; import { URI } from 'vs/base/common/uri'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; -import { EditorInput, EditorOptions } from 'vs/workbench/common/editor'; -import { workbenchInstantiationService, TestStorageService, TestEditorInput } from 'vs/workbench/test/workbenchTestServices'; +import { EditorInput, EditorOptions, IFileEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions, EditorsOrder } from 'vs/workbench/common/editor'; +import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { EditorService, DelegatingEditorService } from 'vs/workbench/services/editor/browser/editorService'; @@ -25,7 +25,7 @@ import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor'; import { timeout } from 'vs/base/common/async'; import { toResource } from 'vs/base/test/common/utils'; import { IFileService } from 'vs/platform/files/common/files'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle'; import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry'; import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel'; import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider'; @@ -33,9 +33,12 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { CancellationToken } from 'vscode'; +const TEST_EDITOR_ID = 'MyTestEditorForEditorService'; +const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorService'; + class TestEditorControl extends BaseEditor { - constructor(@ITelemetryService telemetryService: ITelemetryService) { super('MyTestEditorForEditorService', NullTelemetryService, new TestThemeService(), new TestStorageService()); } + constructor(@ITelemetryService telemetryService: ITelemetryService) { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { super.setInput(input, options, token); @@ -43,11 +46,59 @@ class TestEditorControl extends BaseEditor { await input.resolve(); } - getId(): string { return 'MyTestEditorForEditorService'; } + getId(): string { return TEST_EDITOR_ID; } layout(): void { } createEditor(): any { } } +class TestEditorInput extends EditorInput implements IFileEditorInput { + gotDisposed = false; + gotSaved = false; + gotSavedAs = false; + gotReverted = false; + dirty = false; + private fails = false; + constructor(public resource: URI) { super(); } + + getTypeId() { return TEST_EDITOR_INPUT_ID; } + resolve(): Promise { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); } + matches(other: TestEditorInput): boolean { return other && other.resource && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } + setEncoding(encoding: string) { } + getEncoding() { return undefined; } + setPreferredEncoding(encoding: string) { } + setMode(mode: string) { } + setPreferredMode(mode: string) { } + getResource(): URI { return this.resource; } + setForceOpenAsBinary(): void { } + setFailToOpen(): void { + this.fails = true; + } + save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSaved = true; + return Promise.resolve(true); + } + saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { + this.gotSavedAs = true; + return Promise.resolve(true); + } + revert(options?: IRevertOptions): Promise { + this.gotReverted = true; + this.gotSaved = false; + this.gotSavedAs = false; + return Promise.resolve(true); + } + isDirty(): boolean { + return this.dirty; + } + isReadonly(): boolean { + return false; + } + dispose(): void { + super.dispose(); + this.gotDisposed = true; + } +} + class FileServiceProvider extends Disposable { constructor(scheme: string, @IFileService fileService: IFileService) { super(); @@ -58,11 +109,16 @@ class FileServiceProvider extends Disposable { suite('EditorService', () => { - function registerTestEditorInput(): void { - Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), [new SyncDescriptor(TestEditorInput)]); - } + let disposables: IDisposable[] = []; - registerTestEditorInput(); + setup(() => { + disposables.push(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For Next Editor Service'), [new SyncDescriptor(TestEditorInput)])); + }); + + teardown(() => { + dispose(disposables); + disposables = []; + }); test('basics', async () => { const partInstantiator = workbenchInstantiationService(); @@ -75,8 +131,8 @@ suite('EditorService', () => { const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); + let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); + let otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); let activeEditorChangeEventCounter = 0; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -100,6 +156,9 @@ suite('EditorService', () => { assert.ok(editor instanceof TestEditorControl); assert.equal(editor, service.activeControl); + assert.equal(1, service.count); + assert.equal(input, service.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].editor); + assert.equal(input, service.getEditors(EditorsOrder.SEQUENTIAL)[0].editor); assert.equal(input, service.activeEditor); assert.equal(service.visibleControls.length, 1); assert.equal(service.visibleControls[0], editor); @@ -113,15 +172,30 @@ suite('EditorService', () => { // Close input await editor!.group!.closeEditor(input); + assert.equal(0, service.count); + assert.equal(0, service.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length); + assert.equal(0, service.getEditors(EditorsOrder.SEQUENTIAL).length); assert.equal(didCloseEditorListenerCounter, 1); assert.equal(activeEditorChangeEventCounter, 2); assert.equal(visibleEditorChangeEventCounter, 2); assert.ok(input.gotDisposed); - // Open again 2 inputs + // Open again 2 inputs (disposed editors are ignored!) + await service.openEditor(input, { pinned: true }); + assert.equal(0, service.count); + + // Open again 2 inputs (recreate because disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-basics')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-basics')); + await service.openEditor(input, { pinned: true }); editor = await service.openEditor(otherInput, { pinned: true }); + assert.equal(2, service.count); + assert.equal(otherInput, service.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].editor); + assert.equal(input, service.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].editor); + assert.equal(input, service.getEditors(EditorsOrder.SEQUENTIAL)[0].editor); + assert.equal(otherInput, service.getEditors(EditorsOrder.SEQUENTIAL)[1].editor); assert.equal(service.visibleControls.length, 1); assert.equal(service.isOpen(input), true); assert.equal(service.isOpen(otherInput), true); @@ -308,8 +382,7 @@ suite('EditorService', () => { const ed = instantiationService.createInstance(MyEditor, 'my.editor'); const inp = instantiationService.createInstance(ResourceEditorInput, 'name', 'description', URI.parse('my://resource-delegate'), undefined); - const delegate = instantiationService.createInstance(DelegatingEditorService); - delegate.setEditorOpenHandler((delegate, group, input) => { + const delegate = instantiationService.createInstance(DelegatingEditorService, (delegate, group, input) => { assert.strictEqual(input, inp); done(); @@ -445,8 +518,8 @@ suite('EditorService', () => { const service: EditorServiceImpl = testInstantiationService.createInstance(EditorService); - const input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); - const otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); + let input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + let otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); let activeEditorChangeEventFired = false; const activeEditorChangeListener = service.onDidActiveEditorChange(() => { @@ -497,7 +570,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 2.) open, open same (forced open) + // 2.) open, open same (forced open) (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -508,7 +583,9 @@ suite('EditorService', () => { await closeEditorAndWaitForNextToOpen(group, input); - // 3.) open, open inactive, close + // 3.) open, open inactive, close (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -521,7 +598,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 4.) open, open inactive, close inactive + // 4.) open, open inactive, close inactive (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -538,7 +617,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 5.) add group, remove group + // 5.) add group, remove group (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -559,7 +640,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 6.) open editor in inactive group + // 6.) open editor in inactive group (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -580,7 +663,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 7.) activate group + // 7.) activate group (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -605,7 +690,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 8.) move editor + // 8.) move editor (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); @@ -622,7 +709,9 @@ suite('EditorService', () => { assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); - // 9.) close editor in inactive group + // 9.) close editor in inactive group (recreate inputs that got disposed) + input = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource-active')); + otherInput = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-active')); editor = await service.openEditor(input, { pinned: true }); assertActiveEditorChangedEvent(true); assertVisibleEditorsChangedEvent(true); diff --git a/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts new file mode 100644 index 00000000000..97bb769736a --- /dev/null +++ b/src/vs/workbench/services/editor/test/browser/editorsObserver.test.ts @@ -0,0 +1,568 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { EditorOptions, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputFactory, IFileEditorInput } from 'vs/workbench/common/editor'; +import { URI } from 'vs/base/common/uri'; +import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; +import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; +import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; +import { GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { EditorActivation, IEditorModel } from 'vs/platform/editor/common/editor'; +import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; +import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { EditorsObserver } from 'vs/workbench/browser/parts/editor/editorsObserver'; +import { timeout } from 'vs/base/common/async'; + +const TEST_EDITOR_ID = 'MyTestEditorForEditorsObserver'; +const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorsObserver'; +const TEST_SERIALIZABLE_EDITOR_INPUT_ID = 'testSerializableEditorInputForEditorsObserver'; + +class TestEditorControl extends BaseEditor { + + constructor() { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } + + async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { + super.setInput(input, options, token); + + await input.resolve(); + } + + getId(): string { return TEST_EDITOR_ID; } + layout(): void { } + createEditor(): any { } +} + +class TestEditorInput extends EditorInput implements IFileEditorInput { + + private dirty = false; + + constructor(public resource: URI) { super(); } + + getTypeId() { return TEST_EDITOR_INPUT_ID; } + resolve(): Promise { return Promise.resolve(null); } + matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } + setEncoding(encoding: string) { } + getEncoding() { return undefined; } + setPreferredEncoding(encoding: string) { } + setMode(mode: string) { } + setPreferredMode(mode: string) { } + getResource(): URI { return this.resource; } + setForceOpenAsBinary(): void { } + isDirty(): boolean { return this.dirty; } + setDirty(): void { this.dirty = true; } +} + +class EditorsObserverTestEditorInput extends TestEditorInput { + getTypeId() { return TEST_SERIALIZABLE_EDITOR_INPUT_ID; } +} + +interface ISerializedTestInput { + resource: string; +} + +class EditorsObserverTestEditorInputFactory implements IEditorInputFactory { + + canSerialize(editorInput: EditorInput): boolean { + return true; + } + + serialize(editorInput: EditorInput): string { + let testEditorInput = editorInput; + let testInput: ISerializedTestInput = { + resource: testEditorInput.resource.toString() + }; + + return JSON.stringify(testInput); + } + + deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput { + let testInput: ISerializedTestInput = JSON.parse(serializedEditorInput); + + return new EditorsObserverTestEditorInput(URI.parse(testInput.resource)); + } +} + +suite('EditorsObserver', function () { + + let disposables: IDisposable[] = []; + + setup(() => { + disposables.push(Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory(TEST_SERIALIZABLE_EDITOR_INPUT_ID, EditorsObserverTestEditorInputFactory)); + disposables.push(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For Editors Observer'), [new SyncDescriptor(TestEditorInput), new SyncDescriptor(EditorsObserverTestEditorInput)])); + }); + + teardown(() => { + dispose(disposables); + disposables = []; + }); + + + test('basics (single group)', async () => { + const instantiationService = workbenchInstantiationService(); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + await part.whenRestored; + + const observer = new EditorsObserver(part, new TestStorageService()); + + let observerChangeListenerCalled = false; + const listener = observer.onDidChange(() => { + observerChangeListenerCalled = true; + }); + + let currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 0); + assert.equal(observerChangeListenerCalled, false); + + const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + + await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 1); + assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[0].editor, input1); + assert.equal(observerChangeListenerCalled, true); + + const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); + const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + + await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[0].editor, input2); + assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[1].editor, input3); + assert.equal(currentEditorsMRU[2].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + observerChangeListenerCalled = false; + await part.activeGroup.closeEditor(input1); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 2); + assert.equal(currentEditorsMRU[0].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[0].editor, input2); + assert.equal(currentEditorsMRU[1].groupId, part.activeGroup.id); + assert.equal(currentEditorsMRU[1].editor, input3); + assert.equal(observerChangeListenerCalled, true); + + await part.activeGroup.closeAllEditors(); + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 0); + + part.dispose(); + listener.dispose(); + }); + + test('basics (multi group)', async () => { + const instantiationService = workbenchInstantiationService(); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + await part.whenRestored; + + const rootGroup = part.activeGroup; + + const observer = new EditorsObserver(part, new TestStorageService()); + + let currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 0); + + const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + + const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 2); + assert.equal(currentEditorsMRU[0].groupId, sideGroup.id); + assert.equal(currentEditorsMRU[0].editor, input1); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input1); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 2); + assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[0].editor, input1); + assert.equal(currentEditorsMRU[1].groupId, sideGroup.id); + assert.equal(currentEditorsMRU[1].editor, input1); + + // Opening an editor inactive should not change + // the most recent editor, but rather put it behind + const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); + + await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true })); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[0].editor, input1); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, sideGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + await rootGroup.closeAllEditors(); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 1); + assert.equal(currentEditorsMRU[0].groupId, sideGroup.id); + assert.equal(currentEditorsMRU[0].editor, input1); + + await sideGroup.closeAllEditors(); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 0); + + part.dispose(); + }); + + test('copy group', async () => { + const instantiationService = workbenchInstantiationService(); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + await part.whenRestored; + + const observer = new EditorsObserver(part, new TestStorageService()); + + const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); + const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + + const rootGroup = part.activeGroup; + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + + let currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + const copiedGroup = part.copyGroup(rootGroup, rootGroup, GroupDirection.RIGHT); + copiedGroup.setActive(true); + + currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 6); + assert.equal(currentEditorsMRU[0].groupId, copiedGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input3); + assert.equal(currentEditorsMRU[2].groupId, copiedGroup.id); + assert.equal(currentEditorsMRU[2].editor, input2); + assert.equal(currentEditorsMRU[3].groupId, copiedGroup.id); + assert.equal(currentEditorsMRU[3].editor, input1); + assert.equal(currentEditorsMRU[4].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[4].editor, input2); + assert.equal(currentEditorsMRU[5].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[5].editor, input1); + + part.dispose(); + }); + + test('initial editors are part of observer and state is persisted & restored (single group)', async () => { + const instantiationService = workbenchInstantiationService(); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + await part.whenRestored; + + const rootGroup = part.activeGroup; + + const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); + const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + + const storage = new TestStorageService(); + const observer = new EditorsObserver(part, storage); + await part.whenRestored; + + let currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); + + const restoredObserver = new EditorsObserver(part, storage); + await part.whenRestored; + + currentEditorsMRU = restoredObserver.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + part.dispose(); + }); + + test('initial editors are part of observer (multi group)', async () => { + const instantiationService = workbenchInstantiationService(); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + await part.whenRestored; + + const rootGroup = part.activeGroup; + + const input1 = new EditorsObserverTestEditorInput(URI.parse('foo://bar1')); + const input2 = new EditorsObserverTestEditorInput(URI.parse('foo://bar2')); + const input3 = new EditorsObserverTestEditorInput(URI.parse('foo://bar3')); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + + const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + + const storage = new TestStorageService(); + const observer = new EditorsObserver(part, storage); + await part.whenRestored; + + let currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, sideGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); + + const restoredObserver = new EditorsObserver(part, storage); + await part.whenRestored; + + currentEditorsMRU = restoredObserver.editors; + assert.equal(currentEditorsMRU.length, 3); + assert.equal(currentEditorsMRU[0].groupId, sideGroup.id); + assert.equal(currentEditorsMRU[0].editor, input3); + assert.equal(currentEditorsMRU[1].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[1].editor, input2); + assert.equal(currentEditorsMRU[2].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[2].editor, input1); + + part.dispose(); + }); + + test('observer does not restore editors that cannot be serialized', async () => { + const instantiationService = workbenchInstantiationService(); + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + await part.whenRestored; + + const rootGroup = part.activeGroup; + + const input1 = new TestEditorInput(URI.parse('foo://bar1')); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + + const storage = new TestStorageService(); + const observer = new EditorsObserver(part, storage); + await part.whenRestored; + + let currentEditorsMRU = observer.editors; + assert.equal(currentEditorsMRU.length, 1); + assert.equal(currentEditorsMRU[0].groupId, rootGroup.id); + assert.equal(currentEditorsMRU[0].editor, input1); + + storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); + + const restoredObserver = new EditorsObserver(part, storage); + await part.whenRestored; + + currentEditorsMRU = restoredObserver.editors; + assert.equal(currentEditorsMRU.length, 0); + + part.dispose(); + }); + + test('observer closes editors when limit reached (across all groups)', async () => { + const instantiationService = workbenchInstantiationService(); + + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + part.enforcePartOptions({ limit: { enabled: true, value: 3 } }); + + await part.whenRestored; + + const storage = new TestStorageService(); + const observer = new EditorsObserver(part, storage); + + const rootGroup = part.activeGroup; + const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + + const input1 = new TestEditorInput(URI.parse('foo://bar1')); + const input2 = new TestEditorInput(URI.parse('foo://bar2')); + const input3 = new TestEditorInput(URI.parse('foo://bar3')); + const input4 = new TestEditorInput(URI.parse('foo://bar4')); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + + assert.equal(rootGroup.count, 3); + assert.equal(rootGroup.isOpened(input1), false); + assert.equal(rootGroup.isOpened(input2), true); + assert.equal(rootGroup.isOpened(input3), true); + assert.equal(rootGroup.isOpened(input4), true); + + input2.setDirty(); + part.enforcePartOptions({ limit: { enabled: true, value: 1 } }); + + await timeout(0); + + assert.equal(rootGroup.count, 2); + assert.equal(rootGroup.isOpened(input1), false); + assert.equal(rootGroup.isOpened(input2), true); // dirty + assert.equal(rootGroup.isOpened(input3), false); + assert.equal(rootGroup.isOpened(input4), true); + + const input5 = new TestEditorInput(URI.parse('foo://bar5')); + await sideGroup.openEditor(input5, EditorOptions.create({ pinned: true })); + + assert.equal(rootGroup.count, 1); + assert.equal(rootGroup.isOpened(input1), false); + assert.equal(rootGroup.isOpened(input2), true); // dirty + assert.equal(rootGroup.isOpened(input3), false); + assert.equal(rootGroup.isOpened(input4), false); + + assert.equal(sideGroup.isOpened(input5), true); + + observer.dispose(); + part.dispose(); + }); + + test('observer closes editors when limit reached (in group)', async () => { + const instantiationService = workbenchInstantiationService(); + + instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); + + const part = instantiationService.createInstance(EditorPart); + part.create(document.createElement('div')); + part.layout(400, 300); + + part.enforcePartOptions({ limit: { enabled: true, value: 3, perEditorGroup: true } }); + + await part.whenRestored; + + const storage = new TestStorageService(); + const observer = new EditorsObserver(part, storage); + + const rootGroup = part.activeGroup; + const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); + + const input1 = new TestEditorInput(URI.parse('foo://bar1')); + const input2 = new TestEditorInput(URI.parse('foo://bar2')); + const input3 = new TestEditorInput(URI.parse('foo://bar3')); + const input4 = new TestEditorInput(URI.parse('foo://bar4')); + + await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await rootGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + + assert.equal(rootGroup.count, 3); + assert.equal(rootGroup.isOpened(input1), false); + assert.equal(rootGroup.isOpened(input2), true); + assert.equal(rootGroup.isOpened(input3), true); + assert.equal(rootGroup.isOpened(input4), true); + + await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); + await sideGroup.openEditor(input4, EditorOptions.create({ pinned: true })); + + assert.equal(sideGroup.count, 3); + assert.equal(sideGroup.isOpened(input1), false); + assert.equal(sideGroup.isOpened(input2), true); + assert.equal(sideGroup.isOpened(input3), true); + assert.equal(sideGroup.isOpened(input4), true); + + part.enforcePartOptions({ limit: { enabled: true, value: 1, perEditorGroup: true } }); + + await timeout(10); + + assert.equal(rootGroup.count, 1); + assert.equal(rootGroup.isOpened(input1), false); + assert.equal(rootGroup.isOpened(input2), false); + assert.equal(rootGroup.isOpened(input3), false); + assert.equal(rootGroup.isOpened(input4), true); + + assert.equal(sideGroup.count, 1); + assert.equal(sideGroup.isOpened(input1), false); + assert.equal(sideGroup.isOpened(input2), false); + assert.equal(sideGroup.isOpened(input3), false); + assert.equal(sideGroup.isOpened(input4), true); + + observer.dispose(); + part.dispose(); + }); +}); diff --git a/src/vs/workbench/services/extensions/common/lazyPromise.ts b/src/vs/workbench/services/extensions/common/lazyPromise.ts index 6cd7ce76aa6..0f6c07064a3 100644 --- a/src/vs/workbench/services/extensions/common/lazyPromise.ts +++ b/src/vs/workbench/services/extensions/common/lazyPromise.ts @@ -27,6 +27,10 @@ export class LazyPromise implements Promise { this._err = null; } + get [Symbol.toStringTag](): string { + return this.toString(); + } + private _ensureActual(): Promise { if (!this._actual) { this._actual = new Promise((c, e) => { diff --git a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts index 0ca576029c4..04b014914bb 100644 --- a/src/vs/workbench/services/extensions/electron-browser/extensionService.ts +++ b/src/vs/workbench/services/extensions/electron-browser/extensionService.ts @@ -473,7 +473,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten // set the resolved authority this._remoteAuthorityResolverService.setResolvedAuthority(resolvedAuthority.authority, resolvedAuthority.options); - this._remoteExplorerService.addDetected(resolvedAuthority.tunnelInformation?.detectedTunnels); + this._remoteExplorerService.addEnvironmentTunnels(resolvedAuthority.tunnelInformation?.environmentTunnels); // monitor for breakage const connection = this._remoteAgentService.getConnection(); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 009ad65946f..02ca4db033b 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -7,7 +7,7 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { URI, UriComponents } from 'vs/base/common/uri'; import { IEditor } from 'vs/editor/common/editorCommon'; import { ITextEditorOptions, IResourceInput, ITextEditorSelection } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, Extensions } from 'vs/workbench/common/editor'; +import { IEditorInput, IEditor as IBaseEditor, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { FileChangesEvent, IFileService, FileChangeType, FILES_EXCLUDE_CONFIG } from 'vs/platform/files/common/files'; @@ -16,9 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace import { dispose, Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { Registry } from 'vs/platform/registry/common/platform'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; -import { IEditorGroupsService, IEditorGroup, EditorsOrder, GroupChangeKind, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; import { getCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { getExcludes, ISearchConfiguration } from 'vs/workbench/services/search/common/search'; import { IExpression } from 'vs/base/common/glob'; @@ -34,9 +34,6 @@ import { withNullAsUndefined } from 'vs/base/common/types'; import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; import { Schemas } from 'vs/base/common/network'; -import { LinkedMap, Touch } from 'vs/base/common/map'; - -//#region Text Editor State helper /** * Stores the selection & view state of an editor and allows to compare it to other selection states. @@ -86,295 +83,6 @@ export class TextEditorState { } } -//#endregion - -//#region Editors History - -interface ISerializedEditorHistory { - history: ISerializedEditorIdentifier[]; -} - -interface ISerializedEditorIdentifier { - groupId: GroupIdentifier; - index: number; -} - -/** - * A history of opened editors across all editor groups by most recently used. - * Rules: - * - the last editor in the history is the one most recently activated - * - the first editor in the history is the one that was activated the longest time ago - * - an editor that opens inactive will be placed behind the currently active editor - */ -export class EditorsHistory extends Disposable { - - private static readonly STORAGE_KEY = 'history.editors'; - - private readonly keyMap = new Map>(); - private readonly mostRecentEditorsMap = new LinkedMap(); - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - get editors(): IEditorIdentifier[] { - return this.mostRecentEditorsMap.values(); - } - - constructor( - @IEditorGroupsService private editorGroupsService: IEditorGroupsService, - @IStorageService private readonly storageService: IStorageService - ) { - super(); - - this.registerListeners(); - } - - private registerListeners(): void { - this._register(this.storageService.onWillSaveState(() => this.saveState())); - this._register(this.editorGroupsService.onDidAddGroup(group => this.onGroupAdded(group))); - - this.editorGroupsService.whenRestored.then(() => this.loadState()); - } - - private onGroupAdded(group: IEditorGroup): void { - - // Make sure to add any already existing editor - // of the new group into our history in LRU order - const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); - for (let i = groupEditorsMru.length - 1; i >= 0; i--) { - this.addMostRecentEditor(group, groupEditorsMru[i], false /* is not active */); - } - - // Make sure that active editor is put as first if group is active - if (this.editorGroupsService.activeGroup === group && group.activeEditor) { - this.addMostRecentEditor(group, group.activeEditor, true /* is active */); - } - - // Group Listeners - this.registerGroupListeners(group); - } - - private registerGroupListeners(group: IEditorGroup): void { - const groupDisposables = new DisposableStore(); - groupDisposables.add(group.onDidGroupChange(e => { - switch (e.kind) { - - // Group gets active: put active editor as most recent - case GroupChangeKind.GROUP_ACTIVE: { - if (this.editorGroupsService.activeGroup === group && group.activeEditor) { - this.addMostRecentEditor(group, group.activeEditor, true /* is active */); - } - - break; - } - - // Editor gets active: put active editor as most recent - // if group is active, otherwise second most recent - case GroupChangeKind.EDITOR_ACTIVE: { - if (e.editor) { - this.addMostRecentEditor(group, e.editor, this.editorGroupsService.activeGroup === group); - } - - break; - } - - // Editor opens: put it as second most recent - case GroupChangeKind.EDITOR_OPEN: { - if (e.editor) { - this.addMostRecentEditor(group, e.editor, false /* is not active */); - } - - break; - } - - // Editor closes: remove from recently opened - case GroupChangeKind.EDITOR_CLOSE: { - if (e.editor) { - this.removeMostRecentEditor(group, e.editor); - } - - break; - } - } - })); - - // Make sure to cleanup on dispose - Event.once(group.onWillDispose)(() => dispose(groupDisposables)); - } - - private addMostRecentEditor(group: IEditorGroup, editor: IEditorInput, isActive: boolean): void { - const key = this.ensureKey(group, editor); - const mostRecentEditor = this.mostRecentEditorsMap.first; - - // Active or first entry: add to end of map - if (isActive || !mostRecentEditor) { - this.mostRecentEditorsMap.set(key, key, mostRecentEditor ? Touch.AsOld /* make first */ : undefined); - } - - // Otherwise: insert before most recent - else { - // we have most recent editors. as such we - // put this newly opened editor right before - // the current most recent one because it cannot - // be the most recently active one unless - // it becomes active. but it is still more - // active then any other editor in the list. - this.mostRecentEditorsMap.set(key, key, Touch.AsOld /* make first */); - this.mostRecentEditorsMap.set(mostRecentEditor, mostRecentEditor, Touch.AsOld /* make first */); - } - - // Event - this._onDidChange.fire(); - } - - private removeMostRecentEditor(group: IEditorGroup, editor: IEditorInput): void { - const key = this.findKey(group, editor); - if (key) { - - // Remove from most recent editors - this.mostRecentEditorsMap.delete(key); - - // Remove from key map - const map = this.keyMap.get(group.id); - if (map && map.delete(key.editor) && map.size === 0) { - this.keyMap.delete(group.id); - } - - // Event - this._onDidChange.fire(); - } - } - - private findKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier | undefined { - const groupMap = this.keyMap.get(group.id); - if (!groupMap) { - return undefined; - } - - return groupMap.get(editor); - } - - private ensureKey(group: IEditorGroup, editor: IEditorInput): IEditorIdentifier { - let groupMap = this.keyMap.get(group.id); - if (!groupMap) { - groupMap = new Map(); - - this.keyMap.set(group.id, groupMap); - } - - let key = groupMap.get(editor); - if (!key) { - key = { groupId: group.id, editor }; - groupMap.set(editor, key); - } - - return key; - } - - private saveState(): void { - if (this.mostRecentEditorsMap.isEmpty()) { - this.storageService.remove(EditorsHistory.STORAGE_KEY, StorageScope.WORKSPACE); - } else { - this.storageService.store(EditorsHistory.STORAGE_KEY, JSON.stringify(this.serialize()), StorageScope.WORKSPACE); - } - } - - private serialize(): ISerializedEditorHistory { - const registry = Registry.as(Extensions.EditorInputFactories); - - const history = this.mostRecentEditorsMap.values(); - const mapGroupToSerializableEditorsOfGroup = new Map(); - - return { - history: coalesce(history.map(({ editor, groupId }) => { - - // Find group for entry - const group = this.editorGroupsService.getGroup(groupId); - if (!group) { - return undefined; - } - - // Find serializable editors of group - let serializableEditorsOfGroup = mapGroupToSerializableEditorsOfGroup.get(group); - if (!serializableEditorsOfGroup) { - serializableEditorsOfGroup = group.getEditors(EditorsOrder.SEQUENTIAL).filter(editor => { - const factory = registry.getEditorInputFactory(editor.getTypeId()); - - return factory?.canSerialize(editor); - }); - mapGroupToSerializableEditorsOfGroup.set(group, serializableEditorsOfGroup); - } - - // Only store the index of the editor of that group - // which can be undefined if the editor is not serializable - const index = serializableEditorsOfGroup.indexOf(editor); - if (index === -1) { - return undefined; - } - - return { groupId, index }; - })) - }; - } - - private loadState(): void { - const serialized = this.storageService.get(EditorsHistory.STORAGE_KEY, StorageScope.WORKSPACE); - - // Previous state: - if (serialized) { - - // Load history map from persisted state - this.deserialize(JSON.parse(serialized)); - } - - // No previous state: best we can do is add each editor - // from oldest to most recently used editor group - else { - const groups = this.editorGroupsService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE); - for (let i = groups.length - 1; i >= 0; i--) { - const group = groups[i]; - const groupEditorsMru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); - for (let i = groupEditorsMru.length - 1; i >= 0; i--) { - this.addMostRecentEditor(group, groupEditorsMru[i], true /* enforce as active to preserve order */); - } - } - } - - // Ensure we listen on group changes for those that exist on startup - for (const group of this.editorGroupsService.groups) { - this.registerGroupListeners(group); - } - } - - deserialize(serialized: ISerializedEditorHistory): void { - const mapValues: [IEditorIdentifier, IEditorIdentifier][] = []; - - for (const { groupId, index } of serialized.history) { - - // Find group for entry - const group = this.editorGroupsService.getGroup(groupId); - if (!group) { - continue; - } - - // Find editor for entry - const editor = group.getEditorByIndex(index); - if (!editor) { - continue; - } - - // Make sure key is registered as well - const editorIdentifier = this.ensureKey(group, editor); - mapValues.push([editorIdentifier, editorIdentifier]); - } - - // Fill map with deserialized values - this.mostRecentEditorsMap.fromJSON(mapValues); - } -} - -//#endregion - interface ISerializedEditorHistoryEntry { resourceJSON?: object; editorInputJSON?: { typeId: string; deserialized: string; }; @@ -424,7 +132,7 @@ export class HistoryService extends Disposable implements IHistoryService { this._register(this.storageService.onWillSaveState(() => this.saveState())); this._register(this.fileService.onFileChanges(event => this.onFileChanges(event))); this._register(this.resourceFilter.onExpressionChange(() => this.removeExcludedFromHistory())); - this._register(this.mostRecentlyUsedOpenEditors.onDidChange(() => this.handleEditorEventInRecentEditorsStack())); + this._register(this.editorService.onDidMostRecentlyActiveEditorsChange(() => this.handleEditorEventInRecentEditorsStack())); // if the service is created late enough that an editor is already opened // make sure to trigger the onActiveEditorChanged() to track the editor @@ -1102,7 +810,7 @@ export class HistoryService extends Disposable implements IHistoryService { this.editorHistoryListeners.clear(); } - getHistory(): Array { + getHistory(): ReadonlyArray { this.ensureHistoryLoaded(); return this.history.slice(0); @@ -1266,12 +974,10 @@ export class HistoryService extends Disposable implements IHistoryService { //#region Editor Most Recently Used History - private readonly mostRecentlyUsedOpenEditors = this._register(this.instantiationService.createInstance(EditorsHistory)); - - private recentlyUsedEditorsStack: IEditorIdentifier[] | undefined = undefined; + private recentlyUsedEditorsStack: ReadonlyArray | undefined = undefined; private recentlyUsedEditorsStackIndex = 0; - private recentlyUsedEditorsInGroupStack: IEditorIdentifier[] | undefined = undefined; + private recentlyUsedEditorsInGroupStack: ReadonlyArray | undefined = undefined; private recentlyUsedEditorsInGroupStackIndex = 0; private navigatingInRecentlyUsedEditorsStack = false; @@ -1309,15 +1015,15 @@ export class HistoryService extends Disposable implements IHistoryService { } } - private ensureRecentlyUsedStack(indexModifier: (index: number) => number, groupId?: GroupIdentifier): [IEditorIdentifier[], number] { - let editors: IEditorIdentifier[]; + private ensureRecentlyUsedStack(indexModifier: (index: number) => number, groupId?: GroupIdentifier): [ReadonlyArray, number] { + let editors: ReadonlyArray; let index: number; const group = typeof groupId === 'number' ? this.editorGroupService.getGroup(groupId) : undefined; // Across groups if (!group) { - editors = this.recentlyUsedEditorsStack || this.mostRecentlyUsedOpenEditors.editors; + editors = this.recentlyUsedEditorsStack || this.editorService.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); index = this.recentlyUsedEditorsStackIndex; } @@ -1362,10 +1068,6 @@ export class HistoryService extends Disposable implements IHistoryService { } } - getMostRecentlyUsedOpenEditors(): Array { - return this.mostRecentlyUsedOpenEditors.editors; - } - //#endregion } diff --git a/src/vs/workbench/services/history/common/history.ts b/src/vs/workbench/services/history/common/history.ts index 76f695a27e1..598a446f9df 100644 --- a/src/vs/workbench/services/history/common/history.ts +++ b/src/vs/workbench/services/history/common/history.ts @@ -5,7 +5,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { IResourceInput } from 'vs/platform/editor/common/editor'; -import { IEditorInput, IEditorIdentifier, GroupIdentifier } from 'vs/workbench/common/editor'; +import { IEditorInput, GroupIdentifier } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; export const IHistoryService = createDecorator('historyService'); @@ -57,7 +57,7 @@ export interface IHistoryService { /** * Get the entire history of editors that were opened. */ - getHistory(): Array; + getHistory(): ReadonlyArray; /** * Looking at the editor history, returns the workspace root of the last file that was @@ -87,9 +87,4 @@ export interface IHistoryService { * @param group optional indicator to scope to a specific group. */ openPreviouslyUsedEditor(group?: GroupIdentifier): void; - - /** - * Get a list of most recently used editors that are open. - */ - getMostRecentlyUsedOpenEditors(): Array; } diff --git a/src/vs/workbench/services/history/test/history.test.ts b/src/vs/workbench/services/history/test/history.test.ts index 6c3384cfe8f..0ccce7b7c28 100644 --- a/src/vs/workbench/services/history/test/history.test.ts +++ b/src/vs/workbench/services/history/test/history.test.ts @@ -4,30 +4,34 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import { EditorOptions, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputFactory } from 'vs/workbench/common/editor'; +import { EditorOptions, EditorInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IEditorInputFactory, IFileEditorInput } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; -import { workbenchInstantiationService, TestStorageService, TestEditorInput } from 'vs/workbench/test/workbenchTestServices'; +import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorPart } from 'vs/workbench/browser/parts/editor/editorPart'; import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; -import { GroupDirection, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { EditorActivation } from 'vs/platform/editor/common/editor'; +import { IEditorGroupsService, GroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorModel } from 'vs/platform/editor/common/editor'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; import { CancellationToken } from 'vs/base/common/cancellation'; -import { EditorsHistory, HistoryService } from 'vs/workbench/services/history/browser/history'; -import { WillSaveStateReason } from 'vs/platform/storage/common/storage'; +import { HistoryService } from 'vs/workbench/services/history/browser/history'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { EditorService } from 'vs/workbench/services/editor/browser/editorService'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { IHistoryService } from 'vs/workbench/services/history/common/history'; import { timeout } from 'vs/base/common/async'; -export class TestEditorControl extends BaseEditor { +const TEST_EDITOR_ID = 'MyTestEditorForEditorHistory'; +const TEST_EDITOR_INPUT_ID = 'testEditorInputForHistoyService'; +const TEST_SERIALIZABLE_EDITOR_INPUT_ID = 'testSerializableEditorInputForHistoyService'; - constructor() { super('MyTestEditorForEditorHistory', NullTelemetryService, new TestThemeService(), new TestStorageService()); } +class TestEditorControl extends BaseEditor { + + constructor() { super(TEST_EDITOR_ID, NullTelemetryService, new TestThemeService(), new TestStorageService()); } async setInput(input: EditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { super.setInput(input, options, token); @@ -35,13 +39,29 @@ export class TestEditorControl extends BaseEditor { await input.resolve(); } - getId(): string { return 'MyTestEditorForEditorHistory'; } + getId(): string { return TEST_EDITOR_ID; } layout(): void { } createEditor(): any { } } +class TestEditorInput extends EditorInput implements IFileEditorInput { + + constructor(public resource: URI) { super(); } + + getTypeId() { return TEST_EDITOR_INPUT_ID; } + resolve(): Promise { return Promise.resolve(null); } + matches(other: TestEditorInput): boolean { return other && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } + setEncoding(encoding: string) { } + getEncoding() { return undefined; } + setPreferredEncoding(encoding: string) { } + setMode(mode: string) { } + setPreferredMode(mode: string) { } + getResource(): URI { return this.resource; } + setForceOpenAsBinary(): void { } +} + class HistoryTestEditorInput extends TestEditorInput { - getTypeId() { return 'testEditorInputForEditorsHistory'; } + getTypeId() { return TEST_SERIALIZABLE_EDITOR_INPUT_ID; } } interface ISerializedTestInput { @@ -79,30 +99,41 @@ async function createServices(): Promise<[EditorPart, HistoryService, EditorServ await part.whenRestored; - const collection = new ServiceCollection(); - collection.set(IEditorGroupsService, part); + instantiationService.stub(IEditorGroupsService, part); - const childInstantiator = instantiationService.createChild(collection); + const editorService = instantiationService.createInstance(EditorService); + instantiationService.stub(IEditorService, editorService); - const childCollection = new ServiceCollection(); - const editorService = childInstantiator.createInstance(EditorService); - collection.set(IEditorService, editorService); - - const historyService = childInstantiator.createChild(childCollection).createInstance(HistoryService); + const historyService = instantiationService.createInstance(HistoryService); + instantiationService.stub(IHistoryService, historyService); return [part, historyService, editorService]; } suite('HistoryService', function () { + let disposables: IDisposable[] = []; + + setup(() => { + disposables.push(Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory(TEST_SERIALIZABLE_EDITOR_INPUT_ID, HistoryTestEditorInputFactory)); + disposables.push(Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, TEST_EDITOR_ID, 'My Test Editor For History Editor Service'), [new SyncDescriptor(TestEditorInput), new SyncDescriptor(HistoryTestEditorInput)])); + }); + + teardown(() => { + dispose(disposables); + disposables = []; + }); + test('back / forward', async () => { const [part, historyService] = await createServices(); const input1 = new TestEditorInput(URI.parse('foo://bar1')); await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); + assert.equal(part.activeGroup.activeEditor, input1); const input2 = new TestEditorInput(URI.parse('foo://bar2')); await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); + assert.equal(part.activeGroup.activeEditor, input2); historyService.back(); assert.equal(part.activeGroup.activeEditor, input1); @@ -148,353 +179,6 @@ suite('HistoryService', function () { part.dispose(); }); -}); - - -suite('EditorHistory', function () { - - function registerTestEditorInput(): void { - Registry.as(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, 'MyTestEditorForEditorHistory', 'My Test Editor For History Editor Service'), [new SyncDescriptor(TestEditorInput)]); - } - - function registerEditorInputFactory() { - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory('testEditorInputForEditorsHistory', HistoryTestEditorInputFactory); - } - - registerEditorInputFactory(); - registerTestEditorInput(); - - test('basics (single group)', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const history = new EditorsHistory(part, new TestStorageService()); - - let historyChangeListenerCalled = false; - const listener = history.onDidChange(() => { - historyChangeListenerCalled = true; - }); - - let currentHistory = history.editors; - assert.equal(currentHistory.length, 0); - assert.equal(historyChangeListenerCalled, false); - - const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1')); - - await part.activeGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 1); - assert.equal(currentHistory[0].groupId, part.activeGroup.id); - assert.equal(currentHistory[0].editor, input1); - assert.equal(historyChangeListenerCalled, true); - - const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2')); - const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3')); - - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await part.activeGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, part.activeGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, part.activeGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, part.activeGroup.id); - assert.equal(currentHistory[2].editor, input1); - - await part.activeGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, part.activeGroup.id); - assert.equal(currentHistory[0].editor, input2); - assert.equal(currentHistory[1].groupId, part.activeGroup.id); - assert.equal(currentHistory[1].editor, input3); - assert.equal(currentHistory[2].groupId, part.activeGroup.id); - assert.equal(currentHistory[2].editor, input1); - - historyChangeListenerCalled = false; - await part.activeGroup.closeEditor(input1); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 2); - assert.equal(currentHistory[0].groupId, part.activeGroup.id); - assert.equal(currentHistory[0].editor, input2); - assert.equal(currentHistory[1].groupId, part.activeGroup.id); - assert.equal(currentHistory[1].editor, input3); - assert.equal(historyChangeListenerCalled, true); - - await part.activeGroup.closeAllEditors(); - currentHistory = history.editors; - assert.equal(currentHistory.length, 0); - - part.dispose(); - listener.dispose(); - }); - - test('basics (multi group)', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const rootGroup = part.activeGroup; - - const history = new EditorsHistory(part, new TestStorageService()); - - let currentHistory = history.editors; - assert.equal(currentHistory.length, 0); - - const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - - const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1')); - - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); - await sideGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 2); - assert.equal(currentHistory[0].groupId, sideGroup.id); - assert.equal(currentHistory[0].editor, input1); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input1); - - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true, activation: EditorActivation.ACTIVATE })); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 2); - assert.equal(currentHistory[0].groupId, rootGroup.id); - assert.equal(currentHistory[0].editor, input1); - assert.equal(currentHistory[1].groupId, sideGroup.id); - assert.equal(currentHistory[1].editor, input1); - - // Opening an editor inactive should not change - // the most recent editor, but rather put it behind - const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2')); - - await rootGroup.openEditor(input2, EditorOptions.create({ inactive: true })); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, rootGroup.id); - assert.equal(currentHistory[0].editor, input1); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, sideGroup.id); - assert.equal(currentHistory[2].editor, input1); - - await rootGroup.closeAllEditors(); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 1); - assert.equal(currentHistory[0].groupId, sideGroup.id); - assert.equal(currentHistory[0].editor, input1); - - await sideGroup.closeAllEditors(); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 0); - - part.dispose(); - }); - - test('copy group', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const history = new EditorsHistory(part, new TestStorageService()); - - const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1')); - const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2')); - const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3')); - - const rootGroup = part.activeGroup; - - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - - let currentHistory = history.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, rootGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, rootGroup.id); - assert.equal(currentHistory[2].editor, input1); - - const copiedGroup = part.copyGroup(rootGroup, rootGroup, GroupDirection.RIGHT); - copiedGroup.setActive(true); - - currentHistory = history.editors; - assert.equal(currentHistory.length, 6); - assert.equal(currentHistory[0].groupId, copiedGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input3); - assert.equal(currentHistory[2].groupId, copiedGroup.id); - assert.equal(currentHistory[2].editor, input2); - assert.equal(currentHistory[3].groupId, copiedGroup.id); - assert.equal(currentHistory[3].editor, input1); - assert.equal(currentHistory[4].groupId, rootGroup.id); - assert.equal(currentHistory[4].editor, input2); - assert.equal(currentHistory[5].groupId, rootGroup.id); - assert.equal(currentHistory[5].editor, input1); - - part.dispose(); - }); - - test('initial editors are part of history and state is persisted & restored (single group)', async () => { - const instantiationService = workbenchInstantiationService(); - instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const rootGroup = part.activeGroup; - - const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1')); - const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2')); - const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3')); - - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - - const storage = new TestStorageService(); - const history = new EditorsHistory(part, storage); - await part.whenRestored; - - let currentHistory = history.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, rootGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, rootGroup.id); - assert.equal(currentHistory[2].editor, input1); - - storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); - - const restoredHistory = new EditorsHistory(part, storage); - await part.whenRestored; - - currentHistory = restoredHistory.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, rootGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, rootGroup.id); - assert.equal(currentHistory[2].editor, input1); - - part.dispose(); - }); - - test('initial editors are part of history (multi group)', async () => { - const instantiationService = workbenchInstantiationService(); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const rootGroup = part.activeGroup; - - const input1 = new HistoryTestEditorInput(URI.parse('foo://bar1')); - const input2 = new HistoryTestEditorInput(URI.parse('foo://bar2')); - const input3 = new HistoryTestEditorInput(URI.parse('foo://bar3')); - - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - await rootGroup.openEditor(input2, EditorOptions.create({ pinned: true })); - - const sideGroup = part.addGroup(rootGroup, GroupDirection.RIGHT); - await sideGroup.openEditor(input3, EditorOptions.create({ pinned: true })); - - const storage = new TestStorageService(); - const history = new EditorsHistory(part, storage); - await part.whenRestored; - - let currentHistory = history.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, sideGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, rootGroup.id); - assert.equal(currentHistory[2].editor, input1); - - storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); - - const restoredHistory = new EditorsHistory(part, storage); - await part.whenRestored; - - currentHistory = restoredHistory.editors; - assert.equal(currentHistory.length, 3); - assert.equal(currentHistory[0].groupId, sideGroup.id); - assert.equal(currentHistory[0].editor, input3); - assert.equal(currentHistory[1].groupId, rootGroup.id); - assert.equal(currentHistory[1].editor, input2); - assert.equal(currentHistory[2].groupId, rootGroup.id); - assert.equal(currentHistory[2].editor, input1); - - part.dispose(); - }); - - test('history does not restore editors that cannot be serialized', async () => { - const instantiationService = workbenchInstantiationService(); - instantiationService.invokeFunction(accessor => Registry.as(EditorExtensions.EditorInputFactories).start(accessor)); - - const part = instantiationService.createInstance(EditorPart); - part.create(document.createElement('div')); - part.layout(400, 300); - - await part.whenRestored; - - const rootGroup = part.activeGroup; - - const input1 = new TestEditorInput(URI.parse('foo://bar1')); - - await rootGroup.openEditor(input1, EditorOptions.create({ pinned: true })); - - const storage = new TestStorageService(); - const history = new EditorsHistory(part, storage); - await part.whenRestored; - - let currentHistory = history.editors; - assert.equal(currentHistory.length, 1); - assert.equal(currentHistory[0].groupId, rootGroup.id); - assert.equal(currentHistory[0].editor, input1); - - storage._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN }); - - const restoredHistory = new EditorsHistory(part, storage); - await part.whenRestored; - - currentHistory = restoredHistory.editors; - assert.equal(currentHistory.length, 0); - - part.dispose(); - }); test('open next/previous recently used editor (single group)', async () => { const [part, historyService] = await createServices(); @@ -574,3 +258,4 @@ suite('EditorHistory', function () { }); }); + diff --git a/src/vs/workbench/services/layout/browser/layoutService.ts b/src/vs/workbench/services/layout/browser/layoutService.ts index 94aa3ef9f55..828a900d396 100644 --- a/src/vs/workbench/services/layout/browser/layoutService.ts +++ b/src/vs/workbench/services/layout/browser/layoutService.ts @@ -27,6 +27,26 @@ export const enum Position { BOTTOM } +export function positionToString(position: Position): string { + switch (position) { + case Position.LEFT: return 'left'; + case Position.RIGHT: return 'right'; + case Position.BOTTOM: return 'bottom'; + } + + return 'bottom'; +} + +const positionsByString: { [key: string]: Position } = { + [positionToString(Position.LEFT)]: Position.LEFT, + [positionToString(Position.RIGHT)]: Position.RIGHT, + [positionToString(Position.BOTTOM)]: Position.BOTTOM +}; + +export function positionFromString(str: string): Position { + return positionsByString[str]; +} + export interface IWorkbenchLayoutService extends ILayoutService { _serviceBrand: undefined; diff --git a/src/vs/workbench/services/progress/browser/media/progressService.css b/src/vs/workbench/services/progress/browser/media/progressService.css index 4cf8a37994b..6b73a2f3a4e 100644 --- a/src/vs/workbench/services/progress/browser/media/progressService.css +++ b/src/vs/workbench/services/progress/browser/media/progressService.css @@ -17,8 +17,11 @@ width: 14px; height: 14px; position: absolute; - top: 1px; - left: 1px; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: auto; background-color: currentColor; content: ''; } diff --git a/src/vs/workbench/services/remote/common/remoteExplorerService.ts b/src/vs/workbench/services/remote/common/remoteExplorerService.ts index 01f41b09a1c..3ef12502500 100644 --- a/src/vs/workbench/services/remote/common/remoteExplorerService.ts +++ b/src/vs/workbench/services/remote/common/remoteExplorerService.ts @@ -3,52 +3,83 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as nls from 'vs/nls'; import { Event, Emitter } from 'vs/base/common/event'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; -import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; -import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry'; import { ITunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel'; import { Disposable } from 'vs/base/common/lifecycle'; import { IEditableData } from 'vs/workbench/common/views'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; export const IRemoteExplorerService = createDecorator('remoteExplorerService'); export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType'; +const TUNNELS_TO_RESTORE = 'remote.tunnels.toRestore'; + +export enum TunnelType { + Candidate = 'Candidate', + Detected = 'Detected', + Forwarded = 'Forwarded', + Add = 'Add' +} + +export interface ITunnelItem { + tunnelType: TunnelType; + remoteHost: string; + remotePort: number; + localAddress?: string; + name?: string; + closeable?: boolean; + readonly description?: string; + readonly label: string; +} export interface Tunnel { - remote: number; + remoteHost: string; + remotePort: number; localAddress: string; - local?: number; + localPort?: number; name?: string; description?: string; closeable?: boolean; } +export function MakeAddress(host: string, port: number): string { + if (host = '127.0.0.1') { + host = 'localhost'; + } + return host + ':' + port; +} + export class TunnelModel extends Disposable { - readonly forwarded: Map; - readonly detected: Map; + readonly forwarded: Map; + readonly detected: Map; private _onForwardPort: Emitter = new Emitter(); public onForwardPort: Event = this._onForwardPort.event; - private _onClosePort: Emitter = new Emitter(); - public onClosePort: Event = this._onClosePort.event; - private _onPortName: Emitter = new Emitter(); - public onPortName: Event = this._onPortName.event; - private _candidateFinder: (() => Promise<{ port: number, detail: string }[]>) | undefined; + private _onClosePort: Emitter<{ host: string, port: number }> = new Emitter(); + public onClosePort: Event<{ host: string, port: number }> = this._onClosePort.event; + private _onPortName: Emitter<{ host: string, port: number }> = new Emitter(); + public onPortName: Event<{ host: string, port: number }> = this._onPortName.event; + private _candidates: { host: string, port: number, detail: string }[] = []; + private _candidateFinder: (() => Promise<{ host: string, port: number, detail: string }[]>) | undefined; + private _onCandidatesChanged: Emitter = new Emitter(); + public onCandidatesChanged: Event = this._onCandidatesChanged.event; constructor( - @ITunnelService private readonly tunnelService: ITunnelService + @ITunnelService private readonly tunnelService: ITunnelService, + @IStorageService private readonly storageService: IStorageService, + @IConfigurationService private readonly configurationService: IConfigurationService ) { super(); this.forwarded = new Map(); this.tunnelService.tunnels.then(tunnels => { tunnels.forEach(tunnel => { if (tunnel.localAddress) { - this.forwarded.set(tunnel.tunnelRemotePort, { - remote: tunnel.tunnelRemotePort, + this.forwarded.set(MakeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort), { + remotePort: tunnel.tunnelRemotePort, + remoteHost: tunnel.tunnelRemoteHost, localAddress: tunnel.localAddress, - local: tunnel.tunnelLocalPort + localPort: tunnel.tunnelLocalPort }); } }); @@ -56,79 +87,117 @@ export class TunnelModel extends Disposable { this.detected = new Map(); this._register(this.tunnelService.onTunnelOpened(tunnel => { - if (!this.forwarded.has(tunnel.tunnelRemotePort) && tunnel.localAddress) { - this.forwarded.set(tunnel.tunnelRemotePort, { - remote: tunnel.tunnelRemotePort, + const key = MakeAddress(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort); + if ((!this.forwarded.has(key)) && tunnel.localAddress) { + this.forwarded.set(key, { + remoteHost: tunnel.tunnelRemoteHost, + remotePort: tunnel.tunnelRemotePort, localAddress: tunnel.localAddress, - local: tunnel.tunnelLocalPort, + localPort: tunnel.tunnelLocalPort, closeable: true }); + this.storeForwarded(); } - this._onForwardPort.fire(this.forwarded.get(tunnel.tunnelRemotePort)!); + this._onForwardPort.fire(this.forwarded.get(key)!); })); - this._register(this.tunnelService.onTunnelClosed(remotePort => { - if (this.forwarded.has(remotePort)) { - this.forwarded.delete(remotePort); - this._onClosePort.fire(remotePort); + this._register(this.tunnelService.onTunnelClosed(address => { + const key = MakeAddress(address.host, address.port); + if (this.forwarded.has(key)) { + this.forwarded.delete(key); + this.storeForwarded(); + this._onClosePort.fire(address); } })); + + this.restoreForwarded(); } - async forward(remote: number, local?: number, name?: string): Promise { - if (!this.forwarded.has(remote)) { - const tunnel = await this.tunnelService.openTunnel(remote, local); + private async restoreForwarded() { + if (this.configurationService.getValue('remote.restoreForwardedPorts')) { + const tunnelsString = this.storageService.get(TUNNELS_TO_RESTORE, StorageScope.WORKSPACE); + if (tunnelsString) { + (JSON.parse(tunnelsString))?.forEach(tunnel => { + this.forward({ host: tunnel.remoteHost, port: tunnel.remotePort }, tunnel.localPort, tunnel.name); + }); + } + } + } + + private storeForwarded() { + if (this.configurationService.getValue('remote.restoreForwardedPorts')) { + this.storageService.store(TUNNELS_TO_RESTORE, JSON.stringify(Array.from(this.forwarded.values())), StorageScope.WORKSPACE); + } + } + + async forward(remote: { host: string, port: number }, local?: number, name?: string): Promise { + const key = MakeAddress(remote.host, remote.port); + if (!this.forwarded.has(key)) { + const tunnel = await this.tunnelService.openTunnel(remote.host, remote.port, local); if (tunnel && tunnel.localAddress) { const newForward: Tunnel = { - remote: tunnel.tunnelRemotePort, - local: tunnel.tunnelLocalPort, + remoteHost: tunnel.tunnelRemoteHost, + remotePort: tunnel.tunnelRemotePort, + localPort: tunnel.tunnelLocalPort, name: name, closeable: true, localAddress: tunnel.localAddress }; - this.forwarded.set(remote, newForward); + this.forwarded.set(key, newForward); this._onForwardPort.fire(newForward); return tunnel; } } } - name(remote: number, name: string) { - if (this.forwarded.has(remote)) { - this.forwarded.get(remote)!.name = name; - this._onPortName.fire(remote); - } else if (this.detected.has(remote)) { - this.detected.get(remote)!.name = name; - this._onPortName.fire(remote); + name(host: string, port: number, name: string) { + const key = MakeAddress(host, port); + if (this.forwarded.has(key)) { + this.forwarded.get(key)!.name = name; + this.storeForwarded(); + this._onPortName.fire({ host, port }); + } else if (this.detected.has(key)) { + this.detected.get(key)!.name = name; + this._onPortName.fire({ host, port }); } } - async close(remote: number): Promise { - return this.tunnelService.closeTunnel(remote); + async close(host: string, port: number): Promise { + return this.tunnelService.closeTunnel(host, port); } - address(remote: number): string | undefined { - return (this.forwarded.get(remote) || this.detected.get(remote))?.localAddress; + address(host: string, port: number): string | undefined { + const key = MakeAddress(host, port); + return (this.forwarded.get(key) || this.detected.get(key))?.localAddress; } - addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[]): void { + addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[]): void { tunnels.forEach(tunnel => { - this.detected.set(tunnel.remote.port, { - remote: tunnel.remote.port, + this.detected.set(MakeAddress(tunnel.remoteAddress.host, tunnel.remoteAddress.port), { + remoteHost: tunnel.remoteAddress.host, + remotePort: tunnel.remoteAddress.port, localAddress: tunnel.localAddress, closeable: false }); }); } - registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void { + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void { this._candidateFinder = finder; } - get candidates(): Promise<{ port: number, detail: string }[]> { + get candidates(): Promise<{ host: string, port: number, detail: string }[]> { + return this.updateCandidates().then(() => this._candidates); + } + + private async updateCandidates(): Promise { if (this._candidateFinder) { - return this._candidateFinder(); + this._candidates = await this._candidateFinder(); } - return Promise.resolve([]); + } + + async refresh(): Promise { + await this.updateCandidates(); + this._onCandidatesChanged.fire(); } } @@ -136,75 +205,33 @@ export interface IRemoteExplorerService { _serviceBrand: undefined; onDidChangeTargetType: Event; targetType: string; - readonly helpInformation: HelpInformation[]; readonly tunnelModel: TunnelModel; - onDidChangeEditable: Event; - setEditable(remote: number | undefined, data: IEditableData | null): void; - getEditableData(remote: number | undefined): IEditableData | undefined; - forward(remote: number, local?: number, name?: string): Promise; - close(remote: number): Promise; - addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void; - registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void; + onDidChangeEditable: Event; + setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void; + getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined; + forward(remote: { host: string, port: number }, localPort?: number, name?: string): Promise; + close(remote: { host: string, port: number }): Promise; + addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[] | undefined): void; + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void; + refresh(): Promise; } -export interface HelpInformation { - extensionDescription: IExtensionDescription; - getStarted?: string; - documentation?: string; - feedback?: string; - issues?: string; - remoteName?: string[] | string; -} - -const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'remoteHelp', - jsonSchema: { - description: nls.localize('RemoteHelpInformationExtPoint', 'Contributes help information for Remote'), - type: 'object', - properties: { - 'getStarted': { - description: nls.localize('RemoteHelpInformationExtPoint.getStarted', "The url to your project's Getting Started page"), - type: 'string' - }, - 'documentation': { - description: nls.localize('RemoteHelpInformationExtPoint.documentation', "The url to your project's documentation page"), - type: 'string' - }, - 'feedback': { - description: nls.localize('RemoteHelpInformationExtPoint.feedback', "The url to your project's feedback reporter"), - type: 'string' - }, - 'issues': { - description: nls.localize('RemoteHelpInformationExtPoint.issues', "The url to your project's issues list"), - type: 'string' - } - } - } -}); - class RemoteExplorerService implements IRemoteExplorerService { public _serviceBrand: undefined; private _targetType: string = ''; private readonly _onDidChangeTargetType: Emitter = new Emitter(); public readonly onDidChangeTargetType: Event = this._onDidChangeTargetType.event; - private _helpInformation: HelpInformation[] = []; private _tunnelModel: TunnelModel; - private _editable: { remote: number | undefined, data: IEditableData } | undefined; - private readonly _onDidChangeEditable: Emitter = new Emitter(); - public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; + private _editable: { tunnelItem: ITunnelItem | undefined, data: IEditableData } | undefined; + private readonly _onDidChangeEditable: Emitter = new Emitter(); + public readonly onDidChangeEditable: Event = this._onDidChangeEditable.event; constructor( @IStorageService private readonly storageService: IStorageService, - @ITunnelService tunnelService: ITunnelService) { - this._tunnelModel = new TunnelModel(tunnelService); - remoteHelpExtPoint.setHandler((extensions) => { - let helpInformation: HelpInformation[] = []; - for (let extension of extensions) { - this._handleRemoteInfoExtensionPoint(extension, helpInformation); - } - - this._helpInformation = helpInformation; - }); + @ITunnelService tunnelService: ITunnelService, + @IConfigurationService configurationService: IConfigurationService + ) { + this._tunnelModel = new TunnelModel(tunnelService, storageService, configurationService); } set targetType(name: string) { @@ -219,64 +246,45 @@ class RemoteExplorerService implements IRemoteExplorerService { return this._targetType; } - private _handleRemoteInfoExtensionPoint(extension: IExtensionPointUser, helpInformation: HelpInformation[]) { - if (!extension.description.enableProposedApi) { - return; - } - - if (!extension.value.documentation && !extension.value.feedback && !extension.value.getStarted && !extension.value.issues) { - return; - } - - helpInformation.push({ - extensionDescription: extension.description, - getStarted: extension.value.getStarted, - documentation: extension.value.documentation, - feedback: extension.value.feedback, - issues: extension.value.issues, - remoteName: extension.value.remoteName - }); - } - - get helpInformation(): HelpInformation[] { - return this._helpInformation; - } - get tunnelModel(): TunnelModel { return this._tunnelModel; } - forward(remote: number, local?: number, name?: string): Promise { + forward(remote: { host: string, port: number }, local?: number, name?: string): Promise { return this.tunnelModel.forward(remote, local, name); } - close(remote: number): Promise { - return this.tunnelModel.close(remote); + close(remote: { host: string, port: number }): Promise { + return this.tunnelModel.close(remote.host, remote.port); } - addDetected(tunnels: { remote: { port: number, host: string }, localAddress: string }[] | undefined): void { + addEnvironmentTunnels(tunnels: { remoteAddress: { port: number, host: string }, localAddress: string }[] | undefined): void { if (tunnels) { - this.tunnelModel.addDetected(tunnels); + this.tunnelModel.addEnvironmentTunnels(tunnels); } } - setEditable(remote: number | undefined, data: IEditableData | null): void { + setEditable(tunnelItem: ITunnelItem | undefined, data: IEditableData | null): void { if (!data) { this._editable = undefined; } else { - this._editable = { remote, data }; + this._editable = { tunnelItem, data }; } - this._onDidChangeEditable.fire(remote); + this._onDidChangeEditable.fire(tunnelItem); } - getEditableData(remote: number | undefined): IEditableData | undefined { - return this._editable && this._editable.remote === remote ? this._editable.data : undefined; + getEditableData(tunnelItem: ITunnelItem | undefined): IEditableData | undefined { + return (this._editable && (!tunnelItem || (this._editable.tunnelItem?.remotePort === tunnelItem.remotePort) && (this._editable.tunnelItem.remoteHost === tunnelItem.remoteHost))) ? + this._editable.data : undefined; } - registerCandidateFinder(finder: () => Promise<{ port: number, detail: string }[]>): void { + registerCandidateFinder(finder: () => Promise<{ host: string, port: number, detail: string }[]>): void { this.tunnelModel.registerCandidateFinder(finder); } + refresh(): Promise { + return this.tunnelModel.refresh(); + } } registerSingleton(IRemoteExplorerService, RemoteExplorerService, true); diff --git a/src/vs/workbench/services/remote/node/tunnelService.ts b/src/vs/workbench/services/remote/node/tunnelService.ts index 4e60ad01e15..66e50c7e997 100644 --- a/src/vs/workbench/services/remote/node/tunnelService.ts +++ b/src/vs/workbench/services/remote/node/tunnelService.ts @@ -103,9 +103,9 @@ export class TunnelService implements ITunnelService { private _onTunnelOpened: Emitter = new Emitter(); public onTunnelOpened: Event = this._onTunnelOpened.event; - private _onTunnelClosed: Emitter = new Emitter(); - public onTunnelClosed: Event = this._onTunnelClosed.event; - private readonly _tunnels = new Map }>(); + private _onTunnelClosed: Emitter<{ host: string, port: number }> = new Emitter(); + public onTunnelClosed: Event<{ host: string, port: number }> = this._onTunnelClosed.event; + private readonly _tunnels = new Map }>>(); private _tunnelProvider: ITunnelProvider | undefined; public constructor( @@ -130,23 +130,32 @@ export class TunnelService implements ITunnelService { } public get tunnels(): Promise { - return Promise.all(Array.from(this._tunnels.values()).map(x => x.value)); + const promises: Promise[] = []; + Array.from(this._tunnels.values()).forEach(portMap => Array.from(portMap.values()).forEach(x => promises.push(x.value))); + return Promise.all(promises); } dispose(): void { - for (const { value } of this._tunnels.values()) { - value.then(tunnel => tunnel.dispose()); + for (const portMap of this._tunnels.values()) { + for (const { value } of portMap.values()) { + value.then(tunnel => tunnel.dispose()); + } + portMap.clear(); } this._tunnels.clear(); } - openTunnel(remotePort: number, localPort: number): Promise | undefined { + openTunnel(remoteHost: string | undefined, remotePort: number, localPort: number): Promise | undefined { const remoteAuthority = this.environmentService.configuration.remoteAuthority; if (!remoteAuthority) { return undefined; } - const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remotePort, localPort); + if (!remoteHost || (remoteHost === '127.0.0.1')) { + remoteHost = 'localhost'; + } + + const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remoteHost, remotePort, localPort); if (!resolvedTunnel) { return resolvedTunnel; } @@ -165,48 +174,62 @@ export class TunnelService implements ITunnelService { tunnelLocalPort: tunnel.tunnelLocalPort, localAddress: tunnel.localAddress, dispose: () => { - const existing = this._tunnels.get(tunnel.tunnelRemotePort); - if (existing) { - existing.refcount--; - this.tryDisposeTunnel(tunnel.tunnelRemotePort, existing); + const existingHost = this._tunnels.get(tunnel.tunnelRemoteHost); + if (existingHost) { + const existing = existingHost.get(tunnel.tunnelRemotePort); + if (existing) { + existing.refcount--; + this.tryDisposeTunnel(tunnel.tunnelRemoteHost, tunnel.tunnelRemotePort, existing); + } } } }; } - private async tryDisposeTunnel(remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { + private async tryDisposeTunnel(remoteHost: string, remotePort: number, tunnel: { refcount: number, readonly value: Promise }): Promise { if (tunnel.refcount <= 0) { const disposePromise: Promise = tunnel.value.then(tunnel => { tunnel.dispose(); - this._onTunnelClosed.fire(tunnel.tunnelRemotePort); + this._onTunnelClosed.fire({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }); }); - this._tunnels.delete(remotePort); + if (this._tunnels.has(remoteHost)) { + this._tunnels.get(remoteHost)!.delete(remotePort); + } return disposePromise; } } - async closeTunnel(remotePort: number): Promise { - if (this._tunnels.has(remotePort)) { - const value = this._tunnels.get(remotePort)!; + async closeTunnel(remoteHost: string, remotePort: number): Promise { + const portMap = this._tunnels.get(remoteHost); + if (portMap && portMap.has(remotePort)) { + const value = portMap.get(remotePort)!; value.refcount = 0; - await this.tryDisposeTunnel(remotePort, value); + await this.tryDisposeTunnel(remoteHost, remotePort, value); } } - private retainOrCreateTunnel(remoteAuthority: string, remotePort: number, localPort?: number): Promise | undefined { - const existing = this._tunnels.get(remotePort); + private addTunnelToMap(remoteHost: string, remotePort: number, tunnel: Promise) { + if (!this._tunnels.has(remoteHost)) { + this._tunnels.set(remoteHost, new Map()); + } + this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel }); + } + + private retainOrCreateTunnel(remoteAuthority: string, remoteHost: string, remotePort: number, localPort?: number): Promise | undefined { + const portMap = this._tunnels.get(remoteHost); + const existing = portMap ? portMap.get(remotePort) : undefined; if (existing) { ++existing.refcount; return existing.value; } if (this._tunnelProvider) { - const tunnel = this._tunnelProvider.forwardPort({ remote: { host: 'localhost', port: remotePort } }); + const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } }); if (tunnel) { - this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); + this.addTunnelToMap(remoteHost, remotePort, tunnel); } return tunnel; - } else { + } else if (remoteHost === 'localhost') { const options: IConnectionOptions = { commit: product.commit, socketFactory: nodeSocketFactory, @@ -221,9 +244,10 @@ export class TunnelService implements ITunnelService { }; const tunnel = createRemoteTunnel(options, remotePort, localPort); - this._tunnels.set(remotePort, { refcount: 1, value: tunnel }); + this.addTunnelToMap(remoteHost, remotePort, tunnel); return tunnel; } + return undefined; } } diff --git a/src/vs/workbench/services/search/common/search.ts b/src/vs/workbench/services/search/common/search.ts index 4ef600d4629..5d64c576132 100644 --- a/src/vs/workbench/services/search/common/search.ts +++ b/src/vs/workbench/services/search/common/search.ts @@ -16,16 +16,10 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation' import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry'; import { Event } from 'vs/base/common/event'; import { relative } from 'vs/base/common/path'; -import { Extensions as ViewContainerExtensions, ViewContainer, IViewContainersRegistry } from 'vs/workbench/common/views'; -import { Registry } from 'vs/platform/registry/common/platform'; export const VIEWLET_ID = 'workbench.view.search'; export const PANEL_ID = 'workbench.view.search'; export const VIEW_ID = 'workbench.view.search'; -/** - * Search viewlet container. - */ -export const VIEW_CONTAINER: ViewContainer = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer(VIEWLET_ID, true); export const ISearchService = createDecorator('searchService'); diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index fc838b95e80..8a4da30f57d 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -7,7 +7,7 @@ import * as childProcess from 'child_process'; import * as fs from 'fs'; import * as path from 'vs/base/common/path'; import { Readable } from 'stream'; -import { NodeStringDecoder, StringDecoder } from 'string_decoder'; +import { StringDecoder } from 'string_decoder'; import * as arrays from 'vs/base/common/arrays'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import * as glob from 'vs/base/common/glob'; @@ -360,7 +360,7 @@ export class FileWalker { }); } - private forwardData(stream: Readable, encoding: string, cb: (err: Error | null, stdout?: string) => void): NodeStringDecoder { + private forwardData(stream: Readable, encoding: string, cb: (err: Error | null, stdout?: string) => void): StringDecoder { const decoder = new StringDecoder(encoding); stream.on('data', (data: Buffer) => { cb(null, decoder.write(data)); diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index ac159a60c92..336d4cd0350 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -380,6 +380,7 @@ export class SearchService implements IRawSearchService { */ private preventCancellation(promise: CancelablePromise): CancelablePromise { return new class implements CancelablePromise { + get [Symbol.toStringTag]() { return this.toString(); } cancel() { // Do nothing } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts index 835b0dc041b..99dd5602e4d 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearchEngine.ts @@ -6,7 +6,7 @@ import * as cp from 'child_process'; import { EventEmitter } from 'events'; import * as path from 'vs/base/common/path'; -import { NodeStringDecoder, StringDecoder } from 'string_decoder'; +import { StringDecoder } from 'string_decoder'; import { createRegExp, startsWith, startsWithUTF8BOM, stripUTF8BOM, escapeRegExpCharacters, endsWith } from 'vs/base/common/strings'; import { URI } from 'vs/base/common/uri'; import { IExtendedExtensionSearchOptions, SearchError, SearchErrorCode, serializeSearchError } from 'vs/workbench/services/search/common/search'; @@ -169,7 +169,7 @@ export class RipgrepParser extends EventEmitter { private remainder = ''; private isDone = false; private hitLimit = false; - private stringDecoder: NodeStringDecoder; + private stringDecoder: StringDecoder; private numResults = 0; diff --git a/src/vs/workbench/services/textfile/browser/textFileService.ts b/src/vs/workbench/services/textfile/browser/textFileService.ts index 3a00df0a6e7..5f4ec3ee4b8 100644 --- a/src/vs/workbench/services/textfile/browser/textFileService.ts +++ b/src/vs/workbench/services/textfile/browser/textFileService.ts @@ -433,12 +433,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex modelToRestoreResource = joinPath(target, sourceModelResource.path.substr(source.path.length + 1)); } - let mode: string | undefined = sourceModel.textEditorModel?.getModeId(); - if (mode === PLAINTEXT_MODE_ID) { - mode = undefined; // never enforce plain text mode when moving as it is unspecific - } - - const modelToRestore: ModelToRestore = { resource: modelToRestoreResource, encoding: sourceModel.getEncoding(), mode }; + const modelToRestore: ModelToRestore = { resource: modelToRestoreResource, encoding: sourceModel.getEncoding() }; if (sourceModel.isDirty()) { modelToRestore.snapshot = sourceModel.createSnapshot(); } @@ -771,9 +766,13 @@ export abstract class AbstractTextFileService extends Disposable implements ITex await this.create(target, ''); } - let mode: string | undefined = sourceModel.textEditorModel?.getModeId(); - if (mode === PLAINTEXT_MODE_ID) { - mode = undefined; // never enforce plain text mode when moving as it is unspecific + // Carry over the mode if this is an untitled file and the mode was picked by the user + let mode: string | undefined; + if (sourceModel instanceof UntitledTextEditorModel) { + mode = sourceModel.getMode(); + if (mode === PLAINTEXT_MODE_ID) { + mode = undefined; // never enforce plain text mode when moving as it is unspecific + } } targetModel = await this.models.loadOrCreate(target, { encoding: sourceModel.getEncoding(), mode }); diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index fb8822c7f3c..13c8397c189 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -960,6 +960,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil } } + getMode(): string | undefined { + if (this.textEditorModel) { + return this.textEditorModel.getModeId(); + } + + return this.preferredMode; + } + getEncoding(): string | undefined { return this.preferredEncoding || this.contentEncoding; } diff --git a/src/vs/workbench/services/textfile/common/textfiles.ts b/src/vs/workbench/services/textfile/common/textfiles.ts index 8b6750df496..b7abec08cba 100644 --- a/src/vs/workbench/services/textfile/common/textfiles.ts +++ b/src/vs/workbench/services/textfile/common/textfiles.ts @@ -456,6 +456,8 @@ export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport makeDirty(): void; + getMode(): string | undefined; + isResolved(): this is IResolvedTextFileEditorModel; isDisposed(): boolean; @@ -468,9 +470,6 @@ export interface IResolvedTextFileEditorModel extends ITextFileEditorModel { createSnapshot(): ITextSnapshot; } -/** - * Helper method to convert a snapshot into its full string form. - */ export function snapshotToString(snapshot: ITextSnapshot): string { const chunks: string[] = []; diff --git a/src/vs/workbench/services/textfile/test/textFileService.test.ts b/src/vs/workbench/services/textfile/test/textFileService.test.ts index 79c3a8052dc..1c04252cdd7 100644 --- a/src/vs/workbench/services/textfile/test/textFileService.test.ts +++ b/src/vs/workbench/services/textfile/test/textFileService.test.ts @@ -40,8 +40,8 @@ class ServiceAccessor { class BeforeShutdownEventImpl implements BeforeShutdownEvent { - public value: boolean | Promise | undefined; - public reason = ShutdownReason.CLOSE; + value: boolean | Promise | undefined; + reason = ShutdownReason.CLOSE; veto(value: boolean | Promise): void { this.value = value; diff --git a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts index 74cdf94ddaf..a914477d227 100644 --- a/src/vs/workbench/services/themes/browser/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/browser/workbenchThemeService.ts @@ -6,7 +6,7 @@ import * as nls from 'vs/nls'; import * as types from 'vs/base/common/types'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, DETECT_HC_SETTING, HC_THEME_ID, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IWorkbenchThemeService, IColorTheme, ITokenColorCustomizations, IFileIconTheme, ExtensionData, VS_LIGHT_THEME, VS_DARK_THEME, VS_HC_THEME, COLOR_THEME_SETTING, ICON_THEME_SETTING, CUSTOM_WORKBENCH_COLORS_SETTING, CUSTOM_EDITOR_COLORS_SETTING, IColorCustomizations, CUSTOM_EDITOR_TOKENSTYLES_SETTING, IExperimentalTokenStyleCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -14,7 +14,7 @@ import * as errors from 'vs/base/common/errors'; import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration'; import { IConfigurationRegistry, Extensions as ConfigurationExtensions, IConfigurationPropertySchema, IConfigurationNode } from 'vs/platform/configuration/common/configurationRegistry'; import { ColorThemeData } from 'vs/workbench/services/themes/common/colorThemeData'; -import { ITheme, Extensions as ThemingExtensions, IThemingRegistry } from 'vs/platform/theme/common/themeService'; +import { ITheme, Extensions as ThemingExtensions, IThemingRegistry, ThemeType, LIGHT, DARK, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { Event, Emitter } from 'vs/base/common/event'; import { registerFileIconThemeSchemas } from 'vs/workbench/services/themes/common/fileIconThemeSchema'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; @@ -35,13 +35,25 @@ import { getRemoteAuthority } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService'; import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader'; +// settings + +const PREFERRED_DARK_THEME_SETTING = 'workbench.preferredDarkColorTheme'; +const PREFERRED_LIGHT_THEME_SETTING = 'workbench.preferredLightColorTheme'; +const PREFERRED_HC_THEME_SETTING = 'workbench.preferredHighContrastColorTheme'; +const DETECT_COLOR_SCHEME_SETTING = 'window.autoDetectColorScheme'; +const DETECT_HC_SETTING = 'window.autoDetectHighContrast'; + // implementation const DEFAULT_THEME_ID = 'vs-dark vscode-theme-defaults-themes-dark_plus-json'; const DEFAULT_THEME_SETTING_VALUE = 'Default Dark+'; +const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark+'; +const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light+'; +const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast'; const PERSISTED_THEME_STORAGE_KEY = 'colorThemeData'; const PERSISTED_ICON_THEME_STORAGE_KEY = 'iconThemeData'; +const PERSISTED_OS_COLOR_SCHEME = 'osColorScheme'; const defaultThemeExtensionId = 'vscode-theme-defaults'; const oldDefaultThemeExtensionId = 'vscode-theme-colorful-defaults'; @@ -148,6 +160,7 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { this.initialize().then(undefined, errors.onUnexpectedError).then(_ => { this.installConfigurationListener(); + this.installPreferredSchemeListener(); }); let prevColorId: string | undefined = undefined; @@ -155,8 +168,8 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { // update settings schema setting for theme specific settings this.colorThemeStore.onDidChange(async event => { // updates enum for the 'workbench.colorTheme` setting - colorThemeSettingSchema.enum = event.themes.map(t => t.settingsId); - colorThemeSettingSchema.enumDescriptions = event.themes.map(t => t.description || ''); + colorThemeSettingEnum.splice(0, colorThemeSettingEnum.length, ...event.themes.map(t => t.settingsId)); + colorThemeSettingEnumDescriptions.splice(0, colorThemeSettingEnumDescriptions.length, ...event.themes.map(t => t.description || '')); const themeSpecificWorkbenchColors: IJSONSchema = { properties: {} }; const themeSpecificTokenColors: IJSONSchema = { properties: {} }; @@ -248,44 +261,40 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { } private initialize(): Promise<[IColorTheme | null, IFileIconTheme | null]> { - let detectHCThemeSetting = this.configurationService.getValue(DETECT_HC_SETTING); - - let colorThemeSetting: string; - if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { - colorThemeSetting = HC_THEME_ID; - } else { - colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); - } - - let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); + const colorThemeSetting = this.configurationService.getValue(COLOR_THEME_SETTING); + const iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); const extDevLocs = this.environmentService.extensionDevelopmentLocationURI; - let uri: URI | undefined; - if (extDevLocs && extDevLocs.length > 0) { - // if there are more than one ext dev paths, use first - uri = extDevLocs[0]; - } - return Promise.all([ - this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, DEFAULT_THEME_ID).then(theme => { - return this.colorThemeStore.findThemeDataByParentLocation(uri).then(devThemes => { - if (devThemes.length) { - return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); - } else { - return this.setColorTheme(theme && theme.id, undefined); - } - }); - }), - this.iconThemeStore.findThemeBySettingsId(iconThemeSetting).then(theme => { - return this.iconThemeStore.findThemeDataByParentLocation(uri).then(devThemes => { - if (devThemes.length) { - return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); - } else { - return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined); - } - }); - }), - ]); + const initializeColorTheme = async () => { + if (extDevLocs && extDevLocs.length > 0) { // in dev mode, switch to a theme provided by the extension under dev. + const devThemes = await this.colorThemeStore.findThemeDataByParentLocation(extDevLocs[0]); + if (devThemes.length) { + return this.setColorTheme(devThemes[0].id, ConfigurationTarget.MEMORY); + } + } + let theme = await this.colorThemeStore.findThemeDataBySettingsId(colorThemeSetting, DEFAULT_THEME_ID); + + const persistedColorScheme = this.storageService.get(PERSISTED_OS_COLOR_SCHEME, StorageScope.GLOBAL); + const preferredColorScheme = this.getPreferredColorScheme(); + if (persistedColorScheme && preferredColorScheme && persistedColorScheme !== preferredColorScheme) { + return this.applyPreferredColorTheme(preferredColorScheme); + } + return this.setColorTheme(theme && theme.id, undefined); + }; + + const initializeIconTheme = async () => { + if (extDevLocs && extDevLocs.length > 0) { // in dev mode, switch to a theme provided by the extension under dev. + const devThemes = await this.iconThemeStore.findThemeDataByParentLocation(extDevLocs[0]); + if (devThemes.length) { + return this.setFileIconTheme(devThemes[0].id, ConfigurationTarget.MEMORY); + } + } + const theme = await this.iconThemeStore.findThemeBySettingsId(iconThemeSetting); + return this.setFileIconTheme(theme ? theme.id : DEFAULT_ICON_THEME_ID, undefined); + }; + + return Promise.all([initializeColorTheme(), initializeIconTheme()]); } private installConfigurationListener() { @@ -300,6 +309,18 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }); } } + if (e.affectsConfiguration(DETECT_COLOR_SCHEME_SETTING)) { + this.handlePreferredSchemeUpdated(); + } + if (e.affectsConfiguration(PREFERRED_DARK_THEME_SETTING) && this.getPreferredColorScheme() === DARK) { + this.applyPreferredColorTheme(DARK); + } + if (e.affectsConfiguration(PREFERRED_LIGHT_THEME_SETTING) && this.getPreferredColorScheme() === LIGHT) { + this.applyPreferredColorTheme(LIGHT); + } + if (e.affectsConfiguration(PREFERRED_HC_THEME_SETTING) && this.getPreferredColorScheme() === HIGH_CONTRAST) { + this.applyPreferredColorTheme(HIGH_CONTRAST); + } if (e.affectsConfiguration(ICON_THEME_SETTING)) { let iconThemeSetting = this.configurationService.getValue(ICON_THEME_SETTING); if (iconThemeSetting !== this.currentIconTheme.settingsId) { @@ -330,6 +351,48 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { }); } + // preferred scheme handling + + private installPreferredSchemeListener() { + window.matchMedia('(prefers-color-scheme: dark)').addListener(async () => this.handlePreferredSchemeUpdated()); + } + + private async handlePreferredSchemeUpdated() { + const scheme = this.getPreferredColorScheme(); + this.storageService.store(PERSISTED_OS_COLOR_SCHEME, scheme, StorageScope.GLOBAL); + if (scheme) { + return this.applyPreferredColorTheme(scheme); + } + return undefined; + } + + private getPreferredColorScheme(): ThemeType | undefined { + let detectHCThemeSetting = this.configurationService.getValue(DETECT_HC_SETTING); + if (this.environmentService.configuration.highContrast && detectHCThemeSetting) { + return HIGH_CONTRAST; + } + if (this.configurationService.getValue(DETECT_COLOR_SCHEME_SETTING)) { + if (window.matchMedia(`(prefers-color-scheme: light)`).matches) { + return LIGHT; + } else if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) { + return DARK; + } + } + return undefined; + } + + private async applyPreferredColorTheme(type: ThemeType): Promise { + const settingId = type === DARK ? PREFERRED_DARK_THEME_SETTING : type === LIGHT ? PREFERRED_LIGHT_THEME_SETTING : PREFERRED_HC_THEME_SETTING; + const themeSettingId = this.configurationService.getValue(settingId); + if (themeSettingId) { + const theme = await this.colorThemeStore.findThemeDataBySettingsId(themeSettingId, undefined); + if (theme) { + return this.setColorTheme(theme.id, 'auto'); + } + } + return null; + } + public getColorTheme(): IColorTheme { return this.currentColorTheme; } @@ -352,11 +415,10 @@ export class WorkbenchThemeService implements IWorkbenchThemeService { themeId = validateThemeId(themeId); // migrate theme ids - return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(data => { - if (!data) { + return this.colorThemeStore.findThemeData(themeId, DEFAULT_THEME_ID).then(themeData => { + if (!themeData) { return null; } - const themeData = data; return themeData.ensureLoaded(this.extensionResourceLoaderService).then(_ => { if (themeId === this.currentColorTheme.id && !this.currentColorTheme.isLoaded && this.currentColorTheme.hasEqualData(themeData)) { this.currentColorTheme.clearCaches(); @@ -641,14 +703,46 @@ registerFileIconThemeSchemas(); // Configuration: Themes const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); +const colorThemeSettingEnum: string[] = []; +const colorThemeSettingEnumDescriptions: string[] = []; + const colorThemeSettingSchema: IConfigurationPropertySchema = { type: 'string', description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."), default: DEFAULT_THEME_SETTING_VALUE, - enum: [], - enumDescriptions: [], + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), }; +const preferredDarkThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + description: nls.localize('preferredDarkColorTheme', 'Specifies the preferred color theme for dark OS appearance when \'{0}\' is enabled.', DETECT_COLOR_SCHEME_SETTING), + default: DEFAULT_THEME_DARK_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const preferredLightThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + description: nls.localize('preferredLightColorTheme', 'Specifies the preferred color theme for light OS appearance when \'{0}\' is enabled.', DETECT_COLOR_SCHEME_SETTING), + default: DEFAULT_THEME_LIGHT_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const preferredHCThemeSettingSchema: IConfigurationPropertySchema = { + type: 'string', + description: nls.localize('preferredHCColorTheme', 'Specifies the preferred color theme used in high contrast mode when \'{0}\' is enabled.', DETECT_HC_SETTING), + default: DEFAULT_THEME_HC_SETTING_VALUE, + enum: colorThemeSettingEnum, + enumDescriptions: colorThemeSettingEnumDescriptions, + errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."), +}; +const detectColorSchemeSettingSchema: IConfigurationPropertySchema = { + type: 'boolean', + description: nls.localize('detectColorScheme', 'If set, automatically switch to the preferred color theme based on the OS appearance.'), + default: false +}; const iconThemeSettingSchema: IConfigurationPropertySchema = { type: ['string', 'null'], @@ -675,6 +769,10 @@ const themeSettingsConfiguration: IConfigurationNode = { type: 'object', properties: { [COLOR_THEME_SETTING]: colorThemeSettingSchema, + [PREFERRED_DARK_THEME_SETTING]: preferredDarkThemeSettingSchema, + [PREFERRED_LIGHT_THEME_SETTING]: preferredLightThemeSettingSchema, + [PREFERRED_HC_THEME_SETTING]: preferredHCThemeSettingSchema, + [DETECT_COLOR_SCHEME_SETTING]: detectColorSchemeSettingSchema, [ICON_THEME_SETTING]: iconThemeSettingSchema, [CUSTOM_WORKBENCH_COLORS_SETTING]: colorCustomizationsSchema } diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 9937ca11be9..4756204b595 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -18,7 +18,6 @@ export const VS_HC_THEME = 'hc-black'; export const HC_THEME_ID = 'Default High Contrast'; export const COLOR_THEME_SETTING = 'workbench.colorTheme'; -export const DETECT_HC_SETTING = 'window.autoDetectHighContrast'; export const ICON_THEME_SETTING = 'workbench.iconTheme'; export const CUSTOM_WORKBENCH_COLORS_SETTING = 'workbench.colorCustomizations'; export const CUSTOM_EDITOR_COLORS_SETTING = 'editor.tokenColorCustomizations'; diff --git a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts index bd26f256b6d..a2d778aeb88 100644 --- a/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/baseEditor.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { BaseEditor, EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; import { EditorInput, EditorOptions, IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorExtensions } from 'vs/workbench/common/editor'; -import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import * as Platform from 'vs/platform/registry/common/platform'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; @@ -19,6 +18,7 @@ import { URI } from 'vs/base/common/uri'; import { IEditorRegistry, Extensions, EditorDescriptor } from 'vs/workbench/browser/editor'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IEditorModel } from 'vs/platform/editor/common/editor'; +import { dispose } from 'vs/base/common/lifecycle'; const NullThemeService = new TestThemeService(); @@ -131,8 +131,8 @@ suite('Workbench base editor', () => { let oldEditorsCnt = EditorRegistry.getEditors().length; let oldInputCnt = (EditorRegistry).getEditorInputs().length; - EditorRegistry.registerEditor(d1, [new SyncDescriptor(MyInput)]); - EditorRegistry.registerEditor(d2, [new SyncDescriptor(MyInput), new SyncDescriptor(MyOtherInput)]); + const dispose1 = EditorRegistry.registerEditor(d1, [new SyncDescriptor(MyInput)]); + const dispose2 = EditorRegistry.registerEditor(d2, [new SyncDescriptor(MyInput), new SyncDescriptor(MyOtherInput)]); assert.equal(EditorRegistry.getEditors().length, oldEditorsCnt + 2); assert.equal((EditorRegistry).getEditorInputs().length, oldInputCnt + 3); @@ -143,51 +143,41 @@ suite('Workbench base editor', () => { assert.strictEqual(EditorRegistry.getEditorById('id1'), d1); assert.strictEqual(EditorRegistry.getEditorById('id2'), d2); assert(!EditorRegistry.getEditorById('id3')); + + dispose([dispose1, dispose2]); }); test('Editor Lookup favors specific class over superclass (match on specific class)', function () { let d1 = EditorDescriptor.create(MyEditor, 'id1', 'name'); - let d2 = EditorDescriptor.create(MyOtherEditor, 'id2', 'name'); - let oldEditors = EditorRegistry.getEditors(); - (EditorRegistry).setEditors([]); + const disposable = EditorRegistry.registerEditor(d1, [new SyncDescriptor(MyResourceInput)]); - EditorRegistry.registerEditor(d2, [new SyncDescriptor(ResourceEditorInput)]); - EditorRegistry.registerEditor(d1, [new SyncDescriptor(MyResourceInput)]); - - let inst = new TestInstantiationService(); + let inst = workbenchInstantiationService(); const editor = EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake'), undefined))!.instantiate(inst); assert.strictEqual(editor.getId(), 'myEditor'); const otherEditor = EditorRegistry.getEditor(inst.createInstance(ResourceEditorInput, 'fake', '', URI.file('/fake'), undefined))!.instantiate(inst); - assert.strictEqual(otherEditor.getId(), 'myOtherEditor'); + assert.strictEqual(otherEditor.getId(), 'workbench.editors.textResourceEditor'); - (EditorRegistry).setEditors(oldEditors); + disposable.dispose(); }); test('Editor Lookup favors specific class over superclass (match on super class)', function () { - let d1 = EditorDescriptor.create(MyOtherEditor, 'id1', 'name'); - - let oldEditors = EditorRegistry.getEditors(); - (EditorRegistry).setEditors([]); - - EditorRegistry.registerEditor(d1, [new SyncDescriptor(ResourceEditorInput)]); - - let inst = new TestInstantiationService(); + let inst = workbenchInstantiationService(); const editor = EditorRegistry.getEditor(inst.createInstance(MyResourceInput, 'fake', '', URI.file('/fake'), undefined))!.instantiate(inst); - assert.strictEqual('myOtherEditor', editor.getId()); - - (EditorRegistry).setEditors(oldEditors); + assert.strictEqual('workbench.editors.textResourceEditor', editor.getId()); }); test('Editor Input Factory', function () { workbenchInstantiationService().invokeFunction(accessor => EditorInputRegistry.start(accessor)); - EditorInputRegistry.registerEditorInputFactory('myInputId', MyInputFactory); + const disposable = EditorInputRegistry.registerEditorInputFactory('myInputId', MyInputFactory); let factory = EditorInputRegistry.getEditorInputFactory('myInputId'); assert(factory); + + disposable.dispose(); }); test('EditorMemento - basics', function () { @@ -266,17 +256,17 @@ suite('Workbench base editor', () => { } class TestEditorInput extends EditorInput { - constructor(private resource: URI, private id = 'testEditorInput') { + constructor(private resource: URI, private id = 'testEditorInputForMementoTest') { super(); } - public getTypeId() { return 'testEditorInput'; } - public resolve(): Promise { return Promise.resolve(null!); } + getTypeId() { return 'testEditorInputForMementoTest'; } + resolve(): Promise { return Promise.resolve(null!); } - public matches(other: TestEditorInput): boolean { + matches(other: TestEditorInput): boolean { return other && this.id === other.id && other instanceof TestEditorInput; } - public getResource(): URI { + getResource(): URI { return this.resource; } } diff --git a/src/vs/workbench/test/browser/parts/views/views.test.ts b/src/vs/workbench/test/browser/parts/views/views.test.ts index dbfe6851bae..794ff35443b 100644 --- a/src/vs/workbench/test/browser/parts/views/views.test.ts +++ b/src/vs/workbench/test/browser/parts/views/views.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { ContributableViewsModel, ViewsService, IViewState } from 'vs/workbench/browser/parts/views/views'; -import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService } from 'vs/workbench/common/views'; +import { IViewsRegistry, IViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, IViewsService, ViewContainerLocation } from 'vs/workbench/common/views'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { move } from 'vs/base/common/arrays'; import { Registry } from 'vs/platform/registry/common/platform'; @@ -15,7 +15,7 @@ import { TestInstantiationService } from 'vs/platform/instantiation/test/common/ import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import sinon = require('sinon'); -const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer('test'); +const container = Registry.as(ViewContainerExtensions.ViewContainersRegistry).registerViewContainer({ id: 'test', name: 'test', ctorDescriptor: { ctor: {} } }, ViewContainerLocation.Sidebar); const ViewsRegistry = Registry.as(ViewContainerExtensions.ViewsRegistry); class ViewDescriptorSequence { diff --git a/src/vs/workbench/test/browser/quickopen.test.ts b/src/vs/workbench/test/browser/quickopen.test.ts index cbd8854f8eb..8303a042e22 100644 --- a/src/vs/workbench/test/browser/quickopen.test.ts +++ b/src/vs/workbench/test/browser/quickopen.test.ts @@ -11,7 +11,8 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { QuickOpenHandlerDescriptor, IQuickOpenRegistry, Extensions as QuickOpenExtensions, QuickOpenAction, QuickOpenHandler } from 'vs/workbench/browser/quickopen'; export class TestQuickOpenService implements IQuickOpenService { - public _serviceBrand: undefined; + + _serviceBrand: undefined; private callback?: (prefix?: string) => void; @@ -44,8 +45,8 @@ export class TestQuickOpenService implements IQuickOpenService { return null!; } - public dispose() { } - public navigate(): void { } + dispose() { } + navigate(): void { } } suite('QuickOpen', () => { diff --git a/src/vs/workbench/test/browser/viewlet.test.ts b/src/vs/workbench/test/browser/viewlet.test.ts index 7ca46667704..15a1cb526eb 100644 --- a/src/vs/workbench/test/browser/viewlet.test.ts +++ b/src/vs/workbench/test/browser/viewlet.test.ts @@ -16,7 +16,7 @@ suite('Viewlets', () => { super('id', null!, null!, null!, null!, null!, null!, null!, null!, null!, null!); } - public layout(dimension: any): void { + layout(dimension: any): void { throw new Error('Method not implemented.'); } } diff --git a/src/vs/workbench/test/common/editor/editorGroups.test.ts b/src/vs/workbench/test/common/editor/editorGroups.test.ts index 9aa743a2b34..ae3dad36505 100644 --- a/src/vs/workbench/test/common/editor/editorGroups.test.ts +++ b/src/vs/workbench/test/common/editor/editorGroups.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import { EditorGroup, ISerializedEditorGroup, EditorCloseEvent } from 'vs/workbench/common/editor/editorGroup'; -import { Extensions as EditorExtensions, IEditorInputFactoryRegistry, EditorInput, IFileEditorInput, IEditorInputFactory, CloseDirection } from 'vs/workbench/common/editor'; +import { Extensions as EditorExtensions, IEditorInputFactoryRegistry, EditorInput, IFileEditorInput, IEditorInputFactory, CloseDirection, EditorsOrder } from 'vs/workbench/common/editor'; import { URI } from 'vs/base/common/uri'; import { TestLifecycleService, TestContextService, TestStorageService } from 'vs/workbench/test/workbenchTestServices'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; @@ -20,6 +20,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; function inst(): IInstantiationService { let inst = new TestInstantiationService(); @@ -160,13 +161,16 @@ class TestEditorInputFactory implements IEditorInputFactory { suite('Workbench editor groups', () => { - function registerEditorInputFactory() { - Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory('testEditorInputForGroups', TestEditorInputFactory); - } + let disposables: IDisposable[] = []; - registerEditorInputFactory(); + setup(() => { + disposables.push(Registry.as(EditorExtensions.EditorInputFactories).registerEditorInputFactory('testEditorInputForGroups', TestEditorInputFactory)); + }); teardown(() => { + dispose(disposables); + disposables = []; + index = 1; }); @@ -303,7 +307,7 @@ suite('Workbench editor groups', () => { const events = groupListener(group); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); // Active && Pinned const input1 = input(); @@ -311,7 +315,7 @@ suite('Workbench editor groups', () => { assert.equal(openedEditor, input1); assert.equal(group.count, 1); - assert.equal(group.getEditors(true).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input1); assert.equal(group.isActive(input1), true); assert.equal(group.isPreview(input1), false); @@ -324,7 +328,7 @@ suite('Workbench editor groups', () => { let editor = group.closeEditor(input1); assert.equal(editor, input1); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); assert.equal(group.activeEditor, undefined); assert.equal(events.closed[0].editor, input1); assert.equal(events.closed[0].index, 0); @@ -335,7 +339,7 @@ suite('Workbench editor groups', () => { group.openEditor(input2, { active: true, pinned: false }); assert.equal(group.count, 1); - assert.equal(group.getEditors(true).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input2); assert.equal(group.isActive(input2), true); assert.equal(group.isPreview(input2), true); @@ -347,7 +351,7 @@ suite('Workbench editor groups', () => { group.closeEditor(input2); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); assert.equal(group.activeEditor, undefined); assert.equal(events.closed[1].editor, input2); assert.equal(events.closed[1].index, 0); @@ -356,7 +360,7 @@ suite('Workbench editor groups', () => { editor = group.closeEditor(input2); assert.ok(!editor); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); assert.equal(group.activeEditor, undefined); assert.equal(events.closed[1].editor, input2); @@ -365,7 +369,7 @@ suite('Workbench editor groups', () => { group.openEditor(input3, { active: false, pinned: true }); assert.equal(group.count, 1); - assert.equal(group.getEditors(true).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input3); assert.equal(group.isActive(input3), true); assert.equal(group.isPreview(input3), false); @@ -377,7 +381,7 @@ suite('Workbench editor groups', () => { group.closeEditor(input3); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); assert.equal(group.activeEditor, undefined); assert.equal(events.closed[2].editor, input3); @@ -386,7 +390,7 @@ suite('Workbench editor groups', () => { group.closeEditor(input3); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); assert.equal(group.activeEditor, undefined); assert.equal(events.closed[2].editor, input3); @@ -395,7 +399,7 @@ suite('Workbench editor groups', () => { group.openEditor(input4); assert.equal(group.count, 1); - assert.equal(group.getEditors(true).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input4); assert.equal(group.isActive(input4), true); assert.equal(group.isPreview(input4), true); @@ -407,7 +411,7 @@ suite('Workbench editor groups', () => { group.closeEditor(input4); assert.equal(group.count, 0); - assert.equal(group.getEditors(true).length, 0); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 0); assert.equal(group.activeEditor, undefined); assert.equal(events.closed[3].editor, input4); }); @@ -432,7 +436,7 @@ suite('Workbench editor groups', () => { group.openEditor(input3, { pinned: true, active: true }); assert.equal(group.count, 3); - assert.equal(group.getEditors(true).length, 3); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 3); assert.equal(group.activeEditor, input3); assert.equal(group.isActive(input1), false); assert.equal(group.isPinned(input1), true); @@ -452,7 +456,7 @@ suite('Workbench editor groups', () => { assert.equal(events.activated[1], input2); assert.equal(events.activated[2], input3); - const mru = group.getEditors(true); + const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input3); assert.equal(mru[1], input2); assert.equal(mru[2], input1); @@ -492,12 +496,12 @@ suite('Workbench editor groups', () => { group.openEditor(input2, { pinned: true, active: true }); group.openEditor(input3, { pinned: true, active: true }); - assert.equal(input3, group.getEditors()[2]); + assert.equal(input3, group.getEditors(EditorsOrder.SEQUENTIAL)[2]); const input4 = input(); group.openEditor(input4, { pinned: false, active: true }); // this should cause the preview editor to move after input3 - assert.equal(input4, group.getEditors()[2]); + assert.equal(input4, group.getEditors(EditorsOrder.SEQUENTIAL)[2]); }); test('Multiple Editors - Pinned and Active (DEFAULT_OPEN_EDITOR_DIRECTION = Direction.LEFT)', function () { @@ -524,9 +528,9 @@ suite('Workbench editor groups', () => { group.openEditor(input2, { pinned: true, active: true }); group.openEditor(input3, { pinned: true, active: true }); - assert.equal(group.getEditors()[0], input3); - assert.equal(group.getEditors()[1], input2); - assert.equal(group.getEditors()[2], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input1); group.closeAllEditors(); @@ -547,7 +551,7 @@ suite('Workbench editor groups', () => { group.openEditor(input3, { pinned: true }); assert.equal(group.count, 3); - assert.equal(group.getEditors(true).length, 3); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 3); assert.equal(group.activeEditor, input1); assert.equal(group.isActive(input1), true); assert.equal(group.isPinned(input1), true); @@ -562,7 +566,7 @@ suite('Workbench editor groups', () => { assert.equal(group.isPinned(2), true); assert.equal(group.isPreview(input3), false); - const mru = group.getEditors(true); + const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input1); assert.equal(mru[1], input3); assert.equal(mru[2], input2); @@ -582,7 +586,7 @@ suite('Workbench editor groups', () => { group.openEditor(input3); // overwrites preview assert.equal(group.count, 1); - assert.equal(group.getEditors(true).length, 1); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length, 1); assert.equal(group.activeEditor, input3); assert.equal(group.isActive(input3), true); assert.equal(group.isPinned(input3), false); @@ -596,7 +600,7 @@ suite('Workbench editor groups', () => { assert.equal(events.closed[0].replaced, true); assert.equal(events.closed[1].replaced, true); - const mru = group.getEditors(true); + const mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input3); assert.equal(mru.length, 1); }); @@ -615,7 +619,7 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); - let mru = group.getEditors(true); + let mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input3); assert.equal(mru[1], input2); assert.equal(mru[2], input1); @@ -630,7 +634,7 @@ suite('Workbench editor groups', () => { assert.equal(group.isActive(input2), false); assert.equal(group.isActive(input3), false); - mru = group.getEditors(true); + mru = group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE); assert.equal(mru[0], input1); assert.equal(mru[1], input3); assert.equal(mru[2], input2); @@ -673,8 +677,8 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); assert.equal(group.count, 2); // 2 previews got merged into one - assert.equal(group.getEditors()[0], input2); - assert.equal(group.getEditors()[1], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input3); assert.equal(events.closed[0].editor, input1); assert.equal(group.count, 2); @@ -682,7 +686,7 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, input3); assert.equal(group.count, 1); // pinning replaced the preview - assert.equal(group.getEditors()[0], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input3); assert.equal(events.closed[1].editor, input2); assert.equal(group.count, 1); }); @@ -704,7 +708,7 @@ suite('Workbench editor groups', () => { group.openEditor(input5, { pinned: true, active: true }); assert.equal(group.activeEditor, input5); - assert.equal(group.getEditors(true)[0], input5); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0], input5); assert.equal(group.count, 5); group.closeEditor(input5); @@ -763,7 +767,7 @@ suite('Workbench editor groups', () => { group.openEditor(input5, { pinned: true, active: true }); assert.equal(group.activeEditor, input5); - assert.equal(group.getEditors(true)[0], input5); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0], input5); assert.equal(group.count, 5); group.closeEditor(input5); @@ -810,8 +814,8 @@ suite('Workbench editor groups', () => { group.moveEditor(input1, 1); assert.equal(events.moved[0], input1); - assert.equal(group.getEditors()[0], input2); - assert.equal(group.getEditors()[1], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input1); group.setActive(input1); group.openEditor(input3, { pinned: true, active: true }); @@ -821,20 +825,20 @@ suite('Workbench editor groups', () => { group.moveEditor(input4, 0); assert.equal(events.moved[1], input4); - assert.equal(group.getEditors()[0], input4); - assert.equal(group.getEditors()[1], input2); - assert.equal(group.getEditors()[2], input1); - assert.equal(group.getEditors()[3], input3); - assert.equal(group.getEditors()[4], input5); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input4); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[3], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[4], input5); group.moveEditor(input4, 3); group.moveEditor(input2, 1); - assert.equal(group.getEditors()[0], input1); - assert.equal(group.getEditors()[1], input2); - assert.equal(group.getEditors()[2], input3); - assert.equal(group.getEditors()[3], input4); - assert.equal(group.getEditors()[4], input5); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[3], input4); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[4], input5); }); test('Multiple Editors - move editor across groups', function () { @@ -854,9 +858,9 @@ suite('Workbench editor groups', () => { group1.openEditor(g2_input1, { active: true, pinned: true, index: 1 }); assert.equal(group1.count, 3); - assert.equal(group1.getEditors()[0], g1_input1); - assert.equal(group1.getEditors()[1], g2_input1); - assert.equal(group1.getEditors()[2], g1_input2); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[0], g1_input1); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[1], g2_input1); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[2], g1_input2); }); test('Multiple Editors - move editor across groups (input already exists in group 1)', function () { @@ -878,9 +882,9 @@ suite('Workbench editor groups', () => { group1.openEditor(g2_input1, { active: true, pinned: true, index: 0 }); assert.equal(group1.count, 3); - assert.equal(group1.getEditors()[0], g1_input2); - assert.equal(group1.getEditors()[1], g1_input1); - assert.equal(group1.getEditors()[2], g1_input3); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[0], g1_input2); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[1], g1_input1); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[2], g1_input3); }); test('Multiple Editors - Pinned & Non Active', function () { @@ -890,24 +894,24 @@ suite('Workbench editor groups', () => { group.openEditor(input1); assert.equal(group.activeEditor, input1); assert.equal(group.previewEditor, input1); - assert.equal(group.getEditors()[0], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); assert.equal(group.count, 1); const input2 = input(); group.openEditor(input2, { pinned: true, active: false }); assert.equal(group.activeEditor, input1); assert.equal(group.previewEditor, input1); - assert.equal(group.getEditors()[0], input1); - assert.equal(group.getEditors()[1], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2); assert.equal(group.count, 2); const input3 = input(); group.openEditor(input3, { pinned: true, active: false }); assert.equal(group.activeEditor, input1); assert.equal(group.previewEditor, input1); - assert.equal(group.getEditors()[0], input1); - assert.equal(group.getEditors()[1], input3); - assert.equal(group.getEditors()[2], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input2); assert.equal(group.isPinned(input1), false); assert.equal(group.isPinned(input2), true); assert.equal(group.isPinned(input3), true); @@ -947,9 +951,9 @@ suite('Workbench editor groups', () => { group.closeEditors(group.activeEditor!, CloseDirection.LEFT); assert.equal(group.activeEditor, input3); assert.equal(group.count, 3); - assert.equal(group.getEditors()[0], input3); - assert.equal(group.getEditors()[1], input4); - assert.equal(group.getEditors()[2], input5); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input4); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input5); group.closeAllEditors(); group.openEditor(input1, { active: true, pinned: true }); @@ -964,9 +968,9 @@ suite('Workbench editor groups', () => { group.closeEditors(group.activeEditor!, CloseDirection.RIGHT); assert.equal(group.activeEditor, input3); assert.equal(group.count, 3); - assert.equal(group.getEditors()[0], input1); - assert.equal(group.getEditors()[1], input2); - assert.equal(group.getEditors()[2], input3); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], input1); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], input2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[2], input3); }); test('Multiple Editors - real user example', function () { @@ -978,7 +982,7 @@ suite('Workbench editor groups', () => { assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); - assert.equal(group.getEditors()[0], indexHtml); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], indexHtml); assert.equal(group.count, 1); // /index.html/ -> /index.html/ @@ -987,7 +991,7 @@ suite('Workbench editor groups', () => { assert.equal(openedEditor, indexHtml); assert.equal(group.activeEditor, indexHtml); assert.equal(group.previewEditor, indexHtml); - assert.equal(group.getEditors()[0], indexHtml); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], indexHtml); assert.equal(group.count, 1); // /index.html/ -> /style.css/ @@ -996,7 +1000,7 @@ suite('Workbench editor groups', () => { assert.equal(openedEditor, styleCss); assert.equal(group.activeEditor, styleCss); assert.equal(group.previewEditor, styleCss); - assert.equal(group.getEditors()[0], styleCss); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], styleCss); assert.equal(group.count, 1); // /style.css/ -> [/style.css/, test.js] @@ -1007,8 +1011,8 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor, testJs); assert.equal(group.isPreview(styleCss), true); assert.equal(group.isPinned(testJs), true); - assert.equal(group.getEditors()[0], styleCss); - assert.equal(group.getEditors()[1], testJs); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], styleCss); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], testJs); assert.equal(group.count, 2); // [/style.css/, test.js] -> [test.js, /index.html/] @@ -1018,8 +1022,8 @@ suite('Workbench editor groups', () => { assert.equal(group.previewEditor, indexHtml2); assert.equal(group.isPreview(indexHtml2), true); assert.equal(group.isPinned(testJs), true); - assert.equal(group.getEditors()[0], testJs); - assert.equal(group.getEditors()[1], indexHtml2); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[0], testJs); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], indexHtml2); assert.equal(group.count, 2); // make test.js active @@ -1056,9 +1060,9 @@ suite('Workbench editor groups', () => { group.openEditor(otherTs, { active: true }); assert.equal(group.count, 3); assert.equal(group.activeEditor, otherTs); - assert.ok(group.getEditors()[0].matches(testJs)); - assert.equal(group.getEditors()[1], otherTs); - assert.ok(group.getEditors()[2].matches(indexHtml)); + assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(testJs)); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], otherTs); + assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[2].matches(indexHtml)); // make index.html active const indexHtml4 = input('index.html'); @@ -1069,20 +1073,20 @@ suite('Workbench editor groups', () => { group.closeEditor(indexHtml); assert.equal(group.count, 2); assert.equal(group.activeEditor, otherTs); - assert.ok(group.getEditors()[0].matches(testJs)); - assert.equal(group.getEditors()[1], otherTs); + assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(testJs)); + assert.equal(group.getEditors(EditorsOrder.SEQUENTIAL)[1], otherTs); // [test.js, /other.ts/] -> [test.js] group.closeEditor(otherTs); assert.equal(group.count, 1); assert.equal(group.activeEditor, testJs); - assert.ok(group.getEditors()[0].matches(testJs)); + assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(testJs)); // [test.js] -> /test.js/ group.unpin(testJs); assert.equal(group.count, 1); assert.equal(group.activeEditor, testJs); - assert.ok(group.getEditors()[0].matches(testJs)); + assert.ok(group.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(testJs)); assert.equal(group.isPinned(testJs), false); assert.equal(group.isPreview(testJs), true); @@ -1169,13 +1173,13 @@ suite('Workbench editor groups', () => { assert.equal(group1.previewEditor!.matches(g1_input2), true); assert.equal(group2.previewEditor!.matches(g2_input2), true); - assert.equal(group1.getEditors(true)[0].matches(g1_input2), true); - assert.equal(group1.getEditors(true)[1].matches(g1_input3), true); - assert.equal(group1.getEditors(true)[2].matches(g1_input1), true); + assert.equal(group1.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].matches(g1_input2), true); + assert.equal(group1.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(g1_input3), true); + assert.equal(group1.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(g1_input1), true); - assert.equal(group2.getEditors(true)[0].matches(g2_input1), true); - assert.equal(group2.getEditors(true)[1].matches(g2_input3), true); - assert.equal(group2.getEditors(true)[2].matches(g2_input2), true); + assert.equal(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].matches(g2_input1), true); + assert.equal(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(g2_input3), true); + assert.equal(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(g2_input2), true); // Create model again - should load from storage group1 = inst.createInstance(EditorGroup, group1.serialize()); @@ -1188,13 +1192,13 @@ suite('Workbench editor groups', () => { assert.equal(group1.previewEditor!.matches(g1_input2), true); assert.equal(group2.previewEditor!.matches(g2_input2), true); - assert.equal(group1.getEditors(true)[0].matches(g1_input2), true); - assert.equal(group1.getEditors(true)[1].matches(g1_input3), true); - assert.equal(group1.getEditors(true)[2].matches(g1_input1), true); + assert.equal(group1.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].matches(g1_input2), true); + assert.equal(group1.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(g1_input3), true); + assert.equal(group1.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(g1_input1), true); - assert.equal(group2.getEditors(true)[0].matches(g2_input1), true); - assert.equal(group2.getEditors(true)[1].matches(g2_input3), true); - assert.equal(group2.getEditors(true)[2].matches(g2_input2), true); + assert.equal(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].matches(g2_input1), true); + assert.equal(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(g2_input3), true); + assert.equal(group2.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(g2_input2), true); }); test('Single group, multiple editors - persist (some not persistable)', function () { @@ -1226,9 +1230,9 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor!.matches(nonSerializableInput2), true); assert.equal(group.previewEditor!.matches(nonSerializableInput2), true); - assert.equal(group.getEditors(true)[0].matches(nonSerializableInput2), true); - assert.equal(group.getEditors(true)[1].matches(serializableInput2), true); - assert.equal(group.getEditors(true)[2].matches(serializableInput1), true); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].matches(nonSerializableInput2), true); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(serializableInput2), true); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[2].matches(serializableInput1), true); // Create model again - should load from storage group = inst.createInstance(EditorGroup, group.serialize()); @@ -1237,8 +1241,8 @@ suite('Workbench editor groups', () => { assert.equal(group.activeEditor!.matches(serializableInput2), true); assert.equal(group.previewEditor, null); - assert.equal(group.getEditors(true)[0].matches(serializableInput2), true); - assert.equal(group.getEditors(true)[1].matches(serializableInput1), true); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[0].matches(serializableInput2), true); + assert.equal(group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)[1].matches(serializableInput1), true); }); test('Multiple groups, multiple editors - persist (some not persistable, causes empty group)', function () { @@ -1273,8 +1277,8 @@ suite('Workbench editor groups', () => { group2 = inst.createInstance(EditorGroup, group2.serialize()); assert.equal(group1.count, 2); - assert.equal(group1.getEditors()[0].matches(serializableInput1), true); - assert.equal(group1.getEditors()[1].matches(serializableInput2), true); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[0].matches(serializableInput1), true); + assert.equal(group1.getEditors(EditorsOrder.SEQUENTIAL)[1].matches(serializableInput2), true); }); test('Multiple Editors - Editor Dispose', function () { diff --git a/src/vs/workbench/test/common/editor/editorModel.test.ts b/src/vs/workbench/test/common/editor/editorModel.test.ts index 5b368da847a..c442c63736b 100644 --- a/src/vs/workbench/test/common/editor/editorModel.test.ts +++ b/src/vs/workbench/test/common/editor/editorModel.test.ts @@ -21,7 +21,7 @@ import { TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTe class MyEditorModel extends EditorModel { } class MyTextEditorModel extends BaseTextEditorModel { - public createTextEditorModel(value: ITextBufferFactory, resource?: URI, preferredMode?: string) { + createTextEditorModel(value: ITextBufferFactory, resource?: URI, preferredMode?: string) { return super.createTextEditorModel(value, resource, preferredMode); } diff --git a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts index bc50a3dd00a..fb419d8e700 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTreeViews.test.ts @@ -75,7 +75,7 @@ suite('ExtHostTreeView', function () { rpcProtocol, new NullLogService() ), new NullLogService()); - onDidChangeTreeNode = new Emitter<{ key: string }>(); + onDidChangeTreeNode = new Emitter<{ key: string } | undefined>(); onDidChangeTreeNodeWithId = new Emitter<{ key: string }>(); testObject.createTreeView('testNodeTreeProvider', { treeDataProvider: aNodeTreeDataProvider() }, { enableProposedApi: true } as IExtensionDescription); testObject.createTreeView('testNodeWithIdTreeProvider', { treeDataProvider: aNodeWithIdTreeDataProvider() }, { enableProposedApi: true } as IExtensionDescription); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts index c1132e64713..db13edd9507 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadConfiguration.test.ts @@ -63,7 +63,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', undefined); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -72,7 +72,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -81,7 +81,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', undefined); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -90,7 +90,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', undefined); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -99,7 +99,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -108,7 +108,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -117,7 +117,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', undefined); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.window', 'value', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -126,7 +126,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(null, 'extHostConfiguration.resource', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE_FOLDER, target.args[0][3]); }); @@ -135,7 +135,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(ConfigurationTarget.USER, 'extHostConfiguration.window', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(ConfigurationTarget.USER, 'extHostConfiguration.window', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.USER, target.args[0][3]); }); @@ -144,7 +144,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(ConfigurationTarget.WORKSPACE, 'extHostConfiguration.window', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(ConfigurationTarget.WORKSPACE, 'extHostConfiguration.window', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -153,7 +153,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$updateConfigurationOption(ConfigurationTarget.WORKSPACE_FOLDER, 'extHostConfiguration.window', 'value', URI.file('abc')); + testObject.$updateConfigurationOption(ConfigurationTarget.WORKSPACE_FOLDER, 'extHostConfiguration.window', 'value', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE_FOLDER, target.args[0][3]); }); @@ -162,7 +162,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', undefined); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -171,7 +171,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', URI.file('abc')); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -180,7 +180,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', undefined); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -189,7 +189,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', undefined); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -198,7 +198,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', URI.file('abc')); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -207,7 +207,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', URI.file('abc')); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -216,7 +216,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.FOLDER }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', undefined); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.window', undefined, undefined); assert.equal(ConfigurationTarget.WORKSPACE, target.args[0][3]); }); @@ -225,7 +225,7 @@ suite('MainThreadConfiguration', function () { instantiationService.stub(IWorkspaceContextService, { getWorkbenchState: () => WorkbenchState.WORKSPACE }); const testObject: MainThreadConfiguration = instantiationService.createInstance(MainThreadConfiguration, SingleProxyRPCProtocol(proxy)); - testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', URI.file('abc')); + testObject.$removeConfigurationOption(null, 'extHostConfiguration.resource', { resource: URI.file('abc') }, undefined); assert.equal(ConfigurationTarget.WORKSPACE_FOLDER, target.args[0][3]); }); diff --git a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts index d83eae09e5d..5c3e43ffb5b 100644 --- a/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/quickopen.perf.integrationTest.ts @@ -49,7 +49,7 @@ namespace Timer { } } -declare var __dirname: string; +// declare var __dirname: string; // Checkout sources to run against: // git clone --separate-git-dir=testGit --no-checkout --single-branch https://chromium.googlesource.com/chromium/src testWorkspace diff --git a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts index ca92d4799fd..e79af8a3d3d 100644 --- a/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts +++ b/src/vs/workbench/test/electron-browser/textsearch.perf.integrationTest.ts @@ -36,7 +36,7 @@ import { ITextResourcePropertiesService } from 'vs/editor/common/services/textRe import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/platform/telemetry/common/gdprTypings'; import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService'; -declare var __dirname: string; +// declare var __dirname: string; // Checkout sources to run against: // git clone --separate-git-dir=testGit --no-checkout --single-branch https://chromium.googlesource.com/chromium/src testWorkspace diff --git a/src/vs/workbench/test/workbenchTestServices.ts b/src/vs/workbench/test/workbenchTestServices.ts index f0ca3eed438..5863c631b1c 100644 --- a/src/vs/workbench/test/workbenchTestServices.ts +++ b/src/vs/workbench/test/workbenchTestServices.ts @@ -11,7 +11,7 @@ import * as resources from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; -import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, EditorInput, IFileEditorInput, GroupIdentifier, ISaveOptions } from 'vs/workbench/common/editor'; +import { IEditorInputWithOptions, CloseDirection, IEditorIdentifier, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInput, IEditor, IEditorCloseEvent, IEditorPartOptions, IRevertOptions, GroupIdentifier, EditorInput, EditorOptions, EditorsOrder } from 'vs/workbench/common/editor'; import { IEditorOpeningEvent, EditorServiceImpl, IEditorGroupView, IEditorGroupsAccessor } from 'vs/workbench/browser/parts/editor/editor'; import { Event, Emitter } from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; @@ -20,7 +20,7 @@ import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configur import { IWorkbenchLayoutService, Parts, Position as PartPosition } from 'vs/workbench/services/layout/browser/layoutService'; import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IEditorOptions, IResourceInput, IEditorModel } from 'vs/platform/editor/common/editor'; +import { IEditorOptions, IResourceInput } from 'vs/platform/editor/common/editor'; import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; import { IWorkspaceContextService, IWorkspace as IWorkbenchWorkspace, WorkbenchState, IWorkspaceFolder, IWorkspaceFoldersChangeEvent, Workspace } from 'vs/platform/workspace/common/workspace'; import { ILifecycleService, BeforeShutdownEvent, ShutdownReason, StartupKind, LifecyclePhase, WillShutdownEvent } from 'vs/platform/lifecycle/common/lifecycle'; @@ -33,7 +33,7 @@ import { ITextFileStreamContent, ITextFileService, IResourceEncoding, IReadTextF import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IHistoryService } from 'vs/workbench/services/history/common/history'; -import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor, ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService'; import { MenuBarVisibility, IWindowConfiguration, IWindowOpenable, IOpenWindowOptions, IOpenEmptyWindowOptions, IOpenedWindow } from 'vs/platform/windows/common/windows'; import { TestWorkspace } from 'vs/platform/workspace/test/common/testWorkspace'; @@ -56,8 +56,8 @@ import { IExtensionService, NullExtensionService } from 'vs/workbench/services/e import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IDecorationsService, IResourceDecorationChangeEvent, IDecoration, IDecorationData, IDecorationsProvider } from 'vs/workbench/services/decorations/browser/decorations'; import { IDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle'; -import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, EditorsOrder, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; -import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions, IRevertAllEditorsOptions } from 'vs/workbench/services/editor/common/editorService'; +import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupsArrangement, GroupDirection, IAddGroupOptions, IMergeGroupOptions, IMoveEditorOptions, ICopyEditorOptions, IEditorReplacement, IGroupChangeEvent, IFindGroupScope, EditorGroupLayout, ICloseEditorOptions } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { IEditorService, IOpenEditorOverrideHandler, IVisibleEditor, ISaveEditorsOptions, IRevertAllEditorsOptions, IResourceEditor } from 'vs/workbench/services/editor/common/editorService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon'; @@ -94,6 +94,7 @@ import { IDialogMainService } from 'vs/platform/dialogs/electron-main/dialogs'; import { find } from 'vs/base/common/arrays'; import { WorkingCopyService, IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IFilesConfigurationService, FilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IAccessibilityService, AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; export function createFileInput(instantiationService: IInstantiationService, resource: URI): FileEditorInput { return instantiationService.createInstance(FileEditorInput, resource, undefined, undefined); @@ -102,7 +103,7 @@ export function createFileInput(instantiationService: IInstantiationService, res export const TestEnvironmentService = new NativeWorkbenchEnvironmentService(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0); export class TestContextService implements IWorkspaceContextService { - public _serviceBrand: undefined; + _serviceBrand: undefined; private workspace: Workspace; private options: any; @@ -119,23 +120,23 @@ export class TestContextService implements IWorkspaceContextService { this._onDidChangeWorkbenchState = new Emitter(); } - public get onDidChangeWorkspaceName(): Event { + get onDidChangeWorkspaceName(): Event { return this._onDidChangeWorkspaceName.event; } - public get onDidChangeWorkspaceFolders(): Event { + get onDidChangeWorkspaceFolders(): Event { return this._onDidChangeWorkspaceFolders.event; } - public get onDidChangeWorkbenchState(): Event { + get onDidChangeWorkbenchState(): Event { return this._onDidChangeWorkbenchState.event; } - public getFolders(): IWorkspaceFolder[] { + getFolders(): IWorkspaceFolder[] { return this.workspace ? this.workspace.folders : []; } - public getWorkbenchState(): WorkbenchState { + getWorkbenchState(): WorkbenchState { if (this.workspace.configuration) { return WorkbenchState.WORKSPACE; } @@ -151,27 +152,27 @@ export class TestContextService implements IWorkspaceContextService { return Promise.resolve(this.getWorkspace()); } - public getWorkspace(): IWorkbenchWorkspace { + getWorkspace(): IWorkbenchWorkspace { return this.workspace; } - public getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { + getWorkspaceFolder(resource: URI): IWorkspaceFolder | null { return this.workspace.getFolder(resource); } - public setWorkspace(workspace: any): void { + setWorkspace(workspace: any): void { this.workspace = workspace; } - public getOptions() { + getOptions() { return this.options; } - public updateOptions() { + updateOptions() { } - public isInsideWorkspace(resource: URI): boolean { + isInsideWorkspace(resource: URI): boolean { if (resource && this.workspace) { return resources.isEqualOrParent(resource, this.workspace.folders[0].uri); } @@ -179,17 +180,17 @@ export class TestContextService implements IWorkspaceContextService { return false; } - public toResource(workspaceRelativePath: string): URI { + toResource(workspaceRelativePath: string): URI { return URI.file(join('C:\\', workspaceRelativePath)); } - public isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { + isCurrentWorkspace(workspaceIdentifier: ISingleFolderWorkspaceIdentifier | IWorkspaceIdentifier): boolean { return isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && resources.isEqual(this.workspace.folders[0].uri, workspaceIdentifier); } } export class TestTextFileService extends NativeTextFileService { - public cleanupBackupsBeforeShutdownCalled!: boolean; + cleanupBackupsBeforeShutdownCalled!: boolean; private promptPath!: URI; private resolveTextContentError!: FileOperationError | null; @@ -236,37 +237,36 @@ export class TestTextFileService extends NativeTextFileService { ); } - public setPromptPath(path: URI): void { + setPromptPath(path: URI): void { this.promptPath = path; } - public setResolveTextContentErrorOnce(error: FileOperationError): void { + setResolveTextContentErrorOnce(error: FileOperationError): void { this.resolveTextContentError = error; } - public readStream(resource: URI, options?: IReadTextFileOptions): Promise { + async readStream(resource: URI, options?: IReadTextFileOptions): Promise { if (this.resolveTextContentError) { const error = this.resolveTextContentError; this.resolveTextContentError = null; - return Promise.reject(error); + throw error; } - return this.fileService.readFileStream(resource, options).then(async (content): Promise => { - return { - resource: content.resource, - name: content.name, - mtime: content.mtime, - ctime: content.ctime, - etag: content.etag, - encoding: 'utf8', - value: await createTextBufferFactoryFromStream(content.value), - size: 10 - }; - }); + const content = await this.fileService.readFileStream(resource, options); + return { + resource: content.resource, + name: content.name, + mtime: content.mtime, + ctime: content.ctime, + etag: content.etag, + encoding: 'utf8', + value: await createTextBufferFactoryFromStream(content.value), + size: 10 + }; } - public promptForPath(_resource: URI, _defaultPath: URI): Promise { + promptForPath(_resource: URI, _defaultPath: URI): Promise { return Promise.resolve(this.promptPath); } @@ -276,7 +276,11 @@ export class TestTextFileService extends NativeTextFileService { } } -export function workbenchInstantiationService(): IInstantiationService { +export interface ITestInstantiationService extends IInstantiationService { + stub(service: ServiceIdentifier, ctor: any): T; +} + +export function workbenchInstantiationService(): ITestInstantiationService { let instantiationService = new TestInstantiationService(new ServiceCollection([ILifecycleService, new TestLifecycleService()])); instantiationService.stub(IEnvironmentService, TestEnvironmentService); const contextKeyService = instantiationService.createInstance(MockContextKeyService); @@ -291,6 +295,7 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(IStorageService, new TestStorageService()); instantiationService.stub(IWorkbenchLayoutService, new TestLayoutService()); instantiationService.stub(IDialogService, new TestDialogService()); + instantiationService.stub(IAccessibilityService, new TestAccessibilityService()); instantiationService.stub(IFileDialogService, new TestFileDialogService()); instantiationService.stub(IElectronService, new TestElectronService()); instantiationService.stub(IModeService, instantiationService.createInstance(ModeServiceImpl)); @@ -311,9 +316,10 @@ export function workbenchInstantiationService(): IInstantiationService { instantiationService.stub(ITextModelService, instantiationService.createInstance(TextModelResolverService)); instantiationService.stub(IThemeService, new TestThemeService()); instantiationService.stub(ILogService, new NullLogService()); - instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService([new TestEditorGroupView(0)])); + const editorGroupService = new TestEditorGroupsService([new TestEditorGroupView(0)]); + instantiationService.stub(IEditorGroupsService, editorGroupService); instantiationService.stub(ILabelService, instantiationService.createInstance(LabelService)); - const editorService = new TestEditorService(); + const editorService = new TestEditorService(editorGroupService); instantiationService.stub(IEditorService, editorService); instantiationService.stub(ICodeEditorService, new TestCodeEditorService()); instantiationService.stub(IViewletService, new TestViewletService()); @@ -322,6 +328,17 @@ export function workbenchInstantiationService(): IInstantiationService { return instantiationService; } +export class TestAccessibilityService implements IAccessibilityService { + + _serviceBrand: undefined; + + onDidChangeAccessibilitySupport = Event.None; + + alwaysUnderlineAccessKeys(): Promise { return Promise.resolve(false); } + getAccessibilitySupport(): AccessibilitySupport { return AccessibilitySupport.Unknown; } + setAccessibilitySupport(accessibilitySupport: AccessibilitySupport): void { } +} + export class TestDecorationsService implements IDecorationsService { _serviceBrand: undefined; onDidChangeDecorations: Event = Event.None; @@ -333,7 +350,7 @@ export class TestExtensionService extends NullExtensionService { } export class TestMenuService implements IMenuService { - public _serviceBrand: undefined; + _serviceBrand: undefined; createMenu(_id: MenuId, _scopedKeybindingService: IContextKeyService): IMenu { return { @@ -357,10 +374,9 @@ export class TestHistoryService implements IHistoryService { remove(_input: IEditorInput | IResourceInput): void { } clear(): void { } clearRecentlyOpened(): void { } - getHistory(): Array { return []; } + getHistory(): ReadonlyArray { return []; } openNextRecentlyUsedEditor(group?: GroupIdentifier): void { } openPreviouslyUsedEditor(group?: GroupIdentifier): void { } - getMostRecentlyUsedOpenEditors(): Array { return []; } getLastActiveWorkspaceRoot(_schemeFilter: string): URI | undefined { return this.root; } getLastActiveFile(_schemeFilter: string): URI | undefined { return undefined; } openLastEditLocation(): void { } @@ -368,68 +384,68 @@ export class TestHistoryService implements IHistoryService { export class TestDialogService implements IDialogService { - public _serviceBrand: undefined; + _serviceBrand: undefined; - public confirm(_confirmation: IConfirmation): Promise { + confirm(_confirmation: IConfirmation): Promise { return Promise.resolve({ confirmed: false }); } - public show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { + show(_severity: Severity, _message: string, _buttons: string[], _options?: IDialogOptions): Promise { return Promise.resolve({ choice: 0 }); } - public about(): Promise { + about(): Promise { return Promise.resolve(); } } export class TestFileDialogService implements IFileDialogService { - public _serviceBrand: undefined; + _serviceBrand: undefined; private confirmResult!: ConfirmResult; - public defaultFilePath(_schemeFilter?: string): URI | undefined { + defaultFilePath(_schemeFilter?: string): URI | undefined { return undefined; } - public defaultFolderPath(_schemeFilter?: string): URI | undefined { + defaultFolderPath(_schemeFilter?: string): URI | undefined { return undefined; } - public defaultWorkspacePath(_schemeFilter?: string): URI | undefined { + defaultWorkspacePath(_schemeFilter?: string): URI | undefined { return undefined; } - public pickFileFolderAndOpen(_options: IPickAndOpenOptions): Promise { + pickFileFolderAndOpen(_options: IPickAndOpenOptions): Promise { return Promise.resolve(0); } - public pickFileAndOpen(_options: IPickAndOpenOptions): Promise { + pickFileAndOpen(_options: IPickAndOpenOptions): Promise { return Promise.resolve(0); } - public pickFolderAndOpen(_options: IPickAndOpenOptions): Promise { + pickFolderAndOpen(_options: IPickAndOpenOptions): Promise { return Promise.resolve(0); } - public pickWorkspaceAndOpen(_options: IPickAndOpenOptions): Promise { + pickWorkspaceAndOpen(_options: IPickAndOpenOptions): Promise { return Promise.resolve(0); } - public pickFileToSave(_options: ISaveDialogOptions): Promise { + pickFileToSave(_options: ISaveDialogOptions): Promise { return Promise.resolve(undefined); } - public showSaveDialog(_options: ISaveDialogOptions): Promise { + showSaveDialog(_options: ISaveDialogOptions): Promise { return Promise.resolve(undefined); } - public showOpenDialog(_options: IOpenDialogOptions): Promise { + showOpenDialog(_options: IOpenDialogOptions): Promise { return Promise.resolve(undefined); } - public setConfirmResult(result: ConfirmResult): void { + setConfirmResult(result: ConfirmResult): void { this.confirmResult = result; } - public showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { + showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise { return Promise.resolve(this.confirmResult); } } export class TestLayoutService implements IWorkbenchLayoutService { - public _serviceBrand: undefined; + _serviceBrand: undefined; dimension: IDimension = { width: 800, height: 600 }; @@ -445,27 +461,27 @@ export class TestLayoutService implements IWorkbenchLayoutService { private readonly _onMenubarVisibilityChange = new Emitter(); - public get onMenubarVisibilityChange(): Event { + get onMenubarVisibilityChange(): Event { return this._onMenubarVisibilityChange.event; } - public isRestored(): boolean { + isRestored(): boolean { return true; } - public hasFocus(_part: Parts): boolean { + hasFocus(_part: Parts): boolean { return false; } - public hasWindowBorder(): boolean { + hasWindowBorder(): boolean { return false; } - public getWindowBorderRadius(): string | undefined { + getWindowBorderRadius(): string | undefined { return undefined; } - public isVisible(_part: Parts): boolean { + isVisible(_part: Parts): boolean { return true; } @@ -473,93 +489,93 @@ export class TestLayoutService implements IWorkbenchLayoutService { return new Dimension(0, 0); } - public getContainer(_part: Parts): HTMLElement { + getContainer(_part: Parts): HTMLElement { return null!; } - public isTitleBarHidden(): boolean { + isTitleBarHidden(): boolean { return false; } - public getTitleBarOffset(): number { + getTitleBarOffset(): number { return 0; } - public isStatusBarHidden(): boolean { + isStatusBarHidden(): boolean { return false; } - public isActivityBarHidden(): boolean { + isActivityBarHidden(): boolean { return false; } - public setActivityBarHidden(_hidden: boolean): void { } + setActivityBarHidden(_hidden: boolean): void { } - public isSideBarHidden(): boolean { + isSideBarHidden(): boolean { return false; } - public setEditorHidden(_hidden: boolean): Promise { return Promise.resolve(); } + setEditorHidden(_hidden: boolean): Promise { return Promise.resolve(); } - public setSideBarHidden(_hidden: boolean): Promise { return Promise.resolve(); } + setSideBarHidden(_hidden: boolean): Promise { return Promise.resolve(); } - public isPanelHidden(): boolean { + isPanelHidden(): boolean { return false; } - public setPanelHidden(_hidden: boolean): Promise { return Promise.resolve(); } + setPanelHidden(_hidden: boolean): Promise { return Promise.resolve(); } - public toggleMaximizedPanel(): void { } + toggleMaximizedPanel(): void { } - public isPanelMaximized(): boolean { + isPanelMaximized(): boolean { return false; } - public getMenubarVisibility(): MenuBarVisibility { + getMenubarVisibility(): MenuBarVisibility { throw new Error('not implemented'); } - public getSideBarPosition() { + getSideBarPosition() { return 0; } - public getPanelPosition() { + getPanelPosition() { return 0; } - public setPanelPosition(_position: PartPosition): Promise { + setPanelPosition(_position: PartPosition): Promise { return Promise.resolve(); } - public addClass(_clazz: string): void { } - public removeClass(_clazz: string): void { } + addClass(_clazz: string): void { } + removeClass(_clazz: string): void { } - public getMaximumEditorDimensions(): Dimension { throw new Error('not implemented'); } + getMaximumEditorDimensions(): Dimension { throw new Error('not implemented'); } - public getWorkbenchContainer(): HTMLElement { throw new Error('not implemented'); } - public getWorkbenchElement(): HTMLElement { throw new Error('not implemented'); } + getWorkbenchContainer(): HTMLElement { throw new Error('not implemented'); } + getWorkbenchElement(): HTMLElement { throw new Error('not implemented'); } - public toggleZenMode(): void { } + toggleZenMode(): void { } - public isEditorLayoutCentered(): boolean { return false; } - public centerEditorLayout(_active: boolean): void { } + isEditorLayoutCentered(): boolean { return false; } + centerEditorLayout(_active: boolean): void { } - public resizePart(_part: Parts, _sizeChange: number): void { } + resizePart(_part: Parts, _sizeChange: number): void { } - public registerPart(part: Part): void { } + registerPart(part: Part): void { } isWindowMaximized() { return false; } - public updateWindowMaximizedState(maximized: boolean): void { } + updateWindowMaximizedState(maximized: boolean): void { } } let activeViewlet: Viewlet = {} as any; export class TestViewletService implements IViewletService { - public _serviceBrand: undefined; + _serviceBrand: undefined; onDidViewletRegisterEmitter = new Emitter(); onDidViewletDeregisterEmitter = new Emitter(); @@ -571,93 +587,93 @@ export class TestViewletService implements IViewletService { onDidViewletOpen = this.onDidViewletOpenEmitter.event; onDidViewletClose = this.onDidViewletCloseEmitter.event; - public openViewlet(id: string, focus?: boolean): Promise { + openViewlet(id: string, focus?: boolean): Promise { return Promise.resolve(undefined); } - public getViewlets(): ViewletDescriptor[] { + getViewlets(): ViewletDescriptor[] { return []; } - public getAllViewlets(): ViewletDescriptor[] { + getAllViewlets(): ViewletDescriptor[] { return []; } - public getActiveViewlet(): IViewlet { + getActiveViewlet(): IViewlet { return activeViewlet; } - public dispose() { + dispose() { } - public getDefaultViewletId(): string { + getDefaultViewletId(): string { return 'workbench.view.explorer'; } - public getViewlet(id: string): ViewletDescriptor | undefined { + getViewlet(id: string): ViewletDescriptor | undefined { return undefined; } - public getProgressIndicator(id: string) { + getProgressIndicator(id: string) { return undefined; } - public hideActiveViewlet(): void { } + hideActiveViewlet(): void { } - public getLastActiveViewletId(): string { + getLastActiveViewletId(): string { return undefined!; } } export class TestPanelService implements IPanelService { - public _serviceBrand: undefined; + _serviceBrand: undefined; onDidPanelOpen = new Emitter<{ panel: IPanel, focus: boolean }>().event; onDidPanelClose = new Emitter().event; - public openPanel(id: string, focus?: boolean): undefined { + openPanel(id: string, focus?: boolean): undefined { return undefined; } - public getPanel(id: string): any { + getPanel(id: string): any { return activeViewlet; } - public getPanels() { + getPanels() { return []; } - public getPinnedPanels() { + getPinnedPanels() { return []; } - public getActivePanel(): IViewlet { + getActivePanel(): IViewlet { return activeViewlet; } - public setPanelEnablement(id: string, enabled: boolean): void { } + setPanelEnablement(id: string, enabled: boolean): void { } - public dispose() { + dispose() { } - public showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable { + showActivity(panelId: string, badge: IBadge, clazz?: string): IDisposable { throw new Error('Method not implemented.'); } - public getProgressIndicator(id: string) { + getProgressIndicator(id: string) { return null!; } - public hideActivePanel(): void { } + hideActivePanel(): void { } - public getLastActivePanelId(): string { + getLastActivePanelId(): string { return undefined!; } } export class TestStorageService extends InMemoryStorageService { - readonly _onWillSaveState: Emitter = this._register(new Emitter()); - readonly onWillSaveState: Event = this._onWillSaveState.event; + readonly _onWillSaveState = this._register(new Emitter()); + readonly onWillSaveState = this._onWillSaveState.event; } export class TestEditorGroupsService implements IEditorGroupsService { @@ -673,6 +689,7 @@ export class TestEditorGroupsService implements IEditorGroupsService { onDidMoveGroup: Event = Event.None; onDidGroupIndexChange: Event = Event.None; onDidLayout: Event = Event.None; + onDidEditorPartOptionsChange = Event.None; orientation: any; whenRestored: Promise = Promise.resolve(undefined); @@ -876,54 +893,6 @@ export class TestEditorGroupAccessor implements IEditorGroupsAccessor { arrangeGroups(arrangement: GroupsArrangement, target?: number | IEditorGroupView | undefined): void { throw new Error('Method not implemented.'); } } -export class TestEditorInput extends EditorInput implements IFileEditorInput { - public gotDisposed = false; - public gotSaved = false; - public gotSavedAs = false; - public gotReverted = false; - public dirty = false; - private fails = false; - constructor(public resource: URI) { super(); } - - getTypeId() { return 'testEditorInputForEditorService'; } - resolve(): Promise { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); } - matches(other: TestEditorInput): boolean { return other && other.resource && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; } - setEncoding(encoding: string) { } - getEncoding() { return undefined; } - setPreferredEncoding(encoding: string) { } - setMode(mode: string) { } - setPreferredMode(mode: string) { } - getResource(): URI { return this.resource; } - setForceOpenAsBinary(): void { } - setFailToOpen(): void { - this.fails = true; - } - save(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - this.gotSaved = true; - return Promise.resolve(true); - } - saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise { - this.gotSavedAs = true; - return Promise.resolve(true); - } - revert(options?: IRevertOptions): Promise { - this.gotReverted = true; - this.gotSaved = false; - this.gotSavedAs = false; - return Promise.resolve(true); - } - isDirty(): boolean { - return this.dirty; - } - isReadonly(): boolean { - return false; - } - dispose(): void { - super.dispose(); - this.gotDisposed = true; - } -} - export class TestEditorService implements EditorServiceImpl { _serviceBrand: undefined; @@ -932,14 +901,21 @@ export class TestEditorService implements EditorServiceImpl { onDidVisibleEditorsChange: Event = Event.None; onDidCloseEditor: Event = Event.None; onDidOpenEditorFail: Event = Event.None; + onDidMostRecentlyActiveEditorsChange: Event = Event.None; activeControl!: IVisibleEditor; activeTextEditorWidget: any; activeEditor!: IEditorInput; editors: ReadonlyArray = []; + mostRecentlyActiveEditors: ReadonlyArray = []; visibleControls: ReadonlyArray = []; visibleTextEditorWidgets = []; visibleEditors: ReadonlyArray = []; + count = this.editors.length; + + constructor(private editorGroupService?: IEditorGroupsService) { } + + getEditors() { return []; } overrideOpenEditor(_handler: IOpenEditorOverrideHandler): IDisposable { return toDisposable(() => undefined); @@ -949,6 +925,14 @@ export class TestEditorService implements EditorServiceImpl { throw new Error('not implemented'); } + doResolveEditorOpenRequest(editor: IEditorInput | IResourceEditor): [IEditorGroup, EditorInput, EditorOptions | undefined] | undefined { + if (!this.editorGroupService) { + return undefined; + } + + return [this.editorGroupService.activeGroup, editor as EditorInput, undefined]; + } + openEditors(_editors: any, _group?: any): Promise { throw new Error('not implemented'); } @@ -992,12 +976,13 @@ export class TestEditorService implements EditorServiceImpl { export class TestFileService implements IFileService { - public _serviceBrand: undefined; + _serviceBrand: undefined; private readonly _onFileChanges: Emitter; private readonly _onAfterOperation: Emitter; readonly onWillActivateFileSystemProvider = Event.None; + readonly onDidChangeFileSystemProviderCapabilities = Event.None; readonly onError: Event = Event.None; private content = 'Hello Html'; @@ -1008,31 +993,31 @@ export class TestFileService implements IFileService { this._onAfterOperation = new Emitter(); } - public setContent(content: string): void { + setContent(content: string): void { this.content = content; } - public getContent(): string { + getContent(): string { return this.content; } - public getLastReadFileUri(): URI { + getLastReadFileUri(): URI { return this.lastReadFileUri; } - public get onFileChanges(): Event { + get onFileChanges(): Event { return this._onFileChanges.event; } - public fireFileChanges(event: FileChangesEvent): void { + fireFileChanges(event: FileChangesEvent): void { this._onFileChanges.fire(event); } - public get onAfterOperation(): Event { + get onAfterOperation(): Event { return this._onAfterOperation.event; } - public fireAfterOperation(event: FileOperationEvent): void { + fireAfterOperation(event: FileOperationEvent): void { this._onAfterOperation.fire(event); } @@ -1052,12 +1037,14 @@ export class TestFileService implements IFileService { }); } - resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise { - return Promise.all(toResolve.map(resourceAndOption => this.resolve(resourceAndOption.resource, resourceAndOption.options))).then(stats => stats.map(stat => ({ stat, success: true }))); + async resolveAll(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise { + const stats = await Promise.all(toResolve.map(resourceAndOption => this.resolve(resourceAndOption.resource, resourceAndOption.options))); + + return stats.map(stat => ({ stat, success: true })); } - exists(_resource: URI): Promise { - return Promise.resolve(true); + async exists(_resource: URI): Promise { + return true; } readFile(resource: URI, options?: IReadFileOptions | undefined): Promise { @@ -1102,11 +1089,12 @@ export class TestFileService implements IFileService { }); } - writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise { - return timeout(0).then(() => ({ + async writeFile(resource: URI, bufferOrReadable: VSBuffer | VSBufferReadable, options?: IWriteFileOptions): Promise { + await timeout(0); + + return ({ resource, etag: 'index.txt', - encoding: 'utf8', mtime: Date.now(), ctime: Date.now(), size: 42, @@ -1114,7 +1102,7 @@ export class TestFileService implements IFileService { isDirectory: false, isSymbolicLink: false, name: resources.basename(resource) - })); + }); } move(_source: URI, _target: URI, _overwrite?: boolean): Promise { @@ -1151,7 +1139,13 @@ export class TestFileService implements IFileService { return resource.scheme === 'file' || this.providers.has(resource.scheme); } - hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { return false; } + hasCapability(resource: URI, capability: FileSystemProviderCapabilities): boolean { + if (capability === FileSystemProviderCapabilities.PathCaseSensitive && isLinux) { + return true; + } + + return false; + } del(_resource: URI, _options?: { useTrash?: boolean, recursive?: boolean }): Promise { return Promise.resolve(); @@ -1170,66 +1164,65 @@ export class TestFileService implements IFileService { } export class TestBackupFileService implements IBackupFileService { - public _serviceBrand: undefined; + _serviceBrand: undefined; - public hasBackups(): Promise { + hasBackups(): Promise { return Promise.resolve(false); } - public hasBackup(_resource: URI): Promise { + hasBackup(_resource: URI): Promise { return Promise.resolve(false); } - public hasBackupSync(resource: URI, versionId?: number): boolean { + hasBackupSync(resource: URI, versionId?: number): boolean { return false; } - public loadBackupResource(resource: URI): Promise { - return this.hasBackup(resource).then(hasBackup => { - if (hasBackup) { - return this.toBackupResource(resource); - } + async loadBackupResource(resource: URI): Promise { + const hasBackup = await this.hasBackup(resource); + if (hasBackup) { + return this.toBackupResource(resource); + } - return undefined; - }); + return undefined; } - public registerResourceForBackup(_resource: URI): Promise { + registerResourceForBackup(_resource: URI): Promise { return Promise.resolve(); } - public deregisterResourceForBackup(_resource: URI): Promise { + deregisterResourceForBackup(_resource: URI): Promise { return Promise.resolve(); } - public toBackupResource(_resource: URI): URI { + toBackupResource(_resource: URI): URI { throw new Error('not implemented'); } - public backupResource(_resource: URI, _content: ITextSnapshot, versionId?: number, meta?: T): Promise { + backupResource(_resource: URI, _content: ITextSnapshot, versionId?: number, meta?: T): Promise { return Promise.resolve(); } - public getWorkspaceFileBackups(): Promise { + getWorkspaceFileBackups(): Promise { return Promise.resolve([]); } - public parseBackupContent(textBufferFactory: ITextBufferFactory): string { + parseBackupContent(textBufferFactory: ITextBufferFactory): string { const textBuffer = textBufferFactory.create(DefaultEndOfLine.LF); const lineCount = textBuffer.getLineCount(); const range = new Range(1, 1, lineCount, textBuffer.getLineLength(lineCount) + 1); return textBuffer.getValueInRange(range, EndOfLinePreference.TextDefined); } - public resolveBackupContent(_backup: URI): Promise> { + resolveBackupContent(_backup: URI): Promise> { throw new Error('not implemented'); } - public discardResourceBackup(_resource: URI): Promise { + discardResourceBackup(_resource: URI): Promise { return Promise.resolve(); } - public discardAllWorkspaceBackups(): Promise { + discardAllWorkspaceBackups(): Promise { return Promise.resolve(); } } @@ -1261,10 +1254,10 @@ export class TestCodeEditorService implements ICodeEditorService { export class TestLifecycleService implements ILifecycleService { - public _serviceBrand: undefined; + _serviceBrand: undefined; - public phase!: LifecyclePhase; - public startupKind!: StartupKind; + phase!: LifecyclePhase; + startupKind!: StartupKind; private readonly _onBeforeShutdown = new Emitter(); private readonly _onWillShutdown = new Emitter(); @@ -1274,26 +1267,26 @@ export class TestLifecycleService implements ILifecycleService { return Promise.resolve(); } - public fireShutdown(reason = ShutdownReason.QUIT): void { + fireShutdown(reason = ShutdownReason.QUIT): void { this._onWillShutdown.fire({ join: () => { }, reason }); } - public fireWillShutdown(event: BeforeShutdownEvent): void { + fireWillShutdown(event: BeforeShutdownEvent): void { this._onBeforeShutdown.fire(event); } - public get onBeforeShutdown(): Event { + get onBeforeShutdown(): Event { return this._onBeforeShutdown.event; } - public get onWillShutdown(): Event { + get onWillShutdown(): Event { return this._onWillShutdown.event; } - public get onShutdown(): Event { + get onShutdown(): Event { return this._onShutdown.event; } } @@ -1305,7 +1298,7 @@ export class TestTextResourceConfigurationService implements ITextResourceConfig constructor(private configurationService = new TestConfigurationService()) { } - public onDidChangeConfiguration() { + onDidChangeConfiguration() { return { dispose() { } }; } diff --git a/tslint.json b/tslint.json index 06adddd9c6e..9b5b07a5dd7 100644 --- a/tslint.json +++ b/tslint.json @@ -93,7 +93,7 @@ "target": "**/vs/base/node/**", "restrictions": [ "vs/nls", - "**/vs/base/{common,browser,node}/**", + "**/vs/base/{common,node}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -129,8 +129,8 @@ "target": "**/vs/base/parts/*/node/**", "restrictions": [ "vs/nls", - "**/vs/base/{common,browser,node}/**", - "**/vs/base/parts/*/{common,browser,node}/**", + "**/vs/base/{common,node}/**", + "**/vs/base/parts/*/{common,node}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -148,8 +148,8 @@ "target": "**/vs/base/parts/*/electron-main/**", "restrictions": [ "vs/nls", - "**/vs/base/{common,browser,node,electron-main}/**", - "**/vs/base/parts/*/{common,browser,node,electron-main}/**", + "**/vs/base/{common,node,electron-main}/**", + "**/vs/base/parts/*/{common,node,electron-main}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -187,9 +187,9 @@ "target": "**/vs/platform/*/node/**", "restrictions": [ "vs/nls", - "**/vs/base/{common,browser,node}/**", - "**/vs/base/parts/*/{common,browser,node}/**", - "**/vs/platform/*/{common,browser,node}/**", + "**/vs/base/{common,node}/**", + "**/vs/base/parts/*/{common,node}/**", + "**/vs/platform/*/{common,node}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -208,9 +208,9 @@ "target": "**/vs/platform/*/electron-main/**", "restrictions": [ "vs/nls", - "**/vs/base/{common,browser,node}/**", - "**/vs/base/parts/*/{common,browser,node,electron-browser}/**", - "**/vs/platform/*/{common,browser,node,electron-main}/**", + "**/vs/base/{common,node,electron-main}/**", + "**/vs/base/parts/*/{common,node,electron-main}/**", + "**/vs/platform/*/{common,node,electron-main}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -508,37 +508,6 @@ "!path" // node modules (except path where we have our own impl) ] }, - { - "target": "**/vs/workbench/contrib/files/common/**", - "restrictions": [ - "vs/nls", - "**/vs/base/common/**", - "**/vs/base/parts/*/common/**", - "**/vs/platform/*/common/**", - "**/vs/editor/common/**", - "**/vs/editor/contrib/*/common/**", - "**/vs/workbench/common/**", - "**/vs/workbench/services/*/common/**", - "**/vs/workbench/contrib/files/common/**", - "assert" - ] - }, - { - "target": "**/vs/workbench/contrib/files/browser/**", - "restrictions": [ - "vs/nls", - "vs/css!./**/*", - "**/vs/base/{common,browser}/**", - "**/vs/base/parts/*/{common,browser}/**", - "**/vs/platform/*/{common,browser}/**", - "**/vs/editor/{common,browser}/**", - "**/vs/editor/contrib/**", // editor/contrib is equivalent to /browser/ by convention - "**/vs/workbench/{common,browser}/**", - "**/vs/workbench/services/*/{common,browser}/**", - "**/vs/workbench/contrib/files/{common,browser}/**", - "assert" - ] - }, { "target": "**/vs/workbench/contrib/terminal/browser/**", "restrictions": [ @@ -566,10 +535,10 @@ "target": "**/vs/code/node/**", "restrictions": [ "vs/nls", - "**/vs/base/**/{common,browser,node}/**", - "**/vs/base/parts/**/{common,browser,node}/**", - "**/vs/platform/**/{common,browser,node}/**", - "**/vs/code/**/{common,browser,node}/**", + "**/vs/base/**/{common,node}/**", + "**/vs/base/parts/**/{common,node}/**", + "**/vs/platform/**/{common,node}/**", + "**/vs/code/**/{common,node}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -589,10 +558,10 @@ "target": "**/vs/code/electron-main/**", "restrictions": [ "vs/nls", - "**/vs/base/**/{common,browser,node}/**", - "**/vs/base/parts/**/{common,browser,node,electron-main}/**", - "**/vs/platform/**/{common,browser,node,electron-main}/**", - "**/vs/code/**/{common,browser,node,electron-main}/**", + "**/vs/base/**/{common,node,electron-main}/**", + "**/vs/base/parts/**/{common,node,electron-main}/**", + "**/vs/platform/**/{common,node,electron-main}/**", + "**/vs/code/**/{common,node,electron-main}/**", "!path" // node modules (except path where we have our own impl) ] }, @@ -640,48 +609,6 @@ "restrictions": "**/vs/**" } ], - "no-nodejs-globals": [ - true, - { - "target": "**/vs/base/common/{path,process,platform}.ts", - "allowed": [ - "process" // -> defines safe access to process - ] - }, - { - "target": "**/vs/**/test/{common,browser}/**", - "allowed": [ - "process", - "Buffer", - "__filename", - "__dirname" - ] - }, - { - "target": "**/vs/workbench/api/common/extHostExtensionService.ts", - "allowed": [ - "global" // -> safe access to 'global' - ] - }, - { - "target": "**/vs/**/{common,browser}/**", - "allowed": [ /* none */] - } - ], - "no-dom-globals": [ - true, - { - "target": "**/vs/**/test/{common,node,electron-main}/**", - "allowed": [ - "document", - "HTMLElement" - ] - }, - { - "target": "**/vs/**/{common,node,electron-main}/**", - "allowed": [ /* none */] - } - ], "duplicate-imports": true, "no-new-buffer": true, "translation-remind": true, diff --git a/yarn.lock b/yarn.lock index 8c2e276bbec..22634955751 100644 --- a/yarn.lock +++ b/yarn.lock @@ -167,16 +167,16 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.21.tgz#7e8a0c34cf29f4e17a36e9bd0ea72d45ba03908e" integrity sha512-CBgLNk4o3XMnqMc0rhb6lc77IwShMEglz05deDcn2lQxyXEZivfwgYJu7SMha9V5XcrP6qZuevTHV/QrN2vjKQ== -"@types/node@^10.12.12": - version "10.12.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.12.tgz#e15a9d034d9210f00320ef718a50c4a799417c47" - integrity sha512-Pr+6JRiKkfsFvmU/LK68oBRCQeEg36TyAbPhc2xpez24OOZZCuoIhWGTd39VZy6nGafSbxzGouFPTFD/rR1A0A== - "@types/node@^10.12.18": version "10.17.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.9.tgz#4f251a1ed77ac7ef09d456247d67fc8173f6b9da" integrity sha512-+6VygF9LbG7Gaqeog2G7u1+RUcmo0q1rI+2ZxdIg2fAUngk5Vz9fOCHXdloNUOHEPd1EuuOpL5O0CdgN9Fx5UQ== +"@types/node@^12.11.7": + version "12.12.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2" + integrity sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA== + "@types/semver@^5.4.0", "@types/semver@^5.5.0": version "5.5.0" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-5.5.0.tgz#146c2a29ee7d3bae4bf2fcb274636e264c813c45" @@ -199,6 +199,11 @@ dependencies: source-map "^0.6.1" +"@types/vscode-windows-registry@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/vscode-windows-registry/-/vscode-windows-registry-1.0.0.tgz#333eea7fd5743fa4c99dff13af16de2c08a586d0" + integrity sha512-gyq9tIMbxry5GL2gY7J30E6R3EUx0cAin/k3wfsQez4C5uDWVJmJw142x6KFXtYX7xYQL/IXmm4cRqi4ghg05A== + "@types/webpack@^4.4.10": version "4.4.10" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.10.tgz#2ecf12589142bc531549140612815b7d8b076358"